Files
HaloKeymind/test/test_firmware_picker.js

1061 lines
40 KiB
JavaScript

"use strict";
const assert = require("assert");
const picker = require("../docs/_javascript/firmware_picker.js");
const family = "v2.0.0-dev-abcd1234";
function asset(name, size) {
return {
name: name,
browser_download_url:
"https://github.com/mikecarper/MeshCore/releases/download/test/" + name,
size: size || 1000000,
};
}
function release(tag, publishedAt, assets, extra) {
return Object.assign({
name: tag,
tag_name: tag,
html_url: "https://github.com/mikecarper/MeshCore/releases/tag/" + tag,
published_at: publishedAt,
prerelease: true,
draft: false,
assets: assets,
}, extra || {});
}
const releases = [
release(family, "2026-08-23T12:00:06Z", [
asset("Station_G2_companion_radio_full-" + family + "-merged.bin"),
asset("Station_G2_companion_radio_full-" + family + ".bin"),
asset("Station_G2_logging_repeater-" + family + "-merged.bin"),
asset("Station_G2_logging_repeater-" + family + ".bin"),
asset("Station_G3_ESP32_repeater-" + family + "-merged.bin"),
asset("Station_G3_ESP32_repeater-" + family + ".bin"),
asset("Station_G3_ESP32_logging_repeater-" + family + "-merged.bin"),
asset("Station_G3_ESP32_logging_repeater-" + family + ".bin"),
asset("Heltec_t096_companion_radio_ble_ps_femon-" + family + ".uf2"),
asset("Heltec_t096_companion_radio_ble_ps_femon-" + family + ".zip"),
asset("RAK_4631_companion_radio_full-" + family + ".uf2"),
asset("RAK_4631_companion_radio_full-" + family + ".zip"),
asset("RAK_4631_companion_radio_usb-logging-" + family + ".uf2"),
asset("RAK_4631_companion_radio_usb-logging-" + family + ".zip"),
asset(
"heltec_v4_2_v4_3_companion_radio_full_femon-" + family +
"-merged.bin"
),
asset(
"heltec_v4_2_v4_3_companion_radio_full_femon-" + family + ".bin"
),
asset("heltec_v4_companion_radio_usb-" + family + "-merged.bin"),
asset("heltec_v4_companion_radio_usb-" + family + ".bin"),
asset("heltec_v4_companion_radio_ble-" + family + "-merged.bin"),
asset("heltec_v4_companion_radio_ble-" + family + ".bin"),
asset(
"heltec_v4_companion_radio_wifi_femon-" + family + "-merged.bin"
),
asset("heltec_v4_companion_radio_wifi_femon-" + family + ".bin"),
]),
release("repeater-room-" + family, "2026-08-23T12:00:05Z", [
asset("Station_G2_repeater-" + family + "-deadbee-merged.bin"),
asset("Station_G2_repeater-" + family + "-deadbee.bin"),
asset("Station_G2_repeater-" + family + "-merged.bin"),
asset("Station_G2_repeater-" + family + ".bin"),
asset("solarxiao_33S_repeater-ota-" + family + ".uf2"),
asset("solarxiao_33S_repeater-ota-" + family + ".zip"),
asset("wio-e5-repeater_bridge_rs232-" + family + ".hex"),
]),
release("utility-" + family, "2026-08-23T12:00:04Z", [
asset("RAK_4631_sensor-" + family + ".uf2"),
asset("RAK_4631_sensor-" + family + ".zip"),
]),
release("logging-" + family, "2026-08-23T12:00:03Z", [
asset("ProMicro_terminal_chat-logging-" + family + ".uf2"),
asset("ProMicro_terminal_chat-logging-" + family + ".zip"),
]),
release("lora-ota-" + family, "2026-08-23T12:00:02Z", [
asset(
"Station_G2_repeater_lora_ota_no_external_sensors-ota-" +
family + "-merged.bin"
),
asset(
"Station_G2_repeater_lora_ota_no_external_sensors-ota-" +
family + ".bin"
),
asset(
"solarxiao_33S_repeater_lora_ota_no_external_sensors-ota-" +
family + ".uf2"
),
asset(
"solarxiao_33S_repeater_lora_ota_no_external_sensors-ota-" +
family + ".zip"
),
]),
release("full-profiles-" + family, "2026-08-23T12:00:01Z", [
asset(
"Generic_ESPNOW_repeatr-full-logging-ota-" + family + "-merged.bin"
),
asset("Generic_ESPNOW_repeatr-full-logging-ota-" + family + ".bin"),
asset(
"Station_G2_repeater_observer_mqtt-full-usb-wifi-ota-" +
family + "-merged.bin"
),
asset(
"Station_G2_repeater_observer_mqtt-full-usb-wifi-ota-" + family + ".bin"
),
]),
release(
"nrf52-mota-v1.0.0-to-" + family,
"2026-08-23T12:00:07Z",
[asset("Unrelated_repeater-" + family + ".uf2")]
),
release("v1.9.9", "2026-08-20T12:00:00Z", [], {
prerelease: false,
}),
];
const releaseSet = picker.selectReleaseSet(releases);
assert.strictEqual(releaseSet.familyTag, family);
assert.strictEqual(releaseSet.releases.length, 6);
assert(!releaseSet.releases.some(function (item) {
return item.tag_name.startsWith("nrf52-mota-");
}));
const catalog = picker.buildCatalog(releases);
assert.strictEqual(catalog.releaseSet.familyTag, family);
assert.strictEqual(catalog.profiles.length, 12);
assert.strictEqual(catalog.rows.length, 41);
function profile(target) {
const found = catalog.profiles.find(function (item) {
return item.target === target;
});
assert(found, "missing profile " + target);
return found;
}
const companionFull = profile("Station_G2_companion_radio_full");
assert.strictEqual(companionFull.hardware, "Station_G2");
assert.strictEqual(companionFull.role, "companion");
assert.strictEqual(companionFull.mode, "full");
assert.strictEqual(companionFull.logging, "usb-runtime");
assert.deepStrictEqual(companionFull.loggingModes, ["none", "usb"]);
assert.strictEqual(companionFull.dedicatedUsbLogging, false);
assert.strictEqual(companionFull.ota, "lora-source");
assert.strictEqual(companionFull.feature, "full");
assert.strictEqual(companionFull.variant, "default");
const rakFull = profile("RAK_4631_companion_radio_full");
assert.strictEqual(rakFull.logging, "usb-runtime");
assert.deepStrictEqual(rakFull.loggingModes, ["none", "usb"]);
assert.strictEqual(rakFull.dedicatedUsbLogging, true);
assert(!catalog.profiles.some(function (item) {
return item.target === "RAK_4631_companion_radio_usb-logging";
}));
assert(catalog.rows.some(function (item) {
return item.target === "RAK_4631_companion_radio_usb-logging";
}));
const rakSensor = profile("RAK_4631_sensor");
assert.strictEqual(rakSensor.logging, "usb-runtime");
assert.deepStrictEqual(rakSensor.loggingModes, ["none", "usb"]);
const standardBesideFull = picker.applyMergedStandardUsbLoggingCapabilities([
Object.assign(picker.parseTargetProfile("Example_repeater"), {
hardware: "Example",
role: "repeater",
variant: "default",
feature: "standard",
logging: "none",
ota: "none",
}),
Object.assign(picker.parseTargetProfile("Example_repeater-full-logging-ota"), {
hardware: "Example",
role: "repeater",
variant: "default",
feature: "full",
logging: "usb",
ota: "lora-receiver",
}),
]);
assert.strictEqual(standardBesideFull[0].logging, "usb-runtime");
assert.deepStrictEqual(standardBesideFull[0].loggingModes, ["none", "usb"]);
const v4Full = profile("heltec_v4_2_v4_3_companion_radio_full_femon");
assert.strictEqual(v4Full.logging, "usb-runtime");
assert.deepStrictEqual(v4Full.loggingModes, ["none", "usb"]);
assert.strictEqual(v4Full.dedicatedUsbLogging, false);
["usb", "ble", "wifi"].forEach(function (mode) {
assert(!catalog.profiles.some(function (item) {
return item.hardware === "heltec_v4" && item.role === "companion" &&
item.mode === mode;
}));
});
["RAK_3112", "heltec_rc32", "heltec_rc32_without_display"].forEach(
function (hardware) {
const candidates = ["full", "usb", "ble", "wifi"].map(function (mode) {
return Object.assign(
picker.parseTargetProfile(
hardware + "_companion_radio_" + mode
),
{ installKinds: ["bin"] }
);
});
const classified = picker.applyFullCompanionCapabilities(candidates);
assert.strictEqual(classified[0].logging, "usb-runtime");
assert.deepStrictEqual(classified[0].loggingModes, ["none", "usb"]);
assert.strictEqual(classified[0].dedicatedUsbLogging, false);
assert.deepStrictEqual(
picker.omitTransportsReplacedByFull(classified).map(function (item) {
return item.target;
}),
[hardware + "_companion_radio_full"]
);
}
);
const combinedUsbBle = [
picker.parseTargetProfile("Heltec_E290_companion_usb"),
picker.parseTargetProfile("Heltec_E290_companion_ble"),
picker.parseTargetProfile("Heltec_E290_companion_usb_ble"),
];
combinedUsbBle.forEach(function (item) {
item.installKinds = ["bin"];
});
assert.strictEqual(combinedUsbBle[2].mode, "usb-ble");
assert.strictEqual(combinedUsbBle[2].variant, "default");
assert.deepStrictEqual(
picker.omitTransportsReplacedByFull(combinedUsbBle).map(function (item) {
return item.target;
}),
["Heltec_E290_companion_usb_ble"]
);
const fullWithWiredTransports = [
"ThinkNode_M7_companion_radio_full",
"ThinkNode_M7_companion_radio_usb",
"ThinkNode_M7_companion_radio_ble",
"ThinkNode_M7_companion_radio_wifi",
"ThinkNode_M7_companion_radio_serial",
"ThinkNode_M7_companion_radio_ethernet",
"ThinkNode_M7_terminal_chat",
].map(function (target) {
return Object.assign(picker.parseTargetProfile(target), {
installKinds: ["bin"],
});
});
assert.deepStrictEqual(
picker.omitTransportsReplacedByFull(fullWithWiredTransports).map(
function (item) { return item.target; }
),
["ThinkNode_M7_companion_radio_full"]
);
const terminalWithUsb = [
"PicoW_terminal_chat",
"PicoW_companion_radio_usb",
].map(function (target) {
return Object.assign(picker.parseTargetProfile(target), {
installKinds: ["uf2"],
});
});
assert.deepStrictEqual(
picker.omitTransportsReplacedByFull(terminalWithUsb).map(
function (item) { return item.target; }
),
["PicoW_companion_radio_usb"]
);
const companionBle = picker.parseTargetProfile(
"Heltec_t096_companion_radio_ble_ps_femon"
);
assert.strictEqual(companionBle.hardware, "Heltec_t096");
assert.strictEqual(companionBle.mode, "ble");
assert.strictEqual(companionBle.variant, "default");
assert(!catalog.profiles.some(function (item) {
return item.target === "Heltec_t096_companion_radio_ble_ps_femon";
}));
assert(catalog.rows.some(function (item) {
return item.target === "Heltec_t096_companion_radio_ble_ps_femon";
}));
const fullWifiLogging = picker.parseTargetProfile(
"Heltec_v2_companion_radio_wifi-full-logging"
);
assert.strictEqual(fullWifiLogging.mode, "wifi");
assert.strictEqual(fullWifiLogging.logging, "usb");
assert.strictEqual(fullWifiLogging.feature, "full");
assert.strictEqual(fullWifiLogging.variant, "default");
const fullUsbLogging = picker.parseTargetProfile(
"Meshadventurer_sx1262_companion_radio_usb-full-logging"
);
assert.strictEqual(fullUsbLogging.mode, "usb");
assert.strictEqual(fullUsbLogging.variant, "default");
const heltecV4Full = picker.parseTargetProfile(
"heltec_v4_2_v4_3_companion_radio_full_femon"
);
assert.strictEqual(heltecV4Full.target,
"heltec_v4_2_v4_3_companion_radio_full_femon");
assert.strictEqual(heltecV4Full.hardware, "heltec_v4");
assert.strictEqual(heltecV4Full.mode, "full");
assert.strictEqual(heltecV4Full.variant, "default");
assert.strictEqual(
picker.canonicalHardware("heltec_v4_2_v4_3"),
"heltec_v4"
);
assert.strictEqual(
picker.canonicalHardware("heltec_v4_3_tft"),
"heltec_v4_3_tft"
);
assert.strictEqual(
picker.canonicalHardware("heltec_v4_3_expansionkit_tft"),
"heltec_v4_3_expansionkit_tft"
);
const heltecV4FemOff = picker.parseTargetProfile(
"heltec_v4_3_companion_radio_ble_femoff"
);
assert.strictEqual(heltecV4FemOff.hardware, "heltec_v4_3");
assert.strictEqual(heltecV4FemOff.variant, "default");
const heltecV4TftFemOff = picker.parseTargetProfile(
"heltec_v4_3_tft_companion_radio_wifi_femoff"
);
assert.strictEqual(heltecV4TftFemOff.hardware, "heltec_v4_3_tft");
assert.strictEqual(heltecV4TftFemOff.variant, "default");
const directWifiMqtt = picker.parseTargetProfile(
"Heltec_v3_companion_radio_wifi_mqtt"
);
assert.strictEqual(directWifiMqtt.mode, "wifi");
assert.strictEqual(directWifiMqtt.logging, "wifi");
assert.deepStrictEqual(directWifiMqtt.loggingModes, ["wifi"]);
assert.strictEqual(directWifiMqtt.variant, "default");
const consolidatedV3 = [
"Heltec_v3_companion_radio_full",
"Heltec_v3_companion_radio_wifi_mqtt",
].map(function (target) {
return Object.assign(picker.parseTargetProfile(target), {
installKinds: ["bin", "merged-bin"],
});
});
picker.applyFullCompanionCapabilities(consolidatedV3);
assert.deepStrictEqual(
consolidatedV3[0].loggingModes,
["none", "usb"]
);
assert(!picker.profileMatches(consolidatedV3[0], { logging: "wifi" }));
assert.deepStrictEqual(
picker.omitTransportsReplacedByFull(consolidatedV3).map(function (item) {
return item.target;
}),
["Heltec_v3_companion_radio_full"]
);
const consolidatedV4 = [
"heltec_v4_2_v4_3_companion_radio_full_femon",
"heltec_v4_companion_radio_wifi_mqtt_femon",
"heltec_v4_3_companion_radio_wifi_mqtt_femoff",
].map(function (target) {
return Object.assign(picker.parseTargetProfile(target), {
installKinds: ["bin", "merged-bin"],
});
});
picker.applyFullCompanionCapabilities(consolidatedV4);
assert.deepStrictEqual(
picker.omitTransportsReplacedByFull(consolidatedV4).map(function (item) {
return item.target;
}),
["heltec_v4_2_v4_3_companion_radio_full_femon"]
);
const consolidatedSenseCap = [
"SenseCapIndicator-LoRa_companion_radio_full",
"SenseCapIndicator-LoRa_comp_radio_usb_wifi",
].map(function (target) {
return Object.assign(picker.parseTargetProfile(target), {
installKinds: ["bin", "merged-bin"],
});
});
assert.strictEqual(consolidatedSenseCap[1].variant, "default");
picker.applyFullCompanionCapabilities(consolidatedSenseCap);
assert.deepStrictEqual(
picker.omitTransportsReplacedByFull(consolidatedSenseCap).map(
function (item) { return item.target; }
),
["SenseCapIndicator-LoRa_companion_radio_full"]
);
const nrfUf2OnlyFull = Object.assign(
picker.parseTargetProfile("RAK_4631_companion_radio_full"),
{ installKinds: ["uf2"] }
);
picker.applyFullCompanionCapabilities([nrfUf2OnlyFull]);
assert.strictEqual(nrfUf2OnlyFull.dedicatedUsbLogging, true);
assert.strictEqual(nrfUf2OnlyFull.logging, "usb-runtime");
const g2RxBoosted = picker.parseTargetProfile(
"Station_G2_logging_repeater"
);
assert.strictEqual(g2RxBoosted.sourceHardware, "Station_G2_logging");
assert.strictEqual(g2RxBoosted.hardware, "Station_G2");
assert.strictEqual(g2RxBoosted.variant, "rx-boosted");
assert.strictEqual(
picker.humanizeVariant(g2RxBoosted.variant),
"RX Boosted"
);
assert(!catalog.profiles.some(function (item) {
return item.target === "Station_G2_logging_repeater";
}));
assert(catalog.rows.some(function (item) {
return item.target === "Station_G2_logging_repeater";
}));
const g3Standard = profile("Station_G3_ESP32_repeater");
assert.strictEqual(g3Standard.hardware, "Station_G3_ESP32");
assert(!catalog.profiles.some(function (item) {
return item.target === "Station_G3_ESP32_logging_repeater";
}));
assert(catalog.rows.some(function (item) {
return item.target === "Station_G3_ESP32_logging_repeater";
}));
assert.deepStrictEqual(
picker.omitNrf52TransportsReplacedByFull([
{
target: "RAK_4631_companion_radio_full",
hardware: "RAK_4631",
variant: "default",
role: "companion",
mode: "full",
logging: "none",
installKinds: ["zip", "uf2"],
},
{
target: "RAK_4631_companion_radio_usb",
hardware: "RAK_4631",
variant: "default",
role: "companion",
mode: "usb",
logging: "none",
installKinds: ["zip", "uf2"],
},
{
target: "RAK_4631_companion_radio_ble",
hardware: "RAK_4631",
variant: "default",
role: "companion",
mode: "ble",
logging: "none",
installKinds: ["zip", "uf2"],
},
{
target: "RAK_4631_companion_radio_usb-logging",
hardware: "RAK_4631",
variant: "default",
role: "companion",
mode: "usb",
logging: "usb",
installKinds: ["zip", "uf2"],
},
]).map(function (item) { return item.target; }),
["RAK_4631_companion_radio_full"]
);
const standardRepeater = profile("Station_G2_repeater");
assert.strictEqual(standardRepeater.hardwareFamily, "Station_G2");
assert.strictEqual(standardRepeater.role, "repeater");
assert.strictEqual(standardRepeater.logging, "usb-runtime");
assert.deepStrictEqual(standardRepeater.loggingModes, ["none", "usb"]);
assert.strictEqual(standardRepeater.ota, "none");
assert.strictEqual(standardRepeater.mode, "standard");
assert.strictEqual(standardRepeater.feature, "standard");
const mergedLoggingProfiles = picker.applyMergedStandardUsbLoggingCapabilities([
Object.assign(picker.parseTargetProfile("PicoW_room_server"), {
ota: "none",
}),
Object.assign(picker.parseTargetProfile("PicoW_kiss_modem"), {
ota: "none",
}),
]);
assert.strictEqual(mergedLoggingProfiles[0].logging, "usb-runtime");
assert.deepStrictEqual(mergedLoggingProfiles[0].loggingModes, ["none", "usb"]);
assert.strictEqual(mergedLoggingProfiles[1].logging, "none");
assert.strictEqual(
picker.canonicalAsset(standardRepeater.files, "merged-bin").name,
"Station_G2_repeater-" + family + "-merged.bin"
);
assert.strictEqual(picker.CANDIDATE_RESULT_LIMIT, 5);
assert(picker.shouldShowCandidateResults(new Array(5).fill(standardRepeater)));
assert(!picker.shouldShowCandidateResults(new Array(6).fill(standardRepeater)));
assert.deepStrictEqual(
picker.resolveProfileAssets([standardRepeater], "merged-bin").map(
function (entry) {
return [entry.profile.target, entry.asset.name, entry.installKind];
}
),
[[
"Station_G2_repeater",
"Station_G2_repeater-" + family + "-merged.bin",
"merged-bin",
]]
);
assert.deepStrictEqual(
picker.resolveProfileAssets([standardRepeater], "").map(function (entry) {
return entry.installKind;
}),
["merged-bin", "bin"]
);
const fullLogging = profile("Generic_ESPNOW_repeatr-full-logging");
assert.strictEqual(fullLogging.logging, "usb-runtime");
assert.deepStrictEqual(fullLogging.loggingModes, ["none", "usb"]);
assert.strictEqual(fullLogging.ota, "lora-receiver");
assert.strictEqual(fullLogging.feature, "full");
assert.strictEqual(fullLogging.variant, "default");
const mqtt = profile("Station_G2_repeater_observer_mqtt-full-usb-wifi");
assert.strictEqual(mqtt.logging, "runtime");
assert.deepStrictEqual(mqtt.loggingModes, ["none", "usb", "wifi", "both"]);
assert.strictEqual(mqtt.mode, "standard");
assert.strictEqual(mqtt.ota, "lora-receiver");
assert.strictEqual(mqtt.variant, "default");
assert.strictEqual(
picker.parseTargetProfile("Station_G2_repeater_observer_mqtt").mode,
"standard"
);
assert.strictEqual(
picker.MODE_LABELS.standard,
"Standard / no separate bridge"
);
assert(!Object.prototype.hasOwnProperty.call(picker.MODE_LABELS, "mqtt"));
assert(!picker.uniqueValues(catalog.profiles, "mode").includes("mqtt"));
assert(picker.profileMatchesFacets(mqtt, {
role: "repeater",
logging: "wifi",
ota: "lora-receiver",
mode: "standard",
feature: "full",
}));
const fullCompanionSteps = picker.installSteps(v4Full, "merged-bin");
assert(fullCompanionSteps.some(function (step) {
return step.includes("usb.logging");
}));
assert(!fullCompanionSteps.some(function (step) {
return step.includes("logging.output");
}));
const observerSteps = picker.installSteps(mqtt, "merged-bin");
assert(observerSteps.some(function (step) {
return step.includes("logging.output");
}));
const lora = profile(
"Station_G2_repeater_lora_ota_no_external_sensors"
);
assert.strictEqual(lora.ota, "lora-receiver");
assert.strictEqual(lora.variant, "no-external-sensors");
assert.strictEqual(
picker.OTA_LABELS["lora-receiver"],
"LoRa OTA repeater"
);
assert(!Object.prototype.hasOwnProperty.call(
picker.OTA_LABELS,
"ota-enabled"
));
assert(!picker.uniqueValues(catalog.profiles, "ota").includes("ota-enabled"));
const solarXiao = profile("solarxiao_33S_repeater");
assert.strictEqual(solarXiao.ota, "lora-receiver");
assert.strictEqual(solarXiao.variant, "default");
assert(!catalog.profiles.some(function (item) {
return item.target ===
"solarxiao_33S_repeater_lora_ota_no_external_sensors";
}));
assert(catalog.rows.some(function (item) {
return item.target ===
"solarxiao_33S_repeater_lora_ota_no_external_sensors";
}));
assert.deepStrictEqual(
picker.facetValues(
catalog.profiles,
{
hardware: "solarxiao_33S",
role: "repeater",
logging: "none",
},
"ota"
),
["lora-receiver"]
);
const wio = profile("wio-e5-repeater_bridge_rs232");
assert.strictEqual(wio.hardware, "wio-e5");
assert.strictEqual(wio.role, "repeater");
assert.strictEqual(wio.mode, "rs232");
const mergedRs232 = [
"RAK_4631_repeater",
"RAK_4631_repeater_bridge_rs232_serial1",
"RAK_4631_repeater_bridge_rs232_serial2",
].map(function (target) {
return Object.assign(picker.parseTargetProfile(target), {
installKinds: ["uf2"],
});
});
const mergedRs232Visible = picker.applyMergedRak4631RepeaterCapabilities(
picker.omitTransportsReplacedByFull(mergedRs232)
);
assert.deepStrictEqual(
mergedRs232Visible.map(function (item) {
return item.target;
}),
["RAK_4631_repeater"]
);
assert.deepStrictEqual(
picker.profileFieldValues(mergedRs232Visible[0], "mode"),
["standard", "rs232"]
);
assert(picker.profileMatches(
mergedRs232Visible[0],
{ mode: "standard" },
["mode"]
));
assert(picker.profileMatches(
mergedRs232Visible[0],
{ mode: "rs232" },
["mode"]
));
// Exact legacy target rows must stay attached to their own compatibility
// downloads. They disappear only from recommendations when the canonical
// merged image is present; advertising RS232 on that image must not rewrite
// or coalesce the legacy files into the canonical profile.
const rakCompatibilityCatalog = picker.buildCatalog([
release(family, "2026-08-23T13:00:02Z", []),
release("repeater-room-" + family, "2026-08-23T13:00:01Z", [
asset("RAK_4631_repeater-" + family + ".uf2"),
asset(
"RAK_4631_repeater_bridge_rs232_serial1-" + family + ".uf2"
),
asset(
"RAK_4631_repeater_bridge_rs232_serial2-" + family + ".uf2"
),
]),
release("lora-ota-" + family, "2026-08-23T13:00:00Z", [
asset(
"RAK_4631_repeater_lora_ota_no_external_sensors-ota-" +
family + ".zip"
),
asset(
"RAK_4631_repeater_bridge_rs232_serial1_" +
"lora_ota_no_external_sensors-ota-" + family + ".zip"
),
asset(
"RAK_4631_repeater_bridge_rs232_serial2_" +
"lora_ota_no_external_sensors-ota-" + family + ".zip"
),
]),
]);
const expectedCompatibilityTargets = [
"RAK_4631_repeater_bridge_rs232_serial1",
"RAK_4631_repeater_bridge_rs232_serial2",
"RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors",
"RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors",
];
expectedCompatibilityTargets.forEach(function (target) {
assert(rakCompatibilityCatalog.rows.some(function (row) {
return row.target === target;
}), "missing exact compatibility row " + target);
assert(!rakCompatibilityCatalog.profiles.some(function (item) {
return item.target === target;
}), "legacy compatibility target was recommended " + target);
});
[
"RAK_4631_repeater",
"RAK_4631_repeater_lora_ota_no_external_sensors",
].forEach(function (target) {
const canonical = rakCompatibilityCatalog.profiles.find(function (item) {
return item.target === target;
});
assert(canonical, "missing canonical RAK4631 profile " + target);
assert.deepStrictEqual(
picker.profileFieldValues(canonical, "mode"),
["standard", "rs232"]
);
assert(canonical.files.every(function (file) {
return file.target === target;
}), "legacy compatibility file was mapped to " + target);
});
const constrainedRs232 = [
"wio-e5_repeater",
"wio-e5-repeater_bridge_rs232",
].map(function (target) {
return Object.assign(picker.parseTargetProfile(target), {
installKinds: ["hex"],
});
});
assert.strictEqual(
picker.omitTransportsReplacedByFull(constrainedRs232).length,
2
);
const matches = catalog.profiles.filter(function (item) {
return picker.profileMatches(item, {
hardware: "Station_G2",
role: "repeater",
logging: "usb",
});
});
assert.deepStrictEqual(
matches.map(function (item) { return item.target; }).sort(),
[
"Station_G2_repeater",
"Station_G2_repeater_observer_mqtt-full-usb-wifi",
]
);
assert.deepStrictEqual(
picker.FACET_FIELDS,
[
"chipFamily",
"hardwareFamily",
"hardware",
"role",
"logging",
"ota",
"mode",
"feature",
"variant",
"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 (V4.2 / V4.3 auto-detect)"
);
assert.strictEqual(
picker.humanizeHardwareVariant("heltec_v4_3_tft", "heltec_v4"),
"V4.3 + TFT"
);
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"],
"Full install / layout migration (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, { logging: "both" }, "hardware"),
["Station_G2"]
);
assert.deepStrictEqual(
picker.facetValues(
catalog.profiles,
{
install: "merged-bin",
feature: "full",
logging: "usb",
role: "repeater",
},
"hardware"
),
["Generic_ESPNOW", "Station_G2"]
);
assert.deepStrictEqual(
picker.uniqueValues(catalog.profiles, "hardware").sort(),
[
"Generic_ESPNOW",
"ProMicro",
"RAK_4631",
"Station_G2",
"Station_G3_ESP32",
"heltec_v4",
"solarxiao_33S",
"wio-e5",
].sort()
);
assert.strictEqual(picker.humanizeHardware("RAK_4631"), "RAK 4631");
assert.strictEqual(
picker.humanizeVariant("rak13302-w25q16-lora-ota"),
"RAK13302 + External storage board (W25Q16) LoRa OTA"
);
assert.strictEqual(picker.formatBytes(2097152), "2.00 MiB");
assert.strictEqual(
picker.parseFirmwareAsset(
{ name: "wrong-version.bin", url: "", size: 1 },
family
),
null
);
console.log("generalized firmware picker tests passed");
// Shared features must produce the same commands for every role.
// Keep the old release's workarounds and replacement-image checks independent
// of whichever release the live site currently serves.
const controls = require('./fixtures/firmware_picker_1_17_1_5_controls.json');
const liveFamily = controls.familyTag;
const controlledReleases = [release(liveFamily, '2026-09-01T00:00:00Z', [
asset('heltec_v4_2_v4_3_companion_radio_full_femon-' + liveFamily + '.bin'),
asset('RAK_4631_companion_radio_full-' + liveFamily + '.uf2'),
asset('Heltec_v3_companion_radio_full-' + liveFamily + '.bin'),
asset('SenseCapIndicator-LoRa_companion_radio_full-' + liveFamily + '.bin'),
]), release('full-profiles-' + liveFamily, '2026-09-01T00:00:00Z', [
asset('heltec_v4_repeater_observer_mqtt-full-usb-wifi-ota-' + liveFamily + '.bin'),
]), release('repeater-room-' + liveFamily, '2026-09-01T00:00:00Z', [
asset('RAK_4631_repeater-' + liveFamily + '.uf2'),
asset('Station_G3_ESP32_repeater-' + liveFamily + '.bin'),
])];
const controlled = picker.buildCatalog(controlledReleases, controls);
const findControlled = name => controlled.profiles.find(p => p.target === name);
const commands = sections => sections.flatMap(s => s.actions.flatMap(a => a.commands || []));
const observer = findControlled('heltec_v4_repeater_observer_mqtt-full-usb-wifi');
for (const mode of ['none', 'usb', 'wifi', 'both']) {
const directions = picker.runtimeDirections(observer, {logging: mode});
assert.deepStrictEqual(directions[0].actions[0].commands,
(mode === 'usb' || mode === 'both' ? ['set powersaving off'] : []).concat(
['set logging.output ' + (mode === 'none' ? 'off' : mode), 'get logging.output']));
}
const mqttCompanion = findControlled('heltec_v4_2_v4_3_companion_radio_full_femon');
assert.deepStrictEqual(mqttCompanion.loggingModes, ['none', 'usb', 'wifi', 'both']);
for (const mode of ['usb', 'both']) {
assert.deepStrictEqual(picker.runtimeDirections(mqttCompanion, {logging: mode})[0].actions[0].commands,
['set powersaving off', 'set logging.output ' + mode, 'get logging.output']);
}
assert(picker.installSteps(mqttCompanion, 'bin').some(step => step.includes('set powersaving off')));
// The workaround belongs to the affected release, not every future ESP32 build.
for (const releaseFamily of ['v1.17.1.4-old', 'v1.17.1.5-halo-keymind-cascade-dev-newfix',
'v1.17.1.6-next', 'v1.17.1.50-next', '']) {
const otherRelease = {...mqttCompanion, releaseFamily};
assert.deepStrictEqual(picker.runtimeDirections(otherRelease, {logging: 'usb'})[0].actions[0].commands,
['set logging.output usb', 'get logging.output']);
assert(!picker.installSteps(otherRelease, 'bin').some(step => step.includes('set powersaving off')));
}
const companionWifi = picker.runtimeDirections(mqttCompanion, {logging: 'wifi'});
assert.deepStrictEqual(companionWifi[0].actions[0].commands, ['set logging.output wifi', 'get logging.output']);
assert(companionWifi[0].actions[0].text.includes('enable the desired MQTT'));
assert(commands(companionWifi).includes('set mqtt.enabled on'));
assert(commands(companionWifi).includes('get wifi.cli'));
assert(commands(companionWifi).includes('set usb.logging on') === false);
assert(commands(companionWifi).includes('set gps on'));
const companionOff = picker.runtimeDirections(mqttCompanion, {logging: 'none'});
assert(companionOff[0].actions[0].text.includes('saved broker settings'));
for (const title of ['Device power saving', 'GPS', 'MQTT broker connections', 'WebConfig command terminal']) {
assert.deepStrictEqual(companionWifi.find(s => s.title === title).actions,
picker.runtimeDirections(observer, {}).find(s => s.title === title).actions);
}
const nrf = findControlled('RAK_4631_companion_radio_full');
assert.deepStrictEqual(picker.runtimeDirections(nrf, {logging: 'usb'})[0].actions[0].commands,
['set usb.logging on reboot']);
const rakRepeater = findControlled('RAK_4631_repeater');
const g3Repeater = findControlled('Station_G3_ESP32_repeater');
assert.deepStrictEqual(picker.runtimeDirections(g3Repeater, {logging: 'usb'})[0].actions[0].commands,
['set powersaving off', 'set usb.logging on']);
assert.deepStrictEqual(picker.runtimeDirections(g3Repeater, {logging: 'none'})[0].actions[0].commands,
['set usb.logging off']);
const rakCommands = picker.runtimeDirections(rakRepeater, {logging: 'usb', mode: 'standard'});
assert.deepStrictEqual(rakCommands[0].actions[0].commands, ['set usb.logging on']);
assert.strictEqual(rakCommands.find(s => s.title.startsWith('RS232')).actions[0].label, 'Off');
assert(commands(rakCommands).includes('set gps on'));
assert(!rakCommands.some(s => s.title === 'MQTT broker connections'));
const indicator = picker.runtimeDirections(findControlled('SenseCapIndicator-LoRa_companion_radio_full'), {});
assert(commands(indicator).includes('set companion.transport ble'));
assert(!indicator.some(s => s.title.startsWith('MQTT')));
// Never apply old hardware capabilities to another release, or to an unknown
// exact target. Fall back to role documentation rather than invented switches.
// Capacity directions belong to the corrected image, even when the release
// tag is shared with older assets or a user opens an older offline picker.
const memoryControl = controls.profiles.Heltec_v3_companion_radio_full;
assert(memoryControl.memoryNote.includes('150 contacts'));
const replacementFilename = 'Heltec_v3_companion_radio_full-v1.17.1.5-halo-keymind-cascade-dev-aa20e927.bin';
const replacementCatalog = picker.buildCatalog([release(liveFamily, '2026-09-01T00:00:00Z', [asset(replacementFilename)])], controls);
assert.strictEqual(replacementCatalog.profiles.length, 1, 'A replacement source hash must remain visible on the original release page');
const correctedMemoryProfile = replacementCatalog.profiles[0];
assert.strictEqual(picker.parseFirmwareAsset(asset(replacementFilename.replace('1.17.1.5-', '1.17.1.50-')), liveFamily), null);
const memorySteps = picker.installSteps(correctedMemoryProfile, 'bin');
assert(memorySteps.includes(memoryControl.memoryNote));
assert(memorySteps.indexOf(memoryControl.memoryNote) < memorySteps.findIndex(step => step.startsWith('Use this app-only image')));
assert(!picker.installSteps({...correctedMemoryProfile,
files: [{name: 'Heltec_v3_companion_radio_full-v1.17.1.5-halo-keymind-cascade-dev-26303793.bin'}]}, 'bin').includes(memoryControl.memoryNote));
// During publication, both source revisions can briefly coexist. Directions
// must describe the selected download, even if another image is corrected.
const mixedMemoryCatalog = picker.buildCatalog([release(liveFamily, '2026-09-01T00:00:00Z', [
asset(replacementFilename),
asset(replacementFilename.replace('aa20e927', '26303793')),
asset(replacementFilename.replace('.bin', '-merged.bin')),
])], controls);
const mixedMemoryProfile = mixedMemoryCatalog.profiles[0];
assert(!picker.installSteps(mixedMemoryProfile, 'bin').includes(memoryControl.memoryNote));
assert(picker.installSteps(mixedMemoryProfile, 'merged-bin').includes(memoryControl.memoryNote));
const paperControl = controls.profiles.Heltec_Wireless_Paper_companion_radio_full;
assert(paperControl.memoryNote.includes('350 contacts and 40 channels'));
assert(paperControl.memoryNote.includes('128 while mOTA'));
const paperFilename = 'Heltec_Wireless_Paper_companion_radio_full-' +
liveFamily.replace('26303793', paperControl.memorySource.slice(0, 8)) + '.bin';
const paperCatalog = picker.buildCatalog([
release(liveFamily, '2026-09-08T00:00:00Z', [asset(paperFilename)])
], controls);
assert(picker.installSteps(paperCatalog.profiles[0], 'bin').includes(paperControl.memoryNote));
const oldPaperCatalog = picker.buildCatalog([
release(liveFamily, '2026-09-08T00:00:00Z', [asset(paperFilename.replace('1e4d1e16', 'aa20e927'))])
], controls);
assert(!picker.installSteps(oldPaperCatalog.profiles[0], 'bin').includes(paperControl.memoryNote));
const stale = picker.buildCatalog(controlledReleases, {...controls, familyTag: 'v0.0.0'});
assert(stale.profiles.every(p => !p.controls));
assert(!picker.runtimeDirections({...mqttCompanion, controls: undefined}, {}).some(s => s.title === 'GPS'));
console.log('role-specific runtime directions tests passed');
// Exercise every profile from the current generated catalog while the tests
// 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 tag = currentControls.familyTag.replace(/-[0-9a-f]{8}$/, '-' + source.slice(0, 8));
return asset(target + '-' + tag + (info.platform === 'NRF52_PLATFORM' ? '.uf2' : '.bin'));
});
const currentCatalog = picker.buildCatalog([
release(currentControls.familyTag, '2026-09-13T00:00:00Z', currentAssets),
], currentControls);
assert.strictEqual(currentCatalog.rows.length, currentAssets.length);
assert(currentCatalog.profiles.every(profile => profile.controls && profile.chipFamily !== 'unknown'));
const capacityProfiles = currentCatalog.profiles.filter(profile => profile.controls.memoryNote);
assert(capacityProfiles.length > 0, 'Regeneration must preserve capacity directions');
for (const profile of capacityProfiles) {
assert(picker.installSteps(profile, profile.installKinds[0]).includes(profile.controls.memoryNote), profile.target);
}
const currentObserver = currentCatalog.profiles.find(profile => profile.target === observer.target);
assert.deepStrictEqual(picker.runtimeDirections(currentObserver, {logging: 'usb'})[0].actions[0].commands,
['set logging.output usb', 'get logging.output']);
console.log('current release metadata and capacity directions tests passed');
assert.strictEqual(nrf.chipFamily, 'nrf52');
assert.strictEqual(mqttCompanion.chipFamily, 'esp32');
assert.deepStrictEqual(picker.facetValues(controlled.profiles, {chipFamily: 'nrf52'}, 'hardwareFamily'), ['RAK_4631']);
assert(stale.profiles.every(p => p.chipFamily === 'unknown'));
const chipReleases = [release(family, '2026-09-01T00:00:00Z', [
asset('pico_repeater-' + family + '.uf2'),
asset('pico_room_server-' + family + '.uf2'),
asset('wio-e5-repeater-' + family + '.hex'),
asset('unlisted_repeater-' + family + '.uf2'),
])];
const chipCatalog = picker.buildCatalog(chipReleases, {familyTag: family, profiles: {
pico_repeater: {platform: 'RP2040_PLATFORM'},
'wio-e5-repeater': {platform: 'STM32_PLATFORM'},
}});
assert.deepStrictEqual(picker.uniqueValues(chipCatalog.profiles, 'chipFamily').sort(), ['rp2040', 'stm32', 'unknown']);
const sibling = chipCatalog.profiles.find(p => p.target === 'pico_room_server');
assert.strictEqual(sibling.chipFamily, 'rp2040');
assert.strictEqual(sibling.controls, undefined, 'Chip inheritance must not invent runtime controls');
assert.strictEqual(chipCatalog.profiles.find(p => p.target === 'unlisted_repeater').chipFamily, 'unknown', 'UF2 does not identify a chip family');
console.log('chip-family filtering tests passed');
const shareBase = 'https://example.com/firmware_picker/?h=storage#installation-methods';
const shareFilters = {hardwareFamily: 'RAK_4631', hardware: 'RAK_4631',
chipFamily: 'nrf52', role: 'companion', mode: 'full', feature: 'full',
logging: 'usb', ota: 'lora-source', variant: 'default', install: 'uf2'};
const shareUrl = picker.selectionUrl(shareBase, shareFilters, true);
const restoredShare = picker.selectionFromUrl(shareUrl, controlled.profiles);
assert.deepStrictEqual(restoredShare.unavailable, []);
assert.strictEqual(restoredShare.automaticChipFamily, true);
for (const field of picker.FACET_FIELDS.filter(f => f !== 'chipFamily')) {
assert.strictEqual(restoredShare.filters[field], shareFilters[field], field);
}
assert.strictEqual(new URL(shareUrl).searchParams.get('h'), 'storage');
assert.strictEqual(new URL(shareUrl).hash, '#installation-methods');
assert.strictEqual(picker.selectionUrl(shareUrl, {}, false), shareBase);
const anyChip = picker.selectionFromUrl(picker.selectionUrl(shareBase,
{...shareFilters, chipFamily: ''}, false), controlled.profiles);
assert.strictEqual(anyChip.automaticChipFamily, false);
assert(!anyChip.filters.chipFamily);
const partialShare = picker.selectionFromUrl(shareBase + '&unused=value', controlled.profiles);
assert.deepStrictEqual(partialShare.filters, {});
const hardwareOnly = picker.selectionFromUrl('https://example.com/?hardware=RAK_4631', controlled.profiles);
assert.strictEqual(hardwareOnly.filters.hardwareFamily, 'RAK_4631');
assert.strictEqual(hardwareOnly.automaticChipFamily, true);
const staleShare = picker.selectionFromUrl('https://example.com/?hardware=RAK_4631&role=companion&variant=removed&install=bin', controlled.profiles);
assert.strictEqual(staleShare.filters.hardware, 'RAK_4631');
assert.strictEqual(staleShare.filters.role, 'companion');
assert.deepStrictEqual(staleShare.unavailable, ['variant=removed', 'install=bin']);
const escapedShare = picker.selectionFromUrl('https://example.com/?role=%3Cscript%3E', controlled.profiles);
assert.deepStrictEqual(escapedShare.filters, {});
assert.deepStrictEqual(escapedShare.unavailable, ['role=<script>']);
console.log('shareable picker URL tests passed');