Consolidate onboard QSPI infrastructure build profiles
This commit is contained in:
1 parent
d5d84d325b
commit
ce53567a4c
27 files changed
+1351
-42
No files matched your search
@@ -712,6 +712,7 @@ jobs:
|
||||
python3 -B test/test_waveshare_partition_migration.py -v
|
||||
python3 -B test/test_usb_partition_bridge_set.py -v
|
||||
bash test/test_build_profiles.sh
|
||||
python3 -B test/test_onboard_qspi_profiles.py -v
|
||||
bash test/test_full_infrastructure_selection.sh
|
||||
bash test/test_nrf52_ota_profiles.sh
|
||||
bash test/test_xiao_qspi_combined.sh
|
||||
|
||||
+66
-7
@@ -81,7 +81,7 @@ BUILD_BACKGROUND_HANDOFF_STATE=""
|
||||
BUILD_SCRIPT_LOCK_FD=""
|
||||
INTERACTIVE_BUILD_SELECTION=0
|
||||
|
||||
ENV_VARIANT_SUFFIX_PATTERN='companion_radio_(wifi_mqtt|serial|wifi|usb|ble|full)(_ps)?(_fem(on|off))?|companion_radio_ethernet|comp_radio_usb|companion_usb|companion_ble|repeater_bridge_rs232_serial1_lora_ota_no_external_sensors|repeater_bridge_rs232_serial2_lora_ota_no_external_sensors|repeater_bridge_rs232_lora_ota_no_external_sensors|repeater_rak13302_w25q16_lora_ota|repeater_rak15001_slot_c_lora_ota|repeater_w25q16_lora_ota|repeater_lora_ota_no_external_sensors|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|repeater_observer_mqtt|repeater_ethernet|room_server_ethernet_lora_ota_no_external_sensors|room_server_lora_ota_no_external_sensors|room_svr_lora_ota_no_external_sensors|room_server_lora_ota|room_server_observer_mqtt|room_server_ethernet|terminal_chat|room_server|room_svr|kiss_modem|sensor_lora_ota_no_external_sensors|sensor|repeatr|repeater'
|
||||
ENV_VARIANT_SUFFIX_PATTERN='companion_radio_(wifi_mqtt|serial|wifi|usb|ble|full)(_ps)?(_fem(on|off))?|companion_radio_ethernet|comp_radio_usb|companion_usb|companion_ble|repeater_bridge_rs232_serial1_lora_ota_no_external_sensors|repeater_bridge_rs232_serial2_lora_ota_no_external_sensors|repeater_bridge_rs232_lora_ota_no_external_sensors|repeater_rak13302_w25q16_lora_ota|repeater_rak15001_slot_c_lora_ota|repeater_w25q16_lora_ota|repeater_lora_ota_no_external_sensors|repeater_lora_ota|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|repeater_observer_mqtt|repeater_ethernet|room_server_ethernet_lora_ota_no_external_sensors|room_server_lora_ota_no_external_sensors|room_svr_lora_ota_no_external_sensors|room_server_lora_ota|room_server_observer_mqtt|room_server_ethernet|terminal_chat|room_server|room_svr|kiss_modem|sensor_lora_ota_no_external_sensors|sensor|repeatr|repeater'
|
||||
BOARD_MODIFIER_WITHOUT_DISPLAY="_without_display"
|
||||
BOARD_MODIFIER_LOGGING="_logging"
|
||||
BOARD_MODIFIER_TFT="_tft"
|
||||
@@ -1980,6 +1980,13 @@ print_release_firmware_targets() {
|
||||
;;
|
||||
get-repeater-firmwares-to-build)
|
||||
get_pio_envs_ending_with_string "_repeater"
|
||||
local qspi_target
|
||||
for qspi_target in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if [[ "$qspi_target" == *_repeater_lora_ota ]] \
|
||||
&& ! is_redundant_bulk_build_target "$qspi_target"; then
|
||||
printf '%s\n' "$qspi_target"
|
||||
fi
|
||||
done
|
||||
# MeshTower keeps its deployed SD target identity as the one canonical
|
||||
# repeater recipe, including when SD staging falls back to internal staging.
|
||||
if is_supported_build_env "Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors"; then
|
||||
@@ -2021,8 +2028,9 @@ print_release_firmware_targets() {
|
||||
get_pio_envs_ending_with_string "_room_server"
|
||||
for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if is_room_server_role_target "$env_name" \
|
||||
&& { is_lora_ota_no_external_sensors_target "$env_name" \
|
||||
|| [[ "$env_name" == solarxiao_*_room_server_lora_ota ]]; }; then
|
||||
&& { is_lora_ota_no_external_sensors_target "$env_name" \
|
||||
|| [[ "$env_name" == *_room_server_lora_ota ]]; } \
|
||||
&& ! is_redundant_bulk_build_target "$env_name"; then
|
||||
printf '%s\n' "$env_name"
|
||||
fi
|
||||
done
|
||||
@@ -3445,6 +3453,9 @@ declare_build_capability_contract() {
|
||||
declare_full_logging_application_contract "$env_name"
|
||||
record_build_capability "profile.${BUILD_PROFILE_FOR_TARGET}"
|
||||
pio_env_name=$(get_pio_build_env "$env_name")
|
||||
if is_qspi_hardware_qualification_target "$env_name"; then
|
||||
record_build_capability "hardware.qualification.pending"
|
||||
fi
|
||||
if [ "$BUILD_PROFILE_FOR_TARGET" = "full" ] \
|
||||
&& is_esp32_canonical_full_release_target "$env_name"; then
|
||||
pio_env_name=$(get_exact_identity_full_pio_env "$env_name")
|
||||
@@ -3456,6 +3467,10 @@ declare_build_capability_contract() {
|
||||
# Check both the logical and compiled recipes so substituting sources
|
||||
# cannot silently remove RS-232 while its legacy image is retired.
|
||||
record_build_expectation "bridge.rs232" "_ZN11RS232Bridge5beginEv"
|
||||
if pio_env_option_contains "$pio_env_name" build_flags "RS232_BRIDGE_MERGED=1"; then
|
||||
record_application_expectation "bridge.rs232.merged" \
|
||||
"Error: use set rs232.enabled on|off"
|
||||
fi
|
||||
fi
|
||||
if [ "$BUILD_PROFILE_FOR_TARGET" = "full" ] \
|
||||
&& supports_esp32_full_shared_espnow "$pio_env_name"; then
|
||||
@@ -5666,6 +5681,9 @@ get_merged_rs232_repeater_replacement() {
|
||||
solarxiao_33s_repeater_bridge_rs232)
|
||||
printf '%s\n' solarxiao_33S_repeater
|
||||
;;
|
||||
xiao_nrf52_repeater_bridge_rs232|xiao_nrf52_repeater_bridge_rs232_lora_ota_no_external_sensors)
|
||||
printf '%s\n' Xiao_nrf52_repeater
|
||||
;;
|
||||
heltec_v3_repeater_bridge_rs232)
|
||||
printf '%s\n' Heltec_v3_repeater
|
||||
;;
|
||||
@@ -5779,11 +5797,52 @@ is_solarxiao_internal_room_compatibility_target() {
|
||||
esac
|
||||
}
|
||||
|
||||
get_onboard_qspi_normal_replacement() {
|
||||
local target=$1 base
|
||||
base=${target%_lora_ota_no_external_sensors}
|
||||
case "${base,,}" in
|
||||
solarxiao_30s_room_server|solarxiao_33s_room_server|xiao_nrf52_room_server|\
|
||||
ikoka_nano_nrf_22dbm_room_server|ikoka_nano_nrf_30dbm_room_server|ikoka_nano_nrf_33dbm_room_server|\
|
||||
ikoka_stick_nrf_22dbm_room_server|ikoka_stick_nrf_30dbm_room_server|ikoka_stick_nrf_33dbm_room_server|\
|
||||
ikoka_handheld_nrf_e22_30dbm_room_server|lilygo_t-echo_room_server|\
|
||||
sensecap_solar_room_server|thinknode_m1_room_server|thinknode_m6_room_server|\
|
||||
wiotrackerl1_room_server|lilygo_t-echo-lite_room_server|lilygo_t-echo-lite_repeater)
|
||||
printf '%s_lora_ota\n' "$base"
|
||||
return 0
|
||||
;;
|
||||
lilygo_t-echo_repeater|sensecap_solar_repeater|thinknode_m1_repeater|\
|
||||
thinknode_m6_repeater|wiotrackerl1_repeater)
|
||||
if [ "$base" != "$target" ]; then
|
||||
printf '%s\n' "$base"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
xiao_nrf52_repeater_bridge_rs232)
|
||||
printf '%s\n' Xiao_nrf52_repeater
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_qspi_hardware_qualification_target() {
|
||||
case "${1,,}" in
|
||||
muzi_base_duo_repeater_lora_ota|muzi_base_duo_room_server_lora_ota|\
|
||||
muzi_base_uno_repeater_lora_ota|muzi_base_uno_room_server_lora_ota|\
|
||||
lilygo_t-echo_card_repeater_lora_ota|lilygo_t-echo_card_room_server_lora_ota|\
|
||||
nano_g2_ultra_repeater_lora_ota|nano_g2_ultra_room_server_lora_ota)
|
||||
return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_redundant_bulk_build_target() {
|
||||
# SolarXiao has onboard QSPI. Its internal-only Room recipes retain their
|
||||
# exact legacy OTA identities for explicit compatibility/recovery builds,
|
||||
# but must not be offered as ordinary builds or mapped to the QSPI identity.
|
||||
if is_solarxiao_internal_room_compatibility_target "$1"; then
|
||||
# Known onboard-QSPI normal recipes replace old internal/lean choices.
|
||||
# Retain exact legacy OTA identities for explicit compatibility/recovery;
|
||||
# do not remap them to the QSPI identity. Hardware-pending external ports
|
||||
# are explicit qualification builds, never ordinary release choices.
|
||||
if get_onboard_qspi_normal_replacement "$1" >/dev/null \
|
||||
|| is_qspi_hardware_qualification_target "$1"; then
|
||||
return 0
|
||||
fi
|
||||
# Bench fixtures, migration utilities and the unqualified optional SD
|
||||
|
||||
@@ -219,6 +219,17 @@
|
||||
"ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_full",
|
||||
]);
|
||||
|
||||
const QSPI_REPEATER_ALIAS_HARDWARE = Object.freeze([
|
||||
"lilygo_t-echo", "sensecap_solar", "thinknode_m1", "thinknode_m6", "wiotrackerl1",
|
||||
]);
|
||||
|
||||
const QSPI_ROOM_HARDWARE = Object.freeze([
|
||||
"xiao_nrf52", "ikoka_handheld_nrf_e22_30dbm",
|
||||
"ikoka_nano_nrf_22dbm", "ikoka_nano_nrf_30dbm", "ikoka_nano_nrf_33dbm",
|
||||
"ikoka_stick_nrf_22dbm", "ikoka_stick_nrf_30dbm", "ikoka_stick_nrf_33dbm",
|
||||
"solarxiao_30s", "solarxiao_33s", "lilygo_t-echo-lite",
|
||||
].concat(QSPI_REPEATER_ALIAS_HARDWARE));
|
||||
|
||||
let pickerInstanceCount = 0;
|
||||
let runtimeGroupCount = 0;
|
||||
|
||||
@@ -522,10 +533,14 @@
|
||||
const lowerTarget = target.toLowerCase();
|
||||
return HIDDEN_PROFILE_HARDWARE.includes(profile.sourceHardware) ||
|
||||
HIDDEN_PROFILE_TARGETS.includes(target) ||
|
||||
// SolarXiao room installations use the onboard external-QSPI recipe.
|
||||
// These 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.
|
||||
/^solarxiao_(?:30|33)s_room_server(?:-(?:full|reduced)(?:-ota)?)?$/.test(lowerTarget) ||
|
||||
(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)?)?$/
|
||||
.test(lowerTarget) ||
|
||||
(lowerTarget.includes("companion_radio_") &&
|
||||
/(?:^|[_-])(?:ps|femoff)(?=$|[_-])/.test(lowerTarget));
|
||||
}
|
||||
@@ -709,6 +724,47 @@
|
||||
});
|
||||
}
|
||||
|
||||
function omitReplacedQspiProfiles(profiles) {
|
||||
const canonicalPairs = 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);
|
||||
});
|
||||
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();
|
||||
if (QSPI_REPEATER_ALIAS_HARDWARE.includes(hardware) &&
|
||||
/^_repeater_lora_ota_no_external_sensors(?:-(?:full|reduced)(?:-ota)?)?$/
|
||||
.test(target.slice(hardware.length))) return false;
|
||||
if (hardware === "xiao_nrf52" &&
|
||||
/^xiao_nrf52_repeater_bridge_rs232(?:_lora_ota_no_external_sensors)?(?:-(?:full|reduced)(?:-ota)?)?$/.test(target)) {
|
||||
// A filename or static UART flag cannot prove the merged image's
|
||||
// runtime bridge control. Each qualified policy needs release-bound
|
||||
// packaged control evidence for the same source as its sensor proof.
|
||||
return !["full", "reduced"].every(function (policy) {
|
||||
const controls = pair.get(policy).controls;
|
||||
return controls.rs232 === true && controls.rs232Merged === true &&
|
||||
typeof controls.rs232Source === "string" &&
|
||||
/^[0-9a-f]{40}$/i.test(controls.rs232Source) &&
|
||||
controls.rs232Source.toLowerCase() === controls.sensorProfileSource.toLowerCase();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function applyFullCompanionCapabilities(profiles) {
|
||||
return (profiles || []).map(function (profile) {
|
||||
if (!isFullCompanion(profile)) return profile;
|
||||
@@ -1012,9 +1068,9 @@
|
||||
return {
|
||||
releaseSet: releaseSet,
|
||||
rows: firmwareRows,
|
||||
// Old releases have no shared-slot proof and retain both choices. Raw
|
||||
// assets remain searchable even when a new exact successor replaces R2.
|
||||
profiles: omitStationG3SlotDuplicates(profiles),
|
||||
// Recommendation filters retain original files and target identities in
|
||||
// the raw catalog, including when a qualified successor replaces them.
|
||||
profiles: omitReplacedQspiProfiles(omitStationG3SlotDuplicates(profiles)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+26
-3
@@ -482,9 +482,13 @@ both profiles with matching packaged OTA identity and flash geometry, even
|
||||
when a matrix is resumed or partial publication is requested. A failed build
|
||||
is reported, never presented as a supported download. Companion is unchanged.
|
||||
|
||||
Normal SolarXiao 30S and 33S Room Server builds use the module's onboard QSPI
|
||||
storage (`solarxiao_30S_room_server_lora_ota` and
|
||||
`solarxiao_33S_room_server_lora_ota`), with both sensor profiles retained.
|
||||
Normal SolarXiao, XIAO nRF52, Ikoka Nano/Stick/Handheld, T-Echo, T-Echo Lite,
|
||||
ThinkNode M1/M6, Wio Tracker L1 and SenseCAP Solar Room Server builds use
|
||||
onboard QSPI storage through their exact `*_room_server_lora_ota` targets,
|
||||
with both sensor profiles retained. T-Echo Lite Repeater similarly uses its
|
||||
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
|
||||
@@ -493,6 +497,25 @@ 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.
|
||||
|
||||
T-Echo, SenseCAP Solar, ThinkNode M1/M6 and Wio Tracker L1 already use QSPI
|
||||
in their ordinary Repeater targets. Their older
|
||||
`*_repeater_lora_ota_no_external_sensors` identities remain explicit
|
||||
compatibility builds, not extra normal Full/Reduced choices. The picker hides
|
||||
these aliases only when a source-matched, qualified ordinary QSPI pair exists.
|
||||
XIAO nRF52 Repeater now also includes its runtime RS232 bridge, disabled by
|
||||
default (`set rs232.enabled on|off`). The separate bridge recommendation is
|
||||
hidden only after both ordinary sensor profiles prove the linked driver and
|
||||
packaged runtime controls. Existing bridge OTA identities remain unchanged.
|
||||
|
||||
Muzi Base Uno/Duo, T-Echo Card and Nano G2 Ultra have explicit
|
||||
`*_repeater_lora_ota` and `*_room_server_lora_ota` qualification recipes.
|
||||
Their dedicated bootloader ports still require physical qualification and
|
||||
publication. These experimental recipes reserve raw QSPI for OTA (no shared
|
||||
QSPI filesystem), are excluded from ordinary menus and bulk builds, and carry
|
||||
`hardware.qualification.pending` in their capability manifests so release
|
||||
packaging refuses them. Their current normal builds and picker choices remain
|
||||
available; do not install another board's bootloader to bypass this gate.
|
||||
|
||||
The legacy `no_external_sensors` profile is labeled **Receives LoRa OTA -
|
||||
Reduced optional sensors** on the same button. On RAK3401 and RAK4631 this
|
||||
compact profile uses internal storage without an external storage board. The
|
||||
|
||||
+1
-1
@@ -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=20261016-solar-room-qspi-1
|
||||
- _javascript/firmware_picker.js?v=20261016-onboard-qspi-2
|
||||
- _javascript/telemetry_decoder.js?v=20261007-hosted-form
|
||||
- _javascript/management_decoder.js
|
||||
- _javascript/filter_tool.js?v=20261002-hashbytes-2plus
|
||||
|
||||
@@ -18,7 +18,7 @@ import tempfile
|
||||
from build_esp32_partition_migration import bundle_release, verify_archive
|
||||
from package_esp32_partition_migration import BOARDS, resolve_pio_build_dir
|
||||
from package_cascade_release import (category, collect_artifacts, nrf52_sensor_profile,
|
||||
omit_internal_solar_room_records)
|
||||
omit_retired_onboard_qspi_records)
|
||||
from station_g3_profiles import omit_redundant_slot_records
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
This also removes stale sibling artifacts when a matrix build is resumed.
|
||||
nRF52 Full/reduced sensor OTA records pass through independently, even
|
||||
when their logical target ID and storage layout are the same. MeshTower
|
||||
SD and SolarXiao external-QSPI primaries replace stale internal-only
|
||||
SD and onboard-QSPI primaries replace retired internal-only or lean
|
||||
artifacts on resume; those files must never be relabeled with a primary's
|
||||
different OTA ID.
|
||||
"""
|
||||
@@ -291,7 +291,7 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
|
||||
records = omit_redundant_slot_records(
|
||||
[record for record in records if ordinary_record(record)])
|
||||
records = omit_internal_solar_room_records(records)
|
||||
records = omit_retired_onboard_qspi_records(records)
|
||||
|
||||
tower_primary = "heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors"
|
||||
tower_legacy = {
|
||||
|
||||
@@ -95,6 +95,21 @@ def runtime_metadata(manifest, mqtt, release_source=None):
|
||||
result['loggingSource'] = manifest.get('source_commit', release_source)
|
||||
if 'companion.dedicated_usb_logging' in verified:
|
||||
result['dedicatedUsbLogging'] = True
|
||||
linked = {check['capability'] for check in manifest.get('verification', [])
|
||||
if check.get('present') is True and check.get('source') == 'linked image'
|
||||
and check.get('evidence')}
|
||||
merged_runtime = any(check.get('capability') == 'bridge.rs232.merged'
|
||||
and check.get('present') is True
|
||||
and check.get('source') == 'packaged application'
|
||||
and check.get('evidence')
|
||||
for check in manifest.get('verification', []))
|
||||
if (manifest.get('verified') is True
|
||||
and {'bridge.rs232', 'bridge.rs232.merged'} <= capabilities
|
||||
and 'bridge.rs232' in linked and merged_runtime):
|
||||
source = manifest.get('source_commit', release_source)
|
||||
if not isinstance(source, str) or not re.fullmatch(r'[0-9a-f]{40}', source):
|
||||
raise ValueError('Merged RS232 requires an exact source commit: ' + manifest['target'])
|
||||
result.update(rs232Merged=True, rs232Source=source)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -55,42 +55,82 @@ def nrf52_sensor_profile(manifest):
|
||||
return next(iter(choices))
|
||||
|
||||
|
||||
def omit_internal_solar_room_records(records):
|
||||
"""Omit resumed internal-only Solar Room packages from ordinary releases.
|
||||
def omit_retired_onboard_qspi_records(records):
|
||||
"""Omit retired onboard-QSPI alternatives from ordinary releases.
|
||||
|
||||
Call after package qualification. Keep the original files and OTA
|
||||
identities intact, and require each retired board's matching qualified
|
||||
external-QSPI Full/Reduced pair before excluding its recovery packages.
|
||||
"""
|
||||
legacy_targets = {
|
||||
"solarxiao_30s_room_server": "solarxiao_30s_room_server_lora_ota",
|
||||
"solarxiao_33s_room_server": "solarxiao_33s_room_server_lora_ota",
|
||||
}
|
||||
room_bases = (
|
||||
"solarxiao_30s_room_server", "solarxiao_33s_room_server",
|
||||
"xiao_nrf52_room_server",
|
||||
"ikoka_nano_nrf_22dbm_room_server", "ikoka_nano_nrf_30dbm_room_server",
|
||||
"ikoka_nano_nrf_33dbm_room_server", "ikoka_stick_nrf_22dbm_room_server",
|
||||
"ikoka_stick_nrf_30dbm_room_server", "ikoka_stick_nrf_33dbm_room_server",
|
||||
"ikoka_handheld_nrf_e22_30dbm_room_server",
|
||||
"lilygo_t-echo_room_server", "sensecap_solar_room_server",
|
||||
"thinknode_m1_room_server", "thinknode_m6_room_server", "wiotrackerl1_room_server",
|
||||
"lilygo_t-echo-lite_room_server",
|
||||
)
|
||||
legacy_targets = {}
|
||||
for base in room_bases + ("lilygo_t-echo-lite_repeater",):
|
||||
for legacy in (base, base + "_lora_ota_no_external_sensors"):
|
||||
legacy_targets[legacy] = base + "_lora_ota"
|
||||
for primary in ("lilygo_t-echo_repeater", "sensecap_solar_repeater", "thinknode_m1_repeater",
|
||||
"thinknode_m6_repeater", "wiotrackerl1_repeater"):
|
||||
legacy_targets[primary + "_lora_ota_no_external_sensors"] = primary
|
||||
bridge_legacy = {"xiao_nrf52_repeater_bridge_rs232",
|
||||
"xiao_nrf52_repeater_bridge_rs232_lora_ota_no_external_sensors"}
|
||||
legacy_targets.update({legacy: "xiao_nrf52_repeater" for legacy in bridge_legacy})
|
||||
targets = {record["manifest"]["target"].lower() for record in records}
|
||||
for legacy, primary in legacy_targets.items():
|
||||
if legacy not in targets:
|
||||
continue
|
||||
# collect_artifacts() has already checked the packaged OTA identity
|
||||
# and layout-matched sensor pair. Do not let internal-only leftovers
|
||||
# and layout-matched sensor pair. Do not let retired leftovers
|
||||
# conceal a missing normal release pair for this exact hardware.
|
||||
replacements = [record["manifest"] for record in records
|
||||
if record["manifest"]["target"].lower() == primary]
|
||||
error = (legacy + ": ordinary SolarXiao Room release requires the qualified "
|
||||
error = (legacy + ": ordinary onboard-QSPI release requires the qualified "
|
||||
"external-QSPI Full/Reduced pair for " + primary
|
||||
+ "; internal-only artifacts cannot substitute for its distinct OTA identity")
|
||||
+ "; retired artifacts cannot substitute for its distinct OTA identity")
|
||||
try:
|
||||
profiles = [nrf52_sensor_profile(manifest) for manifest in replacements]
|
||||
except ValueError as cause:
|
||||
raise ValueError(error) from cause
|
||||
if (sorted(profiles) != ["full", "reduced"]
|
||||
or any((manifest.get("ota_update_requirements") or {})
|
||||
or any("hardware.qualification.pending" in manifest.get("capabilities", [])
|
||||
or (manifest.get("ota_update_requirements") or {})
|
||||
.get("lora", {}).get("storage") != "external_qspi"
|
||||
for manifest in replacements)):
|
||||
for manifest in replacements)
|
||||
or replacements[0].get("source_commit") != replacements[1].get("source_commit")
|
||||
or replacements[0].get("ota_update_requirements") !=
|
||||
replacements[1].get("ota_update_requirements")):
|
||||
raise ValueError(error)
|
||||
if legacy in bridge_legacy:
|
||||
# The ordinary QSPI repeater may retire the separate UART role
|
||||
# only when both sensor profiles retain the linked driver and
|
||||
# prove its runtime controls in the packaged application.
|
||||
required = {"bridge.rs232": "linked image",
|
||||
"bridge.rs232.merged": "packaged application"}
|
||||
for manifest in replacements:
|
||||
proven = {check.get("capability") for check in manifest.get("verification", [])
|
||||
if check.get("present") is True
|
||||
and required.get(check.get("capability")) == check.get("source")
|
||||
and isinstance(check.get("evidence"), str) and check["evidence"].strip()}
|
||||
if (not required.keys() <= set(manifest.get("capabilities", []))
|
||||
or not required.keys() <= proven):
|
||||
raise ValueError(error + "; replacement requires a verified runtime RS232 bridge")
|
||||
return [record for record in records
|
||||
if record["manifest"]["target"].lower() not in legacy_targets]
|
||||
|
||||
|
||||
def omit_internal_solar_room_records(records):
|
||||
"""Compatibility entry point for the shared onboard-QSPI retirement policy."""
|
||||
return omit_retired_onboard_qspi_records(records)
|
||||
|
||||
|
||||
def release_profile_label(manifest):
|
||||
sensor = nrf52_sensor_profile(manifest)
|
||||
if sensor:
|
||||
@@ -153,6 +193,8 @@ def validate_nrf52_sensor_pairs(records):
|
||||
|
||||
def validate_manifest(manifest):
|
||||
target = manifest["target"]
|
||||
if "hardware.qualification.pending" in manifest.get("capabilities", []):
|
||||
raise ValueError(f"{target}: hardware qualification pending; experimental firmware cannot be published")
|
||||
if manifest.get("schema_version", 0) < 2 or not manifest.get("verified"):
|
||||
raise ValueError(f"{target}: firmware capability qualification failed")
|
||||
if not is_companion(target) and not manifest.get("ota_update_verified"):
|
||||
@@ -414,7 +456,7 @@ def main():
|
||||
raise ValueError("matrix did not use Cascade runtime defaults")
|
||||
radio = {key: status["radio_" + key] for key in ("frequency", "bandwidth", "sf", "cr")}
|
||||
records = collect_artifacts(args.input, version)
|
||||
records = omit_internal_solar_room_records(records)
|
||||
records = omit_retired_onboard_qspi_records(records)
|
||||
if args.output.exists() and any(args.output.iterdir()):
|
||||
raise ValueError("staging directory must be empty; existing releases are never overwritten")
|
||||
base_tag = version
|
||||
|
||||
@@ -765,6 +765,89 @@ if is_solarxiao_internal_room_compatibility_target Xiao_nrf52_room_server \
|
||||
fail "SolarXiao internal Room policy retired an unrelated recipe"
|
||||
fi
|
||||
|
||||
# Every supported onboard-QSPI Room has a normal exact external identity,
|
||||
# while old internal identities stay directly buildable without being remapped.
|
||||
for qspi_board in Xiao_nrf52 \
|
||||
ikoka_nano_nrf_22dbm ikoka_nano_nrf_30dbm ikoka_nano_nrf_33dbm \
|
||||
ikoka_stick_nrf_22dbm ikoka_stick_nrf_30dbm ikoka_stick_nrf_33dbm \
|
||||
ikoka_handheld_nrf_e22_30dbm LilyGo_T-Echo SenseCap_Solar \
|
||||
ThinkNode_M1 ThinkNode_M6 WioTrackerL1 LilyGo_T-Echo-Lite; do
|
||||
legacy_room=${qspi_board}_room_server
|
||||
legacy_alias=${legacy_room}_lora_ota_no_external_sensors
|
||||
qspi_room=${legacy_room}_lora_ota
|
||||
[ "$(get_onboard_qspi_normal_replacement "$legacy_room")" = "$qspi_room" ] \
|
||||
|| fail "$legacy_room lacks its normal QSPI replacement"
|
||||
for old in "$legacy_room" "$legacy_alias"; do
|
||||
is_supported_build_env "$old" || fail "$old lost explicit compatibility builds"
|
||||
is_redundant_bulk_build_target "$old" || fail "$old remains an ordinary Room choice"
|
||||
[ "$(get_pio_build_env "$old")" = "$legacy_room" ] \
|
||||
|| fail "$old changed its original internal recipe"
|
||||
done
|
||||
for selection in "$room_release_targets" "$solar_room_targets" \
|
||||
"$solar_all_targets" "$solar_logging_targets" "$solar_menu_targets"; do
|
||||
grep -Fxq "$qspi_room" <<<"$selection" || fail "$qspi_room missing from ordinary selection"
|
||||
if grep -Fxq -e "$legacy_room" -e "$legacy_alias" <<<"$selection"; then
|
||||
fail "$legacy_room internal identity leaked into ordinary selection"
|
||||
fi
|
||||
done
|
||||
(
|
||||
REQUIRE_OTA_UPDATES=0
|
||||
resolve_command_targets build-firmware "$legacy_room"
|
||||
[ "${#RESOLVED_BUILD_TARGETS[@]}" -eq 1 ] \
|
||||
&& [ "${RESOLVED_BUILD_TARGETS[0]}" = "$legacy_alias" ] \
|
||||
|| fail "$legacy_room direct build lost its deployed internal pair identity"
|
||||
)
|
||||
done
|
||||
repeater_release_targets=$(print_release_firmware_targets get-repeater-firmwares-to-build)
|
||||
for board in LilyGo_T-Echo SenseCap_Solar ThinkNode_M1 ThinkNode_M6 WioTrackerL1; do
|
||||
canonical=${board}_repeater
|
||||
alias=${canonical}_lora_ota_no_external_sensors
|
||||
is_supported_build_env "$alias" || fail "$alias lost its exact deployed recipe"
|
||||
for selection in "$repeater_release_targets" "$solar_all_targets" \
|
||||
"$solar_logging_targets" "$solar_menu_targets"; do
|
||||
grep -Fxq "$canonical" <<<"$selection" || fail "$canonical missing its normal QSPI pair"
|
||||
if grep -Fxq "$alias" <<<"$selection"; then fail "$alias duplicates its normal QSPI pair"; fi
|
||||
done
|
||||
done
|
||||
for selection in "$repeater_release_targets" "$solar_all_targets" \
|
||||
"$solar_logging_targets" "$solar_menu_targets"; do
|
||||
grep -Fxq LilyGo_T-Echo-Lite_repeater_lora_ota <<<"$selection" \
|
||||
|| fail "Lite QSPI repeater missing from ordinary selection"
|
||||
if grep -Fxq -e LilyGo_T-Echo-Lite_repeater \
|
||||
-e LilyGo_T-Echo-Lite_repeater_lora_ota_no_external_sensors \
|
||||
-e Xiao_nrf52_repeater_bridge_rs232 \
|
||||
-e Xiao_nrf52_repeater_bridge_rs232_lora_ota_no_external_sensors <<<"$selection"; then
|
||||
fail "internal Lite/bridge identity leaked into ordinary selection"
|
||||
fi
|
||||
done
|
||||
for pending in muzi_base_duo_repeater_lora_ota muzi_base_duo_room_server_lora_ota \
|
||||
muzi_base_uno_repeater_lora_ota muzi_base_uno_room_server_lora_ota \
|
||||
LilyGo_T-Echo_Card_repeater_lora_ota LilyGo_T-Echo_Card_room_server_lora_ota \
|
||||
Nano_G2_Ultra_repeater_lora_ota Nano_G2_Ultra_room_server_lora_ota; do
|
||||
is_supported_build_env "$pending" || fail "$pending qualification recipe missing"
|
||||
for selection in "$room_release_targets" "$repeater_release_targets" "$solar_all_targets" \
|
||||
"$solar_logging_targets" "$solar_menu_targets"; do
|
||||
if grep -Fxq "$pending" <<<"$selection"; then fail "$pending escaped qualification quarantine"; fi
|
||||
done
|
||||
(
|
||||
BUILD_PROFILE_FOR_TARGET=standard
|
||||
ESP32_FULL_BUILD=0
|
||||
BUILD_CAPABILITIES=() BUILD_EXPECTATIONS=() BUILD_APPLICATION_EXPECTATIONS=() BUILD_REDUCTIONS=()
|
||||
declare_build_capability_contract "$pending" NRF52_PLATFORM
|
||||
[[ " ${BUILD_CAPABILITIES[*]} " == *" hardware.qualification.pending "* ]] \
|
||||
|| fail "$pending lacks its package publication prohibition"
|
||||
)
|
||||
done
|
||||
(
|
||||
BUILD_PROFILE_FOR_TARGET=standard
|
||||
ESP32_FULL_BUILD=0
|
||||
BUILD_CAPABILITIES=() BUILD_EXPECTATIONS=() BUILD_APPLICATION_EXPECTATIONS=() BUILD_REDUCTIONS=()
|
||||
declare_build_capability_contract Xiao_nrf52_repeater NRF52_PLATFORM
|
||||
[[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.rs232="* \
|
||||
&& " ${BUILD_APPLICATION_EXPECTATIONS[*]} " == *"bridge.rs232.merged="* ]] \
|
||||
|| fail "merged XIAO bridge lacks linked driver and packaged runtime proof"
|
||||
)
|
||||
|
||||
sensor_release_targets=$(resolve_sensor_firmwares)
|
||||
while IFS='|' read -r sensor_env sensor_ota_env; do
|
||||
is_supported_build_env "$sensor_ota_env" \
|
||||
|
||||
@@ -63,6 +63,12 @@ class ReleaseQualificationTest(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "wireless updater"):
|
||||
package.validate_manifest(self.manifest(ota_update_verified=False))
|
||||
|
||||
def test_pending_hardware_qualification_cannot_be_published(self):
|
||||
for platform in ('NRF52_PLATFORM', 'ESP32_PLATFORM'):
|
||||
manifest = self.manifest(platform=platform, capabilities=['hardware.qualification.pending'])
|
||||
with self.subTest(platform=platform), self.assertRaisesRegex(ValueError, 'hardware qualification pending'):
|
||||
package.validate_manifest(manifest)
|
||||
|
||||
def test_usb_companion_is_accepted(self):
|
||||
package.validate_manifest(self.manifest("test_companion_radio_usb", ota_update_verified=False))
|
||||
|
||||
@@ -99,9 +105,11 @@ class ReleaseQualificationTest(unittest.TestCase):
|
||||
|
||||
def sensor_record(self, directory, sensor, *, target='test_repeater', target_id=None,
|
||||
hw_id='TEST', app_base=0x26000, flags=0x10, fw_version=0x01110107,
|
||||
version='v1.17.1.7-dev-deadbeef', storage='internal_flash_and_retained_ram'):
|
||||
version='v1.17.1.7-dev-deadbeef', storage='internal_flash_and_retained_ram',
|
||||
extra_application=b''):
|
||||
stem = target + '-' + sensor + '-ota-' + version
|
||||
application, _ = ensure_endf(ensure_nrf52_layout(b'OTA: status\0invalid in-place patch geometry\0',
|
||||
application, _ = ensure_endf(ensure_nrf52_layout(b'OTA: status\0invalid in-place patch geometry\0'
|
||||
+ extra_application,
|
||||
Nrf52Layout(app_base, 0xED000, 0xED000, flags)),
|
||||
FwIdent(fw_version, target_id_for_env(target) if target_id is None else target_id, hw_id))
|
||||
path = directory / (stem + '.zip')
|
||||
@@ -117,12 +125,23 @@ class ReleaseQualificationTest(unittest.TestCase):
|
||||
ota_update_requirements={'lora': {'storage': storage}})
|
||||
return {'manifest': manifest, 'files': [path]}
|
||||
|
||||
def sensor_package_fixture(self, directory, target, sensor, *, external=False):
|
||||
def sensor_package_fixture(self, directory, target, sensor, *, external=False,
|
||||
merged_bridge=False, extra_capabilities=()):
|
||||
"""Write fictional, structurally valid packages for the real CLI path."""
|
||||
record = self.sensor_record(directory, sensor, target=target,
|
||||
version='v1.17.1.5-halo-keymind-cascade-dev-deadbeef',
|
||||
flags=0x04 if external else 0x10,
|
||||
storage='external_qspi' if external else 'internal_flash_and_retained_ram')
|
||||
storage='external_qspi' if external else 'internal_flash_and_retained_ram',
|
||||
extra_application=b'meshcore.capability.rs232.merged.v1\0' if merged_bridge else b'')
|
||||
manifest = record['manifest']
|
||||
manifest['capabilities'].extend(extra_capabilities)
|
||||
if merged_bridge:
|
||||
for capability, source, evidence in (
|
||||
('bridge.rs232', 'linked image', '_ZN11RS232Bridge5beginEv'),
|
||||
('bridge.rs232.merged', 'packaged application', 'meshcore.capability.rs232.merged.v1')):
|
||||
manifest['capabilities'].append(capability)
|
||||
manifest['verification'].append(dict(capability=capability, source=source,
|
||||
present=True, evidence=evidence))
|
||||
path = record['files'][0]
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
application = archive.read('application.bin')
|
||||
@@ -153,20 +172,30 @@ class ReleaseQualificationTest(unittest.TestCase):
|
||||
'--input', str(directory / 'input'), '--output', str(directory / 'stage'),
|
||||
'--build-status', str(status), '--commit', commit]
|
||||
|
||||
def test_direct_matrix_packaging_omits_resumed_solar_internal_room_packages(self):
|
||||
def test_direct_matrix_packaging_omits_resumed_onboard_qspi_alternatives(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
directory = Path(temp)
|
||||
inputs = directory / 'input'
|
||||
inputs.mkdir()
|
||||
retired = []
|
||||
expected = []
|
||||
for board in ('30S', '33S'):
|
||||
target = 'solarxiao_' + board + '_room_server'
|
||||
cases = [(target, target + '_lora_ota') for target in (
|
||||
'solarxiao_30S_room_server', 'solarxiao_33S_room_server', 'Xiao_nrf52_room_server',
|
||||
'ikoka_nano_nrf_33dbm_room_server', 'ikoka_stick_nrf_22dbm_room_server',
|
||||
'ikoka_handheld_nrf_e22_30dbm_room_server', 'LilyGo_T-Echo_room_server',
|
||||
'SenseCap_Solar_room_server', 'ThinkNode_M1_room_server', 'ThinkNode_M6_room_server',
|
||||
'WioTrackerL1_room_server', 'LilyGo_T-Echo-Lite_room_server', 'LilyGo_T-Echo-Lite_repeater')]
|
||||
cases += [(target + '_lora_ota_no_external_sensors', target) for target in (
|
||||
'LilyGo_T-Echo_repeater', 'SenseCap_Solar_repeater', 'ThinkNode_M1_repeater',
|
||||
'ThinkNode_M6_repeater', 'WioTrackerL1_repeater')]
|
||||
cases.append(('Xiao_nrf52_repeater_bridge_rs232', 'Xiao_nrf52_repeater'))
|
||||
for target, primary in cases:
|
||||
for sensor in ('full', 'reduced'):
|
||||
retired.append(self.sensor_package_fixture(inputs, target, sensor))
|
||||
expected.append(self.sensor_package_fixture(inputs, target + '_lora_ota', sensor,
|
||||
external=True))
|
||||
for target in ('Xiao_nrf52_room_server', 'RAK_3401_room_server'):
|
||||
expected.append(self.sensor_package_fixture(inputs, primary, sensor, external=True,
|
||||
merged_bridge=primary == 'Xiao_nrf52_repeater'))
|
||||
for target in ('muzi_base_uno_room_server', 'RAK_3401_room_server',
|
||||
'Nano_G2_Ultra_room_server', 'LilyGo_T-Echo_Card_room_server'):
|
||||
for sensor in ('full', 'reduced'):
|
||||
expected.append(self.sensor_package_fixture(inputs, target, sensor))
|
||||
before = {p.name: p.read_bytes() for p in inputs.iterdir()}
|
||||
@@ -184,6 +213,7 @@ class ReleaseQualificationTest(unittest.TestCase):
|
||||
staged = by_artifact[manifest['artifact_target']]
|
||||
self.assertEqual(staged['target'], manifest['target'])
|
||||
self.assertEqual(staged['ota_update_requirements'], manifest['ota_update_requirements'])
|
||||
self.assertEqual(staged['capabilities'], manifest['capabilities'])
|
||||
for name in staged['files']:
|
||||
matches = list(stage.glob('*/' + name))
|
||||
self.assertEqual(len(matches), 1)
|
||||
@@ -209,6 +239,38 @@ class ReleaseQualificationTest(unittest.TestCase):
|
||||
self.assertFalse((directory / 'stage').exists())
|
||||
self.assertEqual({p.name: p.read_bytes() for p in inputs.iterdir()}, before)
|
||||
|
||||
def test_direct_matrix_packaging_requires_xiao_runtime_bridge_in_each_profile(self):
|
||||
for missing_sensor in ('full', 'reduced'):
|
||||
with self.subTest(profile=missing_sensor), tempfile.TemporaryDirectory() as temp:
|
||||
directory = Path(temp)
|
||||
inputs = directory / 'input'
|
||||
inputs.mkdir()
|
||||
for sensor in ('full', 'reduced'):
|
||||
self.sensor_package_fixture(inputs, 'Xiao_nrf52_repeater_bridge_rs232', sensor)
|
||||
self.sensor_package_fixture(inputs, 'Xiao_nrf52_repeater', sensor, external=True,
|
||||
merged_bridge=sensor != missing_sensor)
|
||||
before = {p.name: p.read_bytes() for p in inputs.iterdir()}
|
||||
result = subprocess.run(self.matrix_package_command(directory), capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('verified runtime RS232 bridge', result.stderr)
|
||||
self.assertFalse((directory / 'stage').exists())
|
||||
self.assertEqual({p.name: p.read_bytes() for p in inputs.iterdir()}, before)
|
||||
|
||||
def test_direct_matrix_packaging_rejects_experimental_qspi_even_with_complete_pair(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
directory = Path(temp)
|
||||
inputs = directory / 'input'
|
||||
inputs.mkdir()
|
||||
for sensor in ('full', 'reduced'):
|
||||
self.sensor_package_fixture(inputs, 'experimental_repeater_lora_ota', sensor, external=True,
|
||||
extra_capabilities=('hardware.qualification.pending',))
|
||||
before = {p.name: p.read_bytes() for p in inputs.iterdir()}
|
||||
result = subprocess.run(self.matrix_package_command(directory), capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('hardware qualification pending', result.stderr)
|
||||
self.assertFalse((directory / 'stage').exists())
|
||||
self.assertEqual({p.name: p.read_bytes() for p in inputs.iterdir()}, before)
|
||||
|
||||
def test_sensor_pair_requires_one_of_each_with_matching_packaged_identity_and_layout(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
directory = Path(temp)
|
||||
|
||||
@@ -1228,6 +1228,169 @@ assert.deepStrictEqual(internalSolarCatalog.rows.map(row => row.name),
|
||||
internalSolarAssets.map(file => file.name),
|
||||
'Every legacy publication form must remain available to exact filename search');
|
||||
console.log('SolarXiao QSPI-only room recommendations and exact compatibility downloads passed');
|
||||
const fixedQspiHardware = ['LilyGo_T-Echo', 'SenseCap_Solar', 'ThinkNode_M1',
|
||||
'ThinkNode_M6', 'WioTrackerL1'];
|
||||
const newQspiRoomHardware = ['Xiao_nrf52', 'ikoka_handheld_nrf_e22_30dbm',
|
||||
'ikoka_nano_nrf_22dbm', 'ikoka_nano_nrf_30dbm', 'ikoka_nano_nrf_33dbm',
|
||||
'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');
|
||||
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),
|
||||
'Retired room images must remain available for exact compatibility search');
|
||||
}
|
||||
}
|
||||
const qspiFixtureSource = 'deadbeef' + '0'.repeat(32);
|
||||
const qspiFixtureFamily = 'v1.17.1.10-halo-keymind-cascade-dev-deadbeef';
|
||||
const qspiRoomControls = {familyTag: qspiFixtureFamily, profiles: {}};
|
||||
const qspiRoomTargets = [];
|
||||
for (const hardware of [...newQspiRoomHardware, 'solarxiao_30S', 'solarxiao_33S']) {
|
||||
for (const base of [hardware + '_room_server',
|
||||
hardware + '_room_server_lora_ota_no_external_sensors', hardware + '_room_server_lora_ota']) {
|
||||
for (const policy of ['full', 'reduced']) {
|
||||
const target = base + '-' + policy;
|
||||
qspiRoomTargets.push(target);
|
||||
qspiRoomControls.profiles[target] = {
|
||||
platform: 'NRF52_PLATFORM', otaRole: 'lora-receiver', updateMethods: ['lora'],
|
||||
sensorProfile: policy, sensorProfileSource: qspiFixtureSource,
|
||||
updateRequirements: {lora: {storage: base.endsWith('_lora_ota') ? 'external_qspi' : 'internal_flash'}},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const qspiRoomAssets = qspiRoomTargets.map(target => asset(target + '-ota-' + qspiFixtureFamily + '.uf2'));
|
||||
const qspiRoomCatalog = picker.buildCatalog([
|
||||
release(qspiFixtureFamily, '2026-10-16T00:00:00Z', qspiRoomAssets),
|
||||
], qspiRoomControls);
|
||||
assert.deepStrictEqual(qspiRoomCatalog.rows.map(row => row.target), qspiRoomTargets);
|
||||
for (const hardware of [...newQspiRoomHardware, 'solarxiao_30S', 'solarxiao_33S']) {
|
||||
const visible = qspiRoomCatalog.profiles.filter(profile => profile.hardware === hardware);
|
||||
assert.deepStrictEqual(visible.map(profile => profile.target).sort(),
|
||||
['full', 'reduced'].map(policy => hardware + '_room_server_lora_ota-' + policy));
|
||||
assert(visible.every(profile => profile.sensorProfile &&
|
||||
profile.files.every(file => file.target === profile.target)),
|
||||
'New QSPI room recommendations must retain qualified sensor policies and exact identities');
|
||||
}
|
||||
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');
|
||||
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'));
|
||||
const liteRepeaterCatalog = picker.buildCatalog([
|
||||
release(qspiFixtureFamily, '2026-10-16T00:00:00Z', liteRepeaterAssets),
|
||||
]);
|
||||
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');
|
||||
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_'));
|
||||
assert(original.length > 0, 'The fixture must exercise each hardware-pending board');
|
||||
for (const target of original) {
|
||||
assert(currentCatalog.profiles.some(profile => profile.target === target),
|
||||
'Hardware-pending QSPI boards must retain their supported current firmware');
|
||||
}
|
||||
}
|
||||
for (const hardware of fixedQspiHardware) {
|
||||
const base = hardware + '_repeater';
|
||||
const visible = currentCatalog.profiles.filter(profile => profile.hardware === hardware && profile.role === 'repeater');
|
||||
assert.deepStrictEqual(visible.map(profile => profile.target).sort(),
|
||||
['full', 'reduced'].map(policy => base + '-' + policy),
|
||||
'Qualified QSPI canonical pairs must replace duplicate repeater recommendations');
|
||||
for (const policy of ['full', 'reduced']) {
|
||||
assert(currentCatalog.rows.some(row => row.target === base + '_lora_ota_no_external_sensors-' + policy));
|
||||
}
|
||||
for (const fault of ['missing_full', 'missing_reduced', 'stale_source', 'mixed_source',
|
||||
'internal_storage', 'missing_controls']) {
|
||||
const changedControls = JSON.parse(JSON.stringify(currentControls));
|
||||
let files = currentAssets.filter(file => file.name.startsWith(base + '-full-') ||
|
||||
file.name.startsWith(base + '-reduced-') || file.name.startsWith(base + '_lora_ota_no_external_sensors-'));
|
||||
if (fault.startsWith('missing_') && fault !== 'missing_controls') {
|
||||
files = files.filter(file => !file.name.startsWith(base + '-' + fault.slice('missing_'.length) + '-'));
|
||||
} 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(currentControls.familyTag, '2026-10-16T00:00:00Z', files),
|
||||
], fault === 'missing_controls' ? undefined : changedControls);
|
||||
for (const policy of ['full', 'reduced']) {
|
||||
assert(partial.profiles.some(profile => profile.target === base + '_lora_ota_no_external_sensors-' + policy),
|
||||
hardware + ': retain exact aliases when canonical QSPI qualification is incomplete (' + fault + ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
const xiaoBridgeBase = 'Xiao_nrf52_repeater_bridge_rs232';
|
||||
const xiaoBridgeBases = [xiaoBridgeBase, xiaoBridgeBase + '_lora_ota_no_external_sensors'];
|
||||
assert(['full', 'reduced'].every(policy => currentCatalog.profiles.some(profile =>
|
||||
profile.target === xiaoBridgeBase + '-' + policy)),
|
||||
'The old canonical image has no merged bridge proof and must retain both bridge downloads');
|
||||
const mergedXiaoControls = JSON.parse(JSON.stringify(currentControls));
|
||||
for (const policy of ['full', 'reduced']) {
|
||||
Object.assign(mergedXiaoControls.profiles['Xiao_nrf52_repeater-' + policy], {
|
||||
rs232: true, rs232Merged: true,
|
||||
rs232Source: currentControls.profiles['Xiao_nrf52_repeater-' + policy].sensorProfileSource,
|
||||
});
|
||||
mergedXiaoControls.profiles[xiaoBridgeBases[1] + '-' + policy] = {
|
||||
...currentControls.profiles[xiaoBridgeBase + '-' + policy],
|
||||
};
|
||||
}
|
||||
const xiaoRepeaterAssets = currentAssets.filter(file =>
|
||||
file.name.startsWith('Xiao_nrf52_repeater-full-') ||
|
||||
file.name.startsWith('Xiao_nrf52_repeater-reduced-') || file.name.startsWith(xiaoBridgeBase + '-'))
|
||||
.concat(['full', 'reduced'].map(policy =>
|
||||
asset(xiaoBridgeBases[1] + '-' + policy + '-ota-' + currentControls.familyTag + '.uf2')));
|
||||
const mergedXiaoCatalog = picker.buildCatalog([
|
||||
release(currentControls.familyTag, '2026-10-16T00:00:00Z', xiaoRepeaterAssets),
|
||||
], mergedXiaoControls);
|
||||
assert.deepStrictEqual(mergedXiaoCatalog.profiles.map(profile => profile.target).sort(),
|
||||
['full', 'reduced'].map(policy => 'Xiao_nrf52_repeater-' + policy));
|
||||
for (const profile of mergedXiaoCatalog.profiles) {
|
||||
assert.deepStrictEqual(picker.profileFieldValues(profile, 'mode'), ['standard', 'rs232']);
|
||||
assert(profile.files.every(file => file.target === profile.target));
|
||||
}
|
||||
assert(xiaoBridgeBases.every(base => ['full', 'reduced'].every(policy =>
|
||||
mergedXiaoCatalog.rows.some(row => row.target === base + '-' + policy))),
|
||||
'Retired bridge identities must remain exact searchable compatibility files');
|
||||
for (const fault of ['missing_full', 'missing_reduced', 'unmerged', 'missing_source', 'stale_source', 'no_rs232']) {
|
||||
const changedControls = JSON.parse(JSON.stringify(mergedXiaoControls));
|
||||
let files = xiaoRepeaterAssets;
|
||||
const info = changedControls.profiles['Xiao_nrf52_repeater-full'];
|
||||
if (fault === 'missing_full' || fault === 'missing_reduced') {
|
||||
files = files.filter(file => !file.name.startsWith('Xiao_nrf52_repeater-' + fault.slice(8) + '-'));
|
||||
} else if (fault === 'unmerged') {
|
||||
delete info.rs232Merged;
|
||||
} else if (fault === 'missing_source') {
|
||||
delete info.rs232Source;
|
||||
} else if (fault === 'stale_source') {
|
||||
info.rs232Source = 'f'.repeat(40);
|
||||
} else {
|
||||
info.rs232 = false;
|
||||
}
|
||||
const partial = picker.buildCatalog([
|
||||
release(currentControls.familyTag, '2026-10-16T00:00:00Z', files),
|
||||
], changedControls);
|
||||
assert(xiaoBridgeBases.every(base => ['full', 'reduced'].every(policy =>
|
||||
partial.profiles.some(profile => profile.target === base + '-' + policy))),
|
||||
'Keep bridge functionality until both canonical policies prove merged control: ' + fault);
|
||||
}
|
||||
console.log('onboard-QSPI room retirement and qualified repeater alias replacements passed');
|
||||
for (const [hardware, bases] of [
|
||||
['RAK_4631', ['RAK_4631_repeater_unified_lora_ota', 'RAK_4631_repeater_ethernet']],
|
||||
['RAK_3401', ['RAK_3401_repeater_unified_lora_ota']],
|
||||
|
||||
@@ -35,6 +35,37 @@ def sensor_pair(target: str, storage: str = "external_qspi") -> list[dict]:
|
||||
return variants
|
||||
|
||||
|
||||
def onboard_qspi_retirements() -> list[tuple[str, str]]:
|
||||
rooms = ("solarxiao_30S_room_server", "solarxiao_33S_room_server", "Xiao_nrf52_room_server",
|
||||
"ikoka_nano_nrf_22dbm_room_server", "ikoka_nano_nrf_30dbm_room_server",
|
||||
"ikoka_nano_nrf_33dbm_room_server", "ikoka_stick_nrf_22dbm_room_server",
|
||||
"ikoka_stick_nrf_30dbm_room_server", "ikoka_stick_nrf_33dbm_room_server",
|
||||
"ikoka_handheld_nrf_e22_30dbm_room_server", "LilyGo_T-Echo_room_server",
|
||||
"SenseCap_Solar_room_server", "ThinkNode_M1_room_server", "ThinkNode_M6_room_server",
|
||||
"WioTrackerL1_room_server", "LilyGo_T-Echo-Lite_room_server")
|
||||
cases = [(legacy, base + "_lora_ota")
|
||||
for base in rooms + ("LilyGo_T-Echo-Lite_repeater",)
|
||||
for legacy in (base, base + "_lora_ota_no_external_sensors")]
|
||||
cases += [(target + "_lora_ota_no_external_sensors", target) for target in (
|
||||
"LilyGo_T-Echo_repeater", "SenseCap_Solar_repeater", "ThinkNode_M1_repeater",
|
||||
"ThinkNode_M6_repeater", "WioTrackerL1_repeater")]
|
||||
cases += [(target, "Xiao_nrf52_repeater") for target in (
|
||||
"Xiao_nrf52_repeater_bridge_rs232",
|
||||
"Xiao_nrf52_repeater_bridge_rs232_lora_ota_no_external_sensors")]
|
||||
return cases
|
||||
|
||||
|
||||
def prove_runtime_rs232(variants: list[dict]) -> list[dict]:
|
||||
for item in variants:
|
||||
manifest = item["manifest"]
|
||||
for capability, source in (("bridge.rs232", "linked image"),
|
||||
("bridge.rs232.merged", "packaged application")):
|
||||
manifest["capabilities"].append(capability)
|
||||
manifest["verification"].append(dict(capability=capability, source=source,
|
||||
present=True, evidence=capability))
|
||||
return variants
|
||||
|
||||
|
||||
class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
def test_resume_discards_everyday_esp32_standard_profiles_with_or_without_full(self):
|
||||
for target in ("heltec_v4_repeater", "heltec_v4_room_server", "heltec_v4_sensor",
|
||||
@@ -282,7 +313,8 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
legacy.append(record(target, "auto", "NRF52_PLATFORM"))
|
||||
primaries.extend(sensor_pair(target + "_LORA_OTA"))
|
||||
unrelated = [record(target, "auto", "NRF52_PLATFORM") for target in (
|
||||
"Xiao_nrf52_room_server", "Xiao_nrf52_room_server_lora_ota_no_external_sensors",
|
||||
"muzi_base_uno_room_server", "Nano_G2_Ultra_room_server",
|
||||
"LilyGo_T-Echo_Card_room_server", "R1Neo_sensor",
|
||||
"RAK_3401_room_server", "RAK_4631_room_server_lora_ota_no_external_sensors",
|
||||
"solarxiao_30S_repeater", "solarxiao_33S_sensor",
|
||||
)]
|
||||
@@ -346,6 +378,90 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
select_ordinary_full_records(inputs)
|
||||
self.assertEqual(inputs, before)
|
||||
|
||||
def test_onboard_qspi_retirement_preserves_pairs_files_sources_and_original_identities(self):
|
||||
legacy = []
|
||||
primaries = {}
|
||||
for old, primary in onboard_qspi_retirements():
|
||||
legacy.extend(sensor_pair(old.upper(), "internal_flash"))
|
||||
if primary not in primaries:
|
||||
pair = sensor_pair(primary)
|
||||
if primary == "Xiao_nrf52_repeater":
|
||||
prove_runtime_rs232(pair)
|
||||
primaries[primary] = pair
|
||||
for item in legacy:
|
||||
item["manifest"].update(source_commit="original-recovery-source", target_id=1234)
|
||||
current = [item for pair in primaries.values() for item in pair]
|
||||
for item in current:
|
||||
item["manifest"].update(source_commit="qualified-normal-source", target_id=5678)
|
||||
unrelated = [record(target, "auto", "NRF52_PLATFORM") for target in (
|
||||
"muzi_base_duo_repeater", "muzi_base_uno_room_server", "Nano_G2_Ultra_room_server",
|
||||
"LilyGo_T-Echo_Card_room_server", "RAK_3401_room_server", "R1Neo_sensor")]
|
||||
for inputs in (legacy + current + unrelated, unrelated + current + legacy,
|
||||
list(reversed(legacy + current + unrelated))):
|
||||
before = deepcopy(inputs)
|
||||
expected = [item for item in inputs if all(item is not old for old in legacy)]
|
||||
selected = select_ordinary_full_records(inputs)
|
||||
self.assertEqual(selected, expected)
|
||||
self.assertTrue(all(actual is original for actual, original in zip(selected, expected)))
|
||||
self.assertEqual(inputs, before)
|
||||
|
||||
def test_all_onboard_qspi_retirements_require_the_exact_complete_matching_pair(self):
|
||||
for old, primary in onboard_qspi_retirements():
|
||||
for fault in ("missing", "wrong_board", "full_only", "reduced_only", "duplicate",
|
||||
"source_mismatch", "pending_hardware"):
|
||||
pair = sensor_pair(primary)
|
||||
if primary == "Xiao_nrf52_repeater":
|
||||
prove_runtime_rs232(pair)
|
||||
if fault == "missing":
|
||||
pair = []
|
||||
elif fault == "wrong_board":
|
||||
pair = sensor_pair("unrelated_room_server_lora_ota")
|
||||
elif fault == "full_only":
|
||||
pair = pair[:1]
|
||||
elif fault == "reduced_only":
|
||||
pair = pair[1:]
|
||||
elif fault == "duplicate":
|
||||
pair.append(deepcopy(pair[0]))
|
||||
elif fault == "source_mismatch":
|
||||
pair[0]["manifest"]["source_commit"] = "different-source"
|
||||
else:
|
||||
pair[0]["manifest"]["capabilities"].append("hardware.qualification.pending")
|
||||
stale = sensor_pair(old, "internal_flash")
|
||||
inputs = stale + pair
|
||||
before = deepcopy(inputs)
|
||||
with self.subTest(old=old, fault=fault):
|
||||
with self.assertRaisesRegex(ValueError, "qualified external-QSPI Full/Reduced pair"):
|
||||
select_ordinary_full_records(inputs)
|
||||
self.assertEqual(inputs, before)
|
||||
|
||||
def test_xiao_bridge_retirement_requires_packaged_runtime_and_linked_driver_proof_in_both_profiles(self):
|
||||
for old in ("Xiao_nrf52_repeater_bridge_rs232",
|
||||
"Xiao_nrf52_repeater_bridge_rs232_lora_ota_no_external_sensors"):
|
||||
for index in (0, 1):
|
||||
for capability in ("bridge.rs232", "bridge.rs232.merged"):
|
||||
for fault in ("missing_capability", "missing_proof", "false_proof",
|
||||
"wrong_source", "empty_evidence"):
|
||||
pair = prove_runtime_rs232(sensor_pair("Xiao_nrf52_repeater"))
|
||||
manifest = pair[index]["manifest"]
|
||||
check = next(item for item in manifest["verification"]
|
||||
if item["capability"] == capability)
|
||||
if fault == "missing_capability":
|
||||
manifest["capabilities"].remove(capability)
|
||||
elif fault == "missing_proof":
|
||||
manifest["verification"].remove(check)
|
||||
elif fault == "false_proof":
|
||||
check["present"] = False
|
||||
elif fault == "wrong_source":
|
||||
check["source"] = "filename"
|
||||
else:
|
||||
check["evidence"] = ""
|
||||
inputs = sensor_pair(old, "internal_flash") + pair
|
||||
before = deepcopy(inputs)
|
||||
with self.subTest(old=old, profile=index, capability=capability, fault=fault):
|
||||
with self.assertRaisesRegex(ValueError, "verified runtime RS232 bridge"):
|
||||
select_ordinary_full_records(inputs)
|
||||
self.assertEqual(inputs, before)
|
||||
|
||||
def test_nrf52_full_and_reduced_ota_pair_is_never_collapsed(self):
|
||||
for role in ('repeater', 'room_server', 'sensor'):
|
||||
target = 'RAK_3401_' + role
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolved PlatformIO contracts for onboard-NOR Repeater and Room targets.
|
||||
|
||||
Set MESHCORE_TEST_PIO_CONFIG to a previously generated PlatformIO JSON file
|
||||
when sharing a checkout with a build. Otherwise this suite resolves the project
|
||||
once; it must follow the repository's single-PlatformIO-process rule.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
V6 = "boards/nrf52840_s140_v6.ld"
|
||||
V7 = "boards/nrf52840_s140_v7.ld"
|
||||
XIAO = "boards/nrf52840_s140_v7_xiao_bootloader_ota.ld"
|
||||
XIAO_BOARD = "seeed-xiao-afruitnrf52-nrf52840"
|
||||
PENDING = "OTA_QSPI_HARDWARE_QUALIFICATION_PENDING"
|
||||
INTERNAL_ONLY = ("OTA_HYBRID_RAM_STORE", "OTA_INTERNAL_BOOTLOADER_UPDATE")
|
||||
STORAGE_CHANGES = {
|
||||
"ENABLE_OTA", "DISABLE_LORA_OTA", "QSPIFLASH", "OTA_FLASH_STORE",
|
||||
"OTA_QSPI_STORE", "OTA_FOLDER_SERIAL", "OTA_QSPI_BOOTLOADER_UPDATE", PENDING,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Profile:
|
||||
parent: str
|
||||
board: str
|
||||
variant: str
|
||||
linker: str = V6
|
||||
maximum_size: int = 815104
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
return self.parent + "_lora_ota"
|
||||
|
||||
|
||||
SUPPORTED = (
|
||||
Profile("Xiao_nrf52_room_server", XIAO_BOARD, "xiao_nrf52", XIAO, 757760),
|
||||
Profile("ikoka_handheld_nrf_e22_30dbm_room_server", XIAO_BOARD,
|
||||
"ikoka_handheld_nrf", XIAO, 757760),
|
||||
*(Profile(f"ikoka_nano_nrf_{power}dbm_room_server", XIAO_BOARD,
|
||||
"ikoka_nano_nrf", XIAO, 757760) for power in (22, 30, 33)),
|
||||
*(Profile(f"ikoka_stick_nrf_{power}dbm_room_server", XIAO_BOARD,
|
||||
"ikoka_stick_nrf", XIAO, 757760) for power in (22, 30, 33)),
|
||||
Profile("LilyGo_T-Echo_room_server", "t-echo", "lilygo_techo"),
|
||||
Profile("ThinkNode_M1_room_server", "thinknode_m1", "thinknode_m1"),
|
||||
Profile("ThinkNode_M6_room_server", "thinknode_m6", "thinknode_m6"),
|
||||
Profile("WioTrackerL1_room_server", "seeed-wio-tracker-l1",
|
||||
"wio-tracker-l1", V7, 811008),
|
||||
Profile("SenseCap_Solar_room_server", "seeed_sensecap_solar",
|
||||
"sensecap_solar", V7, 811008),
|
||||
Profile("LilyGo_T-Echo-Lite_repeater", "t-echo", "lilygo_techo_lite"),
|
||||
Profile("LilyGo_T-Echo-Lite_room_server", "t-echo", "lilygo_techo_lite"),
|
||||
)
|
||||
PREEXISTING_SOLAR_ROOMS = (
|
||||
Profile("solarxiao_30S_room_server", XIAO_BOARD, "xiao_nrf52", XIAO, 757760),
|
||||
Profile("solarxiao_33S_room_server", XIAO_BOARD, "xiao_nrf52", XIAO, 757760),
|
||||
)
|
||||
QUALIFICATION = (
|
||||
*(Profile(f"muzi_base_{radio}_{role}", "muzi_base", "muzi_base")
|
||||
for radio in ("uno", "duo") for role in ("repeater", "room_server")),
|
||||
*(Profile(f"LilyGo_T-Echo_Card_{role}", "t-echo", "lilygo_techo_card")
|
||||
for role in ("repeater", "room_server")),
|
||||
*(Profile(f"Nano_G2_Ultra_{role}", "nano-g2-ultra", "nano_g2_ultra")
|
||||
for role in ("repeater", "room_server")),
|
||||
)
|
||||
|
||||
|
||||
def text_option(value):
|
||||
if isinstance(value, list):
|
||||
return "\n".join(str(item) for item in value)
|
||||
return str(value)
|
||||
|
||||
|
||||
def macro_operations(value):
|
||||
"""Parse both '-D NAME=1' and '-UNAME', preserving command-line order."""
|
||||
tokens = iter(shlex.split(text_option(value)))
|
||||
operations = []
|
||||
for token in tokens:
|
||||
if token in ("-D", "-U"):
|
||||
kind, argument = token[1], next(tokens)
|
||||
elif token.startswith(("-D", "-U")):
|
||||
kind, argument = token[1], token[2:]
|
||||
else:
|
||||
continue
|
||||
name, separator, value = argument.partition("=")
|
||||
operations.append((kind, name, value if separator else None))
|
||||
return operations
|
||||
|
||||
|
||||
def effective_defines(options):
|
||||
# ProcessFlags emits CPPDEFINES first and appends every -U afterwards in
|
||||
# _CPPDEFFLAGS, regardless of the order in build_flags. ProcessUnFlags
|
||||
# removes matching CPPDEFINES; a bare -D name also removes valued defines.
|
||||
# A defined zero still violates raw-QSPI/QSPIFLASH exclusivity.
|
||||
removed = set(macro_operations(options.get("build_unflags", "")))
|
||||
operations = macro_operations(options.get("build_flags", ""))
|
||||
defines = {}
|
||||
for operation in operations:
|
||||
kind, name, value = operation
|
||||
if kind == "D" and operation not in removed and (kind, name, None) not in removed:
|
||||
defines[name] = value
|
||||
for kind, name, _ in operations:
|
||||
if kind == "U":
|
||||
defines.pop(name, None)
|
||||
return defines
|
||||
|
||||
|
||||
def role_defines(options):
|
||||
return {
|
||||
name: value for name, value in effective_defines(options).items()
|
||||
if name not in STORAGE_CHANGES
|
||||
}
|
||||
|
||||
|
||||
def variant_includes(options):
|
||||
tokens = iter(shlex.split(text_option(options.get("build_flags", ""))))
|
||||
includes = set()
|
||||
for token in tokens:
|
||||
if token == "-I":
|
||||
path = next(tokens)
|
||||
elif token.startswith("-I"):
|
||||
path = token[2:]
|
||||
else:
|
||||
continue
|
||||
if path.startswith("variants/"):
|
||||
includes.add(path)
|
||||
return includes
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def source_matches(pattern, path):
|
||||
# MatchSourceFiles expands glob patterns under PROJECT_SRC_DIR, then
|
||||
# recursively includes matched directories. fnmatch('*.cpp') would also
|
||||
# match helpers/ota/foo.cpp or ../examples/foo.cpp, which PIO does not.
|
||||
source = (ROOT / "src" / path).resolve()
|
||||
for candidate in glob.iglob(str(ROOT / "src" / pattern), recursive=True):
|
||||
candidate = Path(candidate).resolve()
|
||||
if candidate == source or candidate.is_dir() and candidate in source.parents:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def source_selected(options, path):
|
||||
selected = False
|
||||
for action, pattern in re.findall(
|
||||
r"([+-])<([^>]+)>", text_option(options.get("build_src_filter", ""))
|
||||
):
|
||||
if source_matches(pattern, path):
|
||||
selected = action == "+"
|
||||
return selected
|
||||
|
||||
|
||||
class OnboardQspiProfilesTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cached = os.environ.get("MESHCORE_TEST_PIO_CONFIG")
|
||||
if cached:
|
||||
payload = Path(cached).read_text(encoding="utf-8")
|
||||
else:
|
||||
payload = subprocess.check_output(
|
||||
["pio", "project", "config", "--json-output"], cwd=ROOT, text=True
|
||||
)
|
||||
cls.config = {section: dict(options) for section, options in json.loads(payload)}
|
||||
|
||||
def options(self, target):
|
||||
self.assertIn("env:" + target, self.config)
|
||||
return self.config["env:" + target]
|
||||
|
||||
def assert_external_contract(self, profile, pending=False):
|
||||
options = self.options(profile.target)
|
||||
parent = self.options(profile.parent)
|
||||
defines = effective_defines(options)
|
||||
parents = re.split(r"[,\s]+", text_option(options.get("extends", "")).strip())
|
||||
self.assertEqual(parents, ["env:" + profile.parent])
|
||||
self.assertEqual(options.get("board"), profile.board)
|
||||
self.assertEqual(options.get("board"), parent.get("board"))
|
||||
self.assertEqual(role_defines(options), role_defines(parent))
|
||||
self.assertEqual(variant_includes(options), {"variants/" + profile.variant})
|
||||
self.assertEqual(variant_includes(options), variant_includes(parent))
|
||||
self.assertEqual(options.get("board_build.ldscript"), profile.linker)
|
||||
self.assertEqual(int(options.get("board_upload.maximum_size", 0)), profile.maximum_size)
|
||||
for name in ("ENABLE_OTA", "OTA_FLASH_STORE", "OTA_QSPI_STORE"):
|
||||
self.assertEqual(defines.get(name), "1", name)
|
||||
for name in ("QSPIFLASH", "DISABLE_LORA_OTA", "OTA_SD_STORE", *INTERNAL_ONLY):
|
||||
self.assertNotIn(name, defines)
|
||||
if profile.linker == XIAO:
|
||||
self.assertEqual(defines.get("OTA_QSPI_BOOTLOADER_UPDATE"), "1")
|
||||
else:
|
||||
self.assertNotIn("OTA_QSPI_BOOTLOADER_UPDATE", defines)
|
||||
if pending:
|
||||
self.assertEqual(defines.get(PENDING), "1")
|
||||
else:
|
||||
self.assertNotIn(PENDING, defines)
|
||||
self.assertIn("post:tools/mota/pio_endf.py", text_option(options.get("extra_scripts", "")))
|
||||
for source in ("helpers/ota/OtaStoreQspiNrf52.cpp", "helpers/ota/OtaManager.cpp"):
|
||||
self.assertTrue(source_selected(options, source), source)
|
||||
role = "simple_room_server" if "_room_server" in profile.parent else "simple_repeater"
|
||||
self.assertTrue(source_selected(options, "../examples/" + role + "/main.cpp"))
|
||||
|
||||
def test_supported_external_profiles_keep_exact_legacy_hardware(self):
|
||||
self.assertEqual(len(SUPPORTED), 15)
|
||||
for profile in (*SUPPORTED, *PREEXISTING_SOLAR_ROOMS):
|
||||
with self.subTest(target=profile.target):
|
||||
self.assert_external_contract(profile)
|
||||
|
||||
def test_original_internal_recipes_remain_distinct(self):
|
||||
for profile in (*SUPPORTED, *PREEXISTING_SOLAR_ROOMS):
|
||||
with self.subTest(target=profile.parent):
|
||||
options = self.options(profile.parent)
|
||||
defines = effective_defines(options)
|
||||
self.assertEqual(options.get("board"), profile.board)
|
||||
self.assertEqual(options.get("board_build.ldscript"), V7 if profile.linker == XIAO else profile.linker)
|
||||
for name in ("OTA_QSPI_STORE", "OTA_QSPI_BOOTLOADER_UPDATE", PENDING, *INTERNAL_ONLY):
|
||||
self.assertNotIn(name, defines)
|
||||
self.assertNotEqual(profile.target, profile.parent)
|
||||
|
||||
def test_linker_files_keep_the_checked_application_geometry(self):
|
||||
for linker, start, end, maximum_size in (
|
||||
(V6, 0x26000, 0xED000, 815104),
|
||||
(V7, 0x27000, 0xED000, 811008),
|
||||
(XIAO, 0x27000, 0xE0000, 757760),
|
||||
):
|
||||
with self.subTest(linker=linker):
|
||||
source = (ROOT / linker).read_text(encoding="ascii")
|
||||
flash = re.search(
|
||||
r"FLASH\s*\(rx\)\s*:\s*ORIGIN\s*=\s*(0x[0-9A-Fa-f]+),"
|
||||
r"\s*LENGTH\s*=\s*(0x[0-9A-Fa-f]+)\s*-\s*(0x[0-9A-Fa-f]+)",
|
||||
source,
|
||||
)
|
||||
self.assertIsNotNone(flash)
|
||||
self.assertEqual(tuple(int(value, 16) for value in flash.groups()), (start, end, start))
|
||||
self.assertEqual(end - start, maximum_size)
|
||||
|
||||
def test_unqualified_profiles_are_explicit_and_do_not_claim_xiao_layout(self):
|
||||
marked = {
|
||||
section[4:] for section, options in self.config.items()
|
||||
if section.startswith("env:") and PENDING in effective_defines(options)
|
||||
}
|
||||
self.assertEqual(len(QUALIFICATION), 8)
|
||||
self.assertEqual(marked, {profile.target for profile in QUALIFICATION})
|
||||
for profile in QUALIFICATION:
|
||||
with self.subTest(target=profile.target):
|
||||
self.assert_external_contract(profile, pending=True)
|
||||
parent = self.options(profile.parent)
|
||||
self.assertNotIn(PENDING, effective_defines(parent))
|
||||
self.assertNotIn("OTA_QSPI_STORE", effective_defines(parent))
|
||||
if profile.board == "muzi_base":
|
||||
self.assertEqual(effective_defines(parent).get("QSPIFLASH"), "1")
|
||||
self.assertIn(
|
||||
("D", "QSPIFLASH", "1"),
|
||||
macro_operations(self.options(profile.target).get("build_unflags", "")),
|
||||
)
|
||||
|
||||
def test_pending_profiles_preserve_base_optimization_unflags(self):
|
||||
baseline = set(shlex.split(text_option(self.config["nrf52_base"]["build_unflags"])))
|
||||
self.assertIn("-Ofast", baseline)
|
||||
for profile in QUALIFICATION:
|
||||
with self.subTest(target=profile.target):
|
||||
options = self.options(profile.target)
|
||||
unflags = set(shlex.split(text_option(options.get("build_unflags", ""))))
|
||||
self.assertTrue(baseline <= unflags, "missing inherited build_unflags: " + repr(baseline - unflags))
|
||||
self.assertIn("-Oz", shlex.split(text_option(options.get("build_flags", ""))))
|
||||
|
||||
def test_xiao_normal_repeaters_have_a_disabled_merged_uart_driver(self):
|
||||
for target in ("Xiao_nrf52_repeater", "solarxiao_30S_repeater", "solarxiao_33S_repeater"):
|
||||
with self.subTest(target=target):
|
||||
options = self.options(target)
|
||||
defines = effective_defines(options)
|
||||
expected = {
|
||||
"WITH_RS232_BRIDGE": "Serial1",
|
||||
"WITH_RS232_BRIDGE_UART": "1",
|
||||
"WITH_RS232_BRIDGE_RX": "PIN_SERIAL1_RX",
|
||||
"WITH_RS232_BRIDGE_TX": "PIN_SERIAL1_TX",
|
||||
"BRIDGE_MAX_BAUD": "500000",
|
||||
"RS232_BRIDGE_MERGED": "1",
|
||||
}
|
||||
for name, value in expected.items():
|
||||
self.assertEqual(defines.get(name), value, name)
|
||||
self.assertNotIn("RS232_BRIDGE_DEFAULT_ON", defines)
|
||||
self.assertNotIn("WITH_MQTT_BRIDGE", defines)
|
||||
self.assertTrue(source_selected(options, "helpers/bridges/RS232Bridge.cpp"))
|
||||
legacy = self.options("Xiao_nrf52_repeater_bridge_rs232")
|
||||
defines = effective_defines(legacy)
|
||||
self.assertEqual(defines.get("WITH_RS232_BRIDGE"), "Serial1")
|
||||
self.assertNotIn("RS232_BRIDGE_MERGED", defines)
|
||||
self.assertNotIn("OTA_QSPI_STORE", defines)
|
||||
self.assertEqual(legacy.get("board_build.ldscript"), V7)
|
||||
self.assertTrue(source_selected(legacy, "helpers/bridges/RS232Bridge.cpp"))
|
||||
|
||||
def test_effective_defines_honor_undefines_and_removed_definitions(self):
|
||||
options = {
|
||||
"build_flags": ["-D ENABLE_OTA=0 -UENABLE_OTA -DENABLE_OTA=1",
|
||||
"-D QSPIFLASH=1 -D ZERO_IS_DEFINED=0 -UABSENT"],
|
||||
"build_unflags": "-D QSPIFLASH=1",
|
||||
}
|
||||
self.assertEqual(effective_defines(options), {"ZERO_IS_DEFINED": "0"})
|
||||
self.assertEqual(effective_defines({
|
||||
"build_flags": "-D QSPIFLASH=1 -D KEEP=2",
|
||||
"build_unflags": "-DQSPIFLASH",
|
||||
}), {"KEEP": "2"})
|
||||
self.assertEqual(effective_defines({
|
||||
"build_flags": "-D LEVEL=1 -D LEVEL=2",
|
||||
"build_unflags": "-D LEVEL=1",
|
||||
}), {"LEVEL": "2"})
|
||||
|
||||
def test_source_filters_keep_globs_inside_the_selected_directory(self):
|
||||
options = {"build_src_filter": "+<*.cpp> +<helpers/*.cpp>"}
|
||||
self.assertFalse(source_selected(options, "../examples/simple_repeater/main.cpp"))
|
||||
self.assertFalse(source_selected(options, "helpers/ota/OtaStoreQspiNrf52.cpp"))
|
||||
options["build_src_filter"] = "+<*>"
|
||||
self.assertFalse(source_selected(options, "../examples/simple_repeater/main.cpp"))
|
||||
self.assertTrue(source_selected(options, "helpers/ota/OtaStoreQspiNrf52.cpp"))
|
||||
options["build_src_filter"] = "+<../examples/simple_repeater>"
|
||||
self.assertTrue(source_selected(options, "../examples/simple_repeater/main.cpp"))
|
||||
options["build_src_filter"] = "+<helpers/ota> -<helpers/ota/OtaStoreQspiNrf52.cpp>"
|
||||
self.assertFalse(source_selected(options, "helpers/ota/OtaStoreQspiNrf52.cpp"))
|
||||
options["build_src_filter"] += " +<helpers/ota/OtaStoreQspiNrf52.cpp>"
|
||||
self.assertTrue(source_selected(options, "helpers/ota/OtaStoreQspiNrf52.cpp"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -79,6 +79,42 @@ class PickerControlsTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, 'ambiguous'):
|
||||
GENERATOR.runtime_metadata(manifest, False, source)
|
||||
|
||||
def test_merged_rs232_requires_qualified_linked_and_packaged_proof(self):
|
||||
source = 'a' * 40
|
||||
manifest = dict(target='Xiao_nrf52_repeater', source_commit=source, verified=True,
|
||||
capabilities=['bridge.rs232', 'bridge.rs232.merged'],
|
||||
verification=[dict(capability='bridge.rs232', present=True,
|
||||
source='linked image', evidence='RS232Bridge::begin'),
|
||||
dict(capability='bridge.rs232.merged', present=True,
|
||||
source='packaged application', evidence='rs232.enabled')])
|
||||
self.assertEqual(GENERATOR.runtime_metadata(manifest, False),
|
||||
dict(otaRole='none', rs232Merged=True, rs232Source=source))
|
||||
for fault in ('unverified', 'no_driver', 'no_runtime', 'no_capability',
|
||||
'wrong_driver_source', 'wrong_runtime_source', 'no_evidence', 'not_present'):
|
||||
changed = json.loads(json.dumps(manifest))
|
||||
if fault == 'unverified':
|
||||
changed['verified'] = False
|
||||
elif fault == 'no_driver':
|
||||
changed['verification'].pop(0)
|
||||
elif fault == 'no_runtime':
|
||||
changed['verification'].pop()
|
||||
elif fault == 'no_capability':
|
||||
changed['capabilities'].remove('bridge.rs232.merged')
|
||||
elif fault == 'wrong_driver_source':
|
||||
changed['verification'][0]['source'] = 'filename'
|
||||
elif fault == 'wrong_runtime_source':
|
||||
changed['verification'][1]['source'] = 'linked image'
|
||||
elif fault == 'no_evidence':
|
||||
changed['verification'][1]['evidence'] = ''
|
||||
else:
|
||||
changed['verification'][1]['present'] = False
|
||||
with self.subTest(fault=fault):
|
||||
self.assertNotIn('rs232Merged', GENERATOR.runtime_metadata(changed, False))
|
||||
self.assertNotIn('rs232Source', GENERATOR.runtime_metadata(changed, False))
|
||||
manifest['source_commit'] = 'aaaaaaaa'
|
||||
with self.assertRaisesRegex(ValueError, 'exact source commit'):
|
||||
GENERATOR.runtime_metadata(manifest, False)
|
||||
|
||||
def test_generates_independent_controls_for_same_identity_sensor_pair(self):
|
||||
source = 'a' * 40
|
||||
family = 'v1.17.1.7-dev-aaaaaaaa'
|
||||
|
||||
@@ -114,6 +114,17 @@ build_flags = ${ikoka_handheld_nrf.build_flags}
|
||||
build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
|
||||
+<../examples/simple_room_server/*.cpp>
|
||||
|
||||
; Keep the original Room recipe for its distinct deployed internal OTA ID.
|
||||
[env:ikoka_handheld_nrf_e22_30dbm_room_server_lora_ota]
|
||||
extends = env:ikoka_handheld_nrf_e22_30dbm_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_xiao_bootloader_ota.ld
|
||||
board_upload.maximum_size = 757760
|
||||
build_flags =
|
||||
${env:ikoka_handheld_nrf_e22_30dbm_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_BOOTLOADER_UPDATE=1
|
||||
build_src_filter = ${env:ikoka_handheld_nrf_e22_30dbm_room_server.build_src_filter}
|
||||
|
||||
[env:ikoka_handheld_nrf_kiss_modem]
|
||||
extends = ikoka_handheld_nrf
|
||||
build_flags = ${ikoka_handheld_nrf.build_flags}
|
||||
|
||||
@@ -193,6 +193,17 @@ build_src_filter =
|
||||
${ikoka_nano_nrf_e22_22dbm.build_src_filter}
|
||||
|
||||
|
||||
; Raw-QSPI Room OTA keeps this exact radio variant and bootloader layout.
|
||||
[env:ikoka_nano_nrf_22dbm_room_server_lora_ota]
|
||||
extends = env:ikoka_nano_nrf_22dbm_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_xiao_bootloader_ota.ld
|
||||
board_upload.maximum_size = 757760
|
||||
build_flags =
|
||||
${env:ikoka_nano_nrf_22dbm_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_BOOTLOADER_UPDATE=1
|
||||
build_src_filter = ${env:ikoka_nano_nrf_22dbm_room_server.build_src_filter}
|
||||
|
||||
;;; 30dBm EBYTE E22-900M30 variants
|
||||
[env:ikoka_nano_nrf_30dbm_companion_radio_usb]
|
||||
extends =
|
||||
@@ -239,6 +250,17 @@ build_src_filter =
|
||||
${ikoka_nano_nrf_e22_30dbm.build_src_filter}
|
||||
|
||||
|
||||
; Raw-QSPI Room OTA keeps this exact radio variant and bootloader layout.
|
||||
[env:ikoka_nano_nrf_30dbm_room_server_lora_ota]
|
||||
extends = env:ikoka_nano_nrf_30dbm_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_xiao_bootloader_ota.ld
|
||||
board_upload.maximum_size = 757760
|
||||
build_flags =
|
||||
${env:ikoka_nano_nrf_30dbm_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_BOOTLOADER_UPDATE=1
|
||||
build_src_filter = ${env:ikoka_nano_nrf_30dbm_room_server.build_src_filter}
|
||||
|
||||
;;; 33dBm EBYTE E22-900M33 variants
|
||||
[env:ikoka_nano_nrf_33dbm_companion_radio_usb]
|
||||
extends =
|
||||
@@ -284,6 +306,17 @@ build_src_filter =
|
||||
${ikoka_nano_nrf_room_server.build_src_filter}
|
||||
${ikoka_nano_nrf_e22_33dbm.build_src_filter}
|
||||
|
||||
; Original Room targets remain available with their deployed internal OTA IDs.
|
||||
[env:ikoka_nano_nrf_33dbm_room_server_lora_ota]
|
||||
extends = env:ikoka_nano_nrf_33dbm_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_xiao_bootloader_ota.ld
|
||||
board_upload.maximum_size = 757760
|
||||
build_flags =
|
||||
${env:ikoka_nano_nrf_33dbm_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_BOOTLOADER_UPDATE=1
|
||||
build_src_filter = ${env:ikoka_nano_nrf_33dbm_room_server.build_src_filter}
|
||||
|
||||
[env:ikoka_nano_nrf_22dbm_kiss_modem]
|
||||
extends = ikoka_nano_nrf_e22_22dbm
|
||||
build_src_filter = ${ikoka_nano_nrf_e22_22dbm.build_src_filter}
|
||||
|
||||
@@ -203,6 +203,17 @@ build_src_filter =
|
||||
${ikoka_stick_nrf_e22_22dbm.build_src_filter}
|
||||
|
||||
|
||||
; Raw-QSPI Room OTA keeps this exact radio variant and bootloader layout.
|
||||
[env:ikoka_stick_nrf_22dbm_room_server_lora_ota]
|
||||
extends = env:ikoka_stick_nrf_22dbm_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_xiao_bootloader_ota.ld
|
||||
board_upload.maximum_size = 757760
|
||||
build_flags =
|
||||
${env:ikoka_stick_nrf_22dbm_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_BOOTLOADER_UPDATE=1
|
||||
build_src_filter = ${env:ikoka_stick_nrf_22dbm_room_server.build_src_filter}
|
||||
|
||||
;;; 30dBm EBYTE E22-900M30 variants
|
||||
|
||||
[env:ikoka_stick_nrf_30dbm_companion_radio_usb]
|
||||
@@ -250,6 +261,17 @@ build_src_filter =
|
||||
${ikoka_stick_nrf_e22_30dbm.build_src_filter}
|
||||
|
||||
|
||||
; Raw-QSPI Room OTA keeps this exact radio variant and bootloader layout.
|
||||
[env:ikoka_stick_nrf_30dbm_room_server_lora_ota]
|
||||
extends = env:ikoka_stick_nrf_30dbm_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_xiao_bootloader_ota.ld
|
||||
board_upload.maximum_size = 757760
|
||||
build_flags =
|
||||
${env:ikoka_stick_nrf_30dbm_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_BOOTLOADER_UPDATE=1
|
||||
build_src_filter = ${env:ikoka_stick_nrf_30dbm_room_server.build_src_filter}
|
||||
|
||||
;;; 33dBm EBYTE E22-900M33 variants
|
||||
|
||||
[env:ikoka_stick_nrf_33dbm_companion_radio_usb]
|
||||
@@ -296,6 +318,17 @@ build_src_filter =
|
||||
${ikoka_stick_nrf_room_server.build_src_filter}
|
||||
${ikoka_stick_nrf_e22_33dbm.build_src_filter}
|
||||
|
||||
; Original Room targets remain available with their deployed internal OTA IDs.
|
||||
[env:ikoka_stick_nrf_33dbm_room_server_lora_ota]
|
||||
extends = env:ikoka_stick_nrf_33dbm_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_xiao_bootloader_ota.ld
|
||||
board_upload.maximum_size = 757760
|
||||
build_flags =
|
||||
${env:ikoka_stick_nrf_33dbm_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_BOOTLOADER_UPDATE=1
|
||||
build_src_filter = ${env:ikoka_stick_nrf_33dbm_room_server.build_src_filter}
|
||||
|
||||
[env:ikoka_stick_nrf_22dbm_kiss_modem]
|
||||
extends = ikoka_stick_nrf_e22_22dbm
|
||||
build_src_filter = ${ikoka_stick_nrf_e22_22dbm.build_src_filter}
|
||||
|
||||
@@ -78,6 +78,19 @@ build_flags =
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
; Preserve the internal-staging Room target above for compatibility/recovery.
|
||||
; Normal OTA uses the same dedicated QSPI store and app layout as Repeater.
|
||||
[env:LilyGo_T-Echo_room_server_lora_ota]
|
||||
extends = env:LilyGo_T-Echo_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_flags =
|
||||
${env:LilyGo_T-Echo_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
build_src_filter = ${env:LilyGo_T-Echo_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:LilyGo_T-Echo_companion_radio_ble]
|
||||
extends = LilyGo_T-Echo
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
|
||||
@@ -62,6 +62,32 @@ build_flags =
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
; Qualification-only: requires the Card-specific OTAFIX port (NFC retained,
|
||||
; no ordinary LEDs, and the correct flash/radio rail), not the T-Echo loader.
|
||||
[env:LilyGo_T-Echo_Card_repeater_lora_ota]
|
||||
extends = env:LilyGo_T-Echo_Card_repeater
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
build_flags =
|
||||
${env:LilyGo_T-Echo_Card_repeater.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_HARDWARE_QUALIFICATION_PENDING=1
|
||||
build_src_filter = ${env:LilyGo_T-Echo_Card_repeater.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:LilyGo_T-Echo_Card_room_server_lora_ota]
|
||||
extends = env:LilyGo_T-Echo_Card_room_server
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
build_flags =
|
||||
${env:LilyGo_T-Echo_Card_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_HARDWARE_QUALIFICATION_PENDING=1
|
||||
build_src_filter = ${env:LilyGo_T-Echo_Card_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:LilyGo_T-Echo_Card_companion_radio_ble]
|
||||
extends = LilyGo_T-Echo_Card
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
|
||||
@@ -67,6 +67,32 @@ build_flags =
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
; Keep the internal-staging targets above available for compatibility/recovery.
|
||||
; These targets require the Lite-specific QSPI OTAFIX bootloader. Its ordinary
|
||||
; application layout needs no reserved XIAO bootloader-update scratch bank.
|
||||
; TechoBoard::begin keeps the shared radio/flash RT9080 rail at P0.30 enabled.
|
||||
[env:LilyGo_T-Echo-Lite_repeater_lora_ota]
|
||||
extends = env:LilyGo_T-Echo-Lite_repeater
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_flags =
|
||||
${env:LilyGo_T-Echo-Lite_repeater.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
build_src_filter = ${env:LilyGo_T-Echo-Lite_repeater.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:LilyGo_T-Echo-Lite_room_server_lora_ota]
|
||||
extends = env:LilyGo_T-Echo-Lite_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_flags =
|
||||
${env:LilyGo_T-Echo-Lite_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
build_src_filter = ${env:LilyGo_T-Echo-Lite_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:LilyGo_T-Echo-Lite_companion_radio_ble]
|
||||
extends = LilyGo_T-Echo-Lite
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
|
||||
@@ -100,6 +100,45 @@ build_src_filter = ${muzi_base_duo.build_src_filter}
|
||||
+<../examples/simple_room_server>
|
||||
lib_deps = ${muzi_base_duo.lib_deps}
|
||||
|
||||
; Explicit qualification builds only. The source-verified Muzi OTAFIX port
|
||||
; still needs physical qualification and a published board-matched loader.
|
||||
; Raw staging must not share the Companion's QSPI filesystem configuration.
|
||||
[env:muzi_base_duo_repeater_lora_ota]
|
||||
extends = env:muzi_base_duo_repeater
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
build_unflags =
|
||||
${nrf52_base.build_unflags}
|
||||
-D QSPIFLASH=1
|
||||
build_flags =
|
||||
${env:muzi_base_duo_repeater.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ENABLE_OTA=1
|
||||
-D OTA_FLASH_STORE=1
|
||||
-D OTA_FOLDER_SERIAL
|
||||
-D OTA_QSPI_HARDWARE_QUALIFICATION_PENDING=1
|
||||
build_src_filter = ${env:muzi_base_duo_repeater.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:muzi_base_duo_room_server_lora_ota]
|
||||
extends = env:muzi_base_duo_room_server
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
build_unflags =
|
||||
${nrf52_base.build_unflags}
|
||||
-D QSPIFLASH=1
|
||||
build_flags =
|
||||
${env:muzi_base_duo_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ENABLE_OTA=1
|
||||
-D OTA_FLASH_STORE=1
|
||||
-D OTA_FOLDER_SERIAL
|
||||
-D OTA_QSPI_HARDWARE_QUALIFICATION_PENDING=1
|
||||
build_src_filter = ${env:muzi_base_duo_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:muzi_base_duo_companion_radio_usb]
|
||||
extends = muzi_base_duo
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
@@ -227,6 +266,42 @@ build_src_filter = ${muzi_base_uno.build_src_filter}
|
||||
+<../examples/simple_room_server>
|
||||
lib_deps = ${muzi_base_uno.lib_deps}
|
||||
|
||||
[env:muzi_base_uno_repeater_lora_ota]
|
||||
extends = env:muzi_base_uno_repeater
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
build_unflags =
|
||||
${nrf52_base.build_unflags}
|
||||
-D QSPIFLASH=1
|
||||
build_flags =
|
||||
${env:muzi_base_uno_repeater.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ENABLE_OTA=1
|
||||
-D OTA_FLASH_STORE=1
|
||||
-D OTA_FOLDER_SERIAL
|
||||
-D OTA_QSPI_HARDWARE_QUALIFICATION_PENDING=1
|
||||
build_src_filter = ${env:muzi_base_uno_repeater.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:muzi_base_uno_room_server_lora_ota]
|
||||
extends = env:muzi_base_uno_room_server
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
build_unflags =
|
||||
${nrf52_base.build_unflags}
|
||||
-D QSPIFLASH=1
|
||||
build_flags =
|
||||
${env:muzi_base_uno_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ENABLE_OTA=1
|
||||
-D OTA_FLASH_STORE=1
|
||||
-D OTA_FOLDER_SERIAL
|
||||
-D OTA_QSPI_HARDWARE_QUALIFICATION_PENDING=1
|
||||
build_src_filter = ${env:muzi_base_uno_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:muzi_base_uno_companion_radio_usb]
|
||||
extends = muzi_base_uno
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
|
||||
@@ -62,6 +62,35 @@ build_src_filter = ${Nano_G2_Ultra.build_src_filter}
|
||||
+<helpers/ui/SH1106Display.cpp>
|
||||
+<../examples/simple_room_server>
|
||||
|
||||
; Qualification-only until the Nano-specific OTAFIX port is physically
|
||||
; verified and published. Never substitute a generic application USB identity.
|
||||
[env:Nano_G2_Ultra_repeater_lora_ota]
|
||||
extends = env:Nano_G2_Ultra_repeater
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
build_flags =
|
||||
${env:Nano_G2_Ultra_repeater.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_HARDWARE_QUALIFICATION_PENDING=1
|
||||
build_src_filter = ${env:Nano_G2_Ultra_repeater.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:Nano_G2_Ultra_room_server_lora_ota]
|
||||
extends = env:Nano_G2_Ultra_room_server
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
build_flags =
|
||||
${env:Nano_G2_Ultra_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ENABLE_OTA=1
|
||||
-D OTA_FLASH_STORE=1
|
||||
-D OTA_FOLDER_SERIAL
|
||||
-D OTA_QSPI_HARDWARE_QUALIFICATION_PENDING=1
|
||||
build_src_filter = ${env:Nano_G2_Ultra_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:Nano_G2_Ultra_companion_radio_ble]
|
||||
extends = Nano_G2_Ultra
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
|
||||
@@ -72,6 +72,22 @@ build_flags =
|
||||
build_src_filter = ${SenseCap_Solar.build_src_filter}
|
||||
+<../examples/simple_room_server/*.cpp>
|
||||
|
||||
; Preserve the non-OTA Room target above for compatibility/recovery.
|
||||
; QSPI staging owns the same onboard NOR as the existing Repeater target.
|
||||
[env:SenseCap_Solar_room_server_lora_ota]
|
||||
extends = env:SenseCap_Solar_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7.ld
|
||||
board_upload.maximum_size = 811008
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_flags =
|
||||
${env:SenseCap_Solar_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ENABLE_OTA=1
|
||||
-D OTA_FLASH_STORE=1
|
||||
-D OTA_FOLDER_SERIAL
|
||||
build_src_filter = ${env:SenseCap_Solar_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:SenseCap_Solar_companion_radio_ble]
|
||||
extends = SenseCap_Solar
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
|
||||
|
||||
@@ -68,6 +68,19 @@ build_src_filter = ${ThinkNode_M1.build_src_filter}
|
||||
lib_deps =
|
||||
${ThinkNode_M1.lib_deps}
|
||||
|
||||
; Preserve the internal-staging Room target above for compatibility/recovery.
|
||||
; Normal OTA uses the same dedicated QSPI store and app layout as Repeater.
|
||||
[env:ThinkNode_M1_room_server_lora_ota]
|
||||
extends = env:ThinkNode_M1_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_flags =
|
||||
${env:ThinkNode_M1_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
build_src_filter = ${env:ThinkNode_M1_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:ThinkNode_M1_companion_radio_ble]
|
||||
extends = ThinkNode_M1
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
|
||||
@@ -72,6 +72,19 @@ build_src_filter = ${ThinkNode_M6.build_src_filter}
|
||||
lib_deps =
|
||||
${ThinkNode_M6.lib_deps}
|
||||
|
||||
; Preserve the internal-staging Room target above for compatibility/recovery.
|
||||
; Normal OTA uses the same dedicated QSPI store and app layout as Repeater.
|
||||
[env:ThinkNode_M6_room_server_lora_ota]
|
||||
extends = env:ThinkNode_M6_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_upload.maximum_size = 815104
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_flags =
|
||||
${env:ThinkNode_M6_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
build_src_filter = ${env:ThinkNode_M6_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:ThinkNode_M6_companion_radio_ble]
|
||||
extends = ThinkNode_M6
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
|
||||
@@ -83,6 +83,19 @@ build_flags = ${WioTrackerL1.build_flags}
|
||||
lib_deps = ${WioTrackerL1.lib_deps}
|
||||
adafruit/RTClib @ ^2.1.3
|
||||
|
||||
; Preserve the internal-staging Room target above for compatibility/recovery.
|
||||
; Normal OTA uses the same dedicated QSPI store and app layout as Repeater.
|
||||
[env:WioTrackerL1_room_server_lora_ota]
|
||||
extends = env:WioTrackerL1_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7.ld
|
||||
board_upload.maximum_size = 811008
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_flags =
|
||||
${env:WioTrackerL1_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
build_src_filter = ${env:WioTrackerL1_room_server.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
|
||||
[env:WioTrackerL1_companion_radio_usb]
|
||||
extends = WioTrackerL1
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
|
||||
|
||||
@@ -102,8 +102,15 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D MESH_PACKET_LOGGING=1
|
||||
-D WITH_RS232_BRIDGE=Serial1
|
||||
-D WITH_RS232_BRIDGE_UART=1
|
||||
-D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX
|
||||
-D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX
|
||||
-D BRIDGE_MAX_BAUD=500000
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${Xiao_nrf52.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
|
||||
[env:Xiao_nrf52_room_server]
|
||||
@@ -119,6 +126,18 @@ build_flags =
|
||||
build_src_filter = ${Xiao_nrf52.build_src_filter}
|
||||
+<../examples/simple_room_server/*.cpp>
|
||||
|
||||
; Raw-QSPI Room OTA uses the matching repeater's bootloader scratch layout.
|
||||
; Keep the original Room target available under its deployed internal OTA ID.
|
||||
[env:Xiao_nrf52_room_server_lora_ota]
|
||||
extends = env:Xiao_nrf52_room_server
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_xiao_bootloader_ota.ld
|
||||
board_upload.maximum_size = 757760
|
||||
build_flags =
|
||||
${env:Xiao_nrf52_room_server.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_BOOTLOADER_UPDATE=1
|
||||
build_src_filter = ${env:Xiao_nrf52_room_server.build_src_filter}
|
||||
|
||||
[env:Xiao_nrf52_repeater_bridge_rs232]
|
||||
extends = Xiao_nrf52
|
||||
build_flags =
|
||||
|
||||
Reference in new issue
Block a user