diff --git a/.github/workflows/autoassign.yml b/.github/workflows/autoassign.yml index 77bef44..03b36f5 100644 --- a/.github/workflows/autoassign.yml +++ b/.github/workflows/autoassign.yml @@ -1,9 +1,12 @@ name: Auto Assign +# pull_request_target (not pull_request) so auto-assign works on +# fork-submitted PRs; fork pull_request runs only get a read-only token. +# Safe because this workflow never checks out or executes PR code. on: issues: types: [opened] - pull_request: + pull_request_target: types: [opened] permissions: @@ -12,8 +15,6 @@ permissions: jobs: auto-assign: - # Skip auto-assign for pull requests from forks due to GITHUB_TOKEN permission restrictions - if: github.event_name == 'issues' || github.event.pull_request.head.repo.full_name == github.repository uses: ApolloAutomation/Workflows/.github/workflows/autoassign.yml@main with: assignees: bharvey88 diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index 780ac15..c2535cb 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -37,8 +37,8 @@ jobs: strategy: matrix: include: - - { yaml: Integrations/ESPHome/CAST-1_W.yaml, name: firmware-w } - - { yaml: Integrations/ESPHome/CAST-1_ETH.yaml, name: firmware-e } + - { yaml: Integrations/ESPHome/beta-channel/CAST-1_W.yaml, name: firmware-w } + - { yaml: Integrations/ESPHome/beta-channel/CAST-1_ETH.yaml, name: firmware-e } uses: esphome/workflows/.github/workflows/build.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 with: files: ${{ matrix.yaml }} @@ -59,12 +59,12 @@ jobs: path: fw pattern: firmware* - - name: Ensure rolling 'beta' pre-release exists + - name: Ensure rolling 'beta-fw' pre-release exists env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh release view beta -R "${{ github.repository }}" >/dev/null 2>&1 \ - || gh release create beta -R "${{ github.repository }}" \ + 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 CAST-1 beta firmware. Auto-updated on every push to the beta branch." @@ -72,7 +72,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - BASE="https://github.com/${{ github.repository }}/releases/download/beta" + BASE="https://github.com/${{ github.repository }}/releases/download/beta-fw" for v in w e; do man=$(find "fw/firmware-$v" -name manifest.json | head -1) if [ -z "$man" ]; then @@ -87,8 +87,8 @@ jobs: | .builds[0].parts |= map(.path = ($base + "/" + (.path | sub(".*/"; "")))) ' "$man" > "manifest-$v.json" cat "manifest-$v.json" - gh release upload beta "manifest-$v.json" -R "${{ github.repository }}" --clobber + gh release upload beta-fw "manifest-$v.json" -R "${{ github.repository }}" --clobber find "fw/firmware-$v" -name '*.bin' -print -exec \ - gh release upload beta {} -R "${{ github.repository }}" --clobber \; + gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; done echo "Beta assets published." diff --git a/Integrations/ESPHome/CAST-1_ETH.yaml b/Integrations/ESPHome/CAST-1_ETH.yaml index 3463c7a..86d911a 100644 --- a/Integrations/ESPHome/CAST-1_ETH.yaml +++ b/Integrations/ESPHome/CAST-1_ETH.yaml @@ -37,14 +37,6 @@ ota: - platform: http_request id: ota_managed -http_request: - verify_ssl: true - # Beta OTA manifests/binaries are GitHub release assets, which 302-redirect to - # long signed CDN URLs (~900+ bytes). The default 512-byte RX buffer overflows - # ("Out of buffer"), so enlarge it. See esphome/esphome#13786. - buffer_size_rx: 4096 - buffer_size_tx: 1024 - safe_mode: update: diff --git a/Integrations/ESPHome/CAST-1_W.yaml b/Integrations/ESPHome/CAST-1_W.yaml index 2c1e08b..3dcce8e 100644 --- a/Integrations/ESPHome/CAST-1_W.yaml +++ b/Integrations/ESPHome/CAST-1_W.yaml @@ -37,14 +37,6 @@ ota: - platform: http_request id: ota_managed -http_request: - verify_ssl: true - # Beta OTA manifests/binaries are GitHub release assets, which 302-redirect to - # long signed CDN URLs (~900+ bytes). The default 512-byte RX buffer overflows - # ("Out of buffer"), so enlarge it. See esphome/esphome#13786. - buffer_size_rx: 4096 - buffer_size_tx: 1024 - safe_mode: improv_serial: diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 9e34017..563b23f 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,10 +1,40 @@ substitutions: - version: "26.7.7.1" + version: "26.7.14.1" + # 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-fw" pre-release assets (beta branch builds). + stable_manifest_base: "https://apolloautomation.github.io/CAST-1" + beta_manifest_base: "https://github.com/ApolloAutomation/CAST-1/releases/download/beta-fw" packages: wizmote: !include wizmote.yaml +esphome: + # List form so package merging concatenates with each variant's own on_boot + # entries (mapping form would be replaced by the variant's block instead). + on_boot: + # Point the update entity at the selected type/channel manifest. + - priority: -100 + then: + - script.execute: apply_ota_source + # Re-apply the Bluetooth Proxy switch after all components set up, so BLE + # scanning matches the persisted switch (proxy stays off by default). + - priority: -300 + then: + - if: + condition: + switch.is_on: bluetooth_proxy_switch + then: + - esp32_ble_tracker.start_scan: + continuous: true + else: + - esp32_ble_tracker.stop_scan: + esp32: variant: ESP32S3 flash_size: 8MB @@ -26,7 +56,17 @@ esp32: CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y" CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y" +# BT allocations go to PSRAM first (CONFIG_BT_* options above), keeping +# internal RAM free for the audio pipelines. +esp32_ble_tracker: + id: ble_tracker + scan_parameters: + continuous: true + +bluetooth_proxy: + api: + encryption: globals: - id: cycleCounter @@ -55,11 +95,29 @@ globals: psram: mode: octal speed: 80MHz + # All CAST-1 PCBs have PSRAM, so guarantee it (fail boot if absent). This flips + # ESPHome's psram_guaranteed flag, which unlocks the high-performance network + # tier: 512 WiFi RX buffers / 32 TX buffers and a much larger TCP receive + # window instead of the conservative defaults. Music Assistant bulk-downloads + # each track faster than realtime, and the small TCP window is what its flow + # control slams into at track changes/seeks. (VPE sets this too.) + ignore_not_found: false web_server: port: 80 version: 3 +http_request: + verify_ssl: true + # GitHub release-asset downloads answer with a redirect carrying a + # ~3.6 KB Content-Security-Policy header; each header line must fit + # this buffer or the request fails with "HTTP_CLIENT: Out of buffer". + buffer_size_rx: 5120 + # The redirect target is a signed URL with a ~850-char query string; the + # follow-up request line must fit the TX buffer or esp_http_client_open + # fails with "Out of buffer" before sending anything. + buffer_size_tx: 2048 + i2c: sda: GPIO47 scl: GPIO48 @@ -72,11 +130,17 @@ button: - platform: template name: "Firmware Update" id: update_firmware + icon: mdi:cloud-download entity_category: config on_press: - then: - - lambda: |- - id(update_http_request).perform(true); + - logger.log: "Applying firmware update for the selected type and channel" + - delay: 3s + - script.execute: apply_ota_source + - script.wait: apply_ota_source + # The manifest fetch runs in its own task; give it a fixed window to land + # (update.is_available stays false for same-version switches). + - delay: 5s + - lambda: id(update_http_request).perform(true); binary_sensor: - platform: status @@ -132,6 +196,19 @@ switch: id: enable_dac restore_mode: ALWAYS_ON + - platform: template + name: "Bluetooth Proxy" + id: bluetooth_proxy_switch + icon: mdi:bluetooth + entity_category: "config" + restore_mode: RESTORE_DEFAULT_OFF + optimistic: true + on_turn_on: + - esp32_ble_tracker.start_scan: + continuous: true + on_turn_off: + - esp32_ble_tracker.stop_scan: + text_sensor: - platform: sendspin type: title @@ -201,9 +278,9 @@ select: optimistic: true restore_value: true options: - - "Main" + - "Stable" - "Beta" - initial_option: "Main" + initial_option: "${firmware_channel_default}" on_value: then: - script.execute: apply_ota_source @@ -243,30 +320,43 @@ speaker: - id: media_mixer_input timeout: never + # Resampler task stacks stay in internal RAM (default): they are hot audio + # tasks, and a PSRAM stack contends with the decode/ring buffers already in + # PSRAM during Music Assistant's bulk track transfers. (Matches VPE.) + - platform: resampler + id: announcement_resampling_speaker + output_speaker: announcement_mixer_input + sample_rate: 48000 + bits_per_sample: 16 + - platform: resampler id: media_resampling_speaker output_speaker: media_mixer_input - task_stack_in_psram: true sample_rate: 48000 bits_per_sample: 16 sendspin: id: sendspin_hub - task_stack_in_psram: true + # Keep the sendspin server task's stack in internal RAM (VPE does the same). + # That single task services the websocket: audio ingest, JSON, and the + # time-sync replies the clock filter depends on — slowing it down with a + # PSRAM stack skews time sync, which surfaces as hard-sync silence gaps at + # track changes. + task_stack_in_psram: false media_source: - platform: sendspin id: sendspin_source - decode_memory: internal + # decode_memory deliberately unset (component default, matches VPE) - platform: audio_http id: http_media_source - buffer_size: 200000 + buffer_size: 500000 - platform: audio_http id: http_announcement_source - buffer_size: 200000 + buffer_size: 250000 media_player: - platform: sendspin @@ -282,7 +372,7 @@ media_player: format: FLAC # FLAC is the least processor intensive codec num_channels: 1 # Stereo audio is unnecessary for announcements sample_rate: 48000 - speaker: announcement_mixer_input + speaker: announcement_resampling_speaker sources: - http_announcement_source @@ -356,7 +446,8 @@ media_player: script: - id: apply_ota_source # Sets the OTA source URL from the two selectors: Firmware Type (WiFi/Ethernet) - # x Firmware Channel (Main/Beta). Main = GitHub Pages, Beta = GitHub release assets. + # x Firmware Channel (Stable/Beta). Stable = GitHub Pages (main branch builds), + # Beta = rolling "beta-fw" release assets. then: - lambda: |- const bool eth = id(firmware_selector).current_option() == "Ethernet"; @@ -364,13 +455,14 @@ script: std::string url; if (beta) { url = eth - ? "https://github.com/ApolloAutomation/CAST-1/releases/download/beta/manifest-e.json" - : "https://github.com/ApolloAutomation/CAST-1/releases/download/beta/manifest-w.json"; + ? "${beta_manifest_base}/manifest-e.json" + : "${beta_manifest_base}/manifest-w.json"; } else { url = eth - ? "https://apolloautomation.github.io/CAST-1/firmware-e/manifest.json" - : "https://apolloautomation.github.io/CAST-1/firmware-w/manifest.json"; + ? "${stable_manifest_base}/firmware-e/manifest.json" + : "${stable_manifest_base}/firmware-w/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: statusCheck diff --git a/Integrations/ESPHome/beta-channel/CAST-1_ETH.yaml b/Integrations/ESPHome/beta-channel/CAST-1_ETH.yaml new file mode 100644 index 0000000..fe73f73 --- /dev/null +++ b/Integrations/ESPHome/beta-channel/CAST-1_ETH.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of CAST-1_ETH.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 CAST-1_ETH.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../CAST-1_ETH.yaml diff --git a/Integrations/ESPHome/beta-channel/CAST-1_W.yaml b/Integrations/ESPHome/beta-channel/CAST-1_W.yaml new file mode 100644 index 0000000..9b64888 --- /dev/null +++ b/Integrations/ESPHome/beta-channel/CAST-1_W.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of CAST-1_W.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 CAST-1_W.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../CAST-1_W.yaml diff --git a/Integrations/ESPHome/wizmote.yaml b/Integrations/ESPHome/wizmote.yaml index 063d3b1..c643c8a 100644 --- a/Integrations/ESPHome/wizmote.yaml +++ b/Integrations/ESPHome/wizmote.yaml @@ -319,7 +319,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_on).state;' + action: !lambda 'return id(wizmote_action_on).current_option();' button: "on" - if: condition: @@ -327,7 +327,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_off).state;' + action: !lambda 'return id(wizmote_action_off).current_option();' button: "off" - if: condition: @@ -335,7 +335,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_night).state;' + action: !lambda 'return id(wizmote_action_night).current_option();' button: "night" - if: condition: @@ -343,7 +343,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_bright_up).state;' + action: !lambda 'return id(wizmote_action_bright_up).current_option();' button: "brightness_up" - if: condition: @@ -351,7 +351,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_bright_down).state;' + action: !lambda 'return id(wizmote_action_bright_down).current_option();' button: "brightness_down" - if: condition: @@ -359,7 +359,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_btn1).state;' + action: !lambda 'return id(wizmote_action_btn1).current_option();' button: "1" - if: condition: @@ -367,7 +367,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_btn2).state;' + action: !lambda 'return id(wizmote_action_btn2).current_option();' button: "2" - if: condition: @@ -375,7 +375,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_btn3).state;' + action: !lambda 'return id(wizmote_action_btn3).current_option();' button: "3" - if: condition: @@ -383,7 +383,7 @@ script: then: - script.execute: id: run_wizmote_action - action: !lambda 'return id(wizmote_action_btn4).state;' + action: !lambda 'return id(wizmote_action_btn4).current_option();' button: "4" - if: condition: diff --git a/index.html b/index.html deleted file mode 100644 index e69de29..0000000