Fix auto nRF52 Companion OTA builds

This commit is contained in:
mikecarper committed 2026-09-02 12:02:41 -07:00
1 parent cb27fa8539
commit 249c3680b3
6 files changed
+90 -12

No files matched your search

+21 -5
View File
@@ -245,6 +245,10 @@ init_project_context() {
if [ -z "$env_name" ] || [ -z "$env_platform" ]; then
continue
fi
# Native Windows Python writes CRLF even when its output is consumed by
# Git Bash. `read` removes LF but leaves CR on the final field, which can
# make exact board checks (for example heltec-rc32) silently fail.
env_board=${env_board%$'\r'}
SUPPORTED_PIO_ENVS+=("$env_name")
PIO_ENV_PLATFORM_BY_NAME["$env_name"]=$env_platform
PIO_ENV_BOARD_BY_NAME["$env_name"]=$env_board
@@ -2720,6 +2724,16 @@ is_lora_ota_build() {
return 1
fi
# A single-target auto build promises to preserve the capabilities declared
# by its resolved PlatformIO environment and fail if they do not fit. Keep
# that contract for nRF52 Companion and other non-repeater roles which
# deliberately inherit the shared OTA recipe. The standard/release profile
# continues through the opt-in policy below and may select its documented
# portable reductions.
if [ "${BUILD_PROFILE_FOR_TARGET:-$BUILD_PROFILE_EFFECTIVE}" = "auto" ]; then
return 0
fi
# The OTA manager, staging store, and self-install path are deliberately
# opt-in. Most boards use the constrained no-external-sensors sibling. A
# purpose-built external-QSPI target may retain the full board feature set.
@@ -2746,13 +2760,17 @@ is_lora_ota_build() {
esac
}
is_esp32_companion_build() {
is_companion_build() {
case "${1,,}" in
*companion*|*comp_radio*) return 0 ;;
*) return 1 ;;
esac
}
is_esp32_companion_build() {
is_companion_build "$1"
}
requires_esp32_portable_app_slot() {
[ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "ESP32_PLATFORM" ] \
&& ! is_esp32_companion_build "$1"
@@ -2910,8 +2928,7 @@ declare_build_capability_contract() {
record_build_expectation "companion.ble_mota_source" \
"Bluetooth mOTA source"
fi
elif [ "$env_platform" = "ESP32_PLATFORM" ] \
&& is_esp32_companion_build "$env_name" \
elif is_companion_build "$env_name" \
&& is_lora_ota_build "$env_name"; then
record_build_expectation "companion.temp_radio" "ERR usage: tempradio"
record_build_expectation "companion.ota_cli" "OTA: status"
@@ -3323,8 +3340,7 @@ apply_lora_ota_override() {
append_platformio_build_unflags "-UENABLE_OTA -DDISABLE_LORA_OTA=1"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -DOTA_FLASH_STORE=1 -DOTA_FOLDER_SERIAL"
fi
if [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" = "ESP32_PLATFORM" ] \
&& is_esp32_companion_build "$env_name"; then
if is_companion_build "$env_name"; then
# Do not link an OTA manager into a Companion while compiling out the
# terminal controls needed to operate it over LoRa.
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DCOMPANION_FEATURE_TEMP_RADIO=1 -DCOMPANION_FEATURE_OTA_CLI=1"
+3 -2
View File
@@ -28,7 +28,7 @@ and partition policy:
| Selection | Behavior |
| --- | --- |
| `auto` | For one explicit target, pass 1 builds the complete supported LoRa-OTA-capable recipe. ESP32 boards with a qualified expanded profile use it; other repeaters attempt the complete recipe in their current application region. A measured flash/partition overflow starts the standard `no_external_sensors` LoRa OTA pass. Internal-flash nRF52 repeaters publish that reduced pass even when the complete image fits, because the smaller running image leaves more room to stage a delta; matched QSPI/SD repeaters do not need the redundant artifact. Compiler errors and missing-capability checks never trigger or conceal a reduced build. Canonical bulk commands keep their established standard partition contract. |
| `auto` | For one explicit target, pass 1 builds the complete supported LoRa-OTA-capable recipe. ESP32 boards with a qualified expanded profile use it; other repeaters attempt the complete recipe in their current application region. A non-repeater keeps every capability declared by its resolved PlatformIO recipe, including LoRa OTA, and fails instead of silently removing one that does not fit. A measured repeater flash/partition overflow starts the standard `no_external_sensors` LoRa OTA pass. Internal-flash nRF52 repeaters publish that reduced pass even when the complete image fits, because the smaller running image leaves more room to stage a delta; matched QSPI/SD repeaters do not need the redundant artifact. Compiler errors and missing-capability checks never trigger or conceal a reduced build. Canonical bulk commands keep their established standard partition contract. |
| `standard` | Immediately uses the deployed/portable partition contract and its documented reductions. This is useful when the operator already knows the expanded or complete image is unsuitable. |
| `full` | Requires a qualified ESP32 expanded-partition target (or an explicitly named Full Companion). Install a matching merged image when this changes the partition table. |
@@ -38,7 +38,8 @@ logical OTA target, actual PlatformIO base, artifact name target, promised
capabilities, and every reduction selected by the script. The build
fails if a promised linked marker is absent. Current invariants include
`retry.preset` for repeater/room-server roles, WebConfig for the Indicator and
ESP32 Full Companion, TempRadio/mOTA controls for Full Companion, and the
ESP32 Full Companion, TempRadio/OTA controls for install-capable Companions,
host-mOTA controls for Full Companion, and the
advanced flood-rule engine for Full room servers. Resume mode will not accept
an old or failed artifact without a verified sidecar.
+6 -4
View File
@@ -235,7 +235,7 @@ catalog; bytes `0x2C`-`0x31` are parked and `0x35` is unused.
| `0x3F` / `0x40` | `CMD_SET_DEFAULT_FLOOD_SCOPE` / `CMD_GET_DEFAULT_FLOOD_SCOPE` | Write or read the default flood scope. |
| `0x41` | `CMD_SEND_RAW_PACKET` | Queue a fully encoded raw mesh packet. |
| `0x42` | `CMD_RUN_CLI_COMMAND` | Run a local CLI command (protocol v14+). |
| `0x4A` | `CMD_EXEC_LOCAL_OTA_CONTROL` | Run one bounded local TempRadio or OTA command on a Full Companion. |
| `0x4A` | `CMD_EXEC_LOCAL_OTA_CONTROL` | Run one bounded local TempRadio or OTA command when the Companion includes the OTA CLI. |
| `0x4B` | `CMD_BLE_MOTA_SOURCE` | Query, start, or stop an nRF52 Full Companion's Bluetooth-backed LoRa mOTA source. |
| `0x78`-`0x7F` | Deprecated hardware-setting aliases | Receive-only compatibility for clients shipped before command `0x42` became the canonical settings path. |
@@ -326,14 +326,16 @@ custom name is limited to 31 valid UTF-8 bytes and takes effect after reboot.
### Bluetooth LoRa mOTA source
Protocol v14 lets a phone use an nRF52 Full Companion as the source for a
Protocol v14 lets a phone control an install-capable Companion's own LoRa OTA
session. An nRF52 Full Companion can additionally act as the source for a
remote repeater update without a USB computer. The normal Companion service
still carries contacts, repeater login, CLI messages, and these two control
commands. The separate mOTA service carries only host-folder request/response
frames.
`CMD_EXEC_LOCAL_OTA_CONTROL` (`0x4A`) is followed by 1-174 printable ASCII
bytes. Full Companion accepts only these local command families:
bytes. A Companion built with the OTA CLI accepts only these local command
families:
```text
tempradio <freq_kHz>,<bw_kHz>,<sf>,<cr>,<minutes>
@@ -348,7 +350,7 @@ return `ERR_CODE_ILLEGAL_ARG`. A recognized command replies with
`RESP_CODE_OK`, one unsigned reply-length byte, and exactly that many printable
result bytes. Shell metacharacters are rejected as well; the text is dispatched
only to the in-firmware parser and is never passed to a host shell. Firmware
without the Full Companion feature returns `ERR_CODE_UNSUPPORTED_CMD`.
without the OTA CLI feature returns `ERR_CODE_UNSUPPORTED_CMD`.
`CMD_BLE_MOTA_SOURCE` (`0x4B`) has one action byte:
+1 -1
View File
@@ -4572,7 +4572,7 @@ void MyMesh::handleCmdFrame(size_t len) {
}
} else if (cmd_frame[0]
== mesh::companion::CMD_EXEC_LOCAL_OTA_CONTROL) {
#if defined(COMPANION_RADIO_FULL)
#if COMPANION_FEATURE_OTA_CLI
const size_t command_len = len - 1;
if (!mesh::companion::isBleOtaControlCommandAllowed(
&cmd_frame[1], command_len)) {
+45
View File
@@ -319,6 +319,51 @@ require(rak_usb, "build_flags", "FORCE_GPS_ALIVE")
# replace each with that exact board's Full target.
init_project_context >/dev/null
# Auto means that an explicitly selected target keeps the capabilities in its
# resolved PlatformIO recipe. T1000-E BLE/USB both inherit nRF52 LoRa OTA; the
# release builder must not silently replace ENABLE_OTA with -UENABLE_OTA merely
# because Companion is not a repeater role. Standard keeps the existing
# portable-release reduction policy.
for t1000_env in \
t1000e_companion_radio_ble \
t1000e_companion_radio_usb; do
[ "${PIO_ENV_OTA_BY_NAME[$t1000_env]:-0}" = 1 ] \
|| fail "$t1000_env resolved recipe no longer declares LoRa OTA"
(
BUILD_PROFILE_FOR_TARGET=auto
is_lora_ota_build "$t1000_env" \
|| fail "$t1000_env auto profile stripped declared LoRa OTA"
PLATFORMIO_BUILD_FLAGS=""
PLATFORMIO_BUILD_UNFLAGS=""
apply_lora_ota_override "$t1000_env"
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-DENABLE_OTA=1"* ]] \
|| fail "$t1000_env auto overlay omitted ENABLE_OTA"
[[ "$PLATFORMIO_BUILD_FLAGS" != *"-UENABLE_OTA"* ]] \
|| fail "$t1000_env auto overlay still disables LoRa OTA"
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-DCOMPANION_FEATURE_TEMP_RADIO=1"* ]] \
|| fail "$t1000_env auto overlay omitted temporary-radio control"
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-DCOMPANION_FEATURE_OTA_CLI=1"* ]] \
|| fail "$t1000_env auto overlay omitted OTA CLI control"
BUILD_CAPABILITIES=()
BUILD_REDUCTIONS=()
BUILD_EXPECTATIONS=()
declare_build_capability_contract "$t1000_env" NRF52_PLATFORM
expectations=" ${BUILD_EXPECTATIONS[*]} "
[[ "$expectations" == *"companion.temp_radio=ERR usage: tempradio"* ]] \
|| fail "$t1000_env auto contract omitted temporary-radio verification"
[[ "$expectations" == *"companion.ota_cli=OTA: status"* ]] \
|| fail "$t1000_env auto contract omitted OTA CLI verification"
)
(
BUILD_PROFILE_FOR_TARGET=standard
if is_lora_ota_build "$t1000_env"; then
fail "$t1000_env standard profile bypassed portable OTA policy"
fi
)
done
# Arduino-ESP32 3.x OTA-only targets that have always declared larger A/B
# slots must be checked against their real partition table, not the legacy
# 1.25 MiB portable ceiling. Keep that exception OTA-only so ordinary RC32
+14
View File
@@ -100,6 +100,20 @@ assert 'memcmp(command + noun_start, "fold", 4) == 0' in owner_policy
assert 'memcmp(command + pos, "on", 2) == 0' in owner_policy
assert 'memcmp(command + pos, "off", 3) == 0' in owner_policy
# The bounded framed/BLE OTA-control command belongs to the OTA features, not
# only to the synthetic Full profile. Auto builds of a declared nRF52 OTA
# Companion need this route to select TempRadio and drive receive/apply.
ota_frame_start = source.index(
"== mesh::companion::CMD_EXEC_LOCAL_OTA_CONTROL"
)
ota_frame_end = source.index(
"} else if (cmd_frame[0] == mesh::companion::CMD_BLE_MOTA_SOURCE)",
ota_frame_start,
)
ota_frame = source[ota_frame_start:ota_frame_end]
assert "\n#if COMPANION_FEATURE_OTA_CLI\n" in ota_frame
assert "#if defined(COMPANION_RADIO_FULL)" not in ota_frame
# This compile-time contract prevents either the implementation or help entry
# from leaking onto a non-ESP32 build merely because a recipe set the flag.
assert re.search(