Complete ESP-IDF 6 migration hardening (#2123)

* Complete ESP-IDF 6 migration hardening

* Fix PR variant diff selection

* Allow Git diff in CI container

---------

Co-authored-by: Xiaoxia <terrence.huang@tenclass.com>
This commit is contained in:
Xiaoxia
2026-07-17 16:32:44 +08:00
committed by GitHub
co-authored by Xiaoxia
parent 3e78cd73bd
commit 0f6c435f66
26 changed files with 779 additions and 380 deletions
+58 -34
View File
@@ -26,8 +26,8 @@ jobs:
with:
fetch-depth: 0
- name: Install jq
run: apt-get update && apt-get install -y jq
- name: Test release tooling
run: python -m unittest discover -s scripts/tests -v
- id: list
name: Get all variant list
@@ -49,41 +49,20 @@ jobs:
fi
# pull_request 场景
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
echo "Base: $BASE_SHA, Head: $HEAD_SHA"
# actions/checkout checks out GitHub's synthetic PR merge commit. Its
# first and second parents are the exact base and head commits used
# for this run, even when the event SHAs are not exposed as refs in
# the job container.
git config --global --add safe.directory "$GITHUB_WORKSPACE"
BASE_SHA="HEAD^1"
HEAD_SHA="HEAD^2"
echo "Base: $(git rev-parse "$BASE_SHA"), Head: $(git rev-parse "$HEAD_SHA")"
CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA || true)
CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
echo -e "Changed files:\n$CHANGED"
NEED_ALL=0
declare -A AFFECTED
while IFS= read -r file; do
if [[ "$file" == main/* && "$file" != main/boards/* ]]; then
NEED_ALL=1
fi
if [[ "$file" == main/boards/common/* ]]; then
NEED_ALL=1
fi
if [[ "$file" == main/boards/* ]]; then
board=$(echo "$file" | cut -d '/' -f3)
AFFECTED[$board]=1
fi
done <<< "$CHANGED"
if [[ "$NEED_ALL" -eq 1 ]]; then
echo "variants=$ALL_VARIANTS" >> $GITHUB_OUTPUT
else
if [[ ${#AFFECTED[@]} -eq 0 ]]; then
echo "variants=[]" >> $GITHUB_OUTPUT
else
BOARDS_JSON=$(printf '%s\n' "${!AFFECTED[@]}" | sort -u | jq -R -s -c 'split("\n")[:-1]')
FILTERED=$(echo "$ALL_VARIANTS" | jq -c --argjson boards "$BOARDS_JSON" 'map(select(.board as $b | $boards | index($b)))')
echo "variants=$FILTERED" >> $GITHUB_OUTPUT
fi
fi
SELECTED=$(printf '%s\n' "$CHANGED" | python scripts/release.py --select-changed)
echo "variants=$SELECTED" >> $GITHUB_OUTPUT
build:
name: Build ${{ matrix.full_name }}
@@ -112,3 +91,48 @@ jobs:
name: xiaozhi_${{ matrix.full_name }}_${{ github.sha }}
path: build/merged-binary.bin
if-no-files-found: error
compatibility-idf5:
name: IDF 5.5 compatibility - ${{ matrix.name }}
if: ${{ github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
include:
- board: xmini-c3
name: xmini-c3
- board: esp-p4-function-ev-board
name: esp-p4-function-ev-board
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.5.4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build compatibility variant
shell: bash
run: |
source $IDF_PATH/export.sh
python scripts/release.py ${{ matrix.board }} --name ${{ matrix.name }}
blufi-idf6:
name: IDF 6 BluFi option
runs-on: ubuntu-latest
container:
image: espressif/idf:v6.0.1
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build BluFi-enabled firmware
shell: bash
run: |
source $IDF_PATH/export.sh
idf.py -B build-blufi \
-DSDKCONFIG=build-blufi/sdkconfig \
-DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32s3;scripts/ci/blufi.sdkconfig.defaults" \
-DIDF_TARGET=esp32s3 \
-DBOARD_NAME=bread-compact-wifi \
-DBOARD_TYPE=bread-compact-wifi \
build
+17 -17
View File
@@ -12,26 +12,25 @@ As a voice interaction entry, the XiaoZhi AI chatbot leverages the AI capabiliti
<img src="docs/mcp-based-graph.jpg" alt="Control everything via MCP" width="320">
## Version Notes
## Recent Updates
The current v2 version is incompatible with the v1 partition table, so it is not possible to upgrade from v1 to v2 via OTA. For partition table details, see [partitions/v2/README.md](partitions/v2/README.md).
All hardware running v1 can be upgraded to v2 by manually flashing the firmware.
The stable version of v1 is 1.9.2. You can switch to v1 by running `git checkout v1`. The v1 branch will be maintained until February 2026.
- The mainline now targets ESP-IDF v6.0 or later, with v6.0.2 as the preferred stable SDK. The complete 157-variant release matrix has been validated on ESP-IDF v6.0.1.
- MQTT and BluFi cryptographic code has migrated to PSA Crypto. IDF 6 component splits and third-party dependency compatibility have also been addressed.
- Audio pipeline concurrency, MQTT/UDP packet validation, and release-matrix selection have been hardened.
- ESP-IDF v5.5 is retained only for legacy hardware paths, including pre-v3 ESP32-P4 silicon. See the [ESP-IDF 6.0 Migration Guide](docs/esp-idf-6-migration.md) for full compatibility and board-validation details.
### Features Implemented
- Wi-Fi / ML307 Cat.1 4G
- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
- Supports two communication protocols ([Websocket](docs/websocket.md) or MQTT+UDP)
- Uses OPUS audio codec
- Voice interaction based on streaming ASR + LLM + TTS architecture
- Wi-Fi, wired Ethernet, USB RNDIS, and ML307/EC801E or NT26 Cat.1 4G networking; supported boards can switch between Wi-Fi and 4G
- Offline voice wake-up with [ESP-SR](https://github.com/espressif/esp-sr), including customizable wake words
- Two communication transports: [WebSocket](docs/websocket.md) and [MQTT + UDP](docs/mqtt-udp.md)
- Opus audio streaming with conventional streaming ASR + LLM + TTS pipelines and Realtime end-to-end voice models; AEC-capable hardware supports realtime full-duplex interaction
- Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker)
- OLED / LCD display, supports emoji display
- OLED / LCD displays with emoji and rich expression support, plus camera vision input on supported boards
- Battery display and power management
- Multi-language support (Chinese, English, Japanese)
- Supports ESP32-C3, ESP32-S3, ESP32-P4 chip platforms
- 38 interface languages, with localized voice prompts where available and English fallback
- ESP32, ESP32-C3, ESP32-C5, ESP32-C6, ESP32-S3, and ESP32-P4 chip platforms
- Wi-Fi provisioning through hotspot, acoustic signaling, or BluFi
- Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.)
- Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.)
- Customizable wake words, fonts, emojis, and chat backgrounds with online web-based editing ([Custom Assets Generator](https://github.com/78/xiaozhi-assets-generator))
@@ -48,7 +47,7 @@ Breadboard demo:
![Breadboard Demo](docs/v1/wiring2.jpg)
### Supports 70+ Open Source Hardware (Partial List)
### Supports 137 Board Directories and 157 Release Variants (Partial List)
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
@@ -115,12 +114,13 @@ The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by
### Development Environment
- Cursor or VSCode
- Install ESP-IDF plugin, select SDK version 5.4 or above
- Install the ESP-IDF plugin. [ESP-IDF v6.0.2](https://github.com/espressif/esp-idf/releases/tag/v6.0.2) is preferred; use a stable v6.0 or later release. ESP-IDF v5.5.2 is retained only for legacy board compatibility
- Linux is better than Windows for faster compilation and fewer driver issues
- This project uses Google C++ code style, please ensure compliance when submitting code
### Developer Documentation
- [ESP-IDF 6.0 Migration Guide](docs/esp-idf-6-migration.md) - SDK compatibility, component changes, legacy hardware support, and board validation status
- [Custom Board Guide](docs/custom-board.md) - Learn how to create custom boards for XiaoZhi AI
- [MCP Protocol IoT Control Usage](docs/mcp-usage.md) - Learn how to control IoT devices via MCP protocol
- [MCP Protocol Interaction Flow](docs/mcp-protocol.md) - Device-side MCP protocol implementation
@@ -170,4 +170,4 @@ If you have any ideas or suggestions, please feel free to raise Issues or join o
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
</picture>
</a>
</a>
+16 -16
View File
@@ -12,26 +12,25 @@
<img src="docs/mcp-based-graph.jpg" alt="MCPであらゆるものを制御" width="320">
## バージョンノート
## 最近の更新
現在のv2バージョンはv1パーティションテーブルと互換性がないため、v1からv2へOTAでアップグレードすることはできません。パーティションテーブルの詳細については、[partitions/v2/README.md](partitions/v2/README.md)をご参照ください
v1を実行しているすべてのハードウェアは、ファームウェアを手動で書き込むことでv2にアップグレードできます
v1の安定版は1.9.2です。`git checkout v1`でv1に切り替えることができます。v1ブランチは2026年2月まで継続的にメンテナンスされます。
- メインラインはESP-IDF v6.0以降へ移行し、推奨安定版はv6.0.2です。全157リリースバリアントはESP-IDF v6.0.1でビルド検証済みです
- MQTTとBluFiの暗号処理をPSA Cryptoへ移行し、IDF 6のコンポーネント分割およびサードパーティ依存関係にも対応しました。
- オーディオパイプラインの並行処理、MQTT/UDPパケット検証、リリースマトリクス選択処理を強化しました
- ESP-IDF v5.5は、ESP32-P4 v3より前のシリコンを含む旧ハードウェア互換用途にのみ残しています。詳細な互換性とボード検証状況は、[ESP-IDF 6.0移行ガイド](docs/esp-idf-6-migration.md)を参照してください。
### 実装済み機能
- Wi-Fi / ML307 Cat.1 4G
- オフライン音声ウェイクアップ [ESP-SR](https://github.com/espressif/esp-sr)
- 2種類の通信プロトコルに対応([Websocket](docs/websocket.md) または MQTT+UDP
- OPUSオーディオコーデックを採用
- ストリーミングASR + LLM + TTSアーキテクチャに基づく音声インタラクション
- Wi-Fi、有線Ethernet、USB RNDIS、およびML307/EC801EまたはNT26 Cat.1 4Gに対応し、一部のボードではWi-Fiと4Gを切り替え可能
- [ESP-SR](https://github.com/espressif/esp-sr)によるオフライン音声ウェイクアップとカスタムウェイクワード
- 2種類の通信方式:[WebSocket](docs/websocket.md)と[MQTT + UDP](docs/mqtt-udp.md)
- Opusオーディオストリーミングにより、従来のストリーミングASR + LLM + TTS構成とRealtimeエンドツーエンド音声モデルの両方に対応。AEC対応ハードウェアではリアルタイム全二重対話が可能
- 話者認識、現在話している人を識別 [3D Speaker](https://github.com/modelscope/3D-Speaker)
- OLED / LCDディスプレイ、表情表示対応
- OLED / LCDディスプレイで絵文字や豊かな感情表現を表示し、一部のボードではカメラによる視覚入力にも対応
- バッテリー表示と電源管理
- 多言語対応(中国語、英語、日本語)
- ESP32-C3、ESP32-S3、ESP32-P4チッププラットフォーム対応
- 38言語の画面表示に対応し、音声プロンプトはローカライズ済みリソースを優先して、未収録時は英語へフォールバック
- ESP32、ESP32-C3、ESP32-C5、ESP32-C6、ESP32-S3、ESP32-P4チッププラットフォーム
- ホットスポット、音響信号、BluFiによるWi-Fiプロビジョニング
- デバイス側MCPによるデバイス制御(音量・明るさ調整、アクション制御など)
- クラウド側MCPで大規模モデル能力を拡張(スマートホーム制御、PCデスクトップ操作、知識検索、メール送受信など)
- カスタマイズ可能なウェイクワード、フォント、絵文字、チャット背景、オンラインWeb編集に対応 ([カスタムアセットジェネレーター](https://github.com/78/xiaozhi-assets-generator))
@@ -48,7 +47,7 @@ Feishuドキュメントチュートリアルをご覧ください:
![ブレッドボードデモ](docs/v1/wiring2.jpg)
### 70種類以上のオープンソースハードウェアに対応(一部のみ表示)
### 137のボードディレクトリと157のリリースバリアントに対応(一部のみ表示)
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立創・実戦派 ESP32-S3 開発ボード">立創・実戦派 ESP32-S3 開発ボード</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="楽鑫 ESP32-S3-BOX3">楽鑫 ESP32-S3-BOX3</a>
@@ -115,12 +114,13 @@ Feishuドキュメントチュートリアルをご覧ください:
### 開発環境
- Cursor または VSCode
- ESP-IDFプラグインをインストールし、SDKバージョン5.4以上を選択
- ESP-IDFプラグインをインストールし、[ESP-IDF v6.0.2](https://github.com/espressif/esp-idf/releases/tag/v6.0.2)を優先して使用してください。v6.0以降の安定版を推奨し、ESP-IDF v5.5.2は旧ハードウェアとの互換性維持にのみ使用します
- LinuxはWindowsよりも優れており、コンパイルが速く、ドライバの問題も少ない
- 本プロジェクトはGoogle C++コードスタイルを採用、コード提出時は準拠を確認してください
### 開発者ドキュメント
- [ESP-IDF 6.0移行ガイド](docs/esp-idf-6-migration.md) - SDK互換性、コンポーネント変更、旧ハードウェア対応、ボード検証状況
- [カスタム開発ボードガイド](docs/custom-board.md) - シャオジーAI用のカスタム開発ボード作成方法
- [MCPプロトコルIoT制御使用法](docs/mcp-usage.md) - MCPプロトコルでIoTデバイスを制御する方法
- [MCPプロトコルインタラクションフロー](docs/mcp-protocol.md) - デバイス側MCPプロトコルの実装方法
+16 -16
View File
@@ -12,26 +12,25 @@
<img src="docs/mcp-based-graph.jpg" alt="通过MCP控制万物" width="320">
### 版本说明
## 近期更新
当前 v2 版本与 v1 版本分区表不兼容,所以无法从 v1 版本通过 OTA 升级到 v2 版本。分区表说明参见 [partitions/v2/README.md](partitions/v2/README.md)
使用 v1 版本的所有硬件,可以通过手动烧录固件来升级到 v2 版本
v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版本,该分支会持续维护到 2026 年 2 月。
- 项目主线现已迁移到 ESP-IDF v6.0 或以上版本,首选稳定版为 v6.0.2;完整的 157 个发布变体已在 ESP-IDF v6.0.1 上通过构建验证
- MQTT 和 BluFi 加密已迁移到 PSA Crypto,同时完成了 IDF 6 组件拆分及第三方依赖兼容处理。
- 加固了音频流水线并发、MQTT/UDP 数据包校验和发布矩阵选择逻辑
- ESP-IDF v5.5 仅保留用于旧版硬件兼容,包括 ESP32-P4 v3 之前的芯片版本。完整兼容性和板卡验证状态请参阅 [ESP-IDF 6.0 迁移文档](docs/esp-idf-6-migration.md)。
### 已实现功能
- Wi-Fi / ML307 Cat.1 4G
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
- 支持两种通信协议([Websocket](docs/websocket_zh.md) 或 MQTT+UDP
- 采用 OPUS 音频编解码
- 基于流式 ASR + LLM + TTS 架构的语音交互
- 支持 Wi-Fi、有线以太网、USB RNDIS,以及 ML307/EC801E 或 NT26 Cat.1 4G 网络;部分硬件支持 Wi-Fi 与 4G 切换
- 基于 [ESP-SR](https://github.com/espressif/esp-sr) 的离线语音唤醒,支持自定义唤醒词
- 支持两种通信传输方式:[WebSocket](docs/websocket_zh.md) 和 [MQTT + UDP](docs/mqtt-udp_zh.md)
- 采用 Opus 音频流,既支持传统的流式 ASR + LLM + TTS 方案,也支持 Realtime 端到端语音模型;具备 AEC 的硬件可实现实时全双工交互
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker)
- OLED / LCD 显示屏,支持表情显示
- OLED / LCD 显示屏,支持表情和丰富的情绪呈现;部分硬件支持摄像头视觉输入
- 电量显示与电源管理
- 支持多语言(中文、英文、日文)
- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台
- 提供 38 种界面语言;语音提示优先使用本地化资源,缺失时自动回退到英文
- 支持 ESP32、ESP32-C3、ESP32-C5、ESP32-C6、ESP32-S3、ESP32-P4 芯片平台
- 支持热点、声波和 BluFi 三种 Wi-Fi 配网方式
- 通过设备端 MCP 实现设备控制(音量、灯光、电机、GPIO 等)
- 通过云端 MCP 扩展大模型能力(智能家居控制、PC桌面操作、知识搜索、邮件收发等)
- 自定义唤醒词、字体、表情与聊天背景,支持网页端在线修改 ([自定义Assets生成器](https://github.com/78/xiaozhi-assets-generator))
@@ -48,7 +47,7 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版
![面包板效果图](docs/v1/wiring2.jpg)
### 支持 70 多个开源硬件(仅展示部分)
### 支持 137 个板卡目录、157 个固件发布变体(仅展示部分)
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a>
@@ -115,12 +114,13 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版
### 开发环境
- Cursor 或 VSCode
- 安装 ESP-IDF 插件,选择 SDK 版本 5.4 或以上
- 安装 ESP-IDF 插件,首选 [ESP-IDF v6.0.2](https://github.com/espressif/esp-idf/releases/tag/v6.0.2),建议使用 v6.0 或以上的稳定版本;ESP-IDF v5.5.2 仅保留用于旧版硬件兼容
- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
- 本项目使用 Google C++ 代码风格,提交代码时请确保符合规范
### 开发者文档
- [ESP-IDF 6.0 迁移文档](docs/esp-idf-6-migration.md) - SDK 兼容性、组件变更、旧版硬件支持和板卡验证状态
- [自定义开发板指南](docs/custom-board_zh.md) - 学习如何为小智 AI 创建自定义开发板
- [MCP 协议物联网控制用法说明](docs/mcp-usage_zh.md) - 了解如何通过 MCP 协议控制物联网设备
- [MCP 协议交互流程](docs/mcp-protocol_zh.md) - 设备端 MCP 协议的实现方式
+14 -18
View File
@@ -1,16 +1,16 @@
# ESP-IDF 6.0 Migration and Board Compatibility Status
> Last updated: 2026-07-16
> Last updated: 2026-07-17
> Validated SDK: ESP-IDF v6.0.1
> Scope: 137 board directories and 157 supported build variants defined by `main/boards/**/config.json`.
## Current Status
The migration baseline is reproducible from committed project files and published Component Registry packages. All 157 IDF 6 variants are now build-compatible with ESP-IDF 6.0.1: 156 passed the last complete GitHub Actions matrix, and `esp-vocat` subsequently passed a local full build after rebasing onto upstream commit `467b96a` and fixing two IDF 6/C++26 ST77916 initialization errors. A new complete matrix is pending. Fourteen legacy ESP32-P4 Rev < 3 variants are excluded from the IDF 6 matrix but remain available when building with ESP-IDF < 6.
All 157 IDF 6 variants are build-compatible with ESP-IDF 6.0.1 and passed the latest complete GitHub Actions matrix. Fourteen legacy ESP32-P4 Rev < 3 variants are excluded from the IDF 6 matrix but remain available when building with ESP-IDF < 6. Component versions that use ranges are resolved from the Component Registry at build time; until per-target lock snapshots are committed, this is a source-reproducible build rather than a bit-for-bit dependency-reproducible build.
| Status | Variants | Meaning |
|---|---:|---|
| ✅ Build-compatible | 157 | 156 clean full builds completed in GitHub Actions and `esp-vocat` completed locally with ESP-IDF 6.0.1; this does not imply hardware or complete peripheral validation |
| ✅ Build-compatible | 157 | All 157 full builds completed in GitHub Actions with ESP-IDF 6.0.1; this does not imply hardware or complete peripheral validation |
| 🟡 Feature-degraded subset | 1 | `esp-vocat` builds on IDF 6, but its PCB capacitive slider/button support is disabled pending compatible touch-sensor components |
| 🔴 Build-blocked | 0 | No supported release variant remains blocked at compile or link time |
@@ -38,11 +38,11 @@ Representative local full-build results are shown below. Firmware size and free
As a negative test, `esp-p4-function-ev-board` (legacy P4 Rev < 3) was rejected as expected by `esp-sr 2.4.6` during IDF 6.0.1 configuration. Legacy P4 variants are therefore version-gated to ESP-IDF < 6 instead of being carried as permanent IDF 6 blockers.
For backward-compatibility regression coverage, `xmini-c3` also completed a full build with ESP-IDF 5.5.4 (application size `0x234920`, 44% free in the smallest app partition). This confirms that the compatibility changes for I2S port numbering, LCD I2C configuration, and the UHCI DMA dependency did not break the existing 5.5 build chain.
For backward-compatibility regression coverage, `xmini-c3` also completed a full build with ESP-IDF 5.5.4 (application size `0x234920`, 44% free in the smallest app partition). The legacy `esp-p4-function-ev-board` release variant subsequently completed the same 5.5.4 release flow (application size `0x38a130`, 10% free), including merged-binary packaging. This confirms that the compatibility changes for I2S port numbering, LCD I2C configuration, the UHCI DMA dependency, and the pre-v3 P4 selection did not break the existing 5.5 build chain.
## Full-Matrix CI Validation
GitHub Actions run [29477593880](https://github.com/78/xiaozhi-esp32/actions/runs/29477593880) built the complete matrix with the `espressif/idf:v6.0.1` container and only committed files plus published registry components. The matrix-generation job and 156 board jobs passed; exactly 1 board job failed at the then-current configure-time guard for unsupported touch-sensor components. The table below records that historical run; `esp-vocat` has since passed locally and the replacement full-matrix run is pending.
GitHub Actions run [29534954031](https://github.com/78/xiaozhi-esp32/actions/runs/29534954031) built the complete matrix with the `espressif/idf:v6.0.1` container. The matrix-generation job and all 157 board builds passed.
Results by chip target:
@@ -52,15 +52,11 @@ Results by chip target:
| ESP32-C3 | 9 | 9 | 0 |
| ESP32-C5 | 4 | 4 | 0 |
| ESP32-C6 | 9 | 9 | 0 |
| ESP32-S3 | 114 | 113 | 1 |
| ESP32-S3 | 114 | 114 | 0 |
| ESP32-P4 v3.x | 14 | 14 | 0 |
| **Total** | **157** | **156** | **1** |
| **Total** | **157** | **157** | **0** |
The failure in that run and its current resolution are:
| Build variant | Failure in run 29477593880 | Current resolution |
|---|---|---|
| `esp-vocat` | `touch_slider_sensor` and `touch_button_sensor` require IDF < 6.0 | IDF 6 builds omit the PCB capacitive slider/button path; CST816 display touch remains enabled. A local full build passed after the mainline change and ST77916 compatibility fixes |
`esp-vocat` remains feature-degraded because IDF 6 builds omit the PCB capacitive slider/button path; its CST816 display touch remains enabled.
## ESP32-P4 Silicon Scope and Naming
@@ -80,7 +76,7 @@ Espressif's current chip-identification table lists v0.0, v1.0, v1.3, v3.0, v3.1
| Component/module | Version or approach for IDF 6 | Status | Notes |
|---|---|---|---|
| `78/uart-uhci` | `0.3.2` | ✅ Upstream support | The registry release compiles under IDF 6.0.1 in GitHub Actions |
| `78/uart-eth-modem` | `0.6.1` | ✅ Upstream support | Updated for the new event callback and depends on `uart-uhci 0.3.x` |
| `78/uart-eth-modem` | `0.6.0` | ✅ Upstream support | Pinned because it has the required RF-test event API while supporting both ESP-IDF 5.5.2+ and 6.0.1; version 0.6.1 requires IDF 6.0.1+ |
| `espressif/mqtt` | `1.0.0` | ✅ Upstream support | MQTT moved from a built-in SDK component to a Component Manager dependency in IDF 6 |
| `78/esp-ml307` | `3.6.6` + project-level `espressif/mqtt` | ✅ Builds in CI | The upstream source compiles under IDF 6.0.1 when the project supplies the MQTT component moved out of IDF |
| `espressif/esp_hosted` / `esp_wifi_remote` | `2.12.11` / `1.6.2` | ✅ Upstream support | Used for ESP32-P4 Hosted Wi-Fi |
@@ -95,7 +91,7 @@ Espressif's current chip-identification table lists v0.0, v1.0, v1.3, v3.0, v3.1
| `78/esp_lcd_nv3023` | [`1.0.1`](https://components.espressif.com/components/78/esp_lcd_nv3023/versions/1.0.1) | ✅ Upstream-based registry release | Mirrors MakerM0 upstream commit `15dae953`; adds IDF 6 color-order and GPIO compatibility while retaining older IDF branches; validated through the registry in a clean `magiclick-c3` full build |
| `llgok/cpp_bus_driver` | Excluded from the IDF 6 baseline | ⚪ Waiting for upstream | Version `2.1.0` still requires local patches for IDF 6. It is used only by the `LILYGO T-Display-P4` source, and that board has no release `config.json`, so the component has been removed until upstream support is available |
| MQTT protocol AES-CTR | PSA Crypto | ✅ Ported | Replaces the legacy AES context API removed by IDF 6 / Mbed TLS 4 |
| BluFi security negotiation (conditional path) | Legacy Mbed TLS DHM/AES API | ⚪ Not yet migrated | None of the 157 supported release variants enables it. IDF 6 removed `mbedtls/dhm.h`; compatibility of the official PSA FFDH + AES-CTR approach with the existing mobile-client protocol must be evaluated |
| BluFi security negotiation (conditional path) | PSA FFDH + SHA-256 + AES-CTR | ✅ Ported | Uses the ESP-IDF 6 security scheme with ffdhe3072 and passed local full ESP32-S3 builds with BluFi enabled on IDF 5.5.4 and 6.0.1. Legacy 1024-bit BluFi clients are not compatible and must be upgraded |
| `espressif/bmi270_sensor` | [`0.1.2`](https://components.espressif.com/components/espressif/bmi270_sensor/versions/0.1.2/readme?language=en) | ✅ Upstream support | Provides IDF 6.0 prebuilt libraries for ESP32-C5 and ESP32-S3; validated by a full `esp-spot-c5` build |
| `touch_slider_sensor` / `touch_button_sensor` | Disabled for IDF 6 | 🟡 Feature gap | Their manifests require IDF < 6.0, so ESP Vocat omits its PCB capacitive slider/button path on IDF 6. CST816 display touch is unaffected |
| ESP32-P4 Rev < 3 / `espressif/esp-sr` | Version-gated to IDF < 6 | 🟡 Legacy SDK path | The 14 legacy variants are restored for IDF 5.5 builds. They remain excluded from IDF 6 because `esp-sr 2.4.6` rejects the old-silicon configuration; ESP32-P4 v3.x is unaffected |
@@ -139,8 +135,8 @@ In the table below, "Board" is the source directory and "Build variant" is the f
| `esp32s3` | `atk-dnesp32s3` | `atk-dnesp32s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `atk-dnesp32s3-box` | `atk-dnesp32s3-box` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `atk-dnesp32s3-box0` | `atk-dnesp32s3-box0` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `atk-dnesp32s3-box2-4g` | `atk-dnesp32s3-box2-4g` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `atk-dnesp32s3-box2-wifi` | `atk-dnesp32s3-box2-wifi` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `atk-dnesp32s3-box2-4g` | `atk-dnesp32s3-box2-4g` | ✅ Build and hardware validated | GitHub Actions IDF 6.0.1 full build; maintainer hardware validation | Complete for the IDF 6 migration scope |
| `esp32s3` | `atk-dnesp32s3-box2-wifi` | `atk-dnesp32s3-box2-wifi` | ✅ Build and hardware validated | GitHub Actions IDF 6.0.1 full build; maintainer hardware validation | Complete for the IDF 6 migration scope |
| `esp32s3` | `atk-dnesp32s3-box3` | `atk-dnesp32s3-box3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `atom-echos3r` | `atom-echos3r` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `atoms3-echo-base` | `atoms3-echo-base` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
@@ -181,7 +177,7 @@ In the table below, "Board" is the source directory and "Build variant" is the f
| `esp32s3` | `lilygo-t-circle-s3` | `lilygo-t-circle-s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `lilygo-t-display-s3-pro-mvsrlora` | `lilygo-t-display-s3-pro-mvsrlora` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `m5stack-cardputer-adv` | `m5stack-cardputer-adv` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `m5stack-core-s3` | `m5stack-core-s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `m5stack-core-s3` | `m5stack-core-s3` | ✅ Build and hardware validated | GitHub Actions IDF 6.0.1 full build; maintainer hardware validation | Complete for the IDF 6 migration scope |
| `esp32s3` | `m5stack-stick-s3` | `m5stack-stick-s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `m5stack-stopwatch` | `m5stack-stopwatch` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
| `esp32s3` | `magiclick-2p4` | `magiclick-2p4` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
@@ -247,7 +243,7 @@ In the table below, "Board" is the source directory and "Build variant" is the f
## Next Steps and Acceptance Criteria
1. Run the complete 157-variant IDF 6 GitHub Actions matrix on the rebased branch, and add an IDF 5.5 legacy job to build and hardware-test at least one P4 v1.3 device. When IDF 6-compatible releases of `touch_slider_sensor` and `touch_button_sensor` become available, re-enable and hardware-test the ESP Vocat PCB capacitive slider/button path.
1. Run the new IDF 5.5 compatibility and IDF 6 BluFi CI jobs, then hardware-test at least one P4 v1.3 device on the legacy SDK path. When IDF 6-compatible releases of `touch_slider_sensor` and `touch_button_sensor` become available, re-enable and hardware-test the ESP Vocat PCB capacitive slider/button path.
2. For every green variant, complete a minimal hardware smoke test covering boot, networking, audio input/output, display/touch when present, camera when present, and 4G/Ethernet when present.
3. Perform a physical negative test for the `xmini-c3`/`xmini-c3-v3` firmware guard. CI proves that both images compile; it does not prove that a wrong image is safely rejected. Acceptance requires flashing each wrong image to a sacrificial or recoverable board and verifying that startup stops before any board-specific power or peripheral initialization can cause damage.
4. Keep third-party experiments under the ignored `components/` directory out of the migration branch. The reproducible baseline must use the published `78/esp_lcd_nv3023 1.0.1` and `wvirgil123/sscma_client 1.0.3` packages. `espfriends/servo_dog_ctrl 0.2.0` is an upstream registry dependency and needs no local override.
-5
View File
@@ -628,11 +628,6 @@ elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA_NO_BATTERY)
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_P4)
set(BOARD_TYPE "lilygo-t-display-p4")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_30_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3)
set(BOARD_TYPE "movecall-moji-esp32s3")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
+3 -26
View File
@@ -447,9 +447,6 @@ choice BOARD_TYPE
config BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA_NO_BATTERY
bool "LILYGO T-Display-S3-Pro-MVSRLora_No_Battery"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_LILYGO_T_DISPLAY_P4
bool "LILYGO T-Display-P4"
depends on IDF_TARGET_ESP32P4
config BOARD_TYPE_MOVECALL_MOJI_ESP32S3
bool "Movecall Moji 小智AI衍生版"
depends on IDF_TARGET_ESP32S3
@@ -655,26 +652,6 @@ menu "Ethernet Configuration"
endmenu
choice
depends on BOARD_TYPE_LILYGO_T_DISPLAY_P4
prompt "Select the screen type"
default SCREEN_TYPE_HI8561
config SCREEN_TYPE_HI8561
bool "HI8561"
config SCREEN_TYPE_RM69A10
bool "RM69A10"
endchoice
choice
depends on BOARD_TYPE_LILYGO_T_DISPLAY_P4
prompt "Select the color format of the screen"
default SCREEN_PIXEL_FORMAT_RGB565
config SCREEN_PIXEL_FORMAT_RGB565
bool "RGB565"
config SCREEN_PIXEL_FORMAT_RGB888
bool "RGB888"
endchoice
choice ESP_S3_LCD_EV_Board_Version_TYPE
depends on BOARD_TYPE_ESP_S3_LCD_EV_Board
prompt "EV_BOARD Type"
@@ -947,12 +924,12 @@ menu "WiFi Configuration Method"
config USE_ESP_BLUFI_WIFI_PROVISIONING
bool "Esp Blufi"
help
Use esp blufi protocol to transmit WiFi configuration data
select BT_ENABLED
select BT_BLE_42_FEATURES_SUPPORTED
select BT_BLE_BLUFI_ENABLE
select MBEDTLS_DHM_C
help
Use the PSA Crypto based BluFi security protocol adopted by ESP-IDF 6.
The provisioning client must support ffdhe3072, SHA-256, and AES-CTR.
endmenu
config AUDIO_DEBUG_UDP_SERVER
+77 -33
View File
@@ -120,7 +120,7 @@ void AudioService::Initialize(AudioCodec* codec) {
}
void AudioService::Start() {
service_stopped_ = false;
service_stopped_.store(false);
xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING | AS_EVENT_WAKE_WORD_RUNNING |
AS_EVENT_AUDIO_PROCESSOR_RUNNING | AS_EVENT_AUDIO_INPUT_STOP_REQUEST);
@@ -166,20 +166,23 @@ void AudioService::Start() {
void AudioService::Stop() {
esp_timer_stop(audio_power_timer_);
service_stopped_ = true;
service_stopped_.store(true);
xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING |
AS_EVENT_WAKE_WORD_RUNNING |
AS_EVENT_AUDIO_PROCESSOR_RUNNING);
bool notify_drained = false;
{
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
++playback_generation_;
audio_encode_queue_.clear();
audio_decode_queue_.clear();
audio_playback_queue_.clear();
audio_testing_queue_.clear();
notify_drained = MarkPlaybackDrainedLocked();
audio_queue_cv_.notify_all();
}
if (callbacks_.on_playback_drained) {
if (notify_drained && callbacks_.on_playback_drained) {
callbacks_.on_playback_drained();
}
}
@@ -239,7 +242,7 @@ void AudioService::AudioInputTask() {
AS_EVENT_AUDIO_INPUT_STOP_REQUEST,
pdFALSE, pdFALSE, portMAX_DELAY);
if (service_stopped_) {
if (service_stopped_.load()) {
// ADC continuous mode keeps its hardware mutex from start until stop,
// so the input task that started it must also stop it before exiting.
if (codec_->input_enabled()) {
@@ -253,14 +256,17 @@ void AudioService::AudioInputTask() {
// Recheck the active state in this task. Audio capture may have been
// enabled after the timer posted the stop request.
if ((xEventGroupGetBits(event_group_) & kAudioInputActiveBits) == 0 &&
codec_->input_enabled()) {
codec_->EnableInput(false);
bits = xEventGroupGetBits(event_group_);
if ((bits & kAudioInputActiveBits) == 0) {
if (codec_->input_enabled()) {
codec_->EnableInput(false);
}
// Do not process the stale active bits returned by waitBits().
continue;
}
}
if (audio_input_need_warmup_) {
audio_input_need_warmup_ = false;
if (audio_input_need_warmup_.exchange(false)) {
vTaskDelay(pdMS_TO_TICKS(120));
continue;
}
@@ -308,14 +314,16 @@ void AudioService::AudioInputTask() {
void AudioService::AudioOutputTask() {
while (true) {
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
audio_queue_cv_.wait(lock, [this]() { return !audio_playback_queue_.empty() || service_stopped_; });
if (service_stopped_) {
audio_queue_cv_.wait(lock, [this]() {
return !audio_playback_queue_.empty() || service_stopped_.load();
});
if (service_stopped_.load()) {
break;
}
auto task = std::move(audio_playback_queue_.front());
audio_playback_queue_.pop_front();
const bool playback_drained = audio_playback_queue_.empty() && audio_decode_queue_.empty();
output_in_flight_ = true;
audio_queue_cv_.notify_all();
lock.unlock();
@@ -331,17 +339,22 @@ void AudioService::AudioOutputTask() {
last_output_time_ = std::chrono::steady_clock::now();
debug_statistics_.playback_count++;
if (playback_drained && callbacks_.on_playback_drained) {
callbacks_.on_playback_drained();
}
bool notify_drained = false;
lock.lock();
#if CONFIG_USE_SERVER_AEC
/* Record the timestamp for server AEC */
if (task->timestamp > 0) {
lock.lock();
timestamp_queue_.push_back(task->timestamp);
}
#endif
output_in_flight_ = false;
notify_drained = MarkPlaybackDrainedLocked();
audio_queue_cv_.notify_all();
lock.unlock();
if (notify_drained && callbacks_.on_playback_drained) {
callbacks_.on_playback_drained();
}
}
ESP_LOGW(TAG, "Audio output task stopped");
@@ -351,11 +364,11 @@ void AudioService::OpusCodecTask() {
while (true) {
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
audio_queue_cv_.wait(lock, [this]() {
return service_stopped_ ||
return service_stopped_.load() ||
!audio_encode_queue_.empty() ||
(!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE);
});
if (service_stopped_) {
if (service_stopped_.load()) {
break;
}
@@ -363,6 +376,8 @@ void AudioService::OpusCodecTask() {
if (!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE) {
auto packet = std::move(audio_decode_queue_.front());
audio_decode_queue_.pop_front();
decode_in_flight_ = true;
const uint32_t generation = playback_generation_;
audio_queue_cv_.notify_all();
lock.unlock();
@@ -371,6 +386,7 @@ void AudioService::OpusCodecTask() {
task->timestamp = packet->timestamp;
SetDecodeSampleRate(packet->sample_rate, packet->frame_duration);
bool decoded = false;
if (opus_decoder_ != nullptr) {
task->pcm.resize(decoder_frame_size_);
esp_audio_dec_in_raw_t raw = {
@@ -400,25 +416,27 @@ void AudioService::OpusCodecTask() {
resampled.resize(actual_output);
task->pcm = std::move(resampled);
}
lock.lock();
audio_playback_queue_.push_back(std::move(task));
audio_queue_cv_.notify_all();
debug_statistics_.decode_count++;
decoded = true;
} else {
ESP_LOGE(TAG, "Failed to decode audio after resize, error code: %d", ret);
lock.lock();
}
} else {
ESP_LOGE(TAG, "Audio decoder is not configured");
lock.lock();
}
lock.lock();
if (decoded && generation == playback_generation_ && !service_stopped_.load()) {
audio_playback_queue_.push_back(std::move(task));
}
decode_in_flight_ = false;
debug_statistics_.decode_count++;
/* If decoding failed and nothing is left to play, report the drain
* here since AudioOutputTask will not see this packet. */
if (audio_decode_queue_.empty() && audio_playback_queue_.empty() &&
callbacks_.on_playback_drained) {
const bool notify_drained = MarkPlaybackDrainedLocked();
audio_queue_cv_.notify_all();
lock.unlock();
if (notify_drained && callbacks_.on_playback_drained) {
callbacks_.on_playback_drained();
}
lock.lock();
}
/* Encode the audio to send queue */
if (!audio_encode_queue_.empty()) {
@@ -563,11 +581,18 @@ bool AudioService::PushPacketToDecodeQueue(std::unique_ptr<AudioStreamPacket> pa
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
if (audio_decode_queue_.size() >= MAX_DECODE_PACKETS_IN_QUEUE) {
if (wait) {
audio_queue_cv_.wait(lock, [this]() { return audio_decode_queue_.size() < MAX_DECODE_PACKETS_IN_QUEUE; });
audio_queue_cv_.wait(lock, [this]() {
return service_stopped_.load() ||
audio_decode_queue_.size() < MAX_DECODE_PACKETS_IN_QUEUE;
});
} else {
return false;
}
}
if (service_stopped_.load()) {
return false;
}
playback_drained_notified_ = false;
audio_decode_queue_.push_back(std::move(packet));
audio_queue_cv_.notify_all();
return true;
@@ -660,6 +685,9 @@ void AudioService::EnableAudioTesting(bool enable) {
/* Copy audio_testing_queue_ to audio_decode_queue_ */
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
audio_decode_queue_ = std::move(audio_testing_queue_);
if (!audio_decode_queue_.empty()) {
playback_drained_notified_ = false;
}
audio_queue_cv_.notify_all();
}
}
@@ -704,17 +732,19 @@ void AudioService::PlaySound(const std::string_view& ogg) {
bool AudioService::IsIdle() {
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
return audio_encode_queue_.empty() && audio_decode_queue_.empty() && audio_playback_queue_.empty() && audio_testing_queue_.empty();
return audio_encode_queue_.empty() && IsPlaybackDrainedLocked() && audio_testing_queue_.empty();
}
bool AudioService::IsPlaybackIdle() {
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
return audio_decode_queue_.empty() && audio_playback_queue_.empty();
return IsPlaybackDrainedLocked();
}
void AudioService::ResetDecoder() {
bool notify_drained = false;
{
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
++playback_generation_;
std::unique_lock<std::mutex> decoder_lock(decoder_mutex_);
if (opus_decoder_ != nullptr) {
esp_opus_dec_reset(opus_decoder_);
@@ -724,13 +754,27 @@ void AudioService::ResetDecoder() {
audio_decode_queue_.clear();
audio_playback_queue_.clear();
audio_testing_queue_.clear();
notify_drained = MarkPlaybackDrainedLocked();
audio_queue_cv_.notify_all();
}
if (callbacks_.on_playback_drained) {
if (notify_drained && callbacks_.on_playback_drained) {
callbacks_.on_playback_drained();
}
}
bool AudioService::IsPlaybackDrainedLocked() const {
return audio_decode_queue_.empty() && audio_playback_queue_.empty() &&
!decode_in_flight_ && !output_in_flight_;
}
bool AudioService::MarkPlaybackDrainedLocked() {
if (!IsPlaybackDrainedLocked() || playback_drained_notified_) {
return false;
}
playback_drained_notified_ = true;
return true;
}
void AudioService::CheckAndUpdateAudioPowerState() {
auto now = std::chrono::steady_clock::now();
auto input_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_input_time_).count();
+11 -5
View File
@@ -2,6 +2,7 @@
#define AUDIO_SERVICE_H
#include <memory>
#include <atomic>
#include <deque>
#include <condition_variable>
#include <chrono>
@@ -49,7 +50,6 @@
#define AS_EVENT_AUDIO_TESTING_RUNNING (1 << 0)
#define AS_EVENT_WAKE_WORD_RUNNING (1 << 1)
#define AS_EVENT_AUDIO_PROCESSOR_RUNNING (1 << 2)
#define AS_EVENT_PLAYBACK_NOT_EMPTY (1 << 3)
#define AS_EVENT_AUDIO_INPUT_STOP_REQUEST (1 << 4)
#define AS_OPUS_GET_FRAME_DRU_ENUM(duration_ms) \
@@ -80,7 +80,7 @@ struct AudioServiceCallbacks {
std::function<void(const std::string&)> on_wake_word_detected;
std::function<void(bool)> on_vad_change;
std::function<void(void)> on_audio_testing_queue_full;
// Fired when both the decode and playback queues become empty
// Fired when the decode/playback queues and their in-flight work are drained.
std::function<void(void)> on_playback_drained;
};
@@ -94,7 +94,7 @@ enum AudioTaskType {
struct AudioTask {
AudioTaskType type;
std::vector<int16_t> pcm;
uint32_t timestamp;
uint32_t timestamp = 0;
};
struct DebugStatistics {
@@ -174,6 +174,10 @@ private:
std::deque<std::unique_ptr<AudioStreamPacket>> audio_testing_queue_;
std::deque<std::unique_ptr<AudioTask>> audio_encode_queue_;
std::deque<std::unique_ptr<AudioTask>> audio_playback_queue_;
bool decode_in_flight_ = false;
bool output_in_flight_ = false;
bool playback_drained_notified_ = true;
uint32_t playback_generation_ = 0;
// For server AEC
std::deque<uint32_t> timestamp_queue_;
@@ -184,8 +188,8 @@ private:
#else
bool device_aec_enabled_ = false;
#endif
bool service_stopped_ = true;
bool audio_input_need_warmup_ = false;
std::atomic<bool> service_stopped_{true};
std::atomic<bool> audio_input_need_warmup_{false};
esp_timer_handle_t audio_power_timer_ = nullptr;
std::chrono::steady_clock::time_point last_input_time_;
@@ -198,6 +202,8 @@ private:
bool InitializeAudioEngine();
void SetDecodeSampleRate(int sample_rate, int frame_duration);
void CheckAndUpdateAudioPowerState();
bool IsPlaybackDrainedLocked() const;
bool MarkPlaybackDrainedLocked();
};
#endif
+22 -10
View File
@@ -244,7 +244,7 @@ void AfeAudioEngine::EnableVoiceProcessing(bool enable) {
}
void AfeAudioEngine::EnableDeviceAec(bool enable) {
device_aec_enabled_ = enable;
device_aec_enabled_.store(enable);
if (enable && (codec_ == nullptr || !codec_->input_reference())) {
ESP_LOGW(TAG, "Device AEC requires a playback reference channel");
}
@@ -288,6 +288,7 @@ void AfeAudioEngine::UpdateActiveState() {
xEventGroupSetBits(event_group_, kAfeActive);
} else {
xEventGroupClearBits(event_group_, kAfeActive);
control_generation_.fetch_add(1);
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
input_buffer_.clear();
if (afe_data_ != nullptr) {
@@ -324,7 +325,7 @@ void AfeAudioEngine::ApplyAfeControls() {
}
if (codec_->input_reference()) {
const bool enable_aec = (bits & kWakeWordEnabled) ||
(device_aec_enabled_ && (bits & kVoiceProcessingEnabled));
(device_aec_enabled_.load() && (bits & kVoiceProcessingEnabled));
if (enable_aec) {
afe_iface_->enable_aec(afe_data_);
} else {
@@ -333,16 +334,22 @@ void AfeAudioEngine::ApplyAfeControls() {
}
}
void AfeAudioEngine::ApplyPendingReset() {
if (!reset_pending_.exchange(false)) {
return;
}
// Discard audio recorded before (re)activation. Holding input_buffer_mutex_
// serializes the reset against Feed(); fetch/reset both run in this task.
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
input_buffer_.clear();
afe_iface_->reset_buffer(afe_data_);
}
void AfeAudioEngine::ProcessingTask() {
while (true) {
xEventGroupWaitBits(event_group_, kAfeActive, pdFALSE, pdTRUE, portMAX_DELAY);
if (reset_pending_.exchange(false)) {
// Discard audio recorded before (re)activation. Holding
// input_buffer_mutex_ serializes the reset against Feed(); the
// fetch side cannot race as it only runs in this task.
std::lock_guard<std::mutex> lock(input_buffer_mutex_);
input_buffer_.clear();
afe_iface_->reset_buffer(afe_data_);
ApplyPendingReset();
if ((xEventGroupGetBits(event_group_) & kAfeActive) == 0) {
continue;
}
if (afe_control_dirty_.exchange(false)) {
@@ -350,8 +357,13 @@ void AfeAudioEngine::ProcessingTask() {
// so they are applied here, in the task that owns the fetch side.
ApplyAfeControls();
}
const uint32_t generation = control_generation_.load();
auto* result = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
if ((xEventGroupGetBits(event_group_) & kAfeActive) == 0) {
if (generation != control_generation_.load() ||
(xEventGroupGetBits(event_group_) & kAfeActive) == 0) {
// A disable/re-enable may make an old blocked fetch return after the
// AFE is active again. Reset immediately and never process that frame.
ApplyPendingReset();
continue;
}
if (result == nullptr || result->ret_value == ESP_FAIL) {
+5 -1
View File
@@ -66,13 +66,16 @@ private:
TaskHandle_t processing_task_ = nullptr;
int frame_samples_ = 0;
bool is_speaking_ = false;
bool device_aec_enabled_ = false;
std::atomic<bool> device_aec_enabled_{false};
// Deferred AFE buffer reset, performed by ProcessingTask (see UpdateActiveState)
std::atomic<bool> reset_pending_{false};
// Deferred WakeNet/AEC toggles, applied by ProcessingTask (see ApplyAfeControls)
std::atomic<bool> afe_control_dirty_{false};
// Deferred output_buffer_ clear, performed by the output-producing task
std::atomic<bool> output_reset_pending_{false};
// Incremented whenever an active AFE session is invalidated. ProcessingTask
// uses it to reject a fetch result produced before a disable/re-enable cycle.
std::atomic<uint32_t> control_generation_{0};
WakeDetector wake_detector_ = WakeDetector::kNone;
std::unique_ptr<CustomWakeWord> custom_wake_word_;
@@ -98,6 +101,7 @@ private:
void UpdateActiveState();
void UpdateAecState();
void ApplyAfeControls();
void ApplyPendingReset();
void OutputRawAudio(const std::vector<int16_t>& data);
void HandleWakeWordResult(const afe_fetch_result_t* result);
void HandleVoiceResult(const afe_fetch_result_t* result);
@@ -9,7 +9,6 @@
#include <driver/gpio.h>
#include <esp_log.h>
#include <esp_idf_version.h>
#include <driver/i2c_master.h>
#include <driver/spi_master.h>
#include <esp_lcd_panel_vendor.h>
@@ -293,11 +292,7 @@ private:
esp_lcd_panel_handle_t panel_handle = nullptr;
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = LCD_RST_GPIO; // Set to -1 if not use
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#else
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#endif
panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18)
if (config.type == LcdPanelType::kGc9107) {
@@ -9,7 +9,6 @@
#include <driver/gpio.h>
#include <esp_log.h>
#include <esp_idf_version.h>
#include <driver/i2c_master.h>
#include <driver/spi_master.h>
#include <esp_lcd_panel_vendor.h>
@@ -371,11 +370,7 @@ private:
esp_lcd_panel_handle_t panel_handle = nullptr;
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = LCD_RST_GPIO; // Set to -1 if not use
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#else
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#endif
panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18)
if (config.type == LcdPanelType::kGc9107) {
@@ -813,11 +813,7 @@ private:
esp_lcd_panel_handle_t panel_handle = nullptr;
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = LCD_RST_GPIO;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#else
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#endif
panel_config.bits_per_pixel = 16;
if (config.type == LcdPanelType::kGc9107) {
+227 -82
View File
@@ -57,8 +57,6 @@ void esp_blufi_btc_deinit(void);
#include <wifi_station.h>
#include "esp_crc.h"
#include "esp_random.h"
#include "mbedtls/md5.h"
#include "ssid_manager.h"
static const char* BLUFI_TAG = "BLUFI_CLASS";
@@ -345,51 +343,70 @@ esp_err_t Blufi::_controller_deinit() {
}
#endif
static int myrand(void* rng_state, unsigned char* output, size_t len) {
esp_fill_random(output, len);
return 0;
}
namespace {
constexpr uint8_t kDhParamLength = 0x00;
constexpr uint8_t kDhParamData = 0x01;
constexpr size_t kBlufiIvSize = 16;
constexpr size_t kBlufiHashSize = 32;
constexpr char kBlufiEncryptDomain[] = "blufi_enc";
constexpr char kBlufiDecryptDomain[] = "blufi_dec";
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
constexpr auto kBlufiHashError = ESP_BLUFI_CALC_SHA_256_ERROR;
#else
// IDF 5 does not expose a SHA-256-specific BluFi error code.
constexpr auto kBlufiHashError = ESP_BLUFI_CALC_MD5_ERROR;
#endif
} // namespace
void Blufi::_security_init() {
_security_deinit();
psa_status_t status = psa_crypto_init();
if (status != PSA_SUCCESS) {
ESP_LOGE(BLUFI_TAG, "psa_crypto_init failed: %d", status);
btc_blufi_report_error(ESP_BLUFI_INIT_SECURITY_ERROR);
return;
}
m_sec = new BlufiSecurity();
if (m_sec == nullptr) {
ESP_LOGE(BLUFI_TAG, "Failed to allocate security context");
btc_blufi_report_error(ESP_BLUFI_INIT_SECURITY_ERROR);
return;
}
memset(m_sec, 0, sizeof(BlufiSecurity));
m_sec->dhm = new mbedtls_dhm_context();
m_sec->aes = new mbedtls_aes_context();
mbedtls_dhm_init(m_sec->dhm);
mbedtls_aes_init(m_sec->aes);
memset(m_sec->iv, 0x0, sizeof(m_sec->iv));
m_sec->enc_operation = psa_cipher_operation_init();
m_sec->dec_operation = psa_cipher_operation_init();
}
void Blufi::_security_deinit() {
if (m_sec == nullptr)
if (m_sec == nullptr) {
return;
if (m_sec->dh_param) {
free(m_sec->dh_param);
}
mbedtls_dhm_free(m_sec->dhm);
mbedtls_aes_free(m_sec->aes);
delete m_sec->dhm;
delete m_sec->aes;
psa_cipher_abort(&m_sec->enc_operation);
psa_cipher_abort(&m_sec->dec_operation);
if (m_sec->aes_key != PSA_KEY_ID_NULL) {
psa_destroy_key(m_sec->aes_key);
}
free(m_sec->dh_param);
memset(m_sec, 0, sizeof(BlufiSecurity));
delete m_sec;
m_sec = nullptr;
}
void Blufi::_dh_negotiate_data_handler(uint8_t* data, int len, uint8_t** output_data,
int* output_len, bool* need_free) {
if (m_sec == nullptr) {
if (m_sec == nullptr || data == nullptr || output_data == nullptr || output_len == nullptr ||
need_free == nullptr) {
ESP_LOGE(BLUFI_TAG, "Security not initialized in DH handler");
btc_blufi_report_error(ESP_BLUFI_INIT_SECURITY_ERROR);
return;
}
if (len < 1) {
if (len < 3) {
ESP_LOGE(BLUFI_TAG, "DH handler: data too short");
btc_blufi_report_error(ESP_BLUFI_DATA_FORMAT_ERROR);
return;
@@ -397,70 +414,191 @@ void Blufi::_dh_negotiate_data_handler(uint8_t* data, int len, uint8_t** output_
uint8_t type = data[0];
switch (type) {
case 0x00:
if (len < 3) {
ESP_LOGE(BLUFI_TAG, "DH_PARAM_LEN packet too short");
btc_blufi_report_error(ESP_BLUFI_DATA_FORMAT_ERROR);
case kDhParamLength:
m_sec->dh_param_len = (data[1] << 8) | data[2];
if (m_sec->dh_param_len <= 0 || m_sec->dh_param_len > DH_PARAM_LEN_MAX) {
ESP_LOGE(BLUFI_TAG, "Invalid DH parameter length: %d", m_sec->dh_param_len);
m_sec->dh_param_len = 0;
btc_blufi_report_error(ESP_BLUFI_DH_PARAM_ERROR);
return;
}
m_sec->dh_param_len = (data[1] << 8) | data[2];
if (m_sec->dh_param) {
free(m_sec->dh_param);
m_sec->dh_param = nullptr;
free(m_sec->dh_param);
m_sec->dh_param = nullptr;
psa_cipher_abort(&m_sec->enc_operation);
psa_cipher_abort(&m_sec->dec_operation);
m_sec->enc_operation = psa_cipher_operation_init();
m_sec->dec_operation = psa_cipher_operation_init();
if (m_sec->aes_key != PSA_KEY_ID_NULL) {
psa_destroy_key(m_sec->aes_key);
m_sec->aes_key = PSA_KEY_ID_NULL;
}
m_sec->dh_param = (uint8_t*)malloc(m_sec->dh_param_len);
if (m_sec->dh_param == nullptr) {
ESP_LOGE(BLUFI_TAG, "DH malloc failed");
m_sec->dh_param_len = 0;
btc_blufi_report_error(ESP_BLUFI_DH_MALLOC_ERROR);
return;
}
break;
case 0x01: {
case kDhParamData: {
if (m_sec->dh_param == nullptr) {
ESP_LOGE(BLUFI_TAG, "DH param not allocated");
btc_blufi_report_error(ESP_BLUFI_DH_PARAM_ERROR);
return;
}
uint8_t* param = m_sec->dh_param;
if (len != m_sec->dh_param_len + 1) {
ESP_LOGE(BLUFI_TAG, "Invalid DH parameter packet length: %d, expected %d", len,
m_sec->dh_param_len + 1);
btc_blufi_report_error(ESP_BLUFI_DH_PARAM_ERROR);
return;
}
memcpy(m_sec->dh_param, &data[1], m_sec->dh_param_len);
int ret = mbedtls_dhm_read_params(m_sec->dhm, &param, &param[m_sec->dh_param_len]);
if (ret) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_read_params failed %d", ret);
const uint8_t* param = m_sec->dh_param;
const uint8_t* const end = param + m_sec->dh_param_len;
auto read_field = [&param, end](const uint8_t** value, size_t* value_len) -> bool {
if (end - param < 2) {
return false;
}
*value_len = (static_cast<size_t>(param[0]) << 8) | param[1];
param += 2;
if (*value_len == 0 || static_cast<size_t>(end - param) < *value_len) {
return false;
}
*value = param;
param += *value_len;
return true;
};
const uint8_t* prime = nullptr;
const uint8_t* generator = nullptr;
const uint8_t* peer_public_key = nullptr;
size_t prime_len = 0;
size_t generator_len = 0;
size_t peer_public_key_len = 0;
if (!read_field(&prime, &prime_len) || !read_field(&generator, &generator_len) ||
!read_field(&peer_public_key, &peer_public_key_len) || param != end) {
ESP_LOGE(BLUFI_TAG, "Malformed DH parameter data");
btc_blufi_report_error(ESP_BLUFI_READ_PARAM_ERROR);
return;
}
const int dhm_len = mbedtls_dhm_get_len(m_sec->dhm);
// PSA FFDH supports named RFC 7919 groups instead of caller-supplied P/G. The IDF 6
// BluFi protocol uses ffdhe3072; P and G remain in the packet for framing compatibility.
constexpr size_t kDhKeyBits = 3072;
constexpr size_t kDhKeyBytes = kDhKeyBits / 8;
if (prime_len != kDhKeyBytes || peer_public_key_len != kDhKeyBytes ||
generator_len != 1 || generator[0] != 2) {
ESP_LOGE(BLUFI_TAG,
"Unsupported DH group (P=%u, G=%u, public=%u); ffdhe3072 is required",
static_cast<unsigned>(prime_len), static_cast<unsigned>(generator_len),
static_cast<unsigned>(peer_public_key_len));
btc_blufi_report_error(ESP_BLUFI_DH_PARAM_ERROR);
return;
}
ret = mbedtls_dhm_make_public(m_sec->dhm, dhm_len, m_sec->self_public_key, dhm_len,
myrand, NULL);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_make_public failed: %d", ret);
psa_key_attributes_t attributes = psa_key_attributes_init();
psa_set_key_type(&attributes, PSA_KEY_TYPE_DH_KEY_PAIR(PSA_DH_FAMILY_RFC7919));
psa_set_key_bits(&attributes, kDhKeyBits);
psa_set_key_algorithm(&attributes, PSA_ALG_FFDH);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DERIVE);
psa_key_id_t private_key = PSA_KEY_ID_NULL;
psa_status_t status = psa_generate_key(&attributes, &private_key);
psa_reset_key_attributes(&attributes);
if (status != PSA_SUCCESS) {
ESP_LOGE(BLUFI_TAG, "psa_generate_key failed: %d", status);
btc_blufi_report_error(ESP_BLUFI_MAKE_PUBLIC_ERROR);
return;
}
ret = mbedtls_dhm_calc_secret(m_sec->dhm, m_sec->share_key, SHARE_KEY_LEN,
&m_sec->share_len, myrand, NULL);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_calc_secret failed: %d", ret);
size_t public_key_len = 0;
status = psa_export_public_key(private_key, m_sec->self_public_key,
sizeof(m_sec->self_public_key), &public_key_len);
if (status != PSA_SUCCESS || public_key_len != kDhKeyBytes) {
ESP_LOGE(BLUFI_TAG, "psa_export_public_key failed: %d, length: %u", status,
static_cast<unsigned>(public_key_len));
psa_destroy_key(private_key);
btc_blufi_report_error(ESP_BLUFI_MAKE_PUBLIC_ERROR);
return;
}
status = psa_raw_key_agreement(PSA_ALG_FFDH, private_key, peer_public_key,
peer_public_key_len, m_sec->share_key,
sizeof(m_sec->share_key), &m_sec->share_len);
psa_destroy_key(private_key);
if (status != PSA_SUCCESS) {
ESP_LOGE(BLUFI_TAG, "psa_raw_key_agreement failed: %d", status);
btc_blufi_report_error(ESP_BLUFI_ENCRYPT_ERROR);
return;
}
ret = mbedtls_md5(m_sec->share_key, m_sec->share_len, m_sec->psk);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "mbedtls_md5 failed: %d", ret);
btc_blufi_report_error(ESP_BLUFI_CALC_MD5_ERROR);
size_t hash_len = 0;
status = psa_hash_compute(PSA_ALG_SHA_256, m_sec->share_key, m_sec->share_len,
m_sec->psk, sizeof(m_sec->psk), &hash_len);
if (status != PSA_SUCCESS || hash_len != sizeof(m_sec->psk)) {
ESP_LOGE(BLUFI_TAG, "psa_hash_compute failed: %d", status);
btc_blufi_report_error(kBlufiHashError);
return;
}
ret = mbedtls_aes_setkey_enc(m_sec->aes, m_sec->psk, PSK_LEN * 8);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "mbedtls_aes_setkey_enc failed: -0x%04X", -ret);
attributes = psa_key_attributes_init();
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attributes, PSK_LEN * 8);
psa_set_key_algorithm(&attributes, PSA_ALG_CTR);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT);
status = psa_import_key(&attributes, m_sec->psk, sizeof(m_sec->psk), &m_sec->aes_key);
psa_reset_key_attributes(&attributes);
if (status != PSA_SUCCESS) {
ESP_LOGE(BLUFI_TAG, "psa_import_key failed: %d", status);
btc_blufi_report_error(ESP_BLUFI_ENCRYPT_ERROR);
return;
}
auto setup_cipher = [this](psa_cipher_operation_t* operation, const char* domain) {
uint8_t material[sizeof(kBlufiEncryptDomain) - 1 + SHARE_KEY_LEN];
memcpy(material, domain, sizeof(kBlufiEncryptDomain) - 1);
memcpy(material + sizeof(kBlufiEncryptDomain) - 1, m_sec->share_key,
m_sec->share_len);
uint8_t hash[kBlufiHashSize];
size_t hash_len = 0;
psa_status_t status = psa_hash_compute(
PSA_ALG_SHA_256, material,
sizeof(kBlufiEncryptDomain) - 1 + m_sec->share_len, hash, sizeof(hash),
&hash_len);
memset(material, 0, sizeof(material));
if (status != PSA_SUCCESS || hash_len != sizeof(hash)) {
return status == PSA_SUCCESS ? PSA_ERROR_GENERIC_ERROR : status;
}
*operation = psa_cipher_operation_init();
status = psa_cipher_encrypt_setup(operation, m_sec->aes_key, PSA_ALG_CTR);
if (status == PSA_SUCCESS) {
status = psa_cipher_set_iv(operation, hash, kBlufiIvSize);
}
memset(hash, 0, sizeof(hash));
return status;
};
status = setup_cipher(&m_sec->enc_operation, kBlufiEncryptDomain);
if (status == PSA_SUCCESS) {
// CTR decryption uses the same cipher primitive with an independent counter.
status = setup_cipher(&m_sec->dec_operation, kBlufiDecryptDomain);
}
if (status != PSA_SUCCESS) {
ESP_LOGE(BLUFI_TAG, "PSA cipher setup failed: %d", status);
psa_cipher_abort(&m_sec->enc_operation);
psa_cipher_abort(&m_sec->dec_operation);
psa_destroy_key(m_sec->aes_key);
m_sec->aes_key = PSA_KEY_ID_NULL;
btc_blufi_report_error(ESP_BLUFI_ENCRYPT_ERROR);
return;
}
*output_data = m_sec->self_public_key;
*output_len = dhm_len;
*output_len = public_key_len;
*need_free = false;
ESP_LOGI(BLUFI_TAG, "DH negotiation completed successfully");
@@ -471,49 +609,56 @@ void Blufi::_dh_negotiate_data_handler(uint8_t* data, int len, uint8_t** output_
}
default:
ESP_LOGE(BLUFI_TAG, "DH handler unknown type: %d", type);
btc_blufi_report_error(ESP_BLUFI_DATA_FORMAT_ERROR);
}
}
int Blufi::_aes_encrypt(uint8_t iv8, uint8_t* crypt_data, int crypt_len) {
if (!m_sec || !m_sec->aes || !crypt_data || crypt_len <= 0) {
(void)iv8;
if (!m_sec || m_sec->aes_key == PSA_KEY_ID_NULL || !crypt_data || crypt_len < 0) {
ESP_LOGE(BLUFI_TAG, "Invalid parameters for AES encryption");
return -ESP_ERR_INVALID_ARG;
}
size_t iv_offset = 0;
uint8_t iv0[16];
memcpy(iv0, m_sec->iv, 16);
iv0[0] = iv8;
int ret = mbedtls_aes_crypt_cfb128(m_sec->aes, MBEDTLS_AES_ENCRYPT, crypt_len, &iv_offset, iv0,
crypt_data, crypt_data);
if (ret == 0) {
return crypt_len;
} else {
ESP_LOGE(BLUFI_TAG, "AES encrypt failed: %d", ret);
return ret;
if (crypt_len == 0) {
return 0;
}
std::vector<uint8_t> output(PSA_CIPHER_ENCRYPT_OUTPUT_SIZE(
PSA_KEY_TYPE_AES, PSA_ALG_CTR, static_cast<size_t>(crypt_len)));
size_t output_len = 0;
psa_status_t status = psa_cipher_update(&m_sec->enc_operation, crypt_data, crypt_len,
output.data(), output.size(), &output_len);
if (status != PSA_SUCCESS || output_len != static_cast<size_t>(crypt_len)) {
ESP_LOGE(BLUFI_TAG, "AES encrypt failed: %d, length: %u", status,
static_cast<unsigned>(output_len));
return status == PSA_SUCCESS ? -ESP_FAIL : status;
}
memcpy(crypt_data, output.data(), output_len);
return static_cast<int>(output_len);
}
int Blufi::_aes_decrypt(uint8_t iv8, uint8_t* crypt_data, int crypt_len) {
if (!m_sec || !m_sec->aes || !crypt_data || crypt_len < 0) {
ESP_LOGE(BLUFI_TAG, "Invalid parameters for AES decryption %p %p %d", m_sec->aes,
crypt_data, crypt_len);
(void)iv8;
if (!m_sec || m_sec->aes_key == PSA_KEY_ID_NULL || !crypt_data || crypt_len < 0) {
ESP_LOGE(BLUFI_TAG, "Invalid parameters for AES decryption");
return -ESP_ERR_INVALID_ARG;
}
size_t iv_offset = 0;
uint8_t iv0[16];
memcpy(iv0, m_sec->iv, 16);
iv0[0] = iv8;
int ret = mbedtls_aes_crypt_cfb128(m_sec->aes, MBEDTLS_AES_DECRYPT, crypt_len, &iv_offset, iv0,
crypt_data, crypt_data);
if (ret != 0) {
ESP_LOGE(BLUFI_TAG, "AES decrypt failed: %d", ret);
return ret;
} else {
return crypt_len;
if (crypt_len == 0) {
return 0;
}
std::vector<uint8_t> output(PSA_CIPHER_DECRYPT_OUTPUT_SIZE(
PSA_KEY_TYPE_AES, PSA_ALG_CTR, static_cast<size_t>(crypt_len)));
size_t output_len = 0;
psa_status_t status = psa_cipher_update(&m_sec->dec_operation, crypt_data, crypt_len,
output.data(), output.size(), &output_len);
if (status != PSA_SUCCESS || output_len != static_cast<size_t>(crypt_len)) {
ESP_LOGE(BLUFI_TAG, "AES decrypt failed: %d, length: %u", status,
static_cast<unsigned>(output_len));
return status == PSA_SUCCESS ? -ESP_FAIL : status;
}
memcpy(crypt_data, output.data(), output_len);
return static_cast<int>(output_len);
}
uint16_t Blufi::_crc_checksum(uint8_t iv8, uint8_t* data, int len) {
+9 -9
View File
@@ -1,16 +1,15 @@
#pragma once
#include <aes/esp_aes.h>
#include <cassert>
#include <cstring>
#include <vector>
#include "esp_blufi_api.h"
#include "esp_err.h"
#include "esp_idf_version.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/aes.h"
#include "mbedtls/dhm.h"
#include "psa/crypto.h"
#include "wifi_manager.h"
class Blufi {
@@ -112,18 +111,19 @@ private:
// Security context, formerly blufi_sec struct
struct BlufiSecurity {
#define DH_SELF_PUB_KEY_LEN 128
#define DH_PARAM_LEN_MAX 1024
#define DH_SELF_PUB_KEY_LEN 384
uint8_t self_public_key[DH_SELF_PUB_KEY_LEN];
#define SHARE_KEY_LEN 128
#define SHARE_KEY_LEN 384
uint8_t share_key[SHARE_KEY_LEN];
size_t share_len;
#define PSK_LEN 16
#define PSK_LEN 32
uint8_t psk[PSK_LEN];
uint8_t *dh_param;
int dh_param_len;
uint8_t iv[16];
mbedtls_dhm_context *dhm;
esp_aes_context *aes;
psa_key_id_t aes_key;
psa_cipher_operation_t enc_operation;
psa_cipher_operation_t dec_operation;
};
BlufiSecurity *m_sec;
-5
View File
@@ -5,7 +5,6 @@
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_log.h>
#include <esp_idf_version.h>
#include "application.h"
#include "codecs/no_audio_codec.h"
@@ -60,11 +59,7 @@ private:
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = DISPLAY_SPI_RESET_PIN; // Set to -1 if not use
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#else
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#endif
panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18)
ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle));
@@ -7,7 +7,6 @@
#include "led/single_led.h"
#include <esp_log.h>
#include <esp_idf_version.h>
#include <esp_efuse_table.h>
#include <driver/i2c_master.h>
@@ -65,11 +64,7 @@ private:
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = DISPLAY_SPI_RESET_PIN; // Set to -1 if not use
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#else
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#endif
panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18)
ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle));
@@ -7,7 +7,6 @@
#include "led/single_led.h"
#include <esp_log.h>
#include <esp_idf_version.h>
#include <esp_efuse_table.h>
#include <driver/i2c_master.h>
@@ -92,11 +91,7 @@ private:
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = DISPLAY_SPI_RESET_PIN; // Set to -1 if not use
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#else
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#endif
panel_config.bits_per_pixel = 16; // Implemented by LCD command `3Ah` (16/18)
ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle));
@@ -7,7 +7,6 @@
#include "led/single_led.h"
#include "assets/lang_config.h"
#include <esp_log.h>
#include <esp_idf_version.h>
#include <esp_efuse_table.h>
#include <driver/i2c_master.h>
@@ -336,11 +335,7 @@ private:
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = DISPLAY_SPI_RESET_PIN; // Set to -1 if not use
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#else
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
#endif
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle));
+3 -1
View File
@@ -25,7 +25,9 @@ dependencies:
espressif/esp_audio_codec: ~2.5.0
78/esp-ml307: ~3.6.6
78/uart-eth-modem:
version: ^0.6.1
# 0.6.1 requires ESP-IDF >= 6.0.1. Keep 0.6.0 so the shared
# NT26/4G implementation remains buildable with ESP-IDF 5.5.2.
version: ==0.6.0
rules:
- if: target not in [esp32]
78/xiaozhi-fonts: ~1.6.0
+133 -61
View File
@@ -44,12 +44,18 @@ MqttProtocol::~MqttProtocol() {
esp_timer_delete(reconnect_timer_);
}
udp_.reset();
{
std::lock_guard<std::mutex> lock(channel_mutex_);
udp_.reset();
}
mqtt_.reset();
if (aes_key_id_ != PSA_KEY_ID_NULL) {
psa_destroy_key(aes_key_id_);
aes_key_id_ = PSA_KEY_ID_NULL;
{
std::lock_guard<std::mutex> lock(crypto_mutex_);
if (aes_key_id_ != PSA_KEY_ID_NULL) {
psa_destroy_key(aes_key_id_);
aes_key_id_ = PSA_KEY_ID_NULL;
}
}
if (event_group_handle_ != nullptr) {
@@ -174,10 +180,19 @@ bool MqttProtocol::SendAudio(std::unique_ptr<AudioStreamPacket> packet) {
return false;
}
constexpr size_t kAudioHeaderSize = 16;
if (aes_nonce_.size() != kAudioHeaderSize || packet->payload.size() > UINT16_MAX) {
ESP_LOGE(TAG, "Invalid AES nonce or audio payload length: %zu", packet->payload.size());
return false;
}
std::string nonce(aes_nonce_);
*(uint16_t*)&nonce[2] = htons(packet->payload.size());
*(uint32_t*)&nonce[8] = htonl(packet->timestamp);
*(uint32_t*)&nonce[12] = htonl(++local_sequence_);
const uint16_t payload_len = htons(static_cast<uint16_t>(packet->payload.size()));
const uint32_t timestamp = htonl(packet->timestamp);
const uint32_t sequence = htonl(++local_sequence_);
memcpy(nonce.data() + 2, &payload_len, sizeof(payload_len));
memcpy(nonce.data() + 8, &timestamp, sizeof(timestamp));
memcpy(nonce.data() + 12, &sequence, sizeof(sequence));
std::string encrypted;
encrypted.resize(aes_nonce_.size() + packet->payload.size());
@@ -241,36 +256,55 @@ bool MqttProtocol::OpenAudioChannel() {
return false;
}
std::lock_guard<std::mutex> lock(channel_mutex_);
auto network = Board::GetInstance().GetNetwork();
udp_ = network->CreateUdp(2);
udp_->OnMessage([this](const std::string& data) {
auto udp = network->CreateUdp(2);
udp->OnMessage([this](const std::string& data) {
/*
* UDP Encrypted OPUS Packet Format:
* |type 1u|flags 1u|payload_len 2u|ssrc 4u|timestamp 4u|sequence 4u|
* |payload payload_len|
*/
if (data.size() < sizeof(aes_nonce_)) {
ESP_LOGE(TAG, "Invalid audio packet size: %u", data.size());
constexpr size_t kAudioHeaderSize = 16;
if (data.size() < kAudioHeaderSize) {
ESP_LOGE(TAG, "Invalid audio packet size: %zu", data.size());
return;
}
if (data[0] != 0x01) {
ESP_LOGE(TAG, "Invalid audio packet type: %x", data[0]);
if (static_cast<uint8_t>(data[0]) != 0x01) {
ESP_LOGE(TAG, "Invalid audio packet type: %x", static_cast<uint8_t>(data[0]));
return;
}
uint32_t timestamp = ntohl(*(uint32_t*)&data[8]);
uint32_t sequence = ntohl(*(uint32_t*)&data[12]);
if (sequence < remote_sequence_) {
ESP_LOGW(TAG, "Received audio packet with old sequence: %lu, expected: %lu", sequence, remote_sequence_);
uint16_t payload_len = 0;
uint32_t timestamp = 0;
uint32_t sequence = 0;
memcpy(&payload_len, data.data() + 2, sizeof(payload_len));
memcpy(&timestamp, data.data() + 8, sizeof(timestamp));
memcpy(&sequence, data.data() + 12, sizeof(sequence));
payload_len = ntohs(payload_len);
timestamp = ntohl(timestamp);
sequence = ntohl(sequence);
if (data.size() != kAudioHeaderSize + payload_len) {
ESP_LOGE(TAG, "Audio payload length mismatch: header=%u, datagram=%zu",
static_cast<unsigned>(payload_len),
data.size() - kAudioHeaderSize);
return;
}
if (sequence != remote_sequence_ + 1) {
ESP_LOGW(TAG, "Received audio packet with wrong sequence: %lu, expected: %lu", sequence, remote_sequence_ + 1);
}
size_t decrypted_size = data.size() - aes_nonce_.size();
auto nonce = (uint8_t*)data.data();
auto encrypted = (uint8_t*)data.data() + aes_nonce_.size();
{
std::lock_guard<std::mutex> lock(channel_mutex_);
if (sequence <= remote_sequence_) {
ESP_LOGW(TAG, "Received duplicate/old audio sequence: %lu, last: %lu", sequence,
remote_sequence_);
return;
}
if (sequence != remote_sequence_ + 1) {
ESP_LOGW(TAG, "Received audio packet with wrong sequence: %lu, expected: %lu",
sequence, remote_sequence_ + 1);
}
}
const size_t decrypted_size = payload_len;
auto nonce = reinterpret_cast<const uint8_t*>(data.data());
auto encrypted = reinterpret_cast<const uint8_t*>(data.data() + kAudioHeaderSize);
auto packet = std::make_unique<AudioStreamPacket>();
packet->sample_rate = server_sample_rate_;
packet->frame_duration = server_frame_duration_;
@@ -280,14 +314,27 @@ bool MqttProtocol::OpenAudioChannel() {
ESP_LOGE(TAG, "Failed to decrypt audio data");
return;
}
{
std::lock_guard<std::mutex> lock(channel_mutex_);
if (sequence <= remote_sequence_) {
return;
}
remote_sequence_ = sequence;
}
last_incoming_time_ = std::chrono::steady_clock::now();
if (on_incoming_audio_ != nullptr) {
on_incoming_audio_(std::move(packet));
}
remote_sequence_ = sequence;
last_incoming_time_ = std::chrono::steady_clock::now();
});
udp_->Connect(udp_server_, udp_port_);
if (!udp->Connect(udp_server_, udp_port_)) {
ESP_LOGE(TAG, "Failed to connect UDP audio channel");
return false;
}
{
std::lock_guard<std::mutex> lock(channel_mutex_);
udp_ = std::move(udp);
}
if (on_audio_channel_opened_ != nullptr) {
on_audio_channel_opened_();
@@ -322,8 +369,8 @@ std::string MqttProtocol::GetHelloMessage() {
void MqttProtocol::ParseServerHello(const cJSON* root) {
auto transport = cJSON_GetObjectItem(root, "transport");
if (transport == nullptr || strcmp(transport->valuestring, "udp") != 0) {
ESP_LOGE(TAG, "Unsupported transport: %s", transport->valuestring);
if (!cJSON_IsString(transport) || strcmp(transport->valuestring, "udp") != 0) {
ESP_LOGE(TAG, "Unsupported or missing transport");
return;
}
@@ -351,50 +398,68 @@ void MqttProtocol::ParseServerHello(const cJSON* root) {
ESP_LOGE(TAG, "UDP is not specified");
return;
}
udp_server_ = cJSON_GetObjectItem(udp, "server")->valuestring;
udp_port_ = cJSON_GetObjectItem(udp, "port")->valueint;
auto key = cJSON_GetObjectItem(udp, "key")->valuestring;
auto nonce = cJSON_GetObjectItem(udp, "nonce")->valuestring;
auto server = cJSON_GetObjectItem(udp, "server");
auto port = cJSON_GetObjectItem(udp, "port");
auto key_item = cJSON_GetObjectItem(udp, "key");
auto nonce_item = cJSON_GetObjectItem(udp, "nonce");
if (!cJSON_IsString(server) || !cJSON_IsNumber(port) || port->valueint <= 0 ||
port->valueint > UINT16_MAX || !cJSON_IsString(key_item) || !cJSON_IsString(nonce_item)) {
ESP_LOGE(TAG, "Invalid UDP server, port, key, or nonce");
return;
}
const std::string udp_server = server->valuestring;
const int udp_port = port->valueint;
// auto encryption = cJSON_GetObjectItem(udp, "encryption")->valuestring;
// ESP_LOGI(TAG, "UDP server: %s, port: %d, encryption: %s", udp_server_.c_str(), udp_port_, encryption);
aes_nonce_ = DecodeHexString(nonce);
const std::string aes_key = DecodeHexString(key);
if (aes_nonce_.size() != 16 || aes_key.size() != 16) {
std::string aes_nonce;
std::string aes_key;
if (!DecodeHexString(nonce_item->valuestring, aes_nonce) ||
!DecodeHexString(key_item->valuestring, aes_key) ||
aes_nonce.size() != 16 || aes_key.size() != 16) {
ESP_LOGE(TAG, "Invalid AES key or nonce length");
return;
}
if (aes_key_id_ != PSA_KEY_ID_NULL) {
psa_destroy_key(aes_key_id_);
aes_key_id_ = PSA_KEY_ID_NULL;
}
psa_status_t status = psa_crypto_init();
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to initialize PSA Crypto, status: %ld", static_cast<long>(status));
return;
}
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attributes, PSA_ALG_CTR);
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attributes, 128);
status = psa_import_key(&attributes, reinterpret_cast<const uint8_t*>(aes_key.data()),
aes_key.size(), &aes_key_id_);
psa_reset_key_attributes(&attributes);
{
std::lock_guard<std::mutex> lock(crypto_mutex_);
if (aes_key_id_ != PSA_KEY_ID_NULL) {
psa_destroy_key(aes_key_id_);
aes_key_id_ = PSA_KEY_ID_NULL;
}
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attributes, PSA_ALG_CTR);
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attributes, 128);
status = psa_import_key(&attributes, reinterpret_cast<const uint8_t*>(aes_key.data()),
aes_key.size(), &aes_key_id_);
psa_reset_key_attributes(&attributes);
}
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to import AES key, status: %ld", static_cast<long>(status));
aes_key_id_ = PSA_KEY_ID_NULL;
return;
}
local_sequence_ = 0;
remote_sequence_ = 0;
{
std::lock_guard<std::mutex> lock(channel_mutex_);
udp_server_ = udp_server;
udp_port_ = udp_port;
aes_nonce_ = std::move(aes_nonce);
local_sequence_ = 0;
remote_sequence_ = 0;
}
xEventGroupSetBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT);
}
bool MqttProtocol::CryptAesCtr(const uint8_t* input, size_t input_size, const uint8_t* nonce, uint8_t* output) {
std::lock_guard<std::mutex> lock(crypto_mutex_);
if (aes_key_id_ == PSA_KEY_ID_NULL || input == nullptr || nonce == nullptr || output == nullptr) {
return false;
}
@@ -424,25 +489,32 @@ bool MqttProtocol::CryptAesCtr(const uint8_t* input, size_t input_size, const ui
return true;
}
static const char hex_chars[] = "0123456789ABCDEF";
// 辅助函数,将单个十六进制字符转换为对应的数值
static inline uint8_t CharToHex(char c) {
static inline int CharToHex(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return 0; // 对于无效输入,返回0
return -1;
}
std::string MqttProtocol::DecodeHexString(const std::string& hex_string) {
std::string decoded;
bool MqttProtocol::DecodeHexString(const std::string& hex_string, std::string& decoded) {
decoded.clear();
if ((hex_string.size() % 2) != 0) {
return false;
}
decoded.reserve(hex_string.size() / 2);
for (size_t i = 0; i < hex_string.size(); i += 2) {
char byte = (CharToHex(hex_string[i]) << 4) | CharToHex(hex_string[i + 1]);
decoded.push_back(byte);
int high = CharToHex(hex_string[i]);
int low = CharToHex(hex_string[i + 1]);
if (high < 0 || low < 0) {
decoded.clear();
return false;
}
decoded.push_back(static_cast<char>((high << 4) | low));
}
return decoded;
return true;
}
bool MqttProtocol::IsAudioChannelOpened() const {
std::lock_guard<std::mutex> lock(channel_mutex_);
return udp_ != nullptr && !error_occurred_ && !IsTimeout();
}
+3 -2
View File
@@ -42,7 +42,8 @@ private:
std::string publish_topic_;
std::mutex channel_mutex_;
mutable std::mutex channel_mutex_;
std::mutex crypto_mutex_;
std::unique_ptr<Mqtt> mqtt_;
std::unique_ptr<Udp> udp_;
psa_key_id_t aes_key_id_ = PSA_KEY_ID_NULL;
@@ -55,7 +56,7 @@ private:
bool StartMqttClient(bool report_error=false);
void ParseServerHello(const cJSON* root);
std::string DecodeHexString(const std::string& hex_string);
bool DecodeHexString(const std::string& hex_string, std::string& decoded);
bool CryptAesCtr(const uint8_t* input, size_t input_size, const uint8_t* nonce, uint8_t* output);
bool SendText(const std::string& text) override;
+2
View File
@@ -0,0 +1,2 @@
CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI=y
CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING=y
+71 -10
View File
@@ -149,7 +149,7 @@ def _collect_variants(
variants: list[dict[str, str]] = []
errors: list[str] = []
for cfg_path in _BOARDS_DIR.rglob(config_filename):
for cfg_path in sorted(_BOARDS_DIR.rglob(config_filename)):
board_dir = cfg_path.parent
if board_dir.name == "common":
continue
@@ -194,17 +194,68 @@ def _collect_variants(
})
except Exception as e:
print(f"[ERROR] Failed to parse {cfg_path}: {e}", file=sys.stderr)
errors.append(f"{cfg_path}: {e}")
seen_names: dict[str, str] = {}
for variant in variants:
previous_board = seen_names.get(variant["full_name"])
if previous_board is not None:
errors.append(
f"duplicate artifact name {variant['full_name']!r} in "
f"{previous_board} and {variant['board']}"
)
else:
seen_names[variant["full_name"]] = variant["board"]
# Report all errors at once
if errors:
print("\n[ERROR] Found manufacturer configuration issues:", file=sys.stderr)
for err in errors:
print(f" - {err}", file=sys.stderr)
print(file=sys.stderr)
sys.exit(1)
details = "\n".join(f" - {error}" for error in errors)
raise ValueError(f"Invalid board configuration:\n{details}")
return variants
return sorted(variants, key=lambda variant: (variant["board"], variant["name"]))
def _select_variants_for_changes(
variants: list[dict[str, str]], changed_files: list[str]
) -> list[dict[str, str]]:
"""Select variants affected by a git diff.
Board ownership is resolved using the longest known board directory prefix,
so nested paths such as waveshare/esp32-c6-touch-amoled-2.06 are preserved.
"""
known_boards = sorted({variant["board"] for variant in variants}, key=len, reverse=True)
affected: set[str] = set()
global_paths = {
".github/workflows/build.yml",
"CMakeLists.txt",
"scripts/build_default_assets.py",
"scripts/gen_lang.py",
"scripts/release.py",
"scripts/versions.py",
}
for raw_path in changed_files:
path = raw_path.strip()
if not path:
continue
if (path in global_paths or path.startswith("components/") or
path.startswith("partitions/") or
path.startswith("sdkconfig.defaults") or
(path.startswith("main/") and not path.startswith("main/boards/")) or
path.startswith("main/boards/common/")):
return variants
prefix = "main/boards/"
if path.startswith(prefix):
relative = path[len(prefix):]
board = next(
(candidate for candidate in known_boards
if relative == candidate or relative.startswith(f"{candidate}/")),
None,
)
if board is not None:
affected.add(board)
return [variant for variant in variants if variant["board"] in affected]
@@ -327,7 +378,6 @@ _AUTO_SELECT_RULES: dict[str, list[str]] = {
"CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y",
"CONFIG_BT_BLE_50_FEATURES_SUPPORTED=n",
"CONFIG_BT_BLE_BLUFI_ENABLE=y",
"CONFIG_MBEDTLS_DHM_C=y",
],
}
@@ -477,10 +527,21 @@ if __name__ == "__main__":
parser.add_argument("--list-boards", action="store_true", help="List all supported boards and variants")
parser.add_argument("--json", action="store_true", help="Output in JSON format (use with --list-boards)")
parser.add_argument("--name", help="Variant name to compile (original name without manufacturer prefix)")
parser.add_argument(
"--select-changed",
action="store_true",
help="Read changed paths from stdin and output the affected variants as JSON",
)
args = parser.parse_args()
idf_version = _detect_idf_version()
if args.select_changed:
variants = _collect_variants(config_filename=args.config, idf_version=idf_version)
selected = _select_variants_for_changes(variants, sys.stdin.read().splitlines())
print(json.dumps(selected))
sys.exit(0)
# List mode
if args.list_boards:
variants = _collect_variants(config_filename=args.config, idf_version=idf_version)
+92
View File
@@ -0,0 +1,92 @@
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[2]
SPEC = importlib.util.spec_from_file_location("release", ROOT / "scripts/release.py")
release = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(release)
class VersionTests(unittest.TestCase):
def test_parse_and_match(self):
self.assertEqual(release._parse_version("ESP-IDF v6.0.1"), (6, 0, 1))
self.assertTrue(release._version_matches((5, 5, 4), "<6.0"))
self.assertTrue(release._version_matches((6, 0, 1), ">=6.0"))
with self.assertRaises(ValueError):
release._version_matches((6, 0, 1), "~=6.0")
def test_current_matrix_counts_and_uniqueness(self):
idf5 = release._collect_variants(idf_version=(5, 5, 4))
idf6 = release._collect_variants(idf_version=(6, 0, 1))
self.assertEqual(len(idf5), 171)
self.assertEqual(len(idf6), 157)
for variants in (idf5, idf6):
names = [variant["full_name"] for variant in variants]
self.assertEqual(len(names), len(set(names)))
class BoardSelectionTests(unittest.TestCase):
def setUp(self):
self.variants = [
{"board": "bread-compact-wifi", "name": "bread-compact-wifi", "full_name": "bread-compact-wifi"},
{
"board": "waveshare/esp32-c6-touch-amoled-2.06",
"name": "esp32-c6-touch-amoled-2.06",
"full_name": "waveshare-esp32-c6-touch-amoled-2.06",
},
]
def test_nested_manufacturer_board_path(self):
selected = release._select_variants_for_changes(
self.variants,
["main/boards/waveshare/esp32-c6-touch-amoled-2.06/config.h"],
)
self.assertEqual([item["board"] for item in selected], [self.variants[1]["board"]])
def test_common_and_core_changes_select_all(self):
for path in (
"main/boards/common/board.cc",
"main/application.cc",
"components/esp-ml307/src/at_modem.cc",
"scripts/build_default_assets.py",
"scripts/release.py",
):
with self.subTest(path=path):
self.assertEqual(
release._select_variants_for_changes(self.variants, [path]),
self.variants,
)
def test_docs_only_selects_none(self):
self.assertEqual(
release._select_variants_for_changes(self.variants, ["docs/readme.md"]),
[],
)
class InvalidConfigTests(unittest.TestCase):
def test_invalid_version_rule_fails_collection(self):
with tempfile.TemporaryDirectory() as temp_dir:
boards = Path(temp_dir)
board_dir = boards / "bad-board"
board_dir.mkdir()
(board_dir / "config.json").write_text(json.dumps({
"target": "esp32s3",
"builds": [{
"name": "bad-board",
"idf_version": "~=6.0",
}],
}), encoding="utf-8")
with mock.patch.object(release, "_BOARDS_DIR", boards):
with self.assertRaisesRegex(ValueError, "Invalid ESP-IDF version expression"):
release._collect_variants(idf_version=(6, 0, 1))
if __name__ == "__main__":
unittest.main()