diff --git a/.github/workflows/firmware-ci.yml b/.github/workflows/firmware-ci.yml index 0a8e8fe3..dae86129 100644 --- a/.github/workflows/firmware-ci.yml +++ b/.github/workflows/firmware-ci.yml @@ -204,3 +204,35 @@ jobs: idf.py build test "$(stat -c%s build/esp32-csi-node.bin)" -le $((1152 * 1024)) strings build/esp32-csi-node.bin | grep -F "external U.FL" + + publish-release-assets: + name: Publish firmware release assets + if: github.ref_type == 'tag' + needs: [version-guard, build, build-c6-idf6] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download the exact-run firmware artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + pattern: esp32-csi-node-firmware-* + path: release-downloads + + - name: Package variants without filename collisions + run: | + mkdir -p release-assets + for directory in release-downloads/esp32-csi-node-firmware-*; do + variant="${directory##*/}" + tar -C "$directory" -czf "release-assets/${variant}.tar.gz" . + done + (cd release-assets && sha256sum *.tar.gz > SHA256SUMS.txt) + + - name: Publish the downloadable firmware bundles + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 + with: + name: ESP32 firmware ${{ github.ref_name }} + body: | + Exact-run ESP32-S3 8 MB, ESP32-S3 4 MB, and ESP32-C6 4 MB firmware bundles. + Verify each archive against `SHA256SUMS.txt` before flashing. + files: release-assets/* diff --git a/firmware/esp32-csi-node/README.md b/firmware/esp32-csi-node/README.md index 5f7505aa..7448eeda 100644 --- a/firmware/esp32-csi-node/README.md +++ b/firmware/esp32-csi-node/README.md @@ -362,6 +362,14 @@ The `MSYS_NO_PATHCONV=1` prefix prevents Git Bash from mangling the `/project` p - `build/partition_table/partition-table.bin` -- flash partition layout - `build/esp32-csi-node.bin` -- application firmware +### Release bundles + +Firmware downloads are published from dedicated `vX.Y.Z-esp32` tags after all +S3 and C6 matrix builds pass. Each release contains separate S3 8 MB, S3 4 MB, +and C6 4 MB archives plus an archive checksum manifest. The repository's +automated `vNNNN` server releases (for example `v2655`) are not firmware tags; +do not use their presence or absence of assets as firmware provenance. + ### Custom Configuration To change Kconfig settings before building: diff --git a/firmware/esp32-csi-node/main/serial_onboarding.c b/firmware/esp32-csi-node/main/serial_onboarding.c index 2566984d..be706cba 100644 --- a/firmware/esp32-csi-node/main/serial_onboarding.c +++ b/firmware/esp32-csi-node/main/serial_onboarding.c @@ -162,16 +162,9 @@ static void serial_onboarding_task(void *argument) (void)argument; const int original_flags = fcntl(STDIN_FILENO, F_GETFL, 0); if (original_flags >= 0) (void)fcntl(STDIN_FILENO, F_SETFL, original_flags | O_NONBLOCK); - /* ESP-IDF secondary consoles mirror output but deliberately do not forward - * input to STDIN. Native S3/C6 USB Serial/JTAG therefore needs a bounded - * direct VFS reader while UART-console boards continue to use STDIN. */ - const int usb_fd = open("/dev/secondary", O_RDONLY | O_NONBLOCK); char primary_line[ONBOARDING_LINE_MAX]; - char usb_line[ONBOARDING_LINE_MAX]; size_t primary_length = 0; - size_t usb_length = 0; bool primary_overflow = false; - bool usb_overflow = false; for (;;) { uint8_t input[64]; const ssize_t count = read(STDIN_FILENO, input, sizeof(input)); @@ -182,12 +175,6 @@ static void serial_onboarding_task(void *argument) ESP_LOGW(TAG, "serial input unavailable: errno=%d", errno); vTaskDelay(pdMS_TO_TICKS(500)); } - if (usb_fd >= 0) { - const ssize_t usb_count = read(usb_fd, input, sizeof(input)); - if (usb_count > 0) { - consume_bytes(input, usb_count, usb_line, &usb_length, &usb_overflow); - } - } vTaskDelay(pdMS_TO_TICKS(20)); } } diff --git a/firmware/esp32-csi-node/release_bins/c6-onboarding/SHA256SUMS.txt b/firmware/esp32-csi-node/release_bins/c6-onboarding/SHA256SUMS.txt index 75fadb47..5175a85d 100644 --- a/firmware/esp32-csi-node/release_bins/c6-onboarding/SHA256SUMS.txt +++ b/firmware/esp32-csi-node/release_bins/c6-onboarding/SHA256SUMS.txt @@ -1,4 +1,4 @@ -126a4b9f305e207aa2e0db93caf09e27f77b3bee4b4f618a0cb963f10a4364bc bootloader.bin -183c4557a93f6f0be1680f45988b7c7320130c8e0ed5d9c900add038d5cc84fb esp32-csi-node.bin +12e9fe5bd0533c3eb8ebb1cc36f775668cc4dd39f334ea65e26529b87211dc4c bootloader.bin +baff3940ede0ce1658f2f5c3ba458cc9007753eb1519231d67b1ffa724382824 esp32-csi-node.bin 7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f ota_data_initial.bin 4c2cc4ffd52641e23b779bd57b3908014083ac3c1aab395756478c89e70d81f0 partition-table.bin diff --git a/firmware/esp32-csi-node/release_bins/c6-onboarding/bootloader.bin b/firmware/esp32-csi-node/release_bins/c6-onboarding/bootloader.bin index 1ad822b8..e2a33bc1 100644 Binary files a/firmware/esp32-csi-node/release_bins/c6-onboarding/bootloader.bin and b/firmware/esp32-csi-node/release_bins/c6-onboarding/bootloader.bin differ diff --git a/firmware/esp32-csi-node/release_bins/c6-onboarding/esp32-csi-node.bin b/firmware/esp32-csi-node/release_bins/c6-onboarding/esp32-csi-node.bin index 062a65b7..e95a40e1 100644 Binary files a/firmware/esp32-csi-node/release_bins/c6-onboarding/esp32-csi-node.bin and b/firmware/esp32-csi-node/release_bins/c6-onboarding/esp32-csi-node.bin differ diff --git a/firmware/esp32-csi-node/sdkconfig.defaults.esp32c6 b/firmware/esp32-csi-node/sdkconfig.defaults.esp32c6 index 9e8051b0..1f062c84 100644 --- a/firmware/esp32-csi-node/sdkconfig.defaults.esp32c6 +++ b/firmware/esp32-csi-node/sdkconfig.defaults.esp32c6 @@ -19,6 +19,12 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv" CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_FLASHSIZE="4MB" +# Native USB/JTAG is the interactive onboarding console on C6 boards. ESP-IDF +# secondary consoles mirror output only and cannot receive RUVIEW_HELLO_V1. +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set +# CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG is not set + # ── CSI (required) ── CONFIG_ESP_WIFI_CSI_ENABLED=y diff --git a/firmware/esp32-csi-node/test/Makefile b/firmware/esp32-csi-node/test/Makefile index 38321d50..ee32cc02 100644 --- a/firmware/esp32-csi-node/test/Makefile +++ b/firmware/esp32-csi-node/test/Makefile @@ -55,7 +55,8 @@ FUZZ_JOBS ?= 1 .PHONY: all clean run_serialize run_edge run_nvs run_all test_adr110 run_adr110 \ test_vitals run_vitals test_mmwave_detect run_mmwave_detect host_tests \ test_thermal run_thermal test_csi_sanitize run_csi_sanitize \ - test_c6_antenna run_c6_antenna test_serial_onboarding run_serial_onboarding + test_c6_antenna run_c6_antenna test_serial_onboarding run_serial_onboarding \ + run_delivery_contract all: fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \ test_thermal test_csi_sanitize test_c6_antenna test_serial_onboarding @@ -132,9 +133,12 @@ test_serial_onboarding: test_serial_onboarding_protocol.c $(MAIN_DIR)/serial_onb run_serial_onboarding: test_serial_onboarding ./test_serial_onboarding +run_delivery_contract: + python3 test_delivery_contract.py + host_tests: run_adr110 run_vitals run_mmwave_detect run_thermal run_csi_sanitize \ - run_c6_antenna run_serial_onboarding - @echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal + C6 antenna + USB onboarding)" + run_c6_antenna run_serial_onboarding run_delivery_contract + @echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal + C6 antenna + USB onboarding + delivery contract)" # --- Serialize fuzzer --- # Tests csi_serialize_frame() with random wifi_csi_info_t inputs. diff --git a/firmware/esp32-csi-node/test/test_delivery_contract.py b/firmware/esp32-csi-node/test/test_delivery_contract.py new file mode 100644 index 00000000..adcf6daa --- /dev/null +++ b/firmware/esp32-csi-node/test/test_delivery_contract.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Static delivery contracts that do not require an ESP-IDF toolchain.""" + +from pathlib import Path +import unittest + + +FIRMWARE_ROOT = Path(__file__).resolve().parents[1] +REPOSITORY_ROOT = FIRMWARE_ROOT.parents[1] + + +class FirmwareDeliveryContractTests(unittest.TestCase): + def test_c6_uses_native_usb_serial_jtag_as_primary_console(self) -> None: + config = (FIRMWARE_ROOT / "sdkconfig.defaults.esp32c6").read_text() + self.assertIn("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y", config) + self.assertNotIn("CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y", config) + + onboarding = (FIRMWARE_ROOT / "main/serial_onboarding.c").read_text() + self.assertNotIn('open("/dev/secondary"', onboarding) + + def test_tracked_c6_onboarding_app_matches_version_txt(self) -> None: + expected = (FIRMWARE_ROOT / "version.txt").read_text().strip().encode() + app = (FIRMWARE_ROOT / "release_bins/c6-onboarding/esp32-csi-node.bin").read_bytes() + self.assertTrue( + expected + b"\0" in app, + f"tracked C6 app does not embed version {expected.decode()}", + ) + + def test_firmware_tags_publish_downloadable_release_assets(self) -> None: + workflow = (REPOSITORY_ROOT / ".github/workflows/firmware-ci.yml").read_text() + self.assertIn("publish-release-assets:", workflow) + self.assertIn("esp32-csi-node-firmware-*", workflow) + self.assertIn("softprops/action-gh-release@", workflow) + + +if __name__ == "__main__": + unittest.main()