Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/build-beta.yml
Original file line number Diff line number Diff line change
@@ -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."
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 85 additions & 2 deletions Integrations/ESPHome/Core.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions Integrations/ESPHome/MSR-2_BLE.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
substitutions:
ble_firmware: "true"

esphome:
name: "${name}"
friendly_name: Apollo MSR-2
Expand Down
9 changes: 9 additions & 0 deletions Integrations/ESPHome/beta-channel/MSR-2.yaml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions Integrations/ESPHome/beta-channel/MSR-2_BLE.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
<h1>Apollo MSR-2 Installer</h1>

<p class="button-row" align="center">
<esp-web-install-button manifest="./firmware/manifest.json"></esp-web-install-button>
<esp-web-install-button manifest="./firmware-factory/manifest.json"></esp-web-install-button>
</p>

<div class="footer">
Expand Down
Loading