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

+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)