diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml new file mode 100644 index 0000000..1a990cd --- /dev/null +++ b/.github/workflows/build-beta.yml @@ -0,0 +1,105 @@ +name: Build and Publish Beta + +# Builds MSR-2 firmware from the beta branch and publishes it as assets on a +# rolling "beta-fw" pre-release. The on-device "Firmware Channel" select points +# OTA updates at these assets. Stable firmware is built/published separately +# by build.yml (push to main -> GitHub Pages). + +on: + push: + branches: [beta] + paths: + - 'Integrations/ESPHome/**' + workflow_dispatch: + +# Least privilege: read-only by default; only publish-beta is elevated to write. +permissions: + contents: read + +jobs: + version: + name: Read version + runs-on: ubuntu-latest + outputs: + v: ${{ steps.read.outputs.v }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - id: read + run: | + v=$(awk '/substitutions:/ {f=1} f && /version:/ {print $2; exit}' \ + Integrations/ESPHome/Core.yaml | tr -d '"') + echo "v=$v" >> "$GITHUB_OUTPUT" + echo "Beta version: $v" + + build: + name: Build ${{ matrix.name }} + needs: version + strategy: + matrix: + include: + # Beta serves OTA updates only, so it builds the end-user image + # (MSR-2.yaml), not the first-flash Factory image. + - { yaml: Integrations/ESPHome/beta-channel/MSR-2.yaml, name: firmware-standard } + - { yaml: Integrations/ESPHome/beta-channel/MSR-2_BLE.yaml, name: firmware-ble-beta } + uses: esphome/workflows/.github/workflows/build.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 + with: + files: ${{ matrix.yaml }} + esphome-version: stable + combined-name: ${{ matrix.name }} + release-version: ${{ needs.version.outputs.v }} + + publish-beta: + name: Publish beta release assets + needs: [version, build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download firmware artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: fw + pattern: firmware* + + - name: Ensure rolling 'beta-fw' pre-release exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view beta-fw -R "${{ github.repository }}" >/dev/null 2>&1 \ + || gh release create beta-fw -R "${{ github.repository }}" \ + --prerelease --title "Beta (rolling)" \ + --notes "Latest MSR-2 beta firmware. Auto-updated on every push to the beta branch." + + - name: Rewrite manifests to absolute URLs and upload assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BASE="https://github.com/${{ github.repository }}/releases/download/beta-fw" + declare -A DIRS=( [standard]=firmware-standard [ble]=firmware-ble-beta ) + for v in standard ble; do + src="fw/${DIRS[$v]}" + man=$(find "$src" -name manifest.json | head -1) + if [ -z "$man" ]; then + echo "::error::manifest.json not found for ${DIRS[$v]}" + exit 1 + fi + # Both variants share a device name, so their bin filenames match. + # Release assets are a flat namespace: prefix per variant. + find "$src" -name '*.bin' | while read -r bin; do + mv "$bin" "$(dirname "$bin")/$v-$(basename "$bin")" + done + echo "Rewriting $man" + # Make ota.path and parts[].path absolute release-asset URLs so the + # device never has to resolve a relative path against a redirect. + jq --arg base "$BASE" --arg pfx "$v-" ' + .builds[0].ota.path = ($base + "/" + $pfx + (.builds[0].ota.path | sub(".*/"; ""))) + | .builds[0].parts |= map(.path = ($base + "/" + $pfx + (.path | sub(".*/"; "")))) + ' "$man" > "manifest-$v.json" + cat "manifest-$v.json" + gh release upload beta-fw "manifest-$v.json" -R "${{ github.repository }}" --clobber + find "$src" -name '*.bin' -print -exec \ + gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; + done + echo "Beta assets published." diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d5ab44..e270fd2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,10 +24,13 @@ jobs: pull-requests: write with: device-name: msr-2 + # MSR-2.yaml is the end-user image served at firmware/ (OTA updates). The Factory image (improv + + # factory test) is only used for first flashes via the web installer. yaml-files: | + Integrations/ESPHome/MSR-2.yaml Integrations/ESPHome/MSR-2_Factory.yaml Integrations/ESPHome/MSR-2_BLE.yaml - firmware-names: "2_Factory:firmware,2_BLE:firmware-ble" + firmware-names: "2:firmware,2_Factory:firmware-factory,2_BLE:firmware-ble" core-yaml-path: Integrations/ESPHome/Core.yaml esphome-version: stable # Bypass check if manually triggered with bypass option diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index f204468..d88f7c9 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,8 +1,20 @@ substitutions: name: apollo-msr-2 - version: "26.6.10.1" + version: "26.7.8.1" device_description: ${name} made by Apollo Automation - version ${version}. - + # Firmware variant identity: overridden to "true" by MSR-2_BLE.yaml so + # each image tracks its own OTA manifests. + ble_firmware: "false" + # Default update channel on first boot (no stored user choice yet, i.e. a + # fresh flash). The beta-channel builds override this to "Beta" (see + # Integrations/ESPHome/beta-channel/) so firmware obtained from the beta + # channel keeps tracking it instead of offering a stable "downgrade". + firmware_channel_default: "Stable" + # Manifest URL bases. Stable = GitHub Pages (main branch builds). + # Beta = rolling "beta" pre-release assets (beta branch builds). + stable_manifest_base: "https://apolloautomation.github.io/MSR-2" + beta_manifest_base: "https://github.com/ApolloAutomation/MSR-2/releases/download/beta-fw" + esp32: variant: esp32c3 flash_size: 4MB @@ -720,6 +732,40 @@ button: value: 420 id: scd40 + - platform: template + name: "Firmware Update" + id: update_firmware + icon: mdi:cloud-download + entity_category: "config" + on_press: + - logger.log: "Applying firmware based on selected channel and variant" + # Free heap for the TLS download on the BLE variant. Guarded so the + # non-BLE variant compiles the same YAML. + - lambda: |- + #ifdef USE_ESP32_BLE + if (esp32_ble::global_ble && esp32_ble::global_ble->is_active()) { + ESP_LOGI("firmware", "Disabling BLE for firmware update"); + esp32_ble::global_ble->disable(); + } + #endif + - delay: 3s + - script.execute: apply_ota_source + - script.wait: apply_ota_source + # The manifest fetch runs in its own task and YAML has no "fetch done" + # condition to wait on (update.is_available stays false for same-version + # variant switches), so give it a fixed window like R_PRO-1/CAST-1 do. + - delay: 5s + - update.perform: + id: update_http_request + force_update: true + # Only reached if the update did not start (e.g. manifest unreachable). + - lambda: |- + #ifdef USE_ESP32_BLE + if (esp32_ble::global_ble) { + ESP_LOGI("firmware", "Re-enabling BLE (no update performed)"); + esp32_ble::global_ble->enable(); + } + #endif interval: - interval: 1s @@ -812,9 +858,46 @@ select: name: "ld2410 Gate Size" disabled_by_default: true + - platform: template + name: "Firmware Channel" + id: firmware_channel + icon: mdi:source-branch + entity_category: "config" + optimistic: true + restore_value: true + options: + - "Stable" + - "Beta" + initial_option: "${firmware_channel_default}" + on_value: + then: + - script.execute: apply_ota_source + script: + - id: apply_ota_source + # Sets the OTA manifest URL from the Firmware Channel select + # (Stable/Beta) for the firmware variant this image was built as. + # Stable = GitHub Pages, Beta = rolling "beta" release assets. + then: + - lambda: |- + const bool ble = std::string("${ble_firmware}") == "true"; + const bool beta = id(firmware_channel).current_option() == "Beta"; + std::string url; + if (beta) { + url = ble + ? "${beta_manifest_base}/manifest-ble.json" + : "${beta_manifest_base}/manifest-standard.json"; + } else { + url = ble + ? "${stable_manifest_base}/firmware-ble/manifest.json" + : "${stable_manifest_base}/firmware/manifest.json"; + } + ESP_LOGI("firmware", "OTA manifest set to: %s", url.c_str()); + id(update_http_request).set_source_url(url); + - component.update: update_http_request + - id: setCo2AutoCalibration mode: restart parameters: diff --git a/Integrations/ESPHome/MSR-2_BLE.yaml b/Integrations/ESPHome/MSR-2_BLE.yaml index 6e9e103..bed7b39 100644 --- a/Integrations/ESPHome/MSR-2_BLE.yaml +++ b/Integrations/ESPHome/MSR-2_BLE.yaml @@ -1,3 +1,6 @@ +substitutions: + ble_firmware: "true" + esphome: name: "${name}" friendly_name: Apollo MSR-2 diff --git a/Integrations/ESPHome/beta-channel/MSR-2.yaml b/Integrations/ESPHome/beta-channel/MSR-2.yaml new file mode 100644 index 0000000..df3dd71 --- /dev/null +++ b/Integrations/ESPHome/beta-channel/MSR-2.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of MSR-2.yaml: the identical image except the Firmware +# Channel select defaults to "Beta" on first boot, so firmware obtained from +# the beta channel keeps tracking it. Built by build-beta.yml only; the +# stable (GitHub Pages) builds use MSR-2.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../MSR-2.yaml diff --git a/Integrations/ESPHome/beta-channel/MSR-2_BLE.yaml b/Integrations/ESPHome/beta-channel/MSR-2_BLE.yaml new file mode 100644 index 0000000..c29efdf --- /dev/null +++ b/Integrations/ESPHome/beta-channel/MSR-2_BLE.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of MSR-2_BLE.yaml: the identical image except the Firmware +# Channel select defaults to "Beta" on first boot, so firmware obtained from +# the beta channel keeps tracking it. Built by build-beta.yml only; the +# stable (GitHub Pages) builds use MSR-2_BLE.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../MSR-2_BLE.yaml diff --git a/static/index.html b/static/index.html index 1c803db..bf16163 100644 --- a/static/index.html +++ b/static/index.html @@ -82,7 +82,7 @@

Apollo MSR-2 Installer

- +