mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 10:12:25 +00:00
Improve firmware tooling docs and OTA seeder startup checks
This commit is contained in:
1 parent
4086222d85
commit
bd55369aa9
20 files changed
+1396
-16
No files matched your search
@@ -0,0 +1,169 @@
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
function validateCatalog(catalog) {
|
||||
if (!catalog || catalog.schema !== 1 || !Array.isArray(catalog.repositories) ||
|
||||
!Array.isArray(catalog.releases) || !Array.isArray(catalog.builds) ||
|
||||
!Array.isArray(catalog.unresolved) || !catalog.layouts ||
|
||||
typeof catalog.layouts !== "object" || Array.isArray(catalog.layouts)) {
|
||||
throw new Error("Unsupported partition catalog format");
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
function snapshot(catalog) {
|
||||
return {
|
||||
schema: catalog.schema,
|
||||
generatedAt: catalog.generatedAt,
|
||||
repositories: catalog.repositories,
|
||||
counts: {
|
||||
releases: catalog.releases.length,
|
||||
builds: catalog.builds.length,
|
||||
layouts: Object.keys(catalog.layouts).length,
|
||||
unresolved: catalog.unresolved.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function jsonView(catalog, view) {
|
||||
validateCatalog(catalog);
|
||||
if (view === "all") return JSON.stringify(catalog, null, 2);
|
||||
const result = snapshot(catalog);
|
||||
if (view === "layouts") result.layouts = catalog.layouts;
|
||||
else if (view === "releases") result.releases = catalog.releases;
|
||||
else throw new Error("Unknown catalog view");
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
function boardMinimums(catalog) {
|
||||
validateCatalog(catalog);
|
||||
const boards = new Map();
|
||||
catalog.builds.forEach(function (build) {
|
||||
const key = build.board.toLowerCase();
|
||||
if (!boards.has(key)) {
|
||||
boards.set(key, {
|
||||
board: build.board, smallestBytes: null,
|
||||
hasDualOta: false, hasNonDualOta: false, hasUnknownLayout: false,
|
||||
});
|
||||
}
|
||||
const row = boards.get(key);
|
||||
const layout = Object.prototype.hasOwnProperty.call(catalog.layouts, build.layout)
|
||||
? catalog.layouts[build.layout] : null;
|
||||
if (!layout || !Array.isArray(layout.partitions)) {
|
||||
row.hasUnknownLayout = true;
|
||||
return;
|
||||
}
|
||||
const apps = layout.partitions.filter(function (entry) {
|
||||
return Array.isArray(entry) && entry[0] === 0 &&
|
||||
Number.isSafeInteger(entry[3]) && entry[3] > 0;
|
||||
});
|
||||
if (!apps.length) {
|
||||
row.hasUnknownLayout = true;
|
||||
return;
|
||||
}
|
||||
const smallest = Math.min.apply(null, apps.map((entry) => entry[3]));
|
||||
row.smallestBytes = row.smallestBytes === null
|
||||
? smallest : Math.min(row.smallestBytes, smallest);
|
||||
if (layout.dualOta === true) row.hasDualOta = true;
|
||||
else if (layout.dualOta === false) row.hasNonDualOta = true;
|
||||
else row.hasUnknownLayout = true;
|
||||
});
|
||||
return Array.from(boards.values()).sort(function (left, right) {
|
||||
return left.board.localeCompare(right.board, "en", { sensitivity: "base" });
|
||||
});
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
return bytes === null ? "Unknown" : (bytes / 1048576).toLocaleString(
|
||||
"en-US", { maximumFractionDigits: 4 }
|
||||
) + " MiB";
|
||||
}
|
||||
|
||||
function otaLayouts(row) {
|
||||
if (row.hasUnknownLayout) return "Unknown layouts present";
|
||||
if (row.hasDualOta && row.hasNonDualOta) return "Mixed (some lack dual OTA)";
|
||||
return row.hasDualOta ? "Dual-slot OTA" : "No dual-slot OTA";
|
||||
}
|
||||
|
||||
async function initializeViewer() {
|
||||
const root = document.getElementById("partition-catalog");
|
||||
if (!root || root.dataset.initialized) return;
|
||||
root.dataset.initialized = "true";
|
||||
const selector = document.getElementById("partition-catalog-view");
|
||||
const status = document.getElementById("partition-catalog-status");
|
||||
const code = document.getElementById("partition-catalog-json");
|
||||
const link = document.getElementById("partition-catalog-open");
|
||||
const raw = document.getElementById("partition-catalog-raw");
|
||||
const search = document.getElementById("partition-catalog-search");
|
||||
const tbody = document.getElementById("partition-catalog-boards");
|
||||
const count = document.getElementById("partition-catalog-count");
|
||||
try {
|
||||
const response = await global.fetch(link.href, { cache: "no-cache" });
|
||||
if (!response.ok) throw new Error("HTTP " + response.status);
|
||||
const catalog = validateCatalog(await response.json());
|
||||
const render = function () {
|
||||
// Catalog text is data, never HTML. Only format the large builds array
|
||||
// when the reader explicitly requests the complete catalog.
|
||||
code.textContent = jsonView(catalog, selector.value);
|
||||
};
|
||||
raw.addEventListener("toggle", function () { if (raw.open) render(); });
|
||||
if (raw.open) render();
|
||||
selector.disabled = false;
|
||||
selector.addEventListener("change", render);
|
||||
const rows = boardMinimums(catalog).map(function (board) {
|
||||
const tr = document.createElement("tr");
|
||||
const name = document.createElement("th");
|
||||
name.scope = "row";
|
||||
name.textContent = board.board;
|
||||
const size = document.createElement("td");
|
||||
size.textContent = formatSize(board.smallestBytes);
|
||||
if (board.smallestBytes !== null) {
|
||||
const bytes = document.createElement("small");
|
||||
bytes.textContent = board.smallestBytes.toLocaleString("en-US") + " bytes";
|
||||
size.appendChild(bytes);
|
||||
}
|
||||
const layout = document.createElement("td");
|
||||
layout.textContent = otaLayouts(board);
|
||||
tr.appendChild(name);
|
||||
tr.appendChild(size);
|
||||
tr.appendChild(layout);
|
||||
tbody.appendChild(tr);
|
||||
return { name: board.board.toLowerCase(), element: tr };
|
||||
});
|
||||
const filter = function () {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
let visible = 0;
|
||||
rows.forEach(function (row) {
|
||||
row.element.hidden = !row.name.includes(query);
|
||||
if (!row.element.hidden) visible++;
|
||||
});
|
||||
count.textContent = "Showing " + visible + " of " + rows.length + " boards.";
|
||||
};
|
||||
filter();
|
||||
search.disabled = false;
|
||||
search.addEventListener("input", filter);
|
||||
document.getElementById("partition-catalog-table").hidden = false;
|
||||
const counts = snapshot(catalog).counts;
|
||||
status.textContent = "Snapshot " + catalog.generatedAt + ": " +
|
||||
counts.builds.toLocaleString("en-US") + " builds, " +
|
||||
counts.releases.toLocaleString("en-US") + " releases, " +
|
||||
counts.layouts + " layouts, " + counts.unresolved + " unresolved assets.";
|
||||
} catch (error) {
|
||||
status.textContent = "Could not load the catalog: " + error.message +
|
||||
". Use Open JSON or Download JSON to access the file directly.";
|
||||
code.textContent = "Catalog preview unavailable.";
|
||||
}
|
||||
}
|
||||
|
||||
const api = Object.freeze({
|
||||
validateCatalog, jsonView, boardMinimums, formatSize, otaLayouts, initializeViewer,
|
||||
});
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
if (typeof document !== "undefined") {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeViewer, { once: true });
|
||||
} else {
|
||||
initializeViewer();
|
||||
}
|
||||
}
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this);
|
||||
@@ -1079,6 +1079,49 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Count compiled builds, not files: merged and app-only downloads can belong
|
||||
// to the same build, but still require different installation instructions.
|
||||
function selectionProgress(profiles, filters) {
|
||||
const count = (profiles || []).filter(function (profile) {
|
||||
return profileMatchesFacets(profile, filters);
|
||||
}).length;
|
||||
return {
|
||||
count: count,
|
||||
state: count === 1 ? "single" : count ? "multiple" : "empty",
|
||||
title: count === 1 ? "1 firmware build left" : count ? count + " firmware builds remain"
|
||||
: "No matching firmware builds",
|
||||
note: count === 1
|
||||
? filters && filters.install ? "Confirm your board and install choice."
|
||||
: "Choose an install operation."
|
||||
: count ? "Choose options to narrow the results."
|
||||
: "Change or clear choices to see available builds.",
|
||||
};
|
||||
}
|
||||
|
||||
function choicesUseSameFirmware(profiles, filters, field, options) {
|
||||
// Never equate install operations, even when they belong to one build.
|
||||
if (!["hardware", "role", "logging", "mode", "firmwareProfile"].includes(field)) {
|
||||
return false;
|
||||
}
|
||||
const ignored = field === "firmwareProfile" ? PROFILE_FIELDS : field;
|
||||
const compatible = (profiles || []).filter(function (profile) {
|
||||
return profileMatchesFacets(profile, filters, ignored);
|
||||
});
|
||||
const choices = (options || []).filter(function (option) { return option.value; });
|
||||
if (!compatible.length || !choices.length) return false;
|
||||
// Ignore the current choice before comparing alternatives. Having one
|
||||
// result now does not imply that another button would keep the same build.
|
||||
return choices.every(function (option) {
|
||||
const choice = field === "firmwareProfile" ? firmwareProfileFilters(option.value)
|
||||
: { [field]: option.value };
|
||||
if (!choice) return false;
|
||||
const selection = Object.assign({}, filters, choice);
|
||||
return compatible.every(function (profile) {
|
||||
return profileMatchesFacets(profile, selection);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function selectionUrl(url, filters, automaticChipFamily) {
|
||||
const result = new URL(url);
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
@@ -1751,6 +1794,8 @@
|
||||
const shareLink = root.querySelector('[data-role="share-link"]');
|
||||
const copyLinkButton = root.querySelector('[data-action="copy-link"]');
|
||||
const linkStatus = root.querySelector('[data-role="link-status"]');
|
||||
const feedback = root.querySelector('[data-role="filter-feedback"]');
|
||||
const resultLink = root.querySelector('[data-role="view-results"]');
|
||||
const controls = {};
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
controls[field] = form.querySelector('[data-field="' + field + '"]');
|
||||
@@ -1762,6 +1807,10 @@
|
||||
const filters = {};
|
||||
let automaticChipFamily = false;
|
||||
const groupPrefix = "firmware-picker-" + (++pickerInstanceCount);
|
||||
if (resultLink) {
|
||||
result.id = result.id || groupPrefix + "-results";
|
||||
resultLink.href = "#" + result.id;
|
||||
}
|
||||
|
||||
function updateSelectionUrl() {
|
||||
const url = selectionUrl(global.location.href, filters, automaticChipFamily);
|
||||
@@ -1855,6 +1904,35 @@
|
||||
renderRadioChoices(container, options, selected, groupPrefix + "-" + field, field);
|
||||
const fieldset = container.closest("fieldset");
|
||||
if (fieldset) fieldset.disabled = values.length === 0;
|
||||
setRefinementHint(container, field, options);
|
||||
}
|
||||
|
||||
function setRefinementHint(container, field, options) {
|
||||
const fieldset = container.closest("fieldset");
|
||||
if (!fieldset) return;
|
||||
let badge = fieldset.querySelector('[data-role="same-firmware"]');
|
||||
let hint = fieldset.querySelector('[data-role="refinement-hint"]');
|
||||
if (!badge) {
|
||||
badge = createElement("span", "Same firmware");
|
||||
badge.className = "firmware-picker-same-firmware";
|
||||
badge.dataset.role = "same-firmware";
|
||||
fieldset.querySelector("legend").appendChild(badge);
|
||||
hint = createElement("p");
|
||||
hint.className = "firmware-picker-control-help firmware-picker-refinement-hint";
|
||||
hint.dataset.role = "refinement-hint";
|
||||
hint.id = groupPrefix + "-" + field + "-hint";
|
||||
fieldset.insertBefore(hint, container);
|
||||
}
|
||||
const same = choicesUseSameFirmware(catalog.profiles, filters, field, options);
|
||||
badge.hidden = !same;
|
||||
hint.hidden = !same;
|
||||
hint.textContent = same ? "These choices do not narrow the firmware builds." +
|
||||
(["logging", "mode"].includes(field) ? " They can change setup instructions." : "") : "";
|
||||
const describedBy = (fieldset.getAttribute("aria-describedby") || "")
|
||||
.split(/\s+/).filter(function (id) { return id && id !== hint.id; });
|
||||
if (same) describedBy.push(hint.id);
|
||||
if (describedBy.length) fieldset.setAttribute("aria-describedby", describedBy.join(" "));
|
||||
else fieldset.removeAttribute("aria-describedby");
|
||||
}
|
||||
|
||||
function setControlOptions(field, values) {
|
||||
@@ -1931,6 +2009,7 @@
|
||||
renderRadioChoices(profileControl, [{ value: "", label: "Any" }].concat(profileChoices),
|
||||
firmwareProfileValue(filters), groupPrefix + "-firmwareProfile", "firmwareProfile");
|
||||
profileControl.closest("fieldset").disabled = profileChoices.length === 0;
|
||||
setRefinementHint(profileControl, "firmwareProfile", profileChoices);
|
||||
const hardwareVariants = valuesForField("hardware");
|
||||
const hardwareVariantControl = root.querySelector(
|
||||
'[data-role="hardware-variant-control"]'
|
||||
@@ -1963,9 +2042,20 @@
|
||||
return field !== "chipFamily" && !filters[field];
|
||||
});
|
||||
const matches = matchingProfiles();
|
||||
if (feedback) {
|
||||
const progress = selectionProgress(catalog.profiles, filters);
|
||||
feedback.hidden = false;
|
||||
feedback.dataset.state = progress.state;
|
||||
feedback.querySelector('[data-role="filter-count"]').textContent = progress.title;
|
||||
feedback.querySelector('[data-role="filter-note"]').textContent = progress.note;
|
||||
}
|
||||
if (resultLink) resultLink.hidden = true;
|
||||
|
||||
if (missingFields.length) {
|
||||
if (missingFields.length === CHOICE_FIELDS.length - 1 && !filters.chipFamily) {
|
||||
if (matches.length === 1) {
|
||||
status.textContent = "One firmware build remains. Confirm the hardware and install operation. " +
|
||||
"Choices marked 'Same firmware' do not narrow the result further.";
|
||||
} else if (missingFields.length === CHOICE_FIELDS.length - 1 && !filters.chipFamily) {
|
||||
status.textContent = "Pick options in any order. " +
|
||||
matches.length + " compatible configurations are available.";
|
||||
} else {
|
||||
@@ -1993,6 +2083,7 @@
|
||||
);
|
||||
});
|
||||
result.hidden = false;
|
||||
if (resultLink) resultLink.hidden = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -2024,6 +2115,7 @@
|
||||
);
|
||||
});
|
||||
result.hidden = false;
|
||||
if (resultLink) resultLink.hidden = false;
|
||||
}
|
||||
|
||||
function renderBootloader() {
|
||||
@@ -2265,6 +2357,8 @@
|
||||
firmwareProfileFilters: firmwareProfileFilters,
|
||||
firmwareProfileLabel: firmwareProfileLabel,
|
||||
firmwareProfileChoices: firmwareProfileChoices,
|
||||
selectionProgress: selectionProgress,
|
||||
choicesUseSameFirmware: choicesUseSameFirmware,
|
||||
selectionUrl: selectionUrl,
|
||||
selectionFromUrl: selectionFromUrl,
|
||||
canonicalAsset: canonicalAsset,
|
||||
|
||||
Reference in new issue
Block a user