Preserve current Room downloads during QSPI rollout
This commit is contained in:
1 parent
ce53567a4c
commit
04c3d1118d
3 files changed
+114
-25
No files matched your search
@@ -533,13 +533,10 @@
|
||||
const lowerTarget = target.toLowerCase();
|
||||
return HIDDEN_PROFILE_HARDWARE.includes(profile.sourceHardware) ||
|
||||
HIDDEN_PROFILE_TARGETS.includes(target) ||
|
||||
// These room installations use their onboard external-QSPI recipe.
|
||||
// SolarXiao room installations use their onboard external-QSPI recipe.
|
||||
// Retain internal-only downloads for exact compatibility searches,
|
||||
// including when a release has no matching QSPI image.
|
||||
(QSPI_ROOM_HARDWARE.includes(String(profile.sourceHardware || "").toLowerCase()) &&
|
||||
/^_room_server(?:_lora_ota_no_external_sensors)?(?:-(?:full|reduced)(?:-ota)?)?$/
|
||||
.test(lowerTarget.slice(profile.sourceHardware.length))) ||
|
||||
/^lilygo_t-echo-lite_repeater(?:_lora_ota_no_external_sensors)?(?:-(?:full|reduced)(?:-ota)?)?$/
|
||||
/^solarxiao_(?:30|33)s_room_server(?:_lora_ota_no_external_sensors)?(?:-(?:full|reduced)(?:-ota)?)?$/
|
||||
.test(lowerTarget) ||
|
||||
(lowerTarget.includes("companion_radio_") &&
|
||||
/(?:^|[_-])(?:ps|femoff)(?=$|[_-])/.test(lowerTarget));
|
||||
@@ -725,26 +722,40 @@
|
||||
}
|
||||
|
||||
function omitReplacedQspiProfiles(profiles) {
|
||||
const canonicalPairs = new Map();
|
||||
const qspiPairs = new Map();
|
||||
(profiles || []).forEach(function (profile) {
|
||||
const controls = profile.controls;
|
||||
const hardware = String(profile.hardware || "").toLowerCase();
|
||||
const target = String(profile.target || "").toLowerCase();
|
||||
if (!controls || controls.platform !== "NRF52_PLATFORM" ||
|
||||
profile.ota !== "lora-receiver" || !profile.sensorProfile ||
|
||||
!controls.updateRequirements || !controls.updateRequirements.lora ||
|
||||
controls.updateRequirements.lora.storage !== "external_qspi" ||
|
||||
target !== hardware + "_repeater-" + profile.sensorProfile) return;
|
||||
if (!canonicalPairs.has(hardware)) canonicalPairs.set(hardware, new Map());
|
||||
canonicalPairs.get(hardware).set(profile.sensorProfile, profile);
|
||||
!target.endsWith("-" + profile.sensorProfile)) return;
|
||||
const base = target.slice(0, -profile.sensorProfile.length - 1);
|
||||
if (!qspiPairs.has(base)) qspiPairs.set(base, new Map());
|
||||
qspiPairs.get(base).set(profile.sensorProfile, profile);
|
||||
});
|
||||
function qualifiedPair(base) {
|
||||
const pair = qspiPairs.get(base);
|
||||
if (!pair || !pair.has("full") || !pair.has("reduced")) return null;
|
||||
return pair.get("full").controls.sensorProfileSource.toLowerCase() ===
|
||||
pair.get("reduced").controls.sensorProfileSource.toLowerCase() ? pair : null;
|
||||
}
|
||||
return (profiles || []).filter(function (profile) {
|
||||
const hardware = String(profile.hardware || "").toLowerCase();
|
||||
const pair = canonicalPairs.get(hardware);
|
||||
if (!pair || !pair.has("full") || !pair.has("reduced")) return true;
|
||||
if (pair.get("full").controls.sensorProfileSource.toLowerCase() !==
|
||||
pair.get("reduced").controls.sensorProfileSource.toLowerCase()) return true;
|
||||
const target = String(profile.target || "").toLowerCase();
|
||||
// Keep supported roles in older releases until their exact QSPI pair
|
||||
// is downloadable. A source-only recipe does not replace a download.
|
||||
if (QSPI_ROOM_HARDWARE.includes(hardware) &&
|
||||
/^_room_server(?:_lora_ota_no_external_sensors)?(?:-(?:full|reduced)(?:-ota)?)?$/
|
||||
.test(target.slice(hardware.length)) &&
|
||||
qualifiedPair(hardware + "_room_server_lora_ota")) return false;
|
||||
if (hardware === "lilygo_t-echo-lite" &&
|
||||
/^_repeater(?:_lora_ota_no_external_sensors)?(?:-(?:full|reduced)(?:-ota)?)?$/
|
||||
.test(target.slice(hardware.length)) &&
|
||||
qualifiedPair(hardware + "_repeater_lora_ota")) return false;
|
||||
const pair = qualifiedPair(hardware + "_repeater");
|
||||
if (!pair) return true;
|
||||
if (QSPI_REPEATER_ALIAS_HARDWARE.includes(hardware) &&
|
||||
/^_repeater_lora_ota_no_external_sensors(?:-(?:full|reduced)(?:-ota)?)?$/
|
||||
.test(target.slice(hardware.length))) return false;
|
||||
|
||||
@@ -490,9 +490,13 @@ exact `LilyGo_T-Echo-Lite_repeater_lora_ota` target. Each requires the
|
||||
board-matched QSPI-capable OTAFIX bootloader; a shared board-family name is
|
||||
not enough to choose a bootloader.
|
||||
Internal-only Room recipes remain available by exact name for explicit
|
||||
compatibility/recovery builds, but are excluded from ordinary build menus,
|
||||
future releases, and picker recommendations. Existing downloads remain
|
||||
available through exact filename searches. The legacy and QSPI images have
|
||||
compatibility/recovery builds, but are excluded from ordinary build menus
|
||||
and future releases. For newly converted boards, the picker keeps an existing
|
||||
release's internal Room choice until that same release provides its qualified,
|
||||
source-matched QSPI Full/Reduced pair. This preserves working Room links during
|
||||
rollout; SolarXiao's already-retired internal choices stay hidden. Existing
|
||||
downloads remain available through exact filename searches. The legacy and
|
||||
QSPI images have
|
||||
different OTA identities and layouts: a QSPI package is not a cross-target
|
||||
LoRa update for an internal-only installation. Use the matching bootloader
|
||||
and documented local migration path when changing layouts.
|
||||
|
||||
@@ -1235,8 +1235,18 @@ const newQspiRoomHardware = ['Xiao_nrf52', 'ikoka_handheld_nrf_e22_30dbm',
|
||||
'ikoka_stick_nrf_22dbm', 'ikoka_stick_nrf_30dbm', 'ikoka_stick_nrf_33dbm',
|
||||
'LilyGo_T-Echo-Lite', ...fixedQspiHardware];
|
||||
for (const hardware of newQspiRoomHardware) {
|
||||
assert(!currentCatalog.profiles.some(profile => profile.hardware === hardware && profile.role === 'room'),
|
||||
'An older release without the new QSPI room recipe must not recommend internal storage');
|
||||
const oldRoomLink = picker.selectionFromUrl(
|
||||
'https://example.com/p/?s=1.h' + hardware + '.rroom.olora-receiver.ffull.vdefault',
|
||||
currentCatalog.profiles);
|
||||
assert.deepStrictEqual(oldRoomLink.unavailable, [],
|
||||
'Saved Room links must keep their role until a qualified QSPI replacement is published');
|
||||
assert.strictEqual(oldRoomLink.filters.role, 'room');
|
||||
assert.strictEqual(oldRoomLink.filters.ota, 'lora-receiver');
|
||||
const oldRoomMatches = currentCatalog.profiles.filter(profile =>
|
||||
picker.profileMatchesFacets(profile, oldRoomLink.filters));
|
||||
assert.strictEqual(oldRoomMatches.length, 1);
|
||||
assert.strictEqual(oldRoomMatches[0].target,
|
||||
hardware + '_room_server_lora_ota_no_external_sensors-full');
|
||||
for (const policy of ['full', 'reduced']) {
|
||||
const target = hardware + '_room_server_lora_ota_no_external_sensors-' + policy;
|
||||
assert(currentCatalog.rows.some(row => row.target === target),
|
||||
@@ -1278,21 +1288,85 @@ const retiredRoomOnly = picker.buildCatalog([
|
||||
release(qspiFixtureFamily, '2026-10-16T00:00:00Z', qspiRoomAssets.filter(file =>
|
||||
!file.name.includes('_room_server_lora_ota-full-') && !file.name.includes('_room_server_lora_ota-reduced-'))),
|
||||
], qspiRoomControls);
|
||||
assert.deepStrictEqual(retiredRoomOnly.profiles, [],
|
||||
'No retired board may fall back to its internal room recipe without a QSPI replacement');
|
||||
for (const hardware of newQspiRoomHardware) {
|
||||
assert.strictEqual(retiredRoomOnly.profiles.filter(profile => profile.hardware === hardware).length, 4,
|
||||
'Keep current internal Room identities when the release has no replacement downloads');
|
||||
}
|
||||
assert(!retiredRoomOnly.profiles.some(profile => profile.hardware.startsWith('solarxiao_')),
|
||||
'The previously established SolarXiao retirement policy remains unconditional');
|
||||
for (const hardware of newQspiRoomHardware) {
|
||||
const base = hardware + '_room_server_lora_ota';
|
||||
for (const fault of ['missing_full', 'missing_reduced', 'stale_source', 'mixed_source',
|
||||
'internal_storage', 'missing_controls']) {
|
||||
const changedControls = JSON.parse(JSON.stringify(qspiRoomControls));
|
||||
let files = qspiRoomAssets.filter(file => file.name.startsWith(hardware + '_room_server'));
|
||||
if (fault === 'missing_full' || fault === 'missing_reduced') {
|
||||
files = files.filter(file => !file.name.startsWith(base + '-' + fault.slice(8) + '-'));
|
||||
} else if (fault === 'stale_source') {
|
||||
changedControls.profiles[base + '-full'].sensorProfileSource = 'f'.repeat(40);
|
||||
} else if (fault === 'mixed_source') {
|
||||
changedControls.profiles[base + '-full'].sensorProfileSource = 'a'.repeat(40);
|
||||
files = files.map(file => file.name.startsWith(base + '-full-')
|
||||
? asset(file.name.replace(/-[0-9a-f]{8}\.uf2$/, '-aaaaaaaa.uf2')) : file);
|
||||
} else if (fault === 'internal_storage') {
|
||||
changedControls.profiles[base + '-full'].updateRequirements.lora.storage = 'internal_flash';
|
||||
}
|
||||
const partial = picker.buildCatalog([
|
||||
release(qspiFixtureFamily, '2026-10-16T00:00:00Z', files),
|
||||
], fault === 'missing_controls' ? undefined : changedControls);
|
||||
for (const policy of ['full', 'reduced']) {
|
||||
assert(partial.profiles.some(profile =>
|
||||
profile.target === hardware + '_room_server_lora_ota_no_external_sensors-' + policy),
|
||||
hardware + ': keep the existing Room download during incomplete QSPI rollout (' + fault + ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
const liteRepeaterTargets = ['', '_lora_ota_no_external_sensors', '_lora_ota'].flatMap(suffix =>
|
||||
['full', 'reduced'].map(policy => 'LilyGo_T-Echo-Lite_repeater' + suffix + '-' + policy));
|
||||
const liteRepeaterAssets = liteRepeaterTargets.map(target =>
|
||||
asset(target + '-ota-' + qspiFixtureFamily + '.uf2'));
|
||||
for (const target of liteRepeaterTargets) {
|
||||
const policy = target.endsWith('-full') ? 'full' : 'reduced';
|
||||
qspiRoomControls.profiles[target] = {
|
||||
platform: 'NRF52_PLATFORM', otaRole: 'lora-receiver', updateMethods: ['lora'],
|
||||
sensorProfile: policy, sensorProfileSource: qspiFixtureSource,
|
||||
updateRequirements: {lora: {storage: /_lora_ota-(?:full|reduced)$/.test(target)
|
||||
? 'external_qspi' : 'internal_flash'}},
|
||||
};
|
||||
}
|
||||
const liteRepeaterCatalog = picker.buildCatalog([
|
||||
release(qspiFixtureFamily, '2026-10-16T00:00:00Z', liteRepeaterAssets),
|
||||
]);
|
||||
], qspiRoomControls);
|
||||
assert.deepStrictEqual(liteRepeaterCatalog.profiles.map(profile => profile.target).sort(),
|
||||
['full', 'reduced'].map(policy => 'LilyGo_T-Echo-Lite_repeater_lora_ota-' + policy));
|
||||
assert.deepStrictEqual(liteRepeaterCatalog.rows.map(row => row.target), liteRepeaterTargets);
|
||||
assert(!currentCatalog.profiles.some(profile =>
|
||||
profile.hardware === 'LilyGo_T-Echo-Lite' && profile.role === 'repeater'),
|
||||
'The old internal Lite repeater must not be recommended before its QSPI release exists');
|
||||
const oldLiteRepeaterLink = picker.selectionFromUrl(
|
||||
'https://example.com/p/?s=1.hLilyGo_T-Echo-Lite.rrepeater.olora-receiver.ffull.vdefault',
|
||||
currentCatalog.profiles);
|
||||
assert.deepStrictEqual(oldLiteRepeaterLink.unavailable, []);
|
||||
assert.strictEqual(oldLiteRepeaterLink.filters.role, 'repeater');
|
||||
assert.strictEqual(oldLiteRepeaterLink.filters.ota, 'lora-receiver');
|
||||
assert.deepStrictEqual(currentCatalog.profiles.filter(profile =>
|
||||
picker.profileMatchesFacets(profile, oldLiteRepeaterLink.filters)).map(profile => profile.target),
|
||||
['LilyGo_T-Echo-Lite_repeater_lora_ota_no_external_sensors-full']);
|
||||
for (const fault of ['missing_full', 'missing_reduced', 'stale_source', 'missing_controls']) {
|
||||
const changedControls = JSON.parse(JSON.stringify(qspiRoomControls));
|
||||
let files = liteRepeaterAssets;
|
||||
const base = 'LilyGo_T-Echo-Lite_repeater_lora_ota';
|
||||
if (fault === 'missing_full' || fault === 'missing_reduced') {
|
||||
files = files.filter(file => !file.name.startsWith(base + '-' + fault.slice(8) + '-'));
|
||||
} else if (fault === 'stale_source') {
|
||||
changedControls.profiles[base + '-full'].sensorProfileSource = 'f'.repeat(40);
|
||||
}
|
||||
const partial = picker.buildCatalog([
|
||||
release(qspiFixtureFamily, '2026-10-16T00:00:00Z', files),
|
||||
], fault === 'missing_controls' ? undefined : changedControls);
|
||||
for (const policy of ['full', 'reduced']) {
|
||||
assert(partial.profiles.some(profile =>
|
||||
profile.target === 'LilyGo_T-Echo-Lite_repeater_lora_ota_no_external_sensors-' + policy),
|
||||
'Keep existing Lite repeater functionality during incomplete QSPI rollout: ' + fault);
|
||||
}
|
||||
}
|
||||
for (const hardware of ['LilyGo_T-Echo_Card', 'muzi_base_uno', 'muzi_base_duo', 'Nano_G2_Ultra']) {
|
||||
const original = Object.keys(currentControls.profiles).filter(target =>
|
||||
target.startsWith(hardware + '_repeater_') || target.startsWith(hardware + '_room_server_'));
|
||||
|
||||
Reference in new issue
Block a user