mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 11:02:29 +00:00
Simplify firmware selection and canonical build menus
This commit is contained in:
1 parent
08bf174ea3
commit
9202fc2a96
9 files changed
+869
-58
No files matched your search
+46
-12
@@ -1473,15 +1473,11 @@ get_variants_for_board() {
|
||||
local board_family=$1
|
||||
local env
|
||||
|
||||
for env in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if ! is_supported_build_env "$env"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
while IFS= read -r env; do
|
||||
if [ "$(get_board_family_for_env "$env")" == "$board_family" ]; then
|
||||
echo "$env"
|
||||
fi
|
||||
done | sort_lines_case_insensitive
|
||||
done < <(get_interactive_build_targets) | sort_lines_case_insensitive
|
||||
}
|
||||
|
||||
prompt_for_variant_for_board() {
|
||||
@@ -1555,17 +1551,13 @@ prompt_for_board_target() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for env in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if ! is_supported_build_env "$env"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
while IFS= read -r env; do
|
||||
board=$(get_board_family_for_env "$env")
|
||||
if [ -z "${seen_boards[$board]}" ]; then
|
||||
seen_boards["$board"]=1
|
||||
boards+=("$board")
|
||||
fi
|
||||
done
|
||||
done < <(get_interactive_build_targets)
|
||||
|
||||
mapfile -t boards < <(printf '%s\n' "${boards[@]}" | sort_lines_case_insensitive)
|
||||
|
||||
@@ -6548,6 +6540,48 @@ select_ordinary_full_targets() {
|
||||
done
|
||||
}
|
||||
|
||||
# The board menu is for ordinary installations, so use the same Full choices
|
||||
# as the release matrix instead of asking users to select a transport/default
|
||||
# alias or an older partition recipe. Exact names remain accepted by direct
|
||||
# build commands and migration tooling. Preserve other platforms' qualified
|
||||
# hardware/storage contracts and KISS, which is a separate host-modem role.
|
||||
get_interactive_build_targets() {
|
||||
local target key priority pair_target
|
||||
local -a full_targets=() other_keys=()
|
||||
local -A other_choice=() other_priority=()
|
||||
for target in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
is_supported_build_env "$target" || continue
|
||||
is_redundant_bulk_build_target "$target" && continue
|
||||
if [ "${PIO_ENV_PLATFORM_BY_NAME[$target]:-}" = ESP32_PLATFORM ] \
|
||||
&& supports_esp32_full_build "$target"; then
|
||||
full_targets+=("$target")
|
||||
else
|
||||
key=$target
|
||||
priority=0
|
||||
# A normal nRF52 target and its generated reduced alias can both build
|
||||
# the exact same Full/Reduced pair. Offer that operation once, preferring
|
||||
# the ordinary board/role name. The existing resolver preserves distinct
|
||||
# deployed IDs and external-storage contracts, so those remain choices.
|
||||
if pair_target=$(get_nrf52_sensor_ota_pair_target "$target"); then
|
||||
key=$pair_target
|
||||
if ! is_lora_ota_only_target "$target"; then priority=1; fi
|
||||
fi
|
||||
if [ -z "${other_choice[$key]+x}" ]; then
|
||||
other_keys+=("$key")
|
||||
other_choice[$key]=$target
|
||||
other_priority[$key]=$priority
|
||||
elif [ "$priority" -gt "${other_priority[$key]}" ]; then
|
||||
other_choice[$key]=$target
|
||||
other_priority[$key]=$priority
|
||||
fi
|
||||
fi
|
||||
done
|
||||
for key in "${other_keys[@]}"; do
|
||||
printf '%s\n' "${other_choice[$key]}"
|
||||
done
|
||||
select_ordinary_full_targets "${full_targets[@]}"
|
||||
}
|
||||
|
||||
has_esp32_full_profile() {
|
||||
local target
|
||||
local candidate=""
|
||||
|
||||
@@ -1132,6 +1132,67 @@
|
||||
});
|
||||
}
|
||||
|
||||
function selectionRequirements(profiles, filters) {
|
||||
const chosen = filters || {};
|
||||
const hiddenFields = [];
|
||||
let installKind = chosen.install || "";
|
||||
// Board and role are deliberate choices. Once known, do not ask for
|
||||
// settings that leave every compatible build unchanged.
|
||||
if (chosen.hardware && chosen.role) {
|
||||
const context = Object.assign({}, chosen);
|
||||
// Classify against board/role, not mutually narrowing settings. A real
|
||||
// alternative must stay visible so a selected filter can be cleared.
|
||||
["logging", "mode"].concat(PROFILE_FIELDS).forEach(function (field) {
|
||||
delete context[field];
|
||||
});
|
||||
const boardContext = Object.assign({}, context);
|
||||
delete boardContext.install;
|
||||
const boardProfiles = (profiles || []).filter(function (profile) {
|
||||
return profileMatchesFacets(profile, boardContext);
|
||||
});
|
||||
function supportsSelection(field) {
|
||||
const selection = field === "firmwareProfile"
|
||||
? PROFILE_FIELDS.reduce(function (parts, part) {
|
||||
if (chosen[part]) parts[part] = chosen[part];
|
||||
return parts;
|
||||
}, {}) : chosen[field] ? { [field]: chosen[field] } : {};
|
||||
return boardProfiles.some(function (profile) {
|
||||
return profileMatchesFacets(profile, selection);
|
||||
});
|
||||
}
|
||||
// Invalid old links need a visible Any control. Do not conceal their
|
||||
// requested value or replace it with another install format.
|
||||
if (!supportsSelection("install")) delete context.install;
|
||||
["logging", "firmwareProfile", "mode"].forEach(function (field) {
|
||||
const options = field === "firmwareProfile"
|
||||
? firmwareProfileChoices(profiles, context)
|
||||
: facetValues(profiles, context, field).map(function (value) {
|
||||
return { value: value };
|
||||
});
|
||||
if (supportsSelection(field) &&
|
||||
choicesUseSameFirmware(profiles, context, field, options)) {
|
||||
hiddenFields.push(field);
|
||||
}
|
||||
});
|
||||
const installs = facetValues(profiles, context, "install");
|
||||
if (installs.length === 1 && supportsSelection("install")) {
|
||||
hiddenFields.push("install");
|
||||
installKind = installs[0];
|
||||
}
|
||||
}
|
||||
return {
|
||||
hiddenFields: hiddenFields,
|
||||
installKind: installKind,
|
||||
missingFields: CHOICE_FIELDS.filter(function (field) {
|
||||
if (field === "chipFamily" || hiddenFields.includes(field)) return false;
|
||||
if (field === "firmwareProfile") return !PROFILE_FIELDS.every(function (part) {
|
||||
return chosen[part];
|
||||
});
|
||||
return !chosen[field];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function selectionUrl(url, filters, automaticChipFamily) {
|
||||
const result = new URL(url);
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
@@ -1516,6 +1577,7 @@
|
||||
}
|
||||
return {
|
||||
label: LOGGING_LABELS[mode], commands: commands,
|
||||
selection: { logging: mode },
|
||||
text: full && info && info.mqtt
|
||||
? (mode === "wifi" || mode === "both"
|
||||
? "In WebConfig, enable the desired MQTT broker presets/settings and save."
|
||||
@@ -1655,7 +1717,40 @@
|
||||
return sections;
|
||||
}
|
||||
|
||||
function renderRuntimeDirections(card, profile, selection) {
|
||||
function runtimePanelDirections(profile, selection, runtimeFields) {
|
||||
const chosen = Object.assign({}, selection);
|
||||
const moved = runtimeFields || [];
|
||||
if (moved.includes("logging")) delete chosen.logging;
|
||||
if (moved.includes("mode") && !chosen.mode &&
|
||||
profileFieldValues(profile, "mode").includes("standard")) {
|
||||
chosen.mode = "standard";
|
||||
}
|
||||
const sections = runtimeDirections(profile, chosen);
|
||||
sections.forEach(function (item) {
|
||||
if (moved.includes("logging") && item.actions.some(function (action) {
|
||||
return action.selection && action.selection.logging;
|
||||
})) {
|
||||
const mode = selection && selection.logging || "none";
|
||||
const selected = item.actions.findIndex(function (action) {
|
||||
return action.selection && action.selection.logging === mode;
|
||||
});
|
||||
item.selectedIndex = selected < 0 ? 0 : selected;
|
||||
item.title = "Logging / MQTT output";
|
||||
}
|
||||
if (moved.includes("mode") && (/^RS232 bridge/.test(item.title) ||
|
||||
item.title === "ESP-NOW bridge")) {
|
||||
const on = /^RS232 bridge/.test(item.title)
|
||||
? chosen.mode === "rs232" : chosen.mode === "espnow";
|
||||
item.selectedIndex = item.actions.findIndex(function (action) {
|
||||
return action.label === (on ? "On" : "Off");
|
||||
});
|
||||
}
|
||||
});
|
||||
return sections;
|
||||
}
|
||||
|
||||
function renderRuntimeDirections(card, profile, selection, options) {
|
||||
const settings = options || {};
|
||||
const panel = createElement("section");
|
||||
panel.className = "firmware-picker-runtime";
|
||||
panel.appendChild(createElement("h4", "Restore your settings after flashing"));
|
||||
@@ -1675,7 +1770,7 @@
|
||||
: "Use board and ver to identify the node.")));
|
||||
}
|
||||
if (!profile.controls) panel.appendChild(createElement("p", "Additional hardware controls have not been verified for this exact release image. Use the role guide for those settings."));
|
||||
runtimeDirections(profile, selection).forEach(function (item, index) {
|
||||
runtimePanelDirections(profile, selection, settings.runtimeFields).forEach(function (item, index) {
|
||||
const details = createElement("details");
|
||||
details.open = index === 0;
|
||||
details.appendChild(createElement("summary", item.title));
|
||||
@@ -1685,13 +1780,13 @@
|
||||
fieldset.appendChild(createElement("legend", "Show steps for " + item.title));
|
||||
const choices = createElement("div");
|
||||
choices.className = "firmware-picker-radio-options";
|
||||
let actionIndex = item.selectedIndex >= 0 ? item.selectedIndex : 0;
|
||||
renderRadioChoices(choices, item.actions.map(function (action, i) {
|
||||
return { value: String(i), label: action.label };
|
||||
}), "0", "firmware-picker-runtime-" + (++runtimeGroupCount));
|
||||
}), String(actionIndex), "firmware-picker-runtime-" + (++runtimeGroupCount));
|
||||
fieldset.appendChild(choices);
|
||||
details.appendChild(fieldset);
|
||||
const output = createElement("div");
|
||||
let actionIndex = 0;
|
||||
function show() {
|
||||
output.replaceChildren();
|
||||
const action = item.actions[actionIndex];
|
||||
@@ -1717,6 +1812,11 @@
|
||||
choices.addEventListener("change", function (event) {
|
||||
actionIndex = Number(event.target.value) || 0;
|
||||
show();
|
||||
const action = item.actions[actionIndex];
|
||||
if (action.selection && settings.onSelectionChange &&
|
||||
(settings.runtimeFields || []).includes("logging")) {
|
||||
settings.onSelectionChange(action.selection);
|
||||
}
|
||||
});
|
||||
show();
|
||||
details.appendChild(output);
|
||||
@@ -1733,7 +1833,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderProfileCard(container, profile, asset, installKind, selection) {
|
||||
function renderProfileCard(container, profile, asset, installKind, selection, options) {
|
||||
const card = createElement("article");
|
||||
card.className = "firmware-picker-card";
|
||||
card.appendChild(createElement(
|
||||
@@ -1752,7 +1852,8 @@
|
||||
return labelFor("mode", mode);
|
||||
}).join(" / "),
|
||||
],
|
||||
["Logging", labelFor("logging", selection && selection.logging || profile.logging)],
|
||||
["Logging", labelFor("logging", options && (options.runtimeFields || []).includes("logging")
|
||||
? profile.logging : selection && selection.logging || profile.logging)],
|
||||
["Firmware profile", firmwareProfileLabel(profile)],
|
||||
["Install operation", labelFor("install", installKind)],
|
||||
["File", asset.name],
|
||||
@@ -1794,7 +1895,7 @@
|
||||
});
|
||||
steps.appendChild(list);
|
||||
card.appendChild(steps);
|
||||
renderRuntimeDirections(card, profile, selection);
|
||||
renderRuntimeDirections(card, profile, selection, options);
|
||||
container.appendChild(card);
|
||||
}
|
||||
|
||||
@@ -2031,6 +2132,11 @@
|
||||
firmwareProfileValue(filters), groupPrefix + "-firmwareProfile", "firmwareProfile");
|
||||
profileControl.closest("fieldset").disabled = profileChoices.length === 0;
|
||||
setRefinementHint(profileControl, "firmwareProfile", profileChoices);
|
||||
const requirements = selectionRequirements(catalog.profiles, filters);
|
||||
["logging", "firmwareProfile", "mode", "install"].forEach(function (field) {
|
||||
const control = controls[field];
|
||||
if (control) control.closest("fieldset").hidden = requirements.hiddenFields.includes(field);
|
||||
});
|
||||
const hardwareVariants = valuesForField("hardware");
|
||||
const hardwareVariantControl = root.querySelector(
|
||||
'[data-role="hardware-variant-control"]'
|
||||
@@ -2056,26 +2162,32 @@
|
||||
result.hidden = true;
|
||||
missing.hidden = true;
|
||||
resultList.replaceChildren();
|
||||
const missingFields = CHOICE_FIELDS.filter(function (field) {
|
||||
if (field === "firmwareProfile") return !PROFILE_FIELDS.every(function (part) {
|
||||
return filters[part];
|
||||
});
|
||||
return field !== "chipFamily" && !filters[field];
|
||||
});
|
||||
const requirements = selectionRequirements(catalog.profiles, filters);
|
||||
const missingFields = requirements.missingFields;
|
||||
const cardOptions = {
|
||||
runtimeFields: requirements.hiddenFields,
|
||||
onSelectionChange: function (selection) {
|
||||
Object.keys(selection).forEach(function (field) {
|
||||
filters[field] = selection[field];
|
||||
});
|
||||
updateSelectionUrl();
|
||||
},
|
||||
};
|
||||
const matches = matchingProfiles();
|
||||
if (feedback) {
|
||||
const progress = selectionProgress(catalog.profiles, filters);
|
||||
const progress = selectionProgress(catalog.profiles,
|
||||
Object.assign({}, filters, { install: requirements.installKind }));
|
||||
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;
|
||||
feedback.querySelector('[data-role="filter-note"]').textContent = missingFields.length
|
||||
? progress.note : "Matching files and install instructions are shown below.";
|
||||
}
|
||||
if (resultLink) resultLink.hidden = true;
|
||||
|
||||
if (missingFields.length) {
|
||||
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.";
|
||||
status.textContent = "One firmware build remains. Confirm the hardware, role and any remaining install choice.";
|
||||
} else if (missingFields.length === CHOICE_FIELDS.length - 1 && !filters.chipFamily) {
|
||||
status.textContent = "Pick options in any order. " +
|
||||
matches.length + " compatible configurations are available.";
|
||||
@@ -2086,7 +2198,7 @@
|
||||
(missingFields.length === 1 ? "" : "s") + " in any order.";
|
||||
}
|
||||
if (shouldShowCandidateResults(matches)) {
|
||||
const candidates = resolveProfileAssets(matches, filters.install);
|
||||
const candidates = resolveProfileAssets(matches, requirements.installKind);
|
||||
if (candidates.length) {
|
||||
status.textContent += " Possible files are shown below.";
|
||||
resultEyebrow.textContent = "Narrowed firmware candidates";
|
||||
@@ -2100,7 +2212,8 @@
|
||||
entry.profile,
|
||||
entry.asset,
|
||||
entry.installKind,
|
||||
filters
|
||||
filters,
|
||||
cardOptions
|
||||
);
|
||||
});
|
||||
result.hidden = false;
|
||||
@@ -2110,7 +2223,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const installKind = filters.install;
|
||||
const installKind = requirements.installKind;
|
||||
const resolved = resolveProfileAssets(matches, installKind);
|
||||
if (!resolved.length) {
|
||||
missing.hidden = false;
|
||||
@@ -2132,7 +2245,8 @@
|
||||
entry.profile,
|
||||
entry.asset,
|
||||
entry.installKind,
|
||||
filters
|
||||
filters,
|
||||
cardOptions
|
||||
);
|
||||
});
|
||||
result.hidden = false;
|
||||
@@ -2380,12 +2494,14 @@
|
||||
firmwareProfileChoices: firmwareProfileChoices,
|
||||
selectionProgress: selectionProgress,
|
||||
choicesUseSameFirmware: choicesUseSameFirmware,
|
||||
selectionRequirements: selectionRequirements,
|
||||
selectionUrl: selectionUrl,
|
||||
selectionFromUrl: selectionFromUrl,
|
||||
canonicalAsset: canonicalAsset,
|
||||
resolveProfileAssets: resolveProfileAssets,
|
||||
shouldShowCandidateResults: shouldShowCandidateResults,
|
||||
runtimeDirections: runtimeDirections,
|
||||
runtimePanelDirections: runtimePanelDirections,
|
||||
renderRuntimeDirections: renderRuntimeDirections,
|
||||
installSteps: installSteps,
|
||||
migrationReleaseUrl: migrationReleaseUrl,
|
||||
|
||||
+17
-11
@@ -59,17 +59,23 @@ as explicit alternatives with different routing capacities. Build one with
|
||||
it over LoRa with `ota pull <MID8> flash` and `ota install` on compatible
|
||||
hardware and partition layouts.
|
||||
|
||||
Standard and reduced OTA images, including images that fit the 1.25 MiB
|
||||
portable application slot, remain available and remain in the standard matrix
|
||||
pass. Select `--build-profile standard` to retain the original target and
|
||||
partition contract. Moving from a plain target to its MQTT sibling changes the
|
||||
LoRa OTA target identity; a partition layout change needs its matching
|
||||
migration step.
|
||||
For the normal-role partition migrations, the matrix also publishes a
|
||||
canonical Full image under the normal target identity. Install its matching
|
||||
merged image over USB once; mOTA rejects the incompatible partition signature
|
||||
until that migration is complete, then later Full updates use the normal
|
||||
target ID. The portable image remains available while deployed nodes move.
|
||||
The ordinary ESP32 release publishes Full images. Standard and reduced OTA
|
||||
recipes, including those that fit a 1.25 MiB portable application slot, remain
|
||||
available through explicit builds for recovery and migration. Select
|
||||
`--build-profile standard` to retain an exact target's original partition
|
||||
contract. They are omitted from the ordinary release and interactive board
|
||||
menu. That menu also omits transport/default aliases covered by Full and
|
||||
development utilities; it retains distinct hardware, roles, storage, and
|
||||
measured capacity exceptions. Exact legacy names remain accepted by
|
||||
`build-firmware` and `build-matching-firmwares`.
|
||||
|
||||
For the normal-role partition migrations, the release publishes a canonical
|
||||
Full image under the normal target identity. Use its qualified wireless
|
||||
expansion package when one exists, or install its matching merged image over
|
||||
USB. mOTA rejects an incompatible partition signature until that migration
|
||||
is complete, then later Full updates use the normal target ID. Moving from a
|
||||
plain target to a differently named sibling can also change the LoRa OTA
|
||||
identity; partition expansion does not by itself authorize that transition.
|
||||
|
||||
Legacy supported ESP32 Wi-Fi repeaters can use the
|
||||
[Wi-Fi partition migration](esp32_wifi_partition_migration.md) bridge to
|
||||
|
||||
+19
-13
@@ -11,13 +11,15 @@ 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.
|
||||
**Same firmware** marks choices that do not narrow the builds any further;
|
||||
they remain usable because they can change the setup instructions.
|
||||
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 in one choice.
|
||||
or storage differences when a choice between different images is needed.
|
||||
|
||||
The picker reads public release metadata from GitHub. It does not upload device
|
||||
information. Hardware names, target names, and download links come directly
|
||||
@@ -28,11 +30,12 @@ from the published firmware assets.
|
||||
<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, prefer a
|
||||
<strong>FULL / complete profile</strong> when it is available, and select
|
||||
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. Other chip families retain their qualified profiles.
|
||||
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
|
||||
@@ -180,7 +183,7 @@ from the published firmware assets.
|
||||
| USB logging / USB-connected MQTT | Node remains attached to a computer over a data-capable USB cable |
|
||||
| Wi-Fi MQTT observer | Firmware connects directly to MQTT over Wi-Fi; this is not USB logging |
|
||||
| USB logging + Wi-Fi MQTT | Unified FULL image sends to both paths; avoid two publishers aimed at the same broker unless messages are deduplicated |
|
||||
| No logging | Normal standalone operation without the dedicated logging/MQTT profile |
|
||||
| No logging | Normal standalone operation with external logging output disabled |
|
||||
| Receives LoRa OTA | Repeater, room server, or sensor image that can stage an exact matching update received over LoRa; any sensor/storage tradeoff appears on the same Firmware profile button |
|
||||
| Receives LoRa OTA - Reduced optional sensors | Compact OTA image that omits selected optional environmental/ranging drivers while retaining generic I2C and supported board peripherals |
|
||||
| Reduced sensors + LoRa OTA | Qualified nRF52 repeater, room server, or sensor build with the reduced optional sensor recipe and LoRa OTA retained |
|
||||
@@ -193,8 +196,9 @@ 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 under **Logging / MQTT**; it is an output mode,
|
||||
not a second connection choice. Full ESP32 LoRa repeaters include runtime
|
||||
RS-232 image. 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
|
||||
capacity exceptions. Ethernet still needs its exact hardware recipe.
|
||||
|
||||
@@ -278,11 +282,11 @@ guess cannot confirm the installed layout. See the
|
||||
that command.
|
||||
|
||||
Current `full-usb-wifi` profiles use one binary for no external output, USB
|
||||
packet logging/USB-connected MQTT, direct WiFi MQTT, or both. The picker shows
|
||||
that same exact binary for each compatible logging choice; select the saved
|
||||
packet logging/USB-connected MQTT, direct WiFi MQTT, or both. The picker skips
|
||||
the logging filter when these modes use the same file; select the saved
|
||||
runtime mode with `set logging.output off|usb|wifi|both`. A FULL logging-fallback
|
||||
profile is listed only when no WiFi MQTT sibling exists; it appears for both
|
||||
the no-output and USB choices because `set usb.logging off|on` is persistent.
|
||||
profile is listed only when no WiFi MQTT sibling exists; it supports both
|
||||
the no-output and USB settings because `set usb.logging off|on` is persistent.
|
||||
On a fresh unified FULL install with no saved SSID, the setup AP and WiFi radio
|
||||
remain available for 30 minutes per boot, then turn off automatically until the
|
||||
next reboot or power cycle. An explicit administrator `start webconfig` remains
|
||||
@@ -450,6 +454,8 @@ buttons. They separate revisions, display type, expansion kit, radio/PA layout,
|
||||
pin map, and other physical differences without crowding the Hardware dropdown.
|
||||
**Firmware profile** buttons combine OTA support, Full/standard, and choices
|
||||
that need a different image or wiring, such as serial port or external storage.
|
||||
The picker skips this step when all available profile choices lead to the
|
||||
same firmware file.
|
||||
Future nRF52 repeater, room-server and sensor releases publish two separately
|
||||
qualified choices: **Reduced sensors + LoRa OTA** and
|
||||
**Full supported sensors + LoRa OTA**, with `-reduced-ota` and `-full-ota` artifact suffixes. Each keeps
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
# Further firmware choice consolidation audit — 6 October 2026
|
||||
|
||||
Baseline: pulled commit `08bf174ea30e2e075cdd9fc74111580662b461bb`.
|
||||
The user requested fewer choices after the ESP32 Full-only release change.
|
||||
This follow-up combines menu/picker changes with a source,
|
||||
resolved-configuration, and retained-artifact audit. It did not build a matched
|
||||
nRF52 Full/Reduced pair or run hardware tests. Image-removal candidates below
|
||||
remain unqualified; the implemented changes simplify selection without changing
|
||||
firmware sources, partition layouts, or deployed update identities.
|
||||
|
||||
## Implemented selection changes
|
||||
|
||||
The interactive board menu now uses canonical Full ESP32 targets and omits
|
||||
transport/default aliases, compact recovery recipes, and development utilities.
|
||||
On nRF52, normal names and aliases that already build the exact same qualified
|
||||
Full/Reduced pair appear once. Distinct external-storage and deployed OTA IDs
|
||||
remain selectable. Exact hidden names remain directly buildable.
|
||||
|
||||
| Platform | Supported exact names before | Ordinary menu choices after |
|
||||
| --- | ---: | ---: |
|
||||
| ESP32 | 638 | 217 |
|
||||
| nRF52 | 403 | 209 |
|
||||
| RP2040 | 25 | 19 |
|
||||
| STM32 | 16 | 16 |
|
||||
| Total | 1,082 | 461 |
|
||||
|
||||
These are menu targets, including KISS; they are not firmware-file counts. The
|
||||
current ordinary ESP32 release still has **174 Full node images** (52 Companion,
|
||||
58 Repeater, 47 Room Server, 17 Sensor). The pull added two Station G3 Sensor
|
||||
recipes to the previous 172-image plan. No Full image was retired in this
|
||||
follow-up. The [selector evidence](firmware_choice_followup_selection_2026-10-06.json)
|
||||
records exact menu/release agreement, zero duplicate menu names, and the
|
||||
remaining OTA identity constraints.
|
||||
|
||||
After selecting an exact board and role, the web picker skips logging,
|
||||
connection, and profile filters when they do not distinguish firmware builds.
|
||||
Logging and independent bridge controls remain in the result's setup directions;
|
||||
linked logging/bridge settings still select their corresponding commands.
|
||||
A sole install format is inferred; multiple install operations remain explicit.
|
||||
Real alternatives stay visible even after mutually narrowing filters are selected.
|
||||
The measured T-LoRa split and nRF52 sensor/storage alternatives remain available.
|
||||
|
||||
The complete build-profile suite, 28 ordinary release-selection tests, and two
|
||||
picker-package tests passed. Picker tests cover skipped requirements, real
|
||||
alternatives, install formats, existing share links, and runtime command rendering.
|
||||
The complete form was also exercised in the in-app browser using local replayed
|
||||
release metadata: a sole app-only Full download, explicit merged versus app-only
|
||||
installation, nRF52 Full/Reduced UF2 versus ZIP choices, runtime logging changes,
|
||||
and a partial share link with WiFi logging and ESP-NOW enabled. The correct
|
||||
files and commands remained selected. This used fixture metadata and did not
|
||||
download or install firmware, contact a device, or publish the website.
|
||||
|
||||
## nRF52 sensor pairs: policy versus demonstrated need
|
||||
|
||||
At this baseline, `is_nrf52_sensor_ota_pair_target()` in
|
||||
[`build_legacy.sh`](../../build_legacy.sh) includes every nRF52 repeater,
|
||||
room-server, and sensor recipe, including external-QSPI and SD recipes.
|
||||
`run_nrf52_sensor_ota_pair()` requires Full to pass, then builds Reduced; a
|
||||
successful Reduced build cannot replace a failed Full build. Thus the pair is
|
||||
a blanket publication policy, not evidence that each board needs two images.
|
||||
|
||||
| Classification | Evidence | Defensible action |
|
||||
| --- | --- | --- |
|
||||
| Same declared sensor functionality | The 31 concrete recipes below enable none of the drivers removed by Reduced. GPS is outside the omitted set. | Present Full as the ordinary choice. Treat image deduplication as a candidate until packaged applications are compared. |
|
||||
| Actual optional-driver difference | Other recipes can enable environmental/ranging drivers; non-RAK recipes can also lose INA monitors. RAK3401/4631 Reduced keeps the four INA drivers and compatible GPS. | Full for normal installs; Reduced can remain an advanced/recovery choice with its missing drivers stated. Disabling a sensor at runtime does not remove its compiled flash cost. |
|
||||
| Demonstrated internal-OTA headroom constraint | The historical RAK3401 compact chain has several exact transitions with zero staging margin. Current staging capacity depends on the installed image and exact delta, not just the board's nominal flash. | Preserve qualified reduced endpoints and exact-base update routes. This is not proof that every current Full/Reduced pair is necessary. |
|
||||
| Current measured mandatory pair | No current matched Full/Reduced application and delta-package comparison was found in the retained local evidence. | Do not claim a current pair is required by measured capacity, or claim Reduced can safely be deleted everywhere. |
|
||||
|
||||
The baseline [`CLI matrix`](../cli_build_matrix.md) explicitly acknowledges
|
||||
that boards without optional sensor drivers still receive two named choices,
|
||||
with small or zero savings. This audit found no qualified artifact pair proving
|
||||
byte-identical firmware for those boards. The retained RAK3401 1.17.1.8 Reduced
|
||||
[RAM report](../test-results/preview-1.17.1.8/RAK_3401_repeater_lora_ota_no_external_sensors-ota-v1.17.1.8-halo-keymind-cascade-dev-d5853b9c.memory.json)
|
||||
passes with 67,660 available bytes versus 66,904 required, a 756-byte margin;
|
||||
it is historical, not a current matched-pair comparison. The retained T1000-E
|
||||
reports concern Full Companion, outside this infrastructure pair policy.
|
||||
|
||||
Full already includes targeted RAM capacity tuning: T096/T1 repeaters use four
|
||||
flood rules, and RAK3401 unified uses 47. The baseline script records these as
|
||||
measured internal-RAM reductions while retaining the Full sensor support.
|
||||
They justify tuning within Full, not a second sensor profile by themselves.
|
||||
|
||||
## 31 concrete recipes with no enabled removable sensor driver
|
||||
|
||||
Source: `out/esp32-full-audit-2026-10-06/platformio-current-resolved.json`, the
|
||||
current resolved configuration retained for the baseline audit. Definitions
|
||||
and undefinitions were processed in order, and definitions equal to zero were
|
||||
excluded. The tested omitted set is the 12 environmental/ranging macros in
|
||||
`apply_lora_ota_no_external_sensors_profile()`, plus four INA macros except on
|
||||
the RAK recipes which retain them. This is a recipe-level result, not an image
|
||||
hash result or a count of canonical published outputs.
|
||||
|
||||
```text
|
||||
Heltec_mesh_solar_repeater
|
||||
Heltec_mesh_solar_room_server
|
||||
Heltec_tower_v2_repeater
|
||||
Heltec_tower_v2_room_server
|
||||
KeepteenLT1_repeater
|
||||
KeepteenLT1_room_server
|
||||
LilyGo_T-Echo_Card_repeater
|
||||
LilyGo_T-Echo_Card_room_server
|
||||
LilyGo_T-Echo-Lite_repeater
|
||||
LilyGo_T-Echo-Lite_room_server
|
||||
MeshTracker_X1_repeater
|
||||
MeshTracker_X1_room_server
|
||||
Mesh_pocket_repeater
|
||||
Mesh_pocket_room_server
|
||||
muzi_base_duo_repeater
|
||||
muzi_base_duo_room_server
|
||||
muzi_base_uno_repeater
|
||||
muzi_base_uno_room_server
|
||||
Nano_G2_Ultra_repeater
|
||||
Nano_G2_Ultra_room_server
|
||||
t1000e_repeater
|
||||
t1000e_room_server
|
||||
t1000e_sensor
|
||||
ThinkNode_M1_repeater
|
||||
ThinkNode_M1_room_server
|
||||
ThinkNode_M3_repeater
|
||||
ThinkNode_M3_room_server
|
||||
ThinkNode_M4_repeater
|
||||
ThinkNode_M4_room_server
|
||||
ThinkNode_M8_repeater
|
||||
ThinkNode_M8_room_server
|
||||
```
|
||||
|
||||
`ThinkNode_M1_repeater` already declares external QSPI OTA. The plain MeshTower
|
||||
repeater name is an alias for the SD primary, so this list must not be blindly
|
||||
converted into 31 release removals. Aliases, full source options, board headers,
|
||||
storage layout, and deployed target identities must be resolved first.
|
||||
|
||||
## Qualification before removing an image
|
||||
|
||||
Start by showing Full for ordinary installation and moving Reduced to advanced
|
||||
or recovery selection. Preserve existing artifacts and update identities while
|
||||
qualifying actual duplicate removal. For each candidate:
|
||||
|
||||
1. Build both profiles sequentially from the same source/toolchain, with the
|
||||
existing flash, RAM, capability, packaged-application, and OTA checks.
|
||||
2. Extract the DFU application's `bin_file` from its ZIP manifest and compare
|
||||
its complete bytes, EndF identity/body hash, image length, and layout. Outer
|
||||
UF2/ZIP hashes alone can differ because of packaging and are insufficient.
|
||||
3. If applications differ, measure the driver/capacity difference. For internal
|
||||
OTA, create exact-base deltas from representative installed Full and Reduced
|
||||
images and verify staging geometry, reconstruction, and the matched deployed
|
||||
bootloader. A passing local-install image does not prove a delta fits.
|
||||
4. Change publication validation and fixtures deliberately before emitting one
|
||||
artifact. [`validate_nrf52_sensor_pairs()`](../../scripts/package_cascade_release.py)
|
||||
currently requires exactly one qualified Full and one Reduced with matching
|
||||
target, hardware, version, source, and layout; it does not prove either member
|
||||
saves space or is needed. Preserve legacy endpoints needed by qualified chains.
|
||||
|
||||
Focused existing checks include `test/test_build_profiles.sh`,
|
||||
`test/test_nrf52_ota_profiles.sh`, `test/test_ota_store_flash_nrf52_hybrid.py`,
|
||||
`test/test_rak_storage_policy.py`, and the release-packaging tests. These check
|
||||
policy and boundary behavior; they do not substitute for matched image and
|
||||
exact-delta evidence. No PlatformIO process was started for this audit.
|
||||
|
||||
## Storage and bootloader choices that need real distinctions
|
||||
|
||||
[`OtaFlashLayout_nrf52.h`](../../src/helpers/ota/OtaFlashLayout_nrf52.h) derives
|
||||
internal staging space from the trusted live EndF application extent, rounded
|
||||
to a 4 KiB page, and the effective bootloader ceiling.
|
||||
[`OtaStoreFlashNrf52.cpp`](../../src/helpers/ota/OtaStoreFlashNrf52.cpp) adds
|
||||
64 KiB of retained RAM only with the matching `MOTARAMA` capability and at least
|
||||
one available flash page. Its hybrid application path accepts deltas, not full
|
||||
application containers. A **Full sensor firmware** can still be the destination
|
||||
of a delta; the two uses of “Full” must not be confused.
|
||||
|
||||
The historical [RAK3401 chain](rak3401_mota_chain.md) proves tight exact-package
|
||||
geometry under an older bootloader: multiple transitions have zero margin;
|
||||
steps 1–9 were physically passed, while the replacement final step has offline
|
||||
reconstruction and exact-bootloader simulation only. It does not compare the
|
||||
current sensor pair and must not be generalized to all nRF52 boards.
|
||||
|
||||
External NOR/SD removes the internal container-staging bottleneck, but does not
|
||||
increase executable internal flash or RAM. Existing runtime consolidation is
|
||||
already available for qualified [RAK unified storage](../ota_nrf52_qspi.md) and
|
||||
[MeshTower SD/internal fallback](../ota_meshtower_v2_sdcard.md). Keep the exact
|
||||
NOR wiring, bootloader device/storage records, target IDs, and per-transfer
|
||||
store selection. MeshTower's old internal-only identity needs local migration;
|
||||
an alias does not authorize same-target LoRa replacement. Its internal fallback
|
||||
can reject a delta that fits on SD.
|
||||
|
||||
The source explicitly rejects raw OTA QSPI sharing a chip with the QSPIFLASH
|
||||
filesystem, and WisBlock OTA SPI sharing incompatible Ethernet/SD wiring.
|
||||
RAK3401 flash and the 1 W radio need distinct chip selects and the explicit bus
|
||||
handoff contract. Combining those recipes requires hardware/layout evidence,
|
||||
not just runtime sensor switches. Extra-filesystem and bootloader-scratch
|
||||
layouts likewise cannot be relabeled as the same OTA target.
|
||||
|
||||
## Other possible menu simplifications
|
||||
|
||||
The current ESP32 replay reports 174 Full choices, including the two newly
|
||||
pulled Station G3 sensor recipes. Its evidence file
|
||||
`out/esp32-full-audit-2026-10-06/wifi_review_current_consolidation.json` finds two
|
||||
equivalent source-option pairs: Heltec V4 base/Expansion Kit OLED repeaters, and
|
||||
their room-server recipes. Repeated identical I2C pin definitions are the only
|
||||
semantic option difference, but their mOTA target IDs differ. Simplify selection
|
||||
first; retain deployed update artifacts until an explicit migration is reviewed.
|
||||
The Expansion Kit TFT Companion has different display/sensor wiring.
|
||||
|
||||
Pico W already has a combined USB/WiFi/BLE Companion recipe; it is a candidate
|
||||
to replace transport-specific ordinary choices after fresh capacity and
|
||||
transport qualification. This audit has no current qualifying build result.
|
||||
STM32WL's retained flash reports support capacity tuning of its existing
|
||||
256 KiB applications; they do not show a removable duplicate role. Repeater,
|
||||
room-server, sensor, Companion/Terminal Chat, and KISS remain different
|
||||
applications unless a separate combined application is designed and tested.
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"source_commit_current": "08bf174ea30e2e075cdd9fc74111580662b461bb",
|
||||
"cache": "platformio-current-resolved.json",
|
||||
"primary_cache_method": "Exact current resolved cache; remove11 local HIL env sections absent from primary tracked platformio.ini/variants recipes. Local file adds no ordinary overrides.",
|
||||
"planned_current_slots": 174,
|
||||
"current_profiles": {
|
||||
"full": 174
|
||||
},
|
||||
"passes": {
|
||||
"run_logging_matrix_build_targets": 52,
|
||||
"run_full_only_esp32_profile": 122
|
||||
},
|
||||
"duplicate_target_identities": [],
|
||||
"semantically_distinct_resolved_base_recipes": 172,
|
||||
"duplicate_groups": [
|
||||
[
|
||||
{
|
||||
"target": "heltec_v4_repeater",
|
||||
"compiled_env": "heltec_v4_repeater_observer_mqtt"
|
||||
},
|
||||
{
|
||||
"target": "heltec_v4_expansionkit_repeater",
|
||||
"compiled_env": "heltec_v4_expansionkit_repeater_observer_mqtt"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"target": "heltec_v4_room_server",
|
||||
"compiled_env": "heltec_v4_room_server_observer_mqtt"
|
||||
},
|
||||
{
|
||||
"target": "heltec_v4_expansionkit_room_server_observer_mqtt",
|
||||
"compiled_env": "heltec_v4_expansionkit_room_server_observer_mqtt"
|
||||
}
|
||||
]
|
||||
],
|
||||
"caveat": "Two semantically equivalent recipe pairs have distinct mOTA target IDs. This does not authorize relabeling artifacts or app-only OTA across IDs/layouts.",
|
||||
"menu_counts": {
|
||||
"supported_ESP32_logical_targets": 638,
|
||||
"interactive_ESP32_targets_including_KISS": 217,
|
||||
"interactive_ESP32_node_Full_choices": 174,
|
||||
"interactive_KISS_choices": 43,
|
||||
"supported_roles": {
|
||||
"repeater": 196,
|
||||
"Terminal_Chat": 23,
|
||||
"room_server": 121,
|
||||
"legacy_Companion": 162,
|
||||
"KISS": 43,
|
||||
"development_or_auxiliary": 21,
|
||||
"sensor": 17,
|
||||
"Full_Companion": 55
|
||||
},
|
||||
"interactive_roles": {
|
||||
"KISS": 43,
|
||||
"Full_Companion": 52,
|
||||
"repeater": 58,
|
||||
"room_server": 47,
|
||||
"sensor": 17
|
||||
},
|
||||
"ordinary_menu_missing_from_matrix": [],
|
||||
"matrix_missing_from_ordinary_menu": [],
|
||||
"duplicate_menu_targets": [],
|
||||
"ambiguous_board_variant_labels": [],
|
||||
"primary_ESP32_raw_environments": 502,
|
||||
"primary_ESP32_generated_aliases": 136
|
||||
},
|
||||
"source_equivalence_notice": "Software source/configuration equivalence is not binary equivalence: logical target names generate distinct mOTA IDs, image identity records, recipe digests, artifact names, and signed/hashed package contents.",
|
||||
"identity_and_layout": [
|
||||
{
|
||||
"targets": {
|
||||
"heltec_v4_repeater": "0xe792a051",
|
||||
"heltec_v4_expansionkit_repeater": "0xd522a14f"
|
||||
},
|
||||
"storage_equivalence": "Both same board heltec_v4, 16MiB flash, infrastructure Full overlay default_16MB.csv expanded dual OTA. All cached board/storage/sourcefilter options equal.",
|
||||
"migration_gate": "Current get_exact_identity_full_migration_target allows observer suffix to matching plain prefix. It does not map Expansion Kit prefix to base V4. Existing deployed firmware may lack any successor alias.",
|
||||
"safe_next_step": "Consolidate fresh install selection while retaining exact legacy IDs/artifacts and direct links. Retire legacy update feeds only after explicitly reviewed same-layout successor migration. Never relabel an artifact or bypass its EndF target check."
|
||||
},
|
||||
{
|
||||
"targets": {
|
||||
"heltec_v4_room_server": "0xeed78ce4",
|
||||
"heltec_v4_expansionkit_room_server_observer_mqtt": "0x21ee03bd"
|
||||
},
|
||||
"storage_equivalence": "Both same board heltec_v4, 16MiB flash, infrastructure Full overlay default_16MB.csv expanded dual OTA. All cached board/storage/sourcefilter options equal.",
|
||||
"migration_gate": "Current get_exact_identity_full_migration_target allows observer suffix to matching plain prefix. It does not map Expansion Kit prefix to base V4. Existing deployed firmware may lack any successor alias.",
|
||||
"safe_next_step": "Consolidate fresh install selection while retaining exact legacy IDs/artifacts and direct links. Retire legacy update feeds only after explicitly reviewed same-layout successor migration. Never relabel an artifact or bypass its EndF target check."
|
||||
}
|
||||
],
|
||||
"source_refs": {
|
||||
"equivalent_V4_OLED_hardware": "variants/heltec_v4/platformio.ini:55",
|
||||
"duplicate_repeater": "variants/heltec_v4/platformio.ini:223",
|
||||
"duplicate_room_server": "variants/heltec_v4/platformio.ini:366",
|
||||
"current_interactive_helper": "build_legacy.sh:6548",
|
||||
"Full_source_substitution": "build_legacy.sh:2492",
|
||||
"same_partition_alias_gate": "build_legacy.sh:2516",
|
||||
"Full_partition_layout": "scripts/esp32_full_partition.py:94",
|
||||
"automatic_target_choice": "src/helpers/ota/OtaManager.cpp:1149",
|
||||
"received_manifest_target_check": "src/helpers/ota/OtaManager.cpp:1514",
|
||||
"observer_layout_capacity_split": "build_legacy.sh:6500",
|
||||
"RC32_optional_display_requires_review": "variants/heltec_rc32/platformio.ini:68",
|
||||
"R8_portrait_compile_time_layout": "variants/heltec_v4_r8/platformio.ini:373",
|
||||
"portrait_geometry_driver": "src/helpers/ui/ST7789LCDDisplay.h:25",
|
||||
"portrait_dashboard_layout": "src/helpers/ui/ObserverDashboard.h:113",
|
||||
"fresh_vs_resume_filter_tests": "test/test_local_release_full_selection.py:96"
|
||||
},
|
||||
"further_options": [
|
||||
{
|
||||
"proposal": "HeltecV4 OLED Expansion Kit software-source alias consolidation",
|
||||
"remove_ordinary_choices": 2,
|
||||
"readiness": "UI/fresh-install recommendation safe while exact legacy identities remain available; release artifact retirement awaits explicit reviewed identity migration",
|
||||
"fresh_count_if_only_this_change": 172,
|
||||
"keep_distinct": [
|
||||
"heltec_v4_expansionkit_tft_companion_radio_full_femon (different display/sensor wiring)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"proposal": "R8 TFT portrait/landscape runtime layout",
|
||||
"remove_ordinary_choices": 2,
|
||||
"readiness": "Requires actual UI/driver changes, qualification and display testing. Existing display.flip rotates180 and preserves geometry; it does not choose portrait vs landscape."
|
||||
},
|
||||
{
|
||||
"proposal": "RC32 display/headless runtime optional peripheral",
|
||||
"remove_ordinary_choices": 4,
|
||||
"readiness": "Requires runtime display-presence/power controls and hardware/heap checks. Existing source filters/driver pin behavior differ; not currently software-source equivalent."
|
||||
}
|
||||
],
|
||||
"role_distinctions": "52 Full Companion,58 repeater,47 room-server,17 sensor ordinary choices select genuinely different applications/source filters and preference/storage/transport behavior. No existing runtime role switch permits consolidating those application roles without new firmware architecture.",
|
||||
"peer_review": {
|
||||
"interactive_helper": "Uses same canonical release selectors; preserves KISS host-modem role and every174 ordinary Full identity. No duplicate board/variant labels and no source/partition/recipe modifications. All old exact targets remain supported by direct-build APIs.",
|
||||
"tests": "New real-inheritance block tests TLora UART/MQTT split, nRF52 external-storage variants, hiding legacy transports/reduced/utility names while preserving exact-build support, and submenu integration. Existing host suite passed per root; this audit additionally compares complete current menu with174 planned release choices.",
|
||||
"fresh_resume_publication": "Current release filter drops standard/reduced/transport/default/fixture artifacts on resume and requires verified Full. Same-role observer/plain siblings are selected by capability/identity priorities; do not infer that source-equivalent V4 alias pairs are binary-equivalent.",
|
||||
"nRF_pair_dedup": "Groups only through get_nrf52_sensor_ota_pair_target output identity. Alias-first ordering prefers non-LoRa-only ordinary name; tests retain external-storage and distinct deployed IDs. No suffix-only or cross-hardware grouping."
|
||||
},
|
||||
"final_menu_counts_all_platforms": {
|
||||
"supported_target_names": 1082,
|
||||
"final_interactive_choices": 461,
|
||||
"removed_redundant_menu_names": 621,
|
||||
"supported_by_platform": {
|
||||
"ESP32_PLATFORM": 638,
|
||||
"NRF52_PLATFORM": 403,
|
||||
"RP2040_PLATFORM": 25,
|
||||
"STM32_PLATFORM": 16
|
||||
},
|
||||
"menu_by_platform": {
|
||||
"ESP32_PLATFORM": 217,
|
||||
"NRF52_PLATFORM": 209,
|
||||
"RP2040_PLATFORM": 19,
|
||||
"STM32_PLATFORM": 16
|
||||
},
|
||||
"menu_roles_by_platform": {
|
||||
"ESP32_PLATFORM": {
|
||||
"KISS": 43,
|
||||
"Full_Companion": 52,
|
||||
"repeater": 58,
|
||||
"room_server": 47,
|
||||
"sensor": 17
|
||||
},
|
||||
"NRF52_PLATFORM": {
|
||||
"repeater": 59,
|
||||
"room_server": 47,
|
||||
"KISS": 45,
|
||||
"sensor": 8,
|
||||
"legacy_Companion": 3,
|
||||
"Full_Companion": 47
|
||||
},
|
||||
"RP2040_PLATFORM": {
|
||||
"repeater": 4,
|
||||
"room_server": 4,
|
||||
"legacy_Companion": 7,
|
||||
"KISS": 4
|
||||
},
|
||||
"STM32_PLATFORM": {
|
||||
"repeater": 5,
|
||||
"sensor": 3,
|
||||
"legacy_Companion": 4,
|
||||
"KISS": 4
|
||||
}
|
||||
},
|
||||
"duplicate_menu_targets": []
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -124,7 +124,7 @@ extra_css:
|
||||
extra_javascript:
|
||||
- _javascript/notification_builder.js?v=20261006-channel9
|
||||
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
|
||||
- _javascript/firmware_picker.js?v=20261001-feedback-1
|
||||
- _javascript/firmware_picker.js?v=20261006-simplify-1
|
||||
- _javascript/telemetry_decoder.js
|
||||
- _javascript/management_decoder.js
|
||||
- _javascript/filter_tool.js?v=20261002-hashbytes-2plus
|
||||
|
||||
@@ -438,6 +438,51 @@ require(rak_usb, "build_flags", "FORCE_GPS_ALIVE")
|
||||
# replace each with that exact board's Full target.
|
||||
init_project_context >/dev/null
|
||||
|
||||
# The interactive board menu must offer the same ordinary Full choices as the
|
||||
# release, while exact compatibility/development recipes stay directly usable.
|
||||
# Test real board inheritance: dropping aliases by suffix alone would also
|
||||
# discard the measured TLora transport split or special nRF52 storage wiring.
|
||||
(
|
||||
mapfile -t menu_targets < <(get_interactive_build_targets)
|
||||
menu_text=$(printf '%s\n' "${menu_targets[@]}")
|
||||
for expected in Heltec_v3_companion_radio_full Heltec_v3_repeater \
|
||||
LilyGo_TLora_V2_1_1_6_repeater \
|
||||
LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_ \
|
||||
RAK_3401_repeater \
|
||||
RAK_4631_repeater_rak15001_slot_c_lora_ota \
|
||||
RAK_4631_repeater_w25q16_lora_ota \
|
||||
RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors \
|
||||
RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors; do
|
||||
grep -Fxq "$expected" <<<"$menu_text" \
|
||||
|| fail "ordinary board menu omitted needed choice $expected"
|
||||
done
|
||||
for hidden in Heltec_v3_companion_radio_usb Heltec_v3_companion_radio_ble \
|
||||
Heltec_v3_repeater_observer_mqtt \
|
||||
Heltec_v3_repeater_lora_ota_no_external_sensors \
|
||||
RAK_3401_repeater_lora_ota_no_external_sensors \
|
||||
heltec_v4_partition_migrator; do
|
||||
if grep -Fxq "$hidden" <<<"$menu_text"; then
|
||||
fail "ordinary board menu still offers redundant/recovery choice $hidden"
|
||||
fi
|
||||
is_supported_build_env "$hidden" \
|
||||
|| fail "hiding $hidden from the board menu removed its exact build"
|
||||
done
|
||||
board=$(get_board_family_for_env Heltec_v3_repeater)
|
||||
variants=$(get_variants_for_board "$board")
|
||||
grep -Fxq Heltec_v3_companion_radio_full <<<"$variants" \
|
||||
|| fail "board submenu lost its Full Companion"
|
||||
if grep -Eq 'companion_radio_(usb|ble|wifi)|_observer_mqtt|no_external_sensors' <<<"$variants"; then
|
||||
fail "board submenu reintroduced transport/profile choices"
|
||||
fi
|
||||
[ "$(get_nrf52_sensor_ota_pair_target RAK_3401_repeater)" \
|
||||
= "$(get_nrf52_sensor_ota_pair_target RAK_3401_repeater_lora_ota_no_external_sensors)" ] \
|
||||
|| fail "menu folded nRF52 targets with different output identities"
|
||||
# Prefer the ordinary name even when a caller supplies the alias first.
|
||||
SUPPORTED_PIO_ENVS=(RAK_3401_repeater_lora_ota_no_external_sensors RAK_3401_repeater)
|
||||
[ "$(get_interactive_build_targets)" = RAK_3401_repeater ] \
|
||||
|| fail "nRF52 menu depends on alias input order"
|
||||
)
|
||||
|
||||
# Expanded partitions make Full the ordinary ESP32 node release. Legacy lean
|
||||
# names still address installed firmware and must remain directly buildable,
|
||||
# but cannot reappear in either the all-role or a role-specific release list.
|
||||
|
||||
@@ -1743,3 +1743,218 @@ assert(!picker.choicesUseSameFirmware([], {}, 'role', feedbackOptions(['companio
|
||||
assert.strictEqual(JSON.stringify(feedbackCatalog), feedbackSnapshot);
|
||||
assert.deepStrictEqual(feedbackFilters, {hardware: 'Heltec_v3', role: 'companion'});
|
||||
console.log('build counts, non-refining choices, install safety and partial-link feedback passed');
|
||||
|
||||
// Exact board and role are deliberate intent. Runtime settings and a sole
|
||||
// available install format must not keep an already identified download locked.
|
||||
const simpleFilters = {...feedbackFilters, hardwareFamily: 'Heltec_v3'};
|
||||
const simpleRequirements = picker.selectionRequirements(feedbackCatalog, simpleFilters);
|
||||
assert.deepStrictEqual(simpleRequirements.hiddenFields, ['logging', 'firmwareProfile', 'mode']);
|
||||
assert.deepStrictEqual(simpleRequirements.missingFields, ['install']);
|
||||
for (const install of ['bin', 'merged-bin']) {
|
||||
const requirements = picker.selectionRequirements(feedbackCatalog, {...simpleFilters, install});
|
||||
assert.deepStrictEqual(requirements.missingFields, []);
|
||||
assert.strictEqual(requirements.installKind, install);
|
||||
}
|
||||
const soleInstallCatalog = [{...feedbackCatalog[0], installKinds: ['bin']}];
|
||||
const soleInstall = picker.selectionRequirements(soleInstallCatalog, simpleFilters);
|
||||
assert.deepStrictEqual(soleInstall.hiddenFields, ['logging', 'firmwareProfile', 'mode', 'install']);
|
||||
assert.deepStrictEqual(soleInstall.missingFields, []);
|
||||
assert.strictEqual(soleInstall.installKind, 'bin');
|
||||
assert.deepStrictEqual(picker.resolveProfileAssets(soleInstallCatalog, soleInstall.installKind)
|
||||
.map(entry => entry.asset.name), [picker.canonicalAsset(soleInstallCatalog[0].files, 'bin').name]);
|
||||
const soleUf2 = picker.selectionRequirements([{...nrf, installKinds: ['uf2']}], {
|
||||
hardwareFamily: nrf.hardwareFamily, hardware: nrf.hardware, role: nrf.role,
|
||||
});
|
||||
assert.strictEqual(soleUf2.installKind, 'uf2');
|
||||
assert(soleUf2.hiddenFields.includes('install'));
|
||||
assert(picker.installSteps(nrf, soleUf2.installKind).some(step => step.includes('UF2')));
|
||||
for (const filters of [{}, {hardware: 'Heltec_v3'}, {role: 'companion'}]) {
|
||||
const requirements = picker.selectionRequirements(soleInstallCatalog, filters);
|
||||
assert.deepStrictEqual(requirements.hiddenFields, []);
|
||||
if (!filters.hardware) assert(requirements.missingFields.includes('hardware'));
|
||||
if (!filters.role) assert(requirements.missingFields.includes('role'));
|
||||
}
|
||||
assert(picker.selectionRequirements([], simpleFilters).missingFields.includes('install'));
|
||||
|
||||
// Unsupported choices in an old/manual URL must remain visible to clear.
|
||||
// Inferring the sole BIN for an explicitly requested ZIP would conceal the
|
||||
// invalid constraint while actual matching still returned no files.
|
||||
const usbOnlyCatalog = [{...soleInstallCatalog[0],
|
||||
loggingModes: ['none', 'usb'], connectionModes: ['usb']}];
|
||||
for (const [field, value] of [['install', 'zip'], ['logging', 'wifi'], ['mode', 'wifi'],
|
||||
['feature', 'missing'], ['ota', 'missing'], ['variant', 'missing']]) {
|
||||
const selection = {...simpleFilters, [field]: value};
|
||||
const before = JSON.stringify(selection);
|
||||
const requirements = picker.selectionRequirements(usbOnlyCatalog, selection);
|
||||
const control = picker.PROFILE_FIELDS.includes(field) ? 'firmwareProfile' : field;
|
||||
assert(!requirements.hiddenFields.includes(control), control + ' must stay clearable');
|
||||
assert.strictEqual(JSON.stringify(selection), before);
|
||||
assert.strictEqual(new URL(picker.selectionUrl('https://example.test/picker/', selection, false))
|
||||
.searchParams.get(field), value);
|
||||
if (field === 'install') assert.strictEqual(requirements.installKind, 'zip');
|
||||
}
|
||||
|
||||
// A selected setting that currently narrows to one image must remain visible
|
||||
// if another choice would select a different image.
|
||||
const distinctRequirements = picker.selectionRequirements(feedbackAlternatives, {
|
||||
...simpleFilters, logging: 'usb', mode: 'usb',
|
||||
feature: 'full', ota: 'lora-source', variant: 'default',
|
||||
});
|
||||
for (const field of ['logging', 'mode', 'firmwareProfile']) {
|
||||
assert(!distinctRequirements.hiddenFields.includes(field),
|
||||
field + ' must remain clearable even when other selected facets narrow to one image');
|
||||
}
|
||||
// Test each facet independently of other already narrowing choices.
|
||||
assert(!picker.selectionRequirements(feedbackAlternatives, {...simpleFilters, logging: 'usb'})
|
||||
.hiddenFields.includes('logging'));
|
||||
assert(!picker.selectionRequirements(feedbackAlternatives, {...simpleFilters, mode: 'usb'})
|
||||
.hiddenFields.includes('mode'));
|
||||
assert(!picker.selectionRequirements(feedbackAlternatives, {...simpleFilters,
|
||||
feature: 'full', ota: 'lora-source', variant: 'default'})
|
||||
.hiddenFields.includes('firmwareProfile'));
|
||||
assert.strictEqual(distinctRequirements.installKind, '');
|
||||
const tloraRequirements = picker.selectionRequirements(tloraCatalog.profiles, {
|
||||
hardwareFamily: tloraNormalProfile.hardwareFamily,
|
||||
hardware: tloraNormalProfile.hardware, role: 'repeater',
|
||||
});
|
||||
assert(!tloraRequirements.hiddenFields.includes('logging'));
|
||||
assert(!tloraRequirements.hiddenFields.includes('mode'));
|
||||
// Both capacity-safe images share the Full/OTA tuple. Logging and transport
|
||||
// still distinguish their different files; the identical tuple adds no choice.
|
||||
assert(tloraRequirements.hiddenFields.includes('firmwareProfile'));
|
||||
assert(tloraRequirements.missingFields.includes('logging'));
|
||||
assert(tloraRequirements.missingFields.includes('mode'));
|
||||
const selectedTlora = picker.selectionRequirements(tloraCatalog.profiles, {
|
||||
hardwareFamily: tloraNormalProfile.hardwareFamily,
|
||||
hardware: tloraNormalProfile.hardware, role: 'repeater',
|
||||
logging: 'wifi', mode: 'espnow', feature: 'full', ota: tloraObserverProfile.ota,
|
||||
variant: tloraObserverProfile.variant,
|
||||
});
|
||||
assert(!selectedTlora.hiddenFields.includes('logging'));
|
||||
assert(!selectedTlora.hiddenFields.includes('mode'));
|
||||
for (const item of sensorCatalog.profiles) {
|
||||
const requirements = picker.selectionRequirements(sensorCatalog.profiles, {
|
||||
hardwareFamily: item.hardwareFamily, hardware: item.hardware, role: item.role,
|
||||
});
|
||||
assert(!requirements.hiddenFields.includes('firmwareProfile'), item.target);
|
||||
assert(!requirements.hiddenFields.includes('install'), 'ZIP and UF2 stay distinct');
|
||||
const selected = picker.selectionRequirements(sensorCatalog.profiles, {
|
||||
hardwareFamily: item.hardwareFamily, hardware: item.hardware, role: item.role,
|
||||
logging: item.loggingModes[0], mode: item.mode, install: 'zip',
|
||||
ota: item.ota, feature: item.feature, variant: item.variant,
|
||||
});
|
||||
assert(!selected.hiddenFields.includes('firmwareProfile'),
|
||||
'Selected sensor profile must remain clearable: ' + item.target);
|
||||
}
|
||||
for (const partial of [{feature: 'full'}, {ota: 'lora-source'}]) {
|
||||
const selection = {...simpleFilters, ...partial};
|
||||
const before = JSON.stringify(selection);
|
||||
const requirements = picker.selectionRequirements(soleInstallCatalog, selection);
|
||||
assert.deepStrictEqual(requirements.missingFields, []);
|
||||
assert.strictEqual(JSON.stringify(selection), before);
|
||||
const link = picker.selectionUrl('https://example.test/picker/', selection, false);
|
||||
assert(!new URL(link).searchParams.has('install'));
|
||||
for (const field of picker.PROFILE_FIELDS) {
|
||||
if (!partial[field]) assert(!new URL(link).searchParams.has(field));
|
||||
}
|
||||
}
|
||||
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');
|
||||
|
||||
// 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']) {
|
||||
const sections = picker.runtimePanelDirections(observer, {logging}, ['logging', 'mode']);
|
||||
const section = sections.find(item => item.title === 'Logging / MQTT output');
|
||||
assert.deepStrictEqual(section.actions.map(action => action.selection.logging),
|
||||
['none', 'usb', 'wifi', 'both']);
|
||||
assert.strictEqual(section.actions[section.selectedIndex].selection.logging, logging);
|
||||
assert.deepStrictEqual(section.actions[section.selectedIndex].commands,
|
||||
picker.runtimeDirections(observer, {logging})[0].actions[0].commands);
|
||||
}
|
||||
const defaultLogging = picker.runtimePanelDirections(observer, {}, ['logging'])[0];
|
||||
assert.strictEqual(defaultLogging.actions[defaultLogging.selectedIndex].selection.logging, 'none');
|
||||
const nrfLogging = picker.runtimePanelDirections(nrf, {logging: 'usb'}, ['logging'])[0];
|
||||
assert.deepStrictEqual(nrfLogging.actions[nrfLogging.selectedIndex].commands, ['set usb.logging on reboot']);
|
||||
assert.strictEqual(picker.runtimePanelDirections(observer, {logging: 'usb'}, [])[0].actions.length, 1);
|
||||
for (const mode of ['', 'standard', 'rs232', 'espnow']) {
|
||||
const profile = tripleCatalog.profiles[0];
|
||||
const sections = picker.runtimePanelDirections(profile, {mode}, ['mode']);
|
||||
const uart = sections.find(section => /^RS232 bridge/.test(section.title));
|
||||
const wireless = sections.find(section => section.title === 'ESP-NOW bridge');
|
||||
assert.strictEqual(uart.actions[uart.selectedIndex].label, mode === 'rs232' ? 'On' : 'Off');
|
||||
assert.strictEqual(wireless.actions[wireless.selectedIndex].label, mode === 'espnow' ? 'On' : 'Off');
|
||||
}
|
||||
|
||||
// Exercise the actual command renderer, including the preselected radio and
|
||||
// later logging change, without sending anything to a device or the network.
|
||||
class RuntimeElement {
|
||||
constructor(tag, text = '') {
|
||||
this.tagName = tag.toUpperCase();
|
||||
this.children = [];
|
||||
this.dataset = {};
|
||||
this.listeners = {};
|
||||
this.ownText = text;
|
||||
}
|
||||
appendChild(child) { this.children.push(child); return child; }
|
||||
replaceChildren(...children) { this.children = children; this.ownText = ''; }
|
||||
addEventListener(name, handler) { this.listeners[name] = handler; }
|
||||
get textContent() { return this.ownText + this.children.map(child => child.textContent).join(''); }
|
||||
set textContent(text) { this.ownText = text; this.children = []; }
|
||||
}
|
||||
function descendants(element, predicate) {
|
||||
return element.children.flatMap(child =>
|
||||
(predicate(child) ? [child] : []).concat(descendants(child, predicate)));
|
||||
}
|
||||
const previousDocument = global.document;
|
||||
global.document = {
|
||||
createElement: tag => new RuntimeElement(tag),
|
||||
createTextNode: text => new RuntimeElement('#text', text),
|
||||
};
|
||||
try {
|
||||
const card = new RuntimeElement('article');
|
||||
const changes = [];
|
||||
picker.renderRuntimeDirections(card, observer, {logging: 'wifi'}, {
|
||||
runtimeFields: ['logging'], onSelectionChange: selection => changes.push(selection),
|
||||
});
|
||||
const loggingDetails = descendants(card, node => node.tagName === 'DETAILS')
|
||||
.find(node => node.children[0].textContent === 'Logging / MQTT output');
|
||||
const inputs = descendants(loggingDetails, node => node.tagName === 'INPUT');
|
||||
assert.strictEqual(inputs.length, 4);
|
||||
assert.deepStrictEqual(inputs.map(input => !!input.checked), [false, false, true, false]);
|
||||
assert.strictEqual(descendants(loggingDetails, node => node.tagName === 'CODE')[0].textContent,
|
||||
'set logging.output wifi\nget logging.output');
|
||||
const choices = descendants(loggingDetails, node => node.className === 'firmware-picker-radio-options')[0];
|
||||
choices.listeners.change({target: inputs[1]});
|
||||
assert.deepStrictEqual(changes, [{logging: 'usb'}]);
|
||||
assert.strictEqual(descendants(loggingDetails, node => node.tagName === 'CODE')[0].textContent,
|
||||
'set powersaving off\nset logging.output usb\nget logging.output');
|
||||
const url = picker.selectionUrl('https://example.test/picker/', {...simpleFilters, ...changes[0]}, false);
|
||||
assert.strictEqual(new URL(url).searchParams.get('logging'), 'usb');
|
||||
const resolveLogging = logging => picker.resolveProfileAssets([observer].filter(profile =>
|
||||
picker.profileMatchesFacets(profile, {logging})), 'bin').map(entry => entry.asset.name);
|
||||
assert.deepStrictEqual(resolveLogging(changes[0].logging), resolveLogging('wifi'));
|
||||
assert.strictEqual(resolveLogging(changes[0].logging).length, 1);
|
||||
|
||||
// When logging still distinguishes images, candidate command controls are
|
||||
// directions only. Their preview cannot silently change filters or URLs.
|
||||
const refiningCard = new RuntimeElement('article');
|
||||
const refiningChanges = [];
|
||||
picker.renderRuntimeDirections(refiningCard, observer, {}, {
|
||||
runtimeFields: [], onSelectionChange: selection => refiningChanges.push(selection),
|
||||
});
|
||||
const refiningDetails = descendants(refiningCard, node => node.tagName === 'DETAILS')
|
||||
.find(node => node.children[0].textContent === 'Restore the selected logging mode');
|
||||
const refiningInputs = descendants(refiningDetails, node => node.tagName === 'INPUT');
|
||||
const refiningChoices = descendants(refiningDetails,
|
||||
node => node.className === 'firmware-picker-radio-options')[0];
|
||||
refiningChoices.listeners.change({target: refiningInputs[2]});
|
||||
assert.strictEqual(descendants(refiningDetails, node => node.tagName === 'CODE')[0].textContent,
|
||||
'set logging.output wifi\nget logging.output');
|
||||
assert.deepStrictEqual(refiningChanges, []);
|
||||
} finally {
|
||||
if (previousDocument === undefined) delete global.document;
|
||||
else global.document = previousDocument;
|
||||
}
|
||||
console.log('result logging and bridge controls preserve linked modes and show deliberate default commands');
|
||||
Reference in new issue
Block a user