Put firmware selections first and show contextual board notes

This commit is contained in:
mikecarper committed 2026-10-13 22:05:00 -07:00
1 parent 3d539b0a8d
commit ff78f8df7e
6 files changed
+324 -105

No files matched your search

+75
View File
@@ -1502,6 +1502,68 @@
: { url: fallback, label: "Check partition-expansion availability" };
}
function boardNotes(profile) {
if (!profile || typeof profile.target !== "string") return [];
const identity = parseTargetProfile(profile.target);
// Match the exact result, not its broad hardware-family filter. A TFT,
// legacy UART or different role must not inherit another image's claims.
if (identity.hardware !== profile.hardware || identity.role !== profile.role) return [];
const target = profile.target;
const info = profile.controls || {};
const notes = [];
const rak = /^RAK_(3401|4631)_repeater_unified_lora_ota(?:-(?:full|reduced)(?:-ota)?)?$/i.exec(target);
const lora = info.updateRequirements && info.updateRequirements.lora;
if (rak && info.platform === "NRF52_PLATFORM" && profile.ota === "lora-receiver" &&
["full", "reduced"].includes(profile.sensorProfile) && lora &&
lora.storage === "adaptive_internal_or_external_qspi") {
notes.push("This unified repeater chooses supported internal or external OTA storage automatically. Full and Reduced select optional sensor drivers, not a storage module. Exact external-flash wiring and the matching OTAFIX bootloader are still required.");
notes.push("Internal flash accepts in-place deltas only; supported external QSPI storage accepts full images and in-place deltas. The update package must match the installed target and storage layout.");
if (rak[1] === "4631" && info.rs232 === true && info.gps === true) {
notes.push("Use UART2 for new RS-232 installations. UART1 remains reserved for potentially powered GPS hardware, and header-connected W25 flash also occupies its pins. Legacy GPS-free Serial1 images remain exact-filename compatibility downloads for existing wiring; selecting the unified image does not migrate wiring or OTA identity.");
notes.push("RAK4631 Ethernet requires its separate exact hardware image; it is not a storage choice in this unified repeater.");
}
}
if (/^RAK_4631_repeater_bridge_rs232_serial1(?:_lora_ota_no_external_sensors)?(?:-(?:full|reduced))?$/i.test(target) &&
info.platform === "NRF52_PLATFORM" && info.rs232 === true && info.gps === false) {
notes.push("This legacy GPS-free Serial1 bridge uses UART1. Do not connect powered UART GPS or header-connected W25 flash to the same pins. Prefer the unified UART2 image for new wiring, but changing images does not migrate an existing installation's wiring or OTA identity.");
}
if (/^RAK_4631_repeater_ethernet(?:-(?:full|reduced)(?:-ota)?)?$/i.test(target) &&
info.platform === "NRF52_PLATFORM" && profile.mode === "ethernet") {
notes.push("This is the separate RAK4631 Ethernet hardware image, not the unified internal/external-storage repeater. Use its exact Ethernet hardware and update target.");
}
if (/^LilyGo_TLora_V2_1_1_6_repeater(?:_observer_mqtt_?)?-full-(?:usb-wifi|logging)$/i.test(target) &&
info.platform === "ESP32_PLATFORM" && profile.feature === "full" && info.espnowBridge === true) {
const observer = /_observer_mqtt/i.test(target);
if (!observer && info.rs232 === true && info.mqtt === false) {
notes.push("T-LoRa V2.1 retains two RAM-safe Full images: UART plus ESP-NOW, or MQTT plus ESP-NOW. This image is the UART/RS-232 choice; it does not include direct Wi-Fi MQTT. Select the separate MQTT observer image if that is needed.");
} else if (observer && info.mqtt === true && info.rs232 === false) {
notes.push("T-LoRa V2.1 retains two RAM-safe Full images: UART plus ESP-NOW, or MQTT plus ESP-NOW. This image is the MQTT observer choice; it does not include the UART/RS-232 bridge. Select the separate UART image if that is needed.");
}
}
if (/^(?:Heltec_v3|Heltec_WSL3|RAK_3112)_repeater(?:_observer_mqtt_?)?-full-usb-wifi$/i.test(target) &&
info.platform === "ESP32_PLATFORM" && profile.feature === "full" &&
info.rs232 === true && info.mqtt === true && info.espnowBridge === true) {
notes.push("This Full MQTT repeater also retains independent RS-232 and ESP-NOW bridges. Use the separate restore-settings controls for each transport; the UART is not MQTT's historical bridge.enabled alias.");
}
const v4 = /^heltec_v4(?:_3)?(?:_expansionkit)?(?:_r8)?(?:_tft)?$/i.test(profile.hardware);
if (v4 && info.platform === "ESP32_PLATFORM" && info.primaryEspnow === false) {
if (isFullCompanion(profile) && /^heltec_v4(?:_[a-z0-9]+)*_companion_radio_full(?:_fem(?:on|off))?$/i.test(target)) {
notes.push("This Full Companion is LoRa-primary, not the LoRa/ESP-NOW repeater gateway role. For a gateway, choose this exact board's Full repeater when its ESP-NOW bridge is offered.");
} else if (profile.role === "repeater" && profile.feature === "full" && info.espnowBridge === true &&
/^heltec_v4(?:_[a-z0-9]+)*_repeater(?:_observer_mqtt_?)?-full-(?:usb-wifi|logging)$/i.test(target)) {
notes.push("This Full repeater keeps LoRa as its primary mesh radio and can bridge to ESP-NOW when that runtime bridge is enabled. Use the matching restore-settings controls below; the Full Companion is a different role, not the repeater gateway.");
}
}
if (profile.hardware === "wio-e5" && profile.role === "repeater" &&
(!info.platform || info.platform === "STM32_PLATFORM") &&
/^wio-e5(?:_|-)repeater(?:_bridge_rs232)?$/i.test(target)) {
// These exact historical recipes are intentionally separate; do not
// infer the RS-232 capability for an ordinary or Mini Companion image.
notes.push("Wio-E5 is a flash-capacity exception with separate ordinary repeater and RS-232 bridge images. For RS-232 wiring, choose the exact bridge image rather than assuming the ordinary repeater includes that bridge.");
}
return notes;
}
function installSteps(profile, kind) {
const common = [
"Verify that the hardware name and every displayed variant match the physical board.",
@@ -1962,6 +2024,17 @@
}
card.appendChild(actions);
const notes = boardNotes(profile);
if (notes.length) {
const details = createElement("details");
details.className = "firmware-picker-board-notes";
details.appendChild(createElement("summary", "Board notes"));
notes.forEach(function (text) {
details.appendChild(createElement("p", text));
});
card.appendChild(details);
}
const steps = createElement("div");
steps.className = "firmware-picker-steps";
steps.appendChild(createElement("strong", "What to do"));
@@ -2582,6 +2655,8 @@
runtimeDirections: runtimeDirections,
runtimePanelDirections: runtimePanelDirections,
renderRuntimeDirections: renderRuntimeDirections,
renderProfileCard: renderProfileCard,
boardNotes: boardNotes,
installSteps: installSteps,
migrationReleaseUrl: migrationReleaseUrl,
migrationPackageForProfile: migrationPackageForProfile,
+9
View File
@@ -8,6 +8,8 @@
.firmware-picker-intro,
.firmware-picker-result,
.firmware-picker-missing,
.firmware-picker-help,
.firmware-picker-board-notes,
.firmware-asset-browser {
border: 1px solid var(--picker-border);
border-radius: 0.7rem;
@@ -338,6 +340,13 @@
margin-top: 1.2rem;
}
.firmware-picker-help,
.firmware-picker-board-notes {
margin-top: 1.2rem;
}
.firmware-picker-help summary,
.firmware-picker-board-notes summary,
.firmware-asset-browser summary {
cursor: pointer;
font-weight: 700;
+83 -103
View File
@@ -4,75 +4,7 @@
# Firmware picker
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. 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
default ASCII terminal on USB Companion and infrastructure roles.
Pick the choices in any order. The controls stay limited to compatible
firmware combinations that were actually built in the current release set.
The result count stays visible while you scroll through the filters on a phone.
Once the exact board and role are selected, the picker skips logging,
connection, and profile choices that all select the same firmware. Logging
settings move into **Restore your settings after flashing** beneath the files.
Choices that select different images remain visible.
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 when a choice between different images is needed.
RAK3401 and RAK4631 repeaters use one unified image per sensor policy for
supported internal and external OTA storage. **Full** and **Reduced** choose
optional sensor drivers; they do not select a storage module. The unified
RAK4631 image also includes RS-232 on UART2, which is recommended for new
installations. The unified image reserves UART1 for potentially powered GPS
hardware; header-connected W25 flash also occupies UART1 pins. Legacy GPS-free
Serial1 downloads remain available through exact-filename search for existing
wiring, but are omitted from normal choices when the matching unified sensor
profile is available. Ethernet remains a separate RAK4631 hardware image.
LoRa OTA still requires the matching
OTAFIX bootloader and a package compatible with the installed target.
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.9-88c85108" data-bootloaders-url="../_data/bootloader_manifest.json?v=20261002-1" data-share-url="https://mikecarper.github.io/MeshCore/p/">
<div class="firmware-picker-intro" role="note">
<strong>Current release set</strong>
<p data-role="release-set">Loading release information...</p>
<p>
For a new installation, choose the exact board and role, and select
<strong>Full install / layout migration (merged .bin)</strong>. Current ESP32
releases combine the supported features in Full; use runtime settings to
choose the active transports. Where separate profiles remain, prefer
<strong>FULL / complete</strong> when it meets your update and hardware
requirements. Other chip families retain their qualified profiles.
</p>
<p>
Upgrading an existing ESP32 infrastructure node from a smaller partition
layout? The selected Full result links directly to its exact board and
role partition-expansion ZIP when published. Follow its included README
to check the installed OTA target and source layout for a supported Wi-Fi
or LoRa route. If no unique matching ZIP is available, the picker links
to the utility release to check availability. USB installation uses the matching
merged image instead. An app-only .bin cannot change the partition table.
If the installed firmware supports it, run <code>get storage.layout</code>
to see the live flash size and OTA slots. Firmware version alone does not
prove the installed layout.
</p>
</div>
<p class="firmware-picker-order-note">
Pick in any order. Use <strong>Any</strong> to clear one choice, or clear
everything with the button below. The address bar updates with your choices;
use <strong>Copy link to settings</strong> for a shorter link to the same selection.
</p>
<form class="firmware-picker-form" data-role="form">
<div class="firmware-picker-feedback firmware-picker-wide" data-role="filter-feedback" hidden>
<div aria-live="polite" aria-atomic="true">
@@ -82,6 +14,27 @@ from the published firmware assets.
<a data-role="view-results" hidden>View files</a>
</div>
<div class="firmware-picker-control firmware-picker-select-control">
<label for="firmware-picker-hardware-family">Hardware</label>
<select id="firmware-picker-hardware-family" data-field="hardwareFamily" disabled>
<option value="">Loading hardware...</option>
</select>
</div>
<fieldset class="firmware-picker-control firmware-picker-radio-control" data-radio-field="role" disabled>
<legend>Firmware role</legend>
<div class="firmware-picker-radio-options" data-field="role">
Loading roles...
</div>
</fieldset>
<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>
<details class="firmware-picker-chip-family firmware-picker-wide">
<summary data-role="chip-family-summary">Optional: chip family</summary>
<p id="firmware-picker-chip-help">Skip this if you know your board. Picking hardware selects its chip family automatically. Choose Any to clear this filter.</p>
@@ -93,20 +46,6 @@ from the published firmware assets.
</div>
</details>
<div class="firmware-picker-control firmware-picker-select-control">
<label for="firmware-picker-hardware-family">Hardware</label>
<select id="firmware-picker-hardware-family" data-field="hardwareFamily" disabled>
<option value="">Loading hardware...</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>
<div class="firmware-picker-radio-options" data-field="install">
@@ -114,13 +53,6 @@ from the published firmware assets.
</div>
</fieldset>
<fieldset class="firmware-picker-control firmware-picker-radio-control" data-radio-field="role" disabled>
<legend>Firmware role</legend>
<div class="firmware-picker-radio-options" data-field="role">
Loading roles...
</div>
</fieldset>
<fieldset class="firmware-picker-control firmware-picker-radio-control" data-radio-field="logging" disabled>
<legend>Logging / MQTT</legend>
<div class="firmware-picker-radio-options" data-field="logging">
@@ -151,6 +83,11 @@ from the published firmware assets.
</div>
</form>
<div class="firmware-picker-intro" role="note">
<strong>Current release set</strong>
<p data-role="release-set">Loading release information...</p>
</div>
<section class="firmware-picker-result" data-role="bootloader" aria-live="polite" hidden>
<h2 data-role="bootloader-title">nRF52 bootloader</h2>
<div data-role="bootloader-content"></div>
@@ -173,6 +110,27 @@ from the published firmware assets.
<a href="https://github.com/mikecarper/MeshCore/releases">Browse all firmware releases</a>
</section>
<details class="firmware-picker-help">
<summary>Picker help</summary>
<p>
Pick in any order. Use <strong>Any</strong> to clear one choice, or
<strong>Clear all choices</strong> to start over. Use
<strong>Copy link to settings</strong> to share your selection.
</p>
<p>
Choices stay limited to compatible builds. Picking hardware selects its
chip family automatically. Filters disappear when they select the same
file; runtime options appear under
<strong>Restore your settings after flashing</strong> instead.
Installation instructions and board-specific guidance follow your download.
</p>
<p>
The picker reads public release metadata from GitHub. It does not upload
device information. Hardware names, targets, and download links come from
the published firmware assets.
</p>
</details>
<details class="firmware-asset-browser">
<summary>Advanced: search current release filenames</summary>
<p>
@@ -189,6 +147,35 @@ from the published firmware assets.
## What the choices mean
Pick the choices in any order. The controls stay limited to compatible
firmware combinations that were actually built in the current release set.
The result count stays visible while you scroll through the filters on a phone.
Once the exact board and role are selected, the picker skips logging,
connection, and profile choices that all select the same firmware. Logging
settings move into **Restore your settings after flashing** beneath the files.
Choices that select different images remain visible.
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 when a choice between different images is needed.
Use **Any** to clear one choice, or **Clear all choices** to start over.
The address bar updates with your choices; use **Copy link to settings** for a
shorter link to the same selection.
Each result includes **Restore your settings after flashing**. Its commands
follow the selected logging mode and the exact image's verified role/hardware
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
default ASCII terminal on USB Companion and infrastructure roles.
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.
| Choice | Use |
| --- | --- |
| Companion | A phone, computer, or host application controls the radio |
@@ -210,9 +197,8 @@ Connection and bridge choices depend on the selected role. Companion firmware
may offer Full, combined USB + Bluetooth, Bluetooth, USB, Wi-Fi, serial, or
Ethernet transports.
Normal repeater firmware includes runtime-controlled RS-232 support where the
board has room; use `set bridge.enabled on` after configuring `bridge.uart` and
`bridge.baud`. The Wio-E5 remains the capacity exception and offers a separate
RS-232 image. Choose Wi-Fi MQTT in the result's saved logging settings when
board has room; use the selected image's runtime directions to configure it.
Choose Wi-Fi MQTT in the result's saved logging settings when
it shares the Full image; separate MQTT images retain a **Logging / MQTT**
filter. Full ESP32 LoRa repeaters include runtime
ESP-NOW when capacity permits; dedicated ESP-NOW images remain for measured
@@ -229,11 +215,9 @@ supported, `set mqtt.enabled on|off` for MQTT. Newly combined ordinary
repeaters start with ESP-NOW off until explicitly enabled, including the first
upgrade from unmarked legacy preferences. Later reboots preserve that setting.
On combined RS-232 boards without MQTT, `bridge.enabled` controls the UART.
Full Heltec V3, WSL3 and RAK3112 MQTT images also retain RS-232;
use `set rs232.enabled on|off` and `get rs232.running` for their independent
UART. T-LoRa V2.1 keeps two Full choices due to its internal RAM limit:
the normal repeater has UART plus ESP-NOW, and the observer has MQTT plus
ESP-NOW. MQTT and ESP-NOW use one 2.4 GHz radio, so when both are enabled the ESP-NOW bridge channel must
Use the selected download's board notes and runtime controls for images with
independent UART, MQTT, and ESP-NOW services.
MQTT and ESP-NOW use one 2.4 GHz radio, so when both are enabled the ESP-NOW bridge channel must
match the connected WiFi access point's fixed channel. ESP-NOW-only mode does
not require WiFi credentials. Its runtime
`bridge.format` setting chooses the peer protocol: `wrapped` (the
@@ -242,10 +226,6 @@ backward-compatible bridge-to-bridge default using `bridge.secret`) or `raw`
`SenseCapIndicator-ESPNow`, and other primary-ESP-NOW nodes). This is one
firmware choice, not two board images. Match `bridge.channel` to the primary
nodes' `espnow.channel` before selecting `set bridge.format raw`.
For Heltec V4 specifically, `companion_radio_full` is still a LoRa-primary
Companion; choose the combined `heltec_v4_repeater` Full firmware and enable
ESP-NOW to make that board the LoRa/ESP-NOW gateway. Use its exact merged artifact when
changing roles or partition layouts.
## Share a selection
@@ -288,8 +268,8 @@ Current ESP32 ordinary releases use one Full / complete image per exact board
and role. Full keeps the supported feature set and CLI; runtime switches select
USB, Bluetooth, Wi-Fi, logging, and other qualified features. Separate portable,
minimal, and single-transport ESP32 images are installation/recovery recipes,
not additional ordinary release choices. The documented T-LoRa UART/MQTT RAM
split remains two Full images. Other chip families retain their qualified
not additional ordinary release choices. Board-specific capacity exceptions
are shown with the selected download. Other chip families retain their qualified
capacity and sensor-profile choices.
Moving an older ESP32 installation to an expanded Full layout requires a
+2 -2
View File
@@ -120,7 +120,7 @@ extra_css:
- _stylesheets/notification_builder.css?v=20261006-channel9
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.css
- _stylesheets/extra.css
- _stylesheets/firmware_picker.css?v=20261001-feedback-1
- _stylesheets/firmware_picker.css?v=20261013-pick-first-1
- _stylesheets/telemetry_decoder.css
- _stylesheets/management_decoder.css
- _stylesheets/filter_tool.css
@@ -131,7 +131,7 @@ extra_javascript:
- _javascript/hosted_doc_links.js?v=20261007-1
- _javascript/notification_builder.js?v=20261006-channel9
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
- _javascript/firmware_picker.js?v=20261012-short-picker-1
- _javascript/firmware_picker.js?v=20261013-board-notes-1
- _javascript/telemetry_decoder.js?v=20261007-hosted-form
- _javascript/management_decoder.js
- _javascript/filter_tool.js?v=20261002-hashbytes-2plus
+106
View File
@@ -2127,6 +2127,94 @@ assert.strictEqual(JSON.stringify(feedbackCatalog), feedbackSnapshot);
assert.deepStrictEqual(simpleFilters, {...feedbackFilters, hardwareFamily: 'Heltec_v3'});
console.log('same-image selectors skip redundant steps while distinct profiles and install routes stay explicit');
// Board-specific help belongs to exact qualified results, never to a broad
// family filter or an unrelated role which happens to share the board name.
const boardText = profile => picker.boardNotes(profile).join('\n');
const boardProfile = target => currentCatalog.profiles.find(profile => profile.target === target);
assert.deepStrictEqual(picker.boardNotes(null), []);
assert.deepStrictEqual(picker.boardNotes({}), []);
for (const board of ['RAK_3401', 'RAK_4631']) {
for (const policy of ['full', 'reduced']) {
const profile = boardProfile(board + '_repeater_unified_lora_ota-' + policy);
assert(profile);
const before = JSON.stringify(profile);
const notes = boardText(profile);
assert(notes.includes('storage automatically'));
assert(notes.includes('sensor drivers, not a storage module'));
assert(notes.includes('Internal flash accepts in-place deltas only'));
assert(notes.includes('matching OTAFIX bootloader'));
if (board === 'RAK_4631') {
assert(notes.includes('UART2') && notes.includes('UART1') && notes.includes('W25'));
assert(notes.includes('Legacy GPS-free Serial1') && notes.includes('Ethernet'));
} else {
assert(!notes.includes('UART') && !notes.includes('Ethernet'));
}
assert.strictEqual(JSON.stringify(profile), before, 'boardNotes must not mutate release metadata');
for (const change of [
{controls: undefined}, {ota: 'none'}, {sensorProfile: undefined},
{hardware: 'RAK_4631_ethernet'}, {role: 'companion'},
{controls: {...profile.controls, platform: 'ESP32_PLATFORM'}},
{controls: {...profile.controls, updateRequirements: undefined}},
{controls: {...profile.controls, updateRequirements: {lora: {storage: 'external_only'}}}},
]) {
assert.deepStrictEqual(picker.boardNotes({...profile, ...change}), [],
'Unqualified adaptive-storage metadata must not create a storage promise');
}
}
}
const boardRak = boardProfile('RAK_4631_repeater_unified_lora_ota-full');
assert(!boardText({...boardRak, controls: {...boardRak.controls, rs232: false}}).includes('UART2'));
assert(!boardText({...boardRak, controls: {...boardRak.controls, gps: false}}).includes('UART2'));
const boardLegacy = rakPartialCatalog.profiles.find(profile => profile.target === rakBridgeBase + '-reduced');
assert(boardText(boardLegacy).includes('legacy GPS-free Serial1'));
assert(!boardText(boardLegacy).includes('storage automatically'));
assert.deepStrictEqual(picker.boardNotes({...boardLegacy,
controls: {...boardLegacy.controls, gps: true}}), []);
const boardEthernet = boardProfile('RAK_4631_repeater_ethernet-full');
assert(boardText(boardEthernet).includes('separate RAK4631 Ethernet hardware image'));
assert(!boardText(boardEthernet).includes('storage automatically'));
assert(!boardText(boardEthernet).includes('Use UART2'));
for (const [profile, selected] of [[tloraNormalProfile, 'UART/RS-232 choice'],
[tloraObserverProfile, 'MQTT observer choice']]) {
assert(boardText(profile).includes('two RAM-safe Full images'));
assert(boardText(profile).includes(selected));
for (const change of [{controls: undefined}, {feature: 'standard'},
{controls: {...profile.controls, espnowBridge: false}},
{controls: {...profile.controls, rs232: true, mqtt: true}},
{target: profile.target.replace('V2_1_1_6', 'V2_0'), hardware: 'LilyGo_TLora_V2_0'}]) {
assert.deepStrictEqual(picker.boardNotes({...profile, ...change}), []);
}
}
assert(boardText(boardProfile('LilyGo_TLora_V2_1_1_6_repeater-full-logging'))
.includes('UART/RS-232 choice'), 'Current UART-only logging recipe must retain the RAM warning');
for (const profile of tripleCatalog.profiles) {
assert(boardText(profile).includes('independent RS-232 and ESP-NOW'));
assert.deepStrictEqual(picker.boardNotes({...profile,
controls: {...profile.controls, rs232: false}}), []);
}
for (const profile of currentCatalog.profiles.filter(profile =>
/^heltec_v4(?:_[a-z0-9]+)*_companion_radio_full/i.test(profile.target))) {
assert(boardText(profile).includes('Full Companion is LoRa-primary'), profile.target);
assert(!boardText(profile).includes('can bridge to ESP-NOW'));
assert.deepStrictEqual(picker.boardNotes({...profile, controls: undefined}), []);
assert.deepStrictEqual(picker.boardNotes({...profile,
controls: {...profile.controls, primaryEspnow: true}}), []);
}
const boardV4Gateway = boardProfile('heltec_v4_repeater-full-usb-wifi');
assert(boardText(boardV4Gateway).includes('can bridge to ESP-NOW'));
assert.deepStrictEqual(picker.boardNotes({...boardV4Gateway,
controls: {...boardV4Gateway.controls, espnowBridge: false}}), []);
assert.deepStrictEqual(picker.boardNotes({...boardV4Gateway, role: 'companion'}), []);
assert(boardText(wio).includes('flash-capacity exception'));
assert(boardText(picker.parseTargetProfile('wio-e5_repeater')).includes('separate ordinary repeater'));
for (const target of ['wio-e5_companion_radio_usb', 'wio-e5-mini_companion_radio_usb',
'wio-e5-mini_repeater', 'wio-e5-dev_repeater', 'RAK_4631_companion_radio_full',
'RAK_3401_sensor', 't1000e_companion_radio_full', 'heltec_v4_companion_radio_usb']) {
assert.deepStrictEqual(picker.boardNotes(picker.parseTargetProfile(target)), [], target);
}
assert.deepStrictEqual(picker.boardNotes({...wio, controls: {platform: 'ESP32_PLATFORM'}}), []);
console.log('exact-board notes qualify adaptive storage, UART wiring, RAM splits and gateway roles');
// Logging choices remain available beneath the download. Linked choices
// preselect their exact commands; an unselected logging mode shows off steps.
for (const logging of ['none', 'usb', 'wifi', 'both']) {
@@ -2178,6 +2266,24 @@ global.document = {
createTextNode: text => new RuntimeElement('#text', text),
};
try {
const boardContainer = new RuntimeElement('section');
picker.renderProfileCard(boardContainer, boardRak, boardRak.files[0], 'uf2', {}, {});
const boardCard = boardContainer.children[0];
const boardDetails = boardCard.children.find(node => node.className === 'firmware-picker-board-notes');
assert(boardDetails && boardDetails.tagName === 'DETAILS');
assert.strictEqual(!!boardDetails.open, false, 'Board notes start collapsed');
assert.strictEqual(boardDetails.children[0].tagName, 'SUMMARY');
assert.strictEqual(boardDetails.children[0].textContent, 'Board notes');
assert(boardCard.children.indexOf(boardDetails) > boardCard.children.findIndex(node =>
node.className === 'firmware-picker-actions'), 'Exact download actions stay ahead of board notes');
assert(boardCard.children.indexOf(boardDetails) < boardCard.children.findIndex(node =>
node.className === 'firmware-picker-steps'), 'Board notes precede installation steps');
assert.strictEqual(boardDetails.children.filter(node => node.tagName === 'P').length,
picker.boardNotes(boardRak).length);
const noNotesContainer = new RuntimeElement('section');
picker.renderProfileCard(noNotesContainer, companionEsp32, companionEsp32.files[0], 'bin', {}, {});
assert(!noNotesContainer.children[0].children.some(node =>
node.className === 'firmware-picker-board-notes'), 'Unaffected boards must not get an empty notes panel');
const card = new RuntimeElement('article');
const changes = [];
picker.renderRuntimeDirections(card, observer, {logging: 'wifi'}, {
+49
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Offline picker packaging preserves qualified full/reduced OTA choices."""
import importlib.util
from html.parser import HTMLParser
import json
from pathlib import Path
import re
@@ -12,7 +13,54 @@ PACKAGE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PACKAGE)
class PickerMarkup(HTMLParser):
def __init__(self, text):
super().__init__()
self.tags = []
self.feed(text)
def matching(self, attribute, value):
return [(index, tag, attrs) for index, (tag, attrs) in enumerate(self.tags)
if attrs.get(attribute) == value]
def handle_starttag(self, tag, attrs):
self.tags.append((tag, dict(attrs)))
class OfflinePickerPackageTests(unittest.TestCase):
def assert_picker_first(self, text):
markup = PickerMarkup(text)
form = markup.matching('data-role', 'form')
release = markup.matching('data-role', 'release-set')
help_panels = markup.matching('class', 'firmware-picker-help')
self.assertEqual(len(form), 1)
self.assertEqual(len(release), 1)
self.assertEqual(len(help_panels), 1)
help_index, help_tag, help_attrs = help_panels[0]
self.assertEqual(help_tag, 'details')
self.assertNotIn('open', help_attrs)
self.assertLess(form[0][0], release[0][0])
self.assertLess(markup.matching('data-role', 'result')[0][0], help_index)
fields = [(attrs.get('data-field'), index) for index, (_, attrs)
in enumerate(markup.tags) if 'data-field' in attrs]
self.assertEqual(fields[0][0], 'hardwareFamily')
field_positions = dict(fields)
self.assertLess(field_positions['role'], field_positions['install'])
self.assertTrue({'chipFamily', 'hardwareFamily', 'hardware', 'install',
'role', 'logging', 'firmwareProfile', 'mode'}.issubset(
{field for field, _ in fields}))
for _, attrs in markup.tags:
for described_by in attrs.get('aria-describedby', '').split():
self.assertEqual(len(markup.matching('id', described_by)), 1)
def test_hosted_page_starts_with_choices_not_instructions(self):
source = (ROOT / 'docs/firmware_picker.md').read_text(encoding='utf-8')
title_end = source.index('# Firmware picker') + len('# Firmware picker')
picker_start = source.index('<div class="firmware-picker"')
self.assertFalse(source[title_end:picker_start].strip())
self.assert_picker_first(source)
self.assertNotIn('The unified\nRAK4631 image', source)
def test_sensor_policy_pairs_and_individual_downloads_are_retained(self):
source = 'a' * 40
family = 'v1.17.1.7-dev-aaaaaaaa'
@@ -30,6 +78,7 @@ class OfflinePickerPackageTests(unittest.TestCase):
uploader={'token': 'not part of the picker'}))
releases = [dict(name=family, tag_name=family, assets=assets, authorization='private')]
packaged = PACKAGE.package(releases, controls)
self.assert_picker_first(packaged)
match = re.search(r'<script type="application/json" id="firmware-picker-data">(.*?)</script>', packaged)
embedded = json.loads(match[1])
self.assertEqual(embedded['controls'], controls)