diff --git a/docs/_data/bootloader_manifest.json b/docs/_data/bootloader_manifest.json index 59c57ce9f..061435082 100644 --- a/docs/_data/bootloader_manifest.json +++ b/docs/_data/bootloader_manifest.json @@ -536,7 +536,9 @@ "id": "wio_tracker_l1", "label": "Seeed Wio Tracker L1", "meshcoreHardware": [ - "WioTrackerL1" + "WioTrackerL1", + "WioTrackerL1-1W", + "WioTrackerL1Eink" ], "deviceName": "WTL1_DFU", "storage": "qspi", @@ -653,7 +655,17 @@ "id": "xiao_nrf52840_ble", "label": "Seeed XIAO nRF52840 BLE", "meshcoreHardware": [ - "Xiao_nrf52" + "Xiao_nrf52", + "ikoka_handheld_nrf_e22_30dbm", + "ikoka_handheld_nrf_e22_30dbm_096", + "ikoka_nano_nrf_22dbm", + "ikoka_nano_nrf_30dbm", + "ikoka_nano_nrf_33dbm", + "ikoka_stick_nrf_22dbm", + "ikoka_stick_nrf_30dbm", + "ikoka_stick_nrf_33dbm", + "solarxiao_30S", + "solarxiao_33S" ], "deviceName": "XIAO_DFU", "storage": "qspi", @@ -707,5 +719,98 @@ } } } + ], + "unavailableProfiles": [ + { + "id": "gat562_mesh_watch13", + "label": "GAT562 Mesh Watch13", + "meshcoreHardware": [ + "GAT562_Mesh_Watch13" + ], + "reason": "The watch routes a GAT562 bootloader LED output to its vibration motor and has onboard flash. It needs a watch-specific profile." + }, + { + "id": "heltec_mesh_solar", + "label": "Heltec Mesh Solar", + "meshcoreHardware": [ + "Heltec_mesh_solar" + ], + "reason": "Mesh Solar has different LED and peripheral pins from the supported Heltec boards. It needs its own profile." + }, + { + "id": "lilygo_t_impulse_plus", + "label": "LilyGo T-Impulse Plus", + "meshcoreHardware": [ + "LilyGo_T_Impulse_Plus" + ], + "reason": "T-Impulse Plus has different flash data pins and LED wiring from T-Echo Lite. It needs its own profile." + }, + { + "id": "lilygo_techo_card", + "label": "LilyGo T-Echo Card", + "meshcoreHardware": [ + "LilyGo_T-Echo_Card" + ], + "reason": "T-Echo Card has its own NFC configuration, buttons and LED hardware. It needs a Card-specific profile." + }, + { + "id": "meshtiny", + "label": "Meshtiny", + "meshcoreHardware": [ + "Meshtiny" + ], + "reason": "Meshtiny has its own factory bootloader identity and controls. A dedicated Meshtiny profile has not been qualified." + }, + { + "id": "meshtracker_x1", + "label": "MeshTracker X1", + "meshcoreHardware": [ + "MeshTracker_X1" + ], + "reason": "MeshTracker X1 has different flash wiring, power control and button polarity from Wio Tracker L1. It needs its own profile." + }, + { + "id": "muzi_base", + "label": "Muzi Base", + "meshcoreHardware": [ + "muzi_base_duo", + "muzi_base_duo_superIO", + "muzi_base_uno", + "muzi_base_uno_superIO" + ], + "reason": "These four variants share Muzi Base hardware, which needs its own bootloader identity, LED polarity and flash configuration." + }, + { + "id": "nano_g2_ultra", + "label": "Nano G2 Ultra", + "meshcoreHardware": [ + "Nano_G2_Ultra" + ], + "reason": "Nano G2 Ultra has its own flash and button wiring. It needs a dedicated profile." + }, + { + "id": "thinknode_m4", + "label": "ThinkNode M4", + "meshcoreHardware": [ + "ThinkNode_M4" + ], + "reason": "ThinkNode M4 has different LED, button and power-control pins from the supported ThinkNodes. It needs its own profile." + }, + { + "id": "thinknode_m8", + "label": "ThinkNode M8", + "meshcoreHardware": [ + "ThinkNode_M8" + ], + "reason": "ThinkNode M8 shares some flash wiring with M1 but has different buttons and peripheral controls. It needs an M8-specific profile." + }, + { + "id": "wio_wm1110", + "label": "Seeed Wio WM1110", + "meshcoreHardware": [ + "wio_wm1110" + ], + "reason": "Wio WM1110 is a separate tracker board with different LED and peripheral pins. Its XIAO build setting does not make it XIAO hardware." + } ] } diff --git a/docs/_javascript/firmware_picker.js b/docs/_javascript/firmware_picker.js index 282cbc880..7de97a624 100644 --- a/docs/_javascript/firmware_picker.js +++ b/docs/_javascript/firmware_picker.js @@ -152,7 +152,21 @@ }); return Object.assign({}, profile, { files: files }); }).filter(function (profile) { return Object.keys(profile.files).length === 3; }); - return { tag: tag, version: tag.split("OTAFIX")[1], releaseUrl: releaseUrl, profiles: profiles }; + const unavailableProfiles = (manifest.unavailableProfiles || []).map(function (profile) { + if (!/^[a-z0-9_]+$/.test(profile.id) || ids.has(profile.id) || + !Array.isArray(profile.meshcoreHardware) || !profile.meshcoreHardware.length || + typeof profile.reason !== "string" || !profile.reason.trim()) { + throw new Error("Invalid unavailable bootloader profile"); + } + ids.add(profile.id); + profile.meshcoreHardware.forEach(function (hardware) { + if (!hardware || aliases.has(hardware)) throw new Error("Ambiguous bootloader hardware mapping"); + aliases.add(hardware); + }); + return profile; + }); + return { tag: tag, version: tag.split("OTAFIX")[1], releaseUrl: releaseUrl, + profiles: profiles, unavailableProfiles: unavailableProfiles }; } function bootloaderForHardware(catalog, hardware, chipFamily) { @@ -163,6 +177,13 @@ return matches.length === 1 ? matches[0] : null; } + function unavailableBootloaderForHardware(catalog, hardware, chipFamily) { + if (!catalog || chipFamily !== "nrf52" || !hardware) return null; + return catalog.unavailableProfiles.find(function (profile) { + return profile.meshcoreHardware.includes(hardware); + }) || null; + } + const HARDWARE_ALIASES = Object.freeze({ // Generated Full Companion artifact name. It uses the standard Heltec V4 // board definition and auto-detects the V4.2 and V4.3 FEM hardware. @@ -1995,7 +2016,10 @@ content.appendChild(actions); content.appendChild(createElement("p", "UF2 updates an existing compatible bootloader. The DFU ZIP includes SoftDevice and bootloader; HEX is for SWD installation or recovery. Application firmware files do not install a bootloader. Follow the release's migration instructions if your installed identity needs a recovery bridge.")); } else if (bootloaderCatalog) { - content.appendChild(createElement("p", "No verified bootloader download matches this exact hardware in the current catalog. Check the release notes for board support.")); + const unavailable = unavailableBootloaderForHardware(bootloaderCatalog, hardware, chip); + content.appendChild(createElement("p", unavailable ? + "No OTAFIX " + bootloaderCatalog.version + " bootloader has been published for this board. " + unavailable.reason : + "No verified bootloader download matches this exact hardware in the current catalog. Check the release notes for board support.")); } if (bootloaderNotice) content.appendChild(createElement("p", bootloaderNotice)); const notes = createElement("a", "Bootloader release notes and migration instructions"); @@ -2159,6 +2183,7 @@ const api = Object.freeze({ buildBootloaderCatalog: buildBootloaderCatalog, bootloaderForHardware: bootloaderForHardware, + unavailableBootloaderForHardware: unavailableBootloaderForHardware, FILTER_FIELDS: FILTER_FIELDS, FACET_FIELDS: FACET_FIELDS, PROFILE_FIELDS: PROFILE_FIELDS, diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 6e0e35d31..ff7c80cc3 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -20,7 +20,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. -
Loading release information...
@@ -398,6 +398,10 @@ includes the bootloader release named in its saved catalog. If a board has no verified mapping or a file is missing, the picker explains that instead of offering another board's bootloader. MeshTower internal and microSD remain separate; RAK3401 and RAK4631 each use their adaptive storage bootloader. +Ikoka Stick, Nano and Handheld plus SolarXiao use the XIAO bootloader; +Wio Tracker L1 1W and E-Ink use the L1 bootloader. See the +[board coverage audit](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/blob/feature/ota-delta-apply/docs/meshcore-hardware-coverage.md) +for the source checks and boards still awaiting a dedicated, tested release. Board mappings and file names, URLs, sizes and SHA-256 hashes come from `bootloader-manifest.json` in the diff --git a/mkdocs.yml b/mkdocs.yml index 1de208049..059419e87 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,7 +26,7 @@ extra_css: extra_javascript: - https://unpkg.com/leaflet@1.9.4/dist/leaflet.js - - _javascript/firmware_picker.js?v=20260929-3 + - _javascript/firmware_picker.js?v=20260929-4 - _javascript/telemetry_decoder.js - _javascript/management_decoder.js - _javascript/filter_tool.js diff --git a/test/fixtures/firmware_picker_nrf52_1_17_1_7.json b/test/fixtures/firmware_picker_nrf52_1_17_1_7.json new file mode 100644 index 000000000..f6b6f2113 --- /dev/null +++ b/test/fixtures/firmware_picker_nrf52_1_17_1_7.json @@ -0,0 +1,58 @@ +{ + "familyTag": "v1.17.1.7-halo-keymind-cascade-dev-2d03e098", + "hardware": [ + "GAT562_30S_Mesh_Kit", + "GAT562_Mesh_EVB_Pro", + "GAT562_Mesh_Tracker_Pro", + "GAT562_Mesh_Watch13", + "Heltec_mesh_solar", + "Heltec_t096", + "Heltec_t1", + "Heltec_t114", + "Heltec_t114_without_display", + "Heltec_tower_v2", + "Heltec_tower_v2_sdcard", + "KeepteenLT1", + "LilyGo_T-Echo", + "LilyGo_T-Echo-Lite", + "LilyGo_T-Echo-Lite_non_shell", + "LilyGo_T-Echo_Card", + "LilyGo_T_Impulse_Plus", + "MeshTracker_X1", + "Mesh_pocket", + "Meshtiny", + "Minewsemi_me25ls01", + "Nano_G2_Ultra", + "ProMicro", + "R1Neo", + "RAK_3401", + "RAK_4631", + "RAK_WisMesh_Tag", + "SenseCap_Solar", + "ThinkNode_M1", + "ThinkNode_M3", + "ThinkNode_M4", + "ThinkNode_M6", + "ThinkNode_M8", + "WioTrackerL1", + "WioTrackerL1-1W", + "WioTrackerL1Eink", + "Xiao_nrf52", + "ikoka_handheld_nrf_e22_30dbm", + "ikoka_handheld_nrf_e22_30dbm_096", + "ikoka_nano_nrf_22dbm", + "ikoka_nano_nrf_30dbm", + "ikoka_nano_nrf_33dbm", + "ikoka_stick_nrf_22dbm", + "ikoka_stick_nrf_30dbm", + "ikoka_stick_nrf_33dbm", + "muzi_base_duo", + "muzi_base_duo_superIO", + "muzi_base_uno", + "muzi_base_uno_superIO", + "solarxiao_30S", + "solarxiao_33S", + "t1000e", + "wio_wm1110" + ] +} diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js index 16a5ae020..e643682cc 100644 --- a/test/test_firmware_picker.js +++ b/test/test_firmware_picker.js @@ -20,6 +20,29 @@ assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "Heltec_tower assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "RAK_3401", "nrf52").storage, "adaptive"); assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "RAK_4631", "nrf52").id, "wiscore_rak4631_auto"); assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, "ThinkNode_M8", "nrf52"), null); +const nrf52Hardware = require("./fixtures/firmware_picker_nrf52_1_17_1_7.json").hardware; +assert.strictEqual(nrf52Hardware.length, 53); +let mappedNrf52 = 0; +for (const hardware of nrf52Hardware) { + const download = picker.bootloaderForHardware(bootloaderCatalog, hardware, "nrf52"); + const unavailable = picker.unavailableBootloaderForHardware(bootloaderCatalog, hardware, "nrf52"); + assert.strictEqual(Number(!!download) + Number(!!unavailable), 1, hardware + " needs an explicit coverage decision"); + if (download) mappedNrf52++; + else { + assert(unavailable.reason.length > 20); + assert.strictEqual(unavailable.files, undefined); + } + assert.strictEqual(picker.unavailableBootloaderForHardware(bootloaderCatalog, hardware, "esp32"), null); +} +assert.strictEqual(mappedNrf52, 39); +for (const hardware of nrf52Hardware.filter(h => /^(ikoka_|solarxiao_)/.test(h))) { + assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, hardware, "nrf52").id, "xiao_nrf52840_ble"); +} +for (const hardware of ["WioTrackerL1-1W", "WioTrackerL1Eink"]) { + assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, hardware, "nrf52").id, "wio_tracker_l1"); +} +assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "GAT562_Mesh_Watch13", "nrf52").reason.includes("vibration motor")); +assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "wio_wm1110", "nrf52").reason.includes("does not make it XIAO")); const nextBootloaderTag = "v0.11.0-OTAFIX2.4.11"; const nextBootloaderRelease = { tag_name: nextBootloaderTag, draft: false, prerelease: false, @@ -44,6 +67,9 @@ assert.strictEqual(picker.bootloaderForHardware(picker.buildBootloaderCatalog(bo const ambiguousMapping = structuredClone(bootloaderManifest); ambiguousMapping.profiles[1].meshcoreHardware.push(ambiguousMapping.profiles[0].meshcoreHardware[0]); assert.throws(() => picker.buildBootloaderCatalog(ambiguousMapping), /Ambiguous bootloader hardware/); +const shadowedMapping = structuredClone(bootloaderManifest); +shadowedMapping.unavailableProfiles[0].meshcoreHardware.push("ikoka_stick_nrf_33dbm"); +assert.throws(() => picker.buildBootloaderCatalog(shadowedMapping), /Ambiguous bootloader hardware/); assert.throws(() => picker.buildBootloaderCatalog(bootloaderManifest, Object.assign({}, nextBootloaderRelease, {prerelease: true})), /stable OTAFIX/); console.log("exact bootloader downloads, storage variants, chip gating, latest version and unsafe catalog rejection passed");