From 942df304cacdaf95ffaa2cb52725415459976ca0 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 3 Sep 2026 02:33:23 -0700 Subject: [PATCH] Give Full builds exclusive USB mOTA ownership --- build.sh | 4 ++-- docs/companion_radio_full.md | 14 +++++++------ docs/lora_ota_automation.md | 6 ++++-- examples/companion_radio/CompanionFeatures.h | 14 +++++++++---- test/test_build_profiles.sh | 4 ++++ test/test_companion_terminal_profile.py | 21 ++++++++++++++++++++ 6 files changed, 49 insertions(+), 14 deletions(-) diff --git a/build.sh b/build.sh index 97dc681c..4d72b78d 100755 --- a/build.sh +++ b/build.sh @@ -2903,6 +2903,7 @@ declare_build_capability_contract() { record_build_expectation "companion.bluetooth" \ "Companion: starting Bluetooth" record_build_expectation "companion.usb_logging" "get usb.logging" + record_build_expectation "companion.usb_mota_source" "ota folder on" if is_nrf52_companion_radio_full_target "$env_name"; then record_build_expectation "companion.dedicated_usb_logging" \ "get usb.logging" @@ -2924,7 +2925,6 @@ declare_build_capability_contract() { record_build_expectation "companion.direct_mqtt" "mqtt.status" fi else - record_build_expectation "companion.usb_mota_source" "ota folder on" record_build_expectation "companion.ble_mota_source" \ "Bluetooth mOTA source" fi @@ -3427,7 +3427,7 @@ apply_companion_radio_full_profile() { # release matrix (ESP32_FULL_BUILD remains 0 in that path). Mark that # layout explicitly so Full-only runtime policies, including the bounded # first-boot WebConfig window, cannot silently compile with legacy defaults. - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DWIFI_OTA_SEEDER=1 -DCOMPANION_FEATURE_NETWORK_TERMINAL=1 -DCOMPANION_FEATURE_MEMORY_DIAGNOSTICS=1 -DMESHCORE_EXPANDED_PARTITION_PROFILE=1" + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DWIFI_OTA_SEEDER=1 -DCOMPANION_FEATURE_USB_MOTA_SOURCE=1 -DCOMPANION_FEATURE_NETWORK_TERMINAL=1 -DCOMPANION_FEATURE_MEMORY_DIAGNOSTICS=1 -DMESHCORE_EXPANDED_PARTITION_PROFILE=1" # Both Indicator radio layouts share the same original NVS/SPIFFS data # placement. The LoRa WiFi base names that map directly; the ESP-NOW USB diff --git a/docs/companion_radio_full.md b/docs/companion_radio_full.md index 038e618b..e8a8659e 100644 --- a/docs/companion_radio_full.md +++ b/docs/companion_radio_full.md @@ -595,17 +595,19 @@ able to report this event. A different baud rate, including 57600, does not select ASCII mode. On an ESP32 Full Companion built with `OTA_FOLDER_SERIAL`, `motatool` can keep -the shared serial console open as an mOTA folder source when WiFi is -unavailable: +the USB serial port open as an mOTA folder source when WiFi is unavailable: ```sh motatool serve --serial /dev/ttyACM0 --dir ./motas -v ``` -The tool sends `ota folder on` through the text CLI, then uses the shared -serial mOTA framing. This is separate from the nRF52 exclusive USB ownership -mode described below. TCP port 5001 remains the preferred unattended source -transport. +The tool sends the exact completed `ota folder on` line when it opens the port. +Firmware recognizes it in startup ASCII or idle Binary Companion mode, then +transfers the port to exclusive USB mOTA ownership before binary request/reply +frames begin. The text terminal and Binary Companion are unavailable on that +TTY until `motatool` detaches the folder. This matches the nRF52 ownership +behavior described below. TCP port 5001 remains the preferred unattended +source transport. ## nRF52 USB mOTA mode diff --git a/docs/lora_ota_automation.md b/docs/lora_ota_automation.md index e392a67e..8441c774 100644 --- a/docs/lora_ota_automation.md +++ b/docs/lora_ota_automation.md @@ -63,8 +63,10 @@ and is not a controller or source port. With default logging off, interface `02` is not enumerated. BLE remains available. ESP32 Full Companion normally uses its dedicated TCP seeder on port 5001. -ESP32 builds that also retain serial folder support use the shared-console -mOTA framing rather than the nRF52 exclusive USB ownership state. +ESP32 builds that also retain serial folder support use the same exclusive USB +mOTA ownership state as nRF52. The exact `ota folder on` line transfers the USB +port from startup ASCII or idle Binary Companion mode before binary mOTA frames +begin; neither console mode shares that port until the folder detaches. ## Destination requirements diff --git a/examples/companion_radio/CompanionFeatures.h b/examples/companion_radio/CompanionFeatures.h index 9ce92037..73709232 100644 --- a/examples/companion_radio/CompanionFeatures.h +++ b/examples/companion_radio/CompanionFeatures.h @@ -24,7 +24,12 @@ #define COMPANION_FEATURE_DEDICATED_USB_LOGGING 1 #endif #endif - #if defined(NRF52_PLATFORM) && defined(OTA_FOLDER_SERIAL) + // A Full Companion cannot leave its line-oriented terminal active while the + // serial folder source exchanges binary request/reply frames on that same + // stream. Both nRF52 CDC and ESP32's inherited OTA_FOLDER_SERIAL transport + // therefore use the exclusive USB mOTA owner state in main.cpp. + #if defined(OTA_FOLDER_SERIAL) \ + && (defined(NRF52_PLATFORM) || defined(ESP32_PLATFORM)) #ifndef COMPANION_FEATURE_USB_MOTA_SOURCE #define COMPANION_FEATURE_USB_MOTA_SOURCE 1 #endif @@ -68,9 +73,10 @@ #endif #if COMPANION_FEATURE_USB_MOTA_SOURCE \ - && !(defined(NRF52_PLATFORM) && defined(OTA_FOLDER_SERIAL) \ - && defined(ENABLE_USB_INTERFACE) && defined(ENABLE_OTA)) - #error "COMPANION_FEATURE_USB_MOTA_SOURCE requires nRF52 USB folder seeding" + && !((defined(NRF52_PLATFORM) || defined(ESP32_PLATFORM)) \ + && defined(OTA_FOLDER_SERIAL) && defined(ENABLE_USB_INTERFACE) \ + && defined(ENABLE_OTA)) + #error "COMPANION_FEATURE_USB_MOTA_SOURCE requires a USB serial folder source" #endif #if COMPANION_FEATURE_BLE_MOTA_SOURCE \ diff --git a/test/test_build_profiles.sh b/test/test_build_profiles.sh index cfa93ff6..19766676 100644 --- a/test/test_build_profiles.sh +++ b/test/test_build_profiles.sh @@ -433,6 +433,8 @@ for full_env in "${SUPPORTED_PIO_ENVS[@]}"; do || fail "$full_env Full Companion omitted its capability identity" [[ "$PLATFORMIO_BUILD_FLAGS" == *"BLE_PIN_CODE=123456"* ]] \ || fail "$full_env Full Companion omitted Bluetooth pairing" + [[ "$PLATFORMIO_BUILD_FLAGS" == *"COMPANION_FEATURE_USB_MOTA_SOURCE=1"* ]] \ + || fail "$full_env Full Companion left the serial folder source sharing the USB text parser" case "${full_env,,}" in sensecapindicator-espnow_companion_radio_full|\ sensecapindicator-lora_companion_radio_full) @@ -1015,6 +1017,8 @@ declare_build_capability_contract \ expectations=" ${BUILD_EXPECTATIONS[*]} " [[ "$expectations" == *"companion.direct_mqtt=mqtt.status"* ]] \ || fail "MQTT-capable ESP32 Full contract omitted direct MQTT" +[[ "$expectations" == *"companion.usb_mota_source=ota folder on"* ]] \ + || fail "ESP32 Full contract omitted exclusive USB folder seeding" PIO_ENV_PLATFORM_BY_NAME[RAK_3401_companion_radio_full]=NRF52_PLATFORM BUILD_CAPABILITIES=() diff --git a/test/test_companion_terminal_profile.py b/test/test_companion_terminal_profile.py index 28dc7184..5d7e9775 100644 --- a/test/test_companion_terminal_profile.py +++ b/test/test_companion_terminal_profile.py @@ -8,6 +8,27 @@ root = Path(__file__).resolve().parents[1] source = (root / "examples/companion_radio/MyMesh.cpp").read_text() features = (root / "examples/companion_radio/CompanionFeatures.h").read_text() +# Every Full Companion which inherits OTA_FOLDER_SERIAL must put that shared +# byte stream under the exclusive mOTA owner. Otherwise the ASCII terminal can +# consume and echo a delayed lowercase `ms` host response between source +# transactions while valid uppercase `MS` requests continue. +assert re.search( + r"#if defined\(OTA_FOLDER_SERIAL\)\s*\\?\s*" + r"&& \(defined\(NRF52_PLATFORM\) \|\| defined\(ESP32_PLATFORM\)\)\s*" + r"#ifndef COMPANION_FEATURE_USB_MOTA_SOURCE\s+" + r"#define COMPANION_FEATURE_USB_MOTA_SOURCE 1", + features, +) +assert re.search( + r"#if COMPANION_FEATURE_USB_MOTA_SOURCE\s*\\?\s*" + r"&& !\(\(defined\(NRF52_PLATFORM\) \|\| defined\(ESP32_PLATFORM\)\)\s*\\?\s*" + r'&& defined\(OTA_FOLDER_SERIAL\).*?defined\(ENABLE_USB_INTERFACE\).*?' + r'defined\(ENABLE_OTA\)\)\s+' + r'#error "COMPANION_FEATURE_USB_MOTA_SOURCE requires a USB serial folder source"', + features, + re.DOTALL, +) + local_start = source.index("bool MyMesh::handleLocalControlCommand(") local_end = source.index("\n#if COMPANION_FEATURE_TEMP_RADIO\nvoid MyMesh::serviceTempRadio()", local_start) local = source[local_start:local_end]