fix(picker): use release JSON for OTA and logging

This commit is contained in:
mikecarper
2026-09-21 20:02:53 -07:00
parent 5f02575933
commit 3ef490c9f8
4 changed files with 75 additions and 4 deletions
+36 -1
View File
@@ -576,6 +576,39 @@
});
}
function applyReleaseRuntimeCapabilities(profile, controls) {
if (["none", "lora-receiver", "lora-source"].includes(controls.otaRole)) {
profile.ota = controls.otaRole;
}
const modes = controls.loggingModes;
const source = controls.loggingSource;
const sourceMatches = !source || (typeof source === "string" &&
/^[0-9a-f]{40}$/i.test(source) && profile.files.length > 0 &&
profile.files.every(function (file) {
return file.name.toLowerCase().replace(/\.(bin|hex|uf2|zip)$/i, "")
.replace(/-merged$/, "")
.endsWith("-" + source.slice(0, 8).toLowerCase());
}));
if (sourceMatches && Array.isArray(modes)) {
const key = modes.join(",");
if (key === "none") {
profile.logging = "none";
profile.loggingModes = ["none"];
} else if (key === "none,usb" && controls.loggingControl === "usb.logging") {
profile.logging = "usb-runtime";
profile.loggingModes = modes.slice();
} else if (key === "none,usb,wifi,both" &&
controls.loggingControl === "logging.output") {
profile.logging = "runtime";
profile.loggingModes = modes.slice();
}
}
if (typeof controls.dedicatedUsbLogging === "boolean") {
profile.dedicatedUsbLogging = controls.dedicatedUsbLogging;
}
}
function hardwareFamilyFor(hardware, hardwareNames) {
const value = String(hardware || "");
const lowerValue = value.toLowerCase();
@@ -723,7 +756,9 @@
if (controlData && controlData.familyTag === releaseSet.familyTag &&
controlData.profiles && controlData.profiles[profile.target]) {
profile.controls = controlData.profiles[profile.target];
if (isFullCompanion(profile) && profile.controls.mqtt) {
applyReleaseRuntimeCapabilities(profile, profile.controls);
if (isFullCompanion(profile) && profile.controls.mqtt &&
!Array.isArray(profile.controls.loggingModes)) {
profile.loggingModes = ["none", "usb", "wifi", "both"];
}
if (profile.controls.rs232 && profile.role === "repeater" && profile.mode === "standard") {
+6 -1
View File
@@ -17,7 +17,7 @@ The picker reads public release metadata from GitHub. It does not upload device
information. Hardware names, target names, and download links come directly
from the published firmware assets.
<div class="firmware-picker" data-firmware-picker data-release-repo="mikecarper/MeshCore" data-controls-url="../_data/firmware_controls.json?v=1.17.1.6" data-share-url="https://mikecarper.github.io/MeshCore/firmware_picker/">
<div class="firmware-picker" data-firmware-picker data-release-repo="mikecarper/MeshCore" data-controls-url="../_data/firmware_controls.json?v=1.17.1.6-runtime-2" data-share-url="https://mikecarper.github.io/MeshCore/firmware_picker/">
<div class="firmware-picker-intro" role="note">
<strong>Current release set</strong>
<p data-role="release-set">Loading release information...</p>
@@ -438,6 +438,11 @@ Hardware-specific controls are enabled only when `_data/firmware_controls.json`
matches the selected release family and exact target. If that metadata is
missing or belongs to another release, the picker retains basic role/logging
directions and links the complete guide without inventing hardware support.
For 1.17.1.6, that JSON also records the verified LoRa OTA role of each image
and USB logging modes where the packaged application was checked. Logging
metadata is tied to the source hash of its firmware file. The normal iKoka
30 dBm repeater has USB logging and LoRa OTA; its compact no-external-sensors
image has LoRa OTA without logging.
Capacity directions come from each qualified profile's recorded reductions and
are tied to the source hash of the selected download. The older 1.17.1.5 USB
sleep workaround stays limited to that release.
+1 -1
View File
@@ -26,7 +26,7 @@ extra_css:
extra_javascript:
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
- _javascript/firmware_picker.js
- _javascript/firmware_picker.js?v=20260921-1
- _javascript/telemetry_decoder.js
- _javascript/management_decoder.js
- _javascript/filter_tool.js
+32 -1
View File
@@ -1012,7 +1012,7 @@ console.log('role-specific runtime directions tests passed');
// above keep the older release's USB and capacity workarounds intact.
const currentControls = require('../docs/_data/firmware_controls.json');
const currentAssets = Object.entries(currentControls.profiles).map(([target, info]) => {
const source = info.memorySource || currentControls.source;
const source = info.loggingSource || info.memorySource || currentControls.source;
const tag = currentControls.familyTag.replace(/-[0-9a-f]{8}$/, '-' + source.slice(0, 8));
return asset(target + '-' + tag + (info.platform === 'NRF52_PLATFORM' ? '.uf2' : '.bin'));
});
@@ -1021,6 +1021,37 @@ const currentCatalog = picker.buildCatalog([
], currentControls);
assert.strictEqual(currentCatalog.rows.length, currentAssets.length);
assert(currentCatalog.profiles.every(profile => profile.controls && profile.chipFamily !== 'unknown'));
const ikokaNormal = currentCatalog.profiles.find(profile =>
profile.target === 'ikoka_stick_nrf_30dbm_repeater');
const ikokaLean = currentCatalog.profiles.find(profile =>
profile.target === 'ikoka_stick_nrf_30dbm_repeater_lora_ota_no_external_sensors');
assert.strictEqual(ikokaNormal.ota, 'lora-receiver');
assert.strictEqual(ikokaNormal.logging, 'usb-runtime');
assert.deepStrictEqual(ikokaNormal.loggingModes, ['none', 'usb']);
assert.deepStrictEqual(picker.runtimeDirections(ikokaNormal, {logging: 'usb'})[0].actions[0].commands,
['set usb.logging on']);
assert.strictEqual(ikokaLean.ota, 'lora-receiver');
assert.strictEqual(ikokaLean.logging, 'none');
assert.deepStrictEqual(ikokaLean.loggingModes, ['none']);
const ikokaUrl = 'https://example.com/firmware_picker/?chipFamily=nrf52&hardwareFamily=ikoka_stick_nrf_30dbm&hardware=ikoka_stick_nrf_30dbm&role=repeater&variant=default&install=zip&chipAuto=1';
const ikokaRelease = release(currentControls.familyTag, '2026-09-13T00:00:00Z',
[ikokaNormal, ikokaLean].map(profile => {
const source = currentControls.profiles[profile.target].loggingSource;
const tag = currentControls.familyTag.replace(/-[0-9a-f]{8}$/, '-' + source.slice(0, 8));
return asset(profile.target + '-ota-' + tag + '.zip');
}));
const ikokaCatalog = picker.buildCatalog([ikokaRelease], currentControls);
const ikokaSelection = picker.selectionFromUrl(ikokaUrl, ikokaCatalog.profiles);
assert.deepStrictEqual(ikokaSelection.unavailable, []);
assert.deepStrictEqual(ikokaCatalog.profiles.filter(profile =>
picker.profileMatchesFacets(profile, ikokaSelection.filters)).map(profile => profile.target),
['ikoka_stick_nrf_30dbm_repeater']);
const mismatchedIkoka = picker.buildCatalog([
release(currentControls.familyTag, '2026-09-13T00:00:00Z', [
asset('ikoka_stick_nrf_30dbm_repeater-ota-' + currentControls.familyTag + '.zip'),
]),
], currentControls).profiles[0];
assert.strictEqual(mismatchedIkoka.logging, 'none', 'Logging metadata must match the published build source');
const capacityProfiles = currentCatalog.profiles.filter(profile => profile.controls.memoryNote);
assert(capacityProfiles.length > 0, 'Regeneration must preserve capacity directions');
for (const profile of capacityProfiles) {