mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 13:01:37 +00:00
Combine firmware picker profiles and replace extra dropdowns
This commit is contained in:
1 parent
24458b0214
commit
9c9121c391
5 files changed
+272
-76
No files matched your search
@@ -23,6 +23,11 @@
|
||||
]);
|
||||
|
||||
const FACET_FIELDS = Object.freeze(FILTER_FIELDS.concat(["install"]));
|
||||
const PROFILE_FIELDS = Object.freeze(["ota", "feature", "variant"]);
|
||||
const CHOICE_FIELDS = Object.freeze([
|
||||
"chipFamily", "hardwareFamily", "hardware", "role", "logging",
|
||||
"firmwareProfile", "mode", "install",
|
||||
]);
|
||||
|
||||
const CHIP_FAMILY_LABELS = Object.freeze({
|
||||
esp32: "ESP32",
|
||||
@@ -125,6 +130,7 @@
|
||||
]);
|
||||
|
||||
let pickerInstanceCount = 0;
|
||||
let runtimeGroupCount = 0;
|
||||
|
||||
function isSafeGithubUrl(value) {
|
||||
try {
|
||||
@@ -883,6 +889,77 @@
|
||||
}).filter(Boolean)));
|
||||
}
|
||||
|
||||
// Keep the original URL facets, but select the correlated firmware
|
||||
// capabilities together so OTA and its sensor/storage tradeoff take one click.
|
||||
function firmwareProfileValue(filters) {
|
||||
if (!PROFILE_FIELDS.some(function (field) { return filters[field]; })) return "";
|
||||
return JSON.stringify(PROFILE_FIELDS.map(function (field) {
|
||||
return filters[field] || "";
|
||||
}));
|
||||
}
|
||||
|
||||
function firmwareProfileFilters(value) {
|
||||
let values;
|
||||
try {
|
||||
values = value ? JSON.parse(value) : ["", "", ""];
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(values) || values.length !== PROFILE_FIELDS.length ||
|
||||
!values.every(function (entry) { return typeof entry === "string"; })) return null;
|
||||
const filters = {};
|
||||
PROFILE_FIELDS.forEach(function (field, index) { filters[field] = values[index]; });
|
||||
return filters;
|
||||
}
|
||||
|
||||
function firmwareProfileLabel(profile) {
|
||||
const parts = [];
|
||||
if (profile.feature === "full") parts.push("Full");
|
||||
else if (profile.feature === "standard" && profile.ota !== "lora-receiver") {
|
||||
parts.push("Standard");
|
||||
}
|
||||
if (profile.ota) parts.push(profile.ota === "lora-source"
|
||||
? "LoRa OTA source only" : labelFor("ota", profile.ota));
|
||||
if (profile.variant && profile.variant !== "default") {
|
||||
parts.push(humanizeVariant(profile.variant).replace(
|
||||
"Reduced optional environmental/ranging drivers", "Reduced optional sensors"
|
||||
));
|
||||
}
|
||||
if (!PROFILE_FIELDS.every(function (field) { return profile[field]; })) {
|
||||
parts.push("other profile choices open");
|
||||
}
|
||||
return parts.join(" - ");
|
||||
}
|
||||
|
||||
function firmwareProfileChoices(profiles, filters) {
|
||||
const compatible = (profiles || []).filter(function (profile) {
|
||||
return profileMatchesFacets(profile, filters, PROFILE_FIELDS);
|
||||
});
|
||||
const choices = new Map();
|
||||
compatible.forEach(function (profile) {
|
||||
const value = firmwareProfileValue(profile);
|
||||
if (!choices.has(value)) choices.set(value, {
|
||||
value: value,
|
||||
label: firmwareProfileLabel(profile),
|
||||
filters: firmwareProfileFilters(value),
|
||||
});
|
||||
});
|
||||
// Older share links can specify just OTA, feature or variant. Reflect
|
||||
// that constraint visibly until the operator chooses a complete profile.
|
||||
const selected = firmwareProfileValue(filters);
|
||||
if (selected && !choices.has(selected) && compatible.some(function (profile) {
|
||||
return profileMatches(profile, filters, PROFILE_FIELDS);
|
||||
})) choices.set(selected, {
|
||||
value: selected,
|
||||
label: firmwareProfileLabel(filters),
|
||||
filters: firmwareProfileFilters(selected),
|
||||
});
|
||||
return Array.from(choices.values()).sort(function (a, b) {
|
||||
const byFull = Number(b.filters.feature === "full") - Number(a.filters.feature === "full");
|
||||
return byFull || a.label.localeCompare(b.label, undefined, { numeric: true });
|
||||
});
|
||||
}
|
||||
|
||||
function selectionUrl(url, filters, automaticChipFamily) {
|
||||
const result = new URL(url);
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
@@ -1004,6 +1081,23 @@
|
||||
return element;
|
||||
}
|
||||
|
||||
function renderRadioChoices(container, options, selected, name, field) {
|
||||
container.replaceChildren();
|
||||
options.forEach(function (option) {
|
||||
const label = createElement("label");
|
||||
label.className = "firmware-picker-radio-option";
|
||||
const input = createElement("input");
|
||||
input.type = "radio";
|
||||
input.name = name;
|
||||
input.value = option.value;
|
||||
if (field) input.dataset.choiceField = field;
|
||||
input.checked = selected === option.value;
|
||||
label.appendChild(input);
|
||||
label.appendChild(createElement("span", option.label));
|
||||
container.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
function optionSort(field, a, b) {
|
||||
const roleOrder = ["companion", "repeater", "room", "sensor",
|
||||
"partition-expander", "terminal", "kiss", "other"];
|
||||
@@ -1398,20 +1492,21 @@
|
||||
details.open = index === 0;
|
||||
details.appendChild(createElement("summary", item.title));
|
||||
if (item.note) details.appendChild(createElement("p", item.note));
|
||||
const label = createElement("label", "Show steps for ");
|
||||
const select = createElement("select");
|
||||
select.setAttribute("aria-label", item.title);
|
||||
item.actions.forEach(function (action, i) {
|
||||
const option = createElement("option", action.label);
|
||||
option.value = String(i);
|
||||
select.appendChild(option);
|
||||
});
|
||||
label.appendChild(select);
|
||||
details.appendChild(label);
|
||||
const fieldset = createElement("fieldset");
|
||||
fieldset.className = "firmware-picker-radio-control";
|
||||
fieldset.appendChild(createElement("legend", "Show steps for " + item.title));
|
||||
const choices = createElement("div");
|
||||
choices.className = "firmware-picker-radio-options";
|
||||
renderRadioChoices(choices, item.actions.map(function (action, i) {
|
||||
return { value: String(i), label: action.label };
|
||||
}), "0", "firmware-picker-runtime-" + (++runtimeGroupCount));
|
||||
fieldset.appendChild(choices);
|
||||
details.appendChild(fieldset);
|
||||
const output = createElement("div");
|
||||
let actionIndex = 0;
|
||||
function show() {
|
||||
output.replaceChildren();
|
||||
const action = item.actions[Number(select.value) || 0];
|
||||
const action = item.actions[actionIndex];
|
||||
if (action.text) output.appendChild(createElement("p", action.text));
|
||||
if (action.commands) {
|
||||
const pre = createElement("pre");
|
||||
@@ -1431,7 +1526,10 @@
|
||||
output.appendChild(button);
|
||||
}
|
||||
}
|
||||
select.addEventListener("change", show);
|
||||
choices.addEventListener("change", function (event) {
|
||||
actionIndex = Number(event.target.value) || 0;
|
||||
show();
|
||||
});
|
||||
show();
|
||||
details.appendChild(output);
|
||||
panel.appendChild(details);
|
||||
@@ -1467,9 +1565,7 @@
|
||||
}).join(" / "),
|
||||
],
|
||||
["Logging", labelFor("logging", selection && selection.logging || profile.logging)],
|
||||
["OTA", labelFor("ota", profile.ota)],
|
||||
["Feature profile", labelFor("feature", profile.feature)],
|
||||
["Variant", labelFor("variant", profile.variant, profile.hardware)],
|
||||
["Firmware profile", firmwareProfileLabel(profile)],
|
||||
["Install operation", labelFor("install", installKind)],
|
||||
["File", asset.name],
|
||||
["Size", formatBytes(asset.size)],
|
||||
@@ -1535,6 +1631,7 @@
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
controls[field] = form.querySelector('[data-field="' + field + '"]');
|
||||
});
|
||||
controls.firmwareProfile = form.querySelector('[data-field="firmwareProfile"]');
|
||||
let catalog = { releaseSet: null, rows: [], profiles: [] };
|
||||
const filters = {};
|
||||
let automaticChipFamily = false;
|
||||
@@ -1620,27 +1717,16 @@
|
||||
}
|
||||
|
||||
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) };
|
||||
return { value: value, label: field === "hardware"
|
||||
? humanizeHardwareVariant(value, filters.hardwareFamily)
|
||||
: labelFor(field, value, filters.hardware || filters.hardwareFamily) };
|
||||
})
|
||||
);
|
||||
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);
|
||||
});
|
||||
renderRadioChoices(container, options, selected, groupPrefix + "-" + field, field);
|
||||
const fieldset = container.closest("fieldset");
|
||||
if (fieldset) fieldset.disabled = values.length === 0;
|
||||
}
|
||||
@@ -1676,6 +1762,7 @@
|
||||
changed = false;
|
||||
passes += 1;
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
if (PROFILE_FIELDS.includes(field)) return;
|
||||
if (field === "hardware" && !filters.hardwareFamily) {
|
||||
if (filters.hardware) {
|
||||
filters.hardware = "";
|
||||
@@ -1693,10 +1780,17 @@
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
if (firmwareProfileValue(filters) && !matchingProfiles().length) {
|
||||
PROFILE_FIELDS.forEach(function (field) { filters[field] = ""; });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshFacets() {
|
||||
const active = document.activeElement;
|
||||
const focusedChoice = active && form.contains(active) && active.type === "radio"
|
||||
? { field: active.dataset.choiceField, value: active.value } : null;
|
||||
if (automaticChipFamily) filters.chipFamily = "";
|
||||
stabilizeFilters();
|
||||
if (automaticChipFamily && filters.hardwareFamily) {
|
||||
@@ -1706,6 +1800,11 @@
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
setControlOptions(field, valuesForField(field));
|
||||
});
|
||||
const profileChoices = firmwareProfileChoices(catalog.profiles, filters);
|
||||
const profileControl = controls.firmwareProfile;
|
||||
renderRadioChoices(profileControl, [{ value: "", label: "Any" }].concat(profileChoices),
|
||||
firmwareProfileValue(filters), groupPrefix + "-firmwareProfile", "firmwareProfile");
|
||||
profileControl.closest("fieldset").disabled = profileChoices.length === 0;
|
||||
const hardwareVariants = valuesForField("hardware");
|
||||
const hardwareVariantControl = root.querySelector(
|
||||
'[data-role="hardware-variant-control"]'
|
||||
@@ -1717,19 +1816,29 @@
|
||||
(filters.chipFamily ? " - " + labelFor("chipFamily", filters.chipFamily) : "");
|
||||
render();
|
||||
updateSelectionUrl();
|
||||
// Rebuilding the options must not interrupt native radio keyboard navigation.
|
||||
if (focusedChoice) {
|
||||
const control = controls[focusedChoice.field];
|
||||
const replacement = control && Array.from(control.querySelectorAll('input[type="radio"]'))
|
||||
.find(function (input) { return input.value === focusedChoice.value; });
|
||||
if (replacement) replacement.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
result.hidden = true;
|
||||
missing.hidden = true;
|
||||
resultList.replaceChildren();
|
||||
const missingFields = FACET_FIELDS.filter(function (field) {
|
||||
const missingFields = CHOICE_FIELDS.filter(function (field) {
|
||||
if (field === "firmwareProfile") return !PROFILE_FIELDS.every(function (part) {
|
||||
return filters[part];
|
||||
});
|
||||
return field !== "chipFamily" && !filters[field];
|
||||
});
|
||||
const matches = matchingProfiles();
|
||||
|
||||
if (missingFields.length) {
|
||||
if (missingFields.length === FACET_FIELDS.length - 1 && !filters.chipFamily) {
|
||||
if (missingFields.length === CHOICE_FIELDS.length - 1 && !filters.chipFamily) {
|
||||
status.textContent = "Pick options in any order. " +
|
||||
matches.length + " compatible configurations are available.";
|
||||
} else {
|
||||
@@ -1827,6 +1936,14 @@
|
||||
form.addEventListener("change", function (event) {
|
||||
const target = event.target;
|
||||
const field = target.dataset.choiceField || target.dataset.field;
|
||||
if (field === "firmwareProfile") {
|
||||
const chosen = firmwareProfileFilters(target.value);
|
||||
if (!chosen) return;
|
||||
if (linkStatus) linkStatus.textContent = "";
|
||||
PROFILE_FIELDS.forEach(function (part) { filters[part] = chosen[part]; });
|
||||
refreshFacets();
|
||||
return;
|
||||
}
|
||||
if (!FACET_FIELDS.includes(field)) return;
|
||||
if (linkStatus) linkStatus.textContent = "";
|
||||
if (field === "chipFamily") {
|
||||
@@ -1918,6 +2035,7 @@
|
||||
const api = Object.freeze({
|
||||
FILTER_FIELDS: FILTER_FIELDS,
|
||||
FACET_FIELDS: FACET_FIELDS,
|
||||
PROFILE_FIELDS: PROFILE_FIELDS,
|
||||
ROLE_LABELS: ROLE_LABELS,
|
||||
LOGGING_LABELS: LOGGING_LABELS,
|
||||
OTA_LABELS: OTA_LABELS,
|
||||
@@ -1953,6 +2071,10 @@
|
||||
profileMatchesFacets: profileMatchesFacets,
|
||||
facetValues: facetValues,
|
||||
uniqueValues: uniqueValues,
|
||||
firmwareProfileValue: firmwareProfileValue,
|
||||
firmwareProfileFilters: firmwareProfileFilters,
|
||||
firmwareProfileLabel: firmwareProfileLabel,
|
||||
firmwareProfileChoices: firmwareProfileChoices,
|
||||
selectionUrl: selectionUrl,
|
||||
selectionFromUrl: selectionFromUrl,
|
||||
canonicalAsset: canonicalAsset,
|
||||
|
||||
@@ -98,10 +98,17 @@
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.firmware-picker-control-help {
|
||||
margin: 0 0 0.65rem;
|
||||
color: var(--md-default-fg-color--light);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-option {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -123,6 +130,7 @@
|
||||
background: var(--md-default-bg-color);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-option input:checked + span {
|
||||
@@ -328,7 +336,6 @@
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.firmware-picker-runtime select,
|
||||
.firmware-picker-runtime button {
|
||||
padding: 0.5rem;
|
||||
margin: 0.4rem 0;
|
||||
|
||||
+34
-41
@@ -2,7 +2,7 @@
|
||||
|
||||
Each result now includes **Restore your settings after flashing**. Its commands
|
||||
follow the selected logging mode and the exact image's verified role/hardware
|
||||
controls. Select On, Off, or Check to view and copy the commands; Companion
|
||||
controls. Click On, Off, or Check to view and copy the commands; Companion
|
||||
MQTT and GPS show their app/WebConfig steps instead. See
|
||||
[feature switches by role](role_feature_switches.md) for the full reference.
|
||||
The [USB web console](https://flasher.meshcore.io/console) works with the
|
||||
@@ -12,6 +12,9 @@ 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 optional chip-family filter (ESP32, nRF52, RP2040, or STM32) can narrow the
|
||||
hardware list first. You can skip it: picking hardware fills it in automatically.
|
||||
Chip family and hardware are the only dropdowns. All remaining choices use
|
||||
buttons. **Firmware profile** combines OTA support, Full/standard, and sensor
|
||||
or storage differences in one choice.
|
||||
|
||||
The picker reads public release metadata from GitHub. It does not upload device
|
||||
information. Hardware names, target names, and download links come directly
|
||||
@@ -66,12 +69,12 @@ from the published firmware assets.
|
||||
</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="hardware" data-role="hardware-variant-control" disabled hidden>
|
||||
<legend>Hardware variant</legend>
|
||||
<div class="firmware-picker-radio-options" data-field="hardware">
|
||||
Choose hardware first
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="firmware-picker-control firmware-picker-radio-control firmware-picker-wide" data-radio-field="install" disabled>
|
||||
<legend>Install operation</legend>
|
||||
@@ -94,34 +97,21 @@ from the published firmware assets.
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<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...
|
||||
<fieldset class="firmware-picker-control firmware-picker-radio-control firmware-picker-wide" data-radio-field="firmwareProfile" aria-describedby="firmware-picker-profile-help" disabled>
|
||||
<legend>Firmware profile</legend>
|
||||
<p id="firmware-picker-profile-help" class="firmware-picker-control-help">Each button combines OTA support with the feature, sensor, and storage choices for that image. Reduced optional sensors means fewer environmental/ranging drivers; I2C and supported board peripherals remain available.</p>
|
||||
<div class="firmware-picker-radio-options" data-field="firmwareProfile">
|
||||
Loading firmware profiles...
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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...
|
||||
<fieldset class="firmware-picker-control firmware-picker-radio-control firmware-picker-wide" data-radio-field="mode" disabled>
|
||||
<legend>Connection / bridge mode</legend>
|
||||
<div class="firmware-picker-radio-options" data-field="mode">
|
||||
Loading modes...
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="firmware-picker-control firmware-picker-select-control">
|
||||
<label for="firmware-picker-variant">Firmware variant</label>
|
||||
<select id="firmware-picker-variant" data-field="variant" disabled>
|
||||
<option value="">Loading variants...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="firmware-picker-form-actions firmware-picker-wide">
|
||||
<button type="reset" data-action="clear" disabled>Clear all choices</button>
|
||||
<button type="button" data-action="copy-link" disabled>Copy link to settings</button>
|
||||
@@ -174,7 +164,8 @@ from the published firmware assets.
|
||||
| Wi-Fi MQTT observer | Firmware connects directly to MQTT over Wi-Fi; this is not USB logging |
|
||||
| USB logging + Wi-Fi MQTT | Unified FULL image sends to both paths; avoid two publishers aimed at the same broker unless messages are deduplicated |
|
||||
| No logging | Normal standalone operation without the dedicated logging/MQTT profile |
|
||||
| Receives LoRa OTA | Repeater, room server, or sensor image that can stage an exact matching update received over LoRa |
|
||||
| Receives LoRa OTA | Repeater, room server, or sensor image that can stage an exact matching update received over LoRa; any sensor/storage tradeoff appears on the same Firmware profile button |
|
||||
| Receives LoRa OTA - Reduced optional sensors | Compact OTA image that omits selected optional environmental/ranging drivers while retaining generic I2C and supported board peripherals |
|
||||
| LoRa OTA source only | Full Companion serving a host-supplied update to another node without self-installing it |
|
||||
|
||||
Connection and bridge choices depend on the selected role. Companion firmware
|
||||
@@ -223,7 +214,10 @@ preselects that board, role, OTA capability, and storage profile.
|
||||
|
||||
The query parameters are `chipFamily`, `hardwareFamily`, `hardware`, `role`,
|
||||
`logging`, `ota`, `mode`, `feature`, `variant`, and `install`. Values use the
|
||||
picker's internal identifiers rather than the displayed labels. The generated
|
||||
picker's internal identifiers rather than the displayed labels. One Firmware
|
||||
profile button sets `ota`, `feature`, and `variant` together. Existing links
|
||||
with only some of those choices still work and show their partial selection.
|
||||
The generated
|
||||
link also records `chipAuto` so automatic chip-family selection or an explicit
|
||||
**Any** choice behaves the same after reopening. Existing section anchors and
|
||||
unrelated query parameters are preserved.
|
||||
@@ -397,16 +391,15 @@ bootloader-update UF2 for the application image being installed.
|
||||
|
||||
## Hardware and variant names
|
||||
|
||||
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 choices that still require different
|
||||
code or wiring, such as serial port or the legacy `no_external_sensors` target
|
||||
suffix. For RAK3401 and RAK4631, the picker labels that compact LoRa OTA option
|
||||
**Internal storage (no external storage board)**. Its result details explain
|
||||
the omitted optional environmental/ranging sensor drivers; the reduction does
|
||||
not disable generic I2C or unrelated board-integrated peripherals. Other boards
|
||||
retain the **Reduced optional environmental/ranging drivers** label.
|
||||
Hardware families with multiple released targets get **Hardware variant**
|
||||
buttons. They separate revisions, display type, expansion kit, radio/PA layout,
|
||||
pin map, and other physical differences without crowding the Hardware dropdown.
|
||||
**Firmware profile** buttons combine OTA support, Full/standard, and choices
|
||||
that need a different image or wiring, such as serial port or external storage.
|
||||
The legacy `no_external_sensors` profile is labeled **Receives LoRa OTA -
|
||||
Reduced optional sensors** on the same button. On RAK3401 and RAK4631 this
|
||||
compact profile uses internal storage without an external storage board. The
|
||||
reduction does not disable generic I2C or unrelated board-integrated peripherals.
|
||||
Reduced RAK3401 and RAK4631 targets retain INA219/INA226/INA260/INA3221 as
|
||||
voltage/current entries in the optional sensor table. They are not the only I2C
|
||||
users: the SSD1306 OLED, supported autodiscovered RTCs, and RAK12500 GPS remain
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ theme:
|
||||
extra_css:
|
||||
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.css
|
||||
- _stylesheets/extra.css
|
||||
- _stylesheets/firmware_picker.css
|
||||
- _stylesheets/firmware_picker.css?v=20260929-1
|
||||
- _stylesheets/telemetry_decoder.css
|
||||
- _stylesheets/management_decoder.css
|
||||
- _stylesheets/filter_tool.css
|
||||
@@ -26,7 +26,7 @@ extra_css:
|
||||
|
||||
extra_javascript:
|
||||
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
|
||||
- _javascript/firmware_picker.js?v=20260927-4
|
||||
- _javascript/firmware_picker.js?v=20260929-1
|
||||
- _javascript/telemetry_decoder.js
|
||||
- _javascript/management_decoder.js
|
||||
- _javascript/filter_tool.js
|
||||
|
||||
@@ -1291,3 +1291,77 @@ const escapedShare = picker.selectionFromUrl('https://example.com/?role=%3Cscrip
|
||||
assert.deepStrictEqual(escapedShare.filters, {});
|
||||
assert.deepStrictEqual(escapedShare.unavailable, ['role=<script>']);
|
||||
console.log('shareable picker URL tests passed');
|
||||
|
||||
// One choice carries OTA, feature set and sensor/storage tradeoffs together.
|
||||
// Every existing profile remains reachable and resolves to its original files.
|
||||
for (const candidateCatalog of [catalog, controlled, currentCatalog, chipCatalog, withMigrations]) {
|
||||
for (const profile of candidateCatalog.profiles) {
|
||||
const context = {
|
||||
hardwareFamily: profile.hardwareFamily, hardware: profile.hardware,
|
||||
role: profile.role, mode: profile.mode, install: profile.installKinds[0],
|
||||
};
|
||||
const choices = picker.firmwareProfileChoices(candidateCatalog.profiles, context);
|
||||
const value = picker.firmwareProfileValue(profile);
|
||||
const choice = choices.find(item => item.value === value);
|
||||
assert(choice, profile.target);
|
||||
assert.deepStrictEqual(choice.filters, {
|
||||
ota: profile.ota, feature: profile.feature, variant: profile.variant,
|
||||
});
|
||||
assert.deepStrictEqual(picker.firmwareProfileFilters(value), choice.filters);
|
||||
const chosen = Object.assign({}, context, choice.filters);
|
||||
const matches = candidateCatalog.profiles.filter(item => picker.profileMatchesFacets(item, chosen));
|
||||
assert(matches.includes(profile), profile.target);
|
||||
assert(picker.resolveProfileAssets(matches, context.install)
|
||||
.some(item => item.profile === profile), profile.target);
|
||||
const linked = picker.selectionFromUrl(picker.selectionUrl(shareBase, chosen, true), candidateCatalog.profiles);
|
||||
assert.deepStrictEqual(linked.unavailable, [], profile.target);
|
||||
assert.strictEqual(picker.firmwareProfileValue(linked.filters), value, profile.target);
|
||||
assert.strictEqual(new Set(choices.map(item => item.value)).size, choices.length);
|
||||
}
|
||||
}
|
||||
|
||||
const compactChoice = picker.firmwareProfileChoices(catalog.profiles, {
|
||||
hardware: 'Station_G2', role: 'repeater',
|
||||
}).find(item => item.filters.variant === 'no-external-sensors');
|
||||
assert(compactChoice);
|
||||
assert.strictEqual(compactChoice.label, 'Receives LoRa OTA - Reduced optional sensors');
|
||||
assert.strictEqual(compactChoice.filters.ota, 'lora-receiver');
|
||||
assert(!compactChoice.label.includes('No I2C'));
|
||||
const fullChoice = picker.firmwareProfileChoices(catalog.profiles, {
|
||||
hardware: 'Station_G2', role: 'companion',
|
||||
})[0];
|
||||
assert.strictEqual(fullChoice.label, 'Full - LoRa OTA source only');
|
||||
assert.strictEqual(fullChoice.filters.ota, 'lora-source');
|
||||
assert.strictEqual(fullChoice.filters.feature, 'full');
|
||||
assert.strictEqual(picker.firmwareProfileLabel({ota: 'lora-receiver', feature: 'full', variant: 'default'}),
|
||||
'Full - Receives LoRa OTA');
|
||||
assert(picker.firmwareProfileLabel({ota: 'lora-receiver', feature: 'standard', variant: 'w25q16'})
|
||||
.includes('External storage board (W25Q16)'));
|
||||
|
||||
// A combined profile can be replaced in one click even when its former
|
||||
// OTA/feature/variant facets have narrowed the catalog to a different image.
|
||||
const replacementChoices = picker.firmwareProfileChoices(catalog.profiles, {
|
||||
hardware: 'Station_G2', role: 'repeater',
|
||||
ota: 'none', feature: 'standard', variant: 'default',
|
||||
});
|
||||
assert(replacementChoices.some(item => item.value === compactChoice.value));
|
||||
assert.deepStrictEqual(picker.firmwareProfileFilters(''), {ota: '', feature: '', variant: ''});
|
||||
assert.strictEqual(picker.firmwareProfileValue({}), '');
|
||||
for (const invalid of ['invalid', '{}', '["none"]', '["none",false,"default"]']) {
|
||||
assert.strictEqual(picker.firmwareProfileFilters(invalid), null);
|
||||
}
|
||||
|
||||
// Legacy partial links keep a visible selection without guessing a sensor
|
||||
// recipe, Full layout or OTA capability that the link did not request.
|
||||
for (const partial of [{ota: 'lora-receiver'}, {feature: 'standard'}, {variant: 'no-external-sensors'}]) {
|
||||
const context = Object.assign({hardware: 'Station_G2', role: 'repeater'}, partial);
|
||||
const value = picker.firmwareProfileValue(context);
|
||||
const selected = picker.firmwareProfileChoices(catalog.profiles, context).find(item => item.value === value);
|
||||
assert(selected);
|
||||
assert(selected.label.includes('other profile choices open'));
|
||||
for (const field of picker.PROFILE_FIELDS) assert.strictEqual(selected.filters[field], partial[field] || '');
|
||||
}
|
||||
assert(!picker.firmwareProfileChoices(catalog.profiles, {
|
||||
hardware: 'Station_G2', role: 'repeater', variant: 'missing',
|
||||
}).some(item => item.filters.variant === 'missing'));
|
||||
console.log('combined firmware profile choices and legacy links passed');
|
||||
Reference in new issue
Block a user