diff --git a/.env.example b/.env.example
index f4beab8..72a262e 100644
--- a/.env.example
+++ b/.env.example
@@ -8,12 +8,18 @@ VIBE_STICK_PROVIDER=auto
VIBE_STICK_BRIDGE_TOKEN=
# Set to on only if you want real Claude 5H/7D usage and Claude Code is installed/logged in.
VIBE_STICK_CLAUDE_USAGE=off
-# Recommended ASR for China: SiliconFlow, OpenAI-compatible.
-# Get a key at https://cloud.siliconflow.cn/i/7ZCoy9fU
-VIBE_STICK_ASR_PROVIDER=openai-compatible
-VIBE_STICK_ASR_BASE_URL=https://api.siliconflow.cn/v1
+# Optional server-side Sub2-Usage source. Keep this token on the Bridge;
+# never compile it into the StickS3 firmware.
+VIBE_STICK_SUB2_USAGE_TOKEN=
+VIBE_STICK_SUB2_USAGE_BASE_URL=https://88api.ai/sub2-usage
+VIBE_STICK_SUB2_CODEX_ACCOUNT_ID=52
+VIBE_STICK_SUB2_CLAUDE_ACCOUNT_ID=29
+VIBE_STICK_SUB2_USAGE_INTERVAL_SECONDS=60
+# Recommended for a LAN FunASR server: no API key is required.
+VIBE_STICK_ASR_PROVIDER=local-funasr
+VIBE_STICK_ASR_BASE_URL=ws://192.168.31.100:10095
VIBE_STICK_ASR_API_KEY=
-VIBE_STICK_ASR_MODEL=FunAudioLLM/SenseVoiceSmall
+VIBE_STICK_ASR_MODEL=paraformer-zh-streaming
# OPTIONAL -- defaults are fine, leave empty to use defaults.
VIBE_STICK_PROJECT_ROOT=
@@ -24,6 +30,13 @@ CLAUDE_CODE_OAUTH_TOKEN=
VIBE_STICK_ASR_LANGUAGE=zh
VIBE_STICK_ASR_TIMEOUT_SECONDS=15
VIBE_STICK_ASR_ATTEMPTS=2
+VIBE_STICK_FUNASR_HOTWORDS_URL=http://192.168.31.100:10096/api/hotwords
+VIBE_STICK_FUNASR_CHUNK_DELAY_MS=2
+
+# Optional SiliconFlow fallback (OpenAI-compatible).
+# VIBE_STICK_ASR_PROVIDER=openai-compatible
+# VIBE_STICK_ASR_BASE_URL=https://api.siliconflow.cn/v1
+# VIBE_STICK_ASR_MODEL=FunAudioLLM/SenseVoiceSmall
# Optional overseas Groq ASR preset.
# VIBE_STICK_ASR_PROVIDER=groq
@@ -40,6 +53,6 @@ VIBE_STICK_GROQ_LANGUAGE=zh
VIBE_STICK_TRANSCRIBE_CMD=
VIBE_STICK_TRANSCRIBE_TIMEOUT_SECONDS=120
-# Recording behavior.
+# Recording behavior. Mac microphone capture is macOS-only; StickS3 audio upload works on Windows.
VIBE_STICK_RECORDING_USE_MAC_MIC=1
VIBE_STICK_AUTO_ENTER=0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9bdc47d..29c07da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## v0.1.6 - 2026-07-15
+
+- Added low-latency StickS3-to-FunASR streaming with stable segment locking, tail-only correction, silence filtering, hotwords, and URL/developer-term normalization.
+- Added the Windows input-relay protocol used to stream provisional and final text into the focused local or remote-desktop application.
+- Added server-side Sub2-Usage polling for Codex/Claude 5-hour and 7-day remaining quota plus account-name display; credentials remain on the Bridge.
+- Restored front-button Send/Enter and Codex/Claude task completion, approval, and error sounds, including deferred playback while recording.
+- Added BMI270 pickup/flip display control, 60-minute battery idle shutdown, PMIC rail cleanup, and deep-sleep fallback.
+- Added Unraid Docker deployment assets, Windows PowerShell helpers, a larger firmware partition, and expanded automated tests.
+
+See [the complete 0.1.6 release record](docs/releases/0.1.6.md).
+
## v0.1.4
Initial public release of VibeStick — a tiny desktop companion for coding agents on M5Stack StickS3.
diff --git a/README.md b/README.md
index 6e5fb77..2fe1723 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,10 @@ VibeStick turns an M5Stack StickS3 into a tiny desktop companion for coding agen
VibeStick targets M5Stack StickS3 hardware and is not an official M5Stack project. Third-party agent names such as Codex and Claude describe compatible local tools and integrations only.
+This branch can fetch Codex and Claude 5H/7D remaining quota through Sub2-Usage on the server-side Bridge. The token never enters the firmware; StickS3 shows `account_name` when available and falls back to the configured account id.
+
+Current integrated firmware/Bridge record: [VibeStick 0.1.6](docs/releases/0.1.6.md).
+
## What you'll need (prepare first)
- [ ] M5Stack StickS3 and a USB-C data cable.
@@ -24,6 +28,21 @@ Building the firmware needs ESP-IDF v5.5.x — a one-time toolchain install (~1
You can do this manually, or hand the command steps to an AI coding agent such as Claude Code and Codex.
+### Windows bridge quick start
+
+Windows supports the Python bridge, local Codex / Claude observation, StickS3 audio uploads, and automatic paste into the focused window. The macOS HUD and Mac microphone fallback remain macOS-only; on Windows, voice input uses audio uploaded by the StickS3.
+
+Run in PowerShell:
+
+```powershell
+git clone https://github.com/GaryGaryyy/VibeStick.git
+cd VibeStick
+.\scripts\setup.ps1
+.\scripts\dev.ps1
+```
+
+Edit Wi-Fi values in `firmware\sticks3\include\vibe_stick_secrets.h` and the ASR key in `.env` before using the device. Allow private-network access if Windows Firewall prompts, then open `http://127.0.0.1:8765/health` to verify the bridge. Firmware builds still require ESP-IDF v5.5.x; use Espressif's Windows Installation Manager and run `idf.py build flash` from an ESP-IDF PowerShell.
+
> Legend: steps marked 👤 are PHYSICAL steps that need a human to act directly, such as plugging in the cable, long-pressing or short-pressing the power button, and granting macOS permissions in System Settings. AI agents should run the shell steps in order, then pause at each 👤 step and ask the user to complete it before continuing.
1. Clone the repo and create local config files:
@@ -169,7 +188,25 @@ Empty values in `.env` generally mean "use the built-in default". `scripts/dev.s
- `VIBE_STICK_RECORDING_USE_MAC_MIC`: set to `0` to disable Mac microphone fallback.
- `VIBE_STICK_AUTO_ENTER`: set to `1` to press Return after pasting.
-### ASR option 1: SiliconFlow (recommended default)
+### StickS3 power behavior
+
+- The screen backlight turns off after 30 seconds; this is standby, not power-off.
+- On battery, 60 minutes without interaction triggers PMIC power-off. Wi-Fi, audio, LCD and the unused IMU rail are released first; deep sleep is used if the PMIC rail doesn't cut successfully.
+- Double-click the side reset/power button for hardware power-off. Long-pressing it enters download mode and still consumes power while waiting to be flashed.
+
+### ASR option 1: LAN FunASR (lowest latency)
+
+This sends the completed StickS3 WAV to a FunASR WebSocket service on the LAN. It needs no API key and can load hotwords and normalization rules from the companion hotword API.
+
+```sh
+VIBE_STICK_ASR_PROVIDER=local-funasr
+VIBE_STICK_ASR_BASE_URL=ws://192.168.31.100:10095
+VIBE_STICK_FUNASR_HOTWORDS_URL=http://192.168.31.100:10096/api/hotwords
+VIBE_STICK_FUNASR_CHUNK_DELAY_MS=2
+VIBE_STICK_ASR_LANGUAGE=zh
+```
+
+### ASR option 2: SiliconFlow
```sh
VIBE_STICK_ASR_PROVIDER=openai-compatible
@@ -183,7 +220,7 @@ VIBE_STICK_ASR_ATTEMPTS=2
Audio sent to a cloud ASR provider leaves the Mac.
-### ASR option 2: any OpenAI-compatible provider
+### ASR option 3: any OpenAI-compatible provider
Use any provider that accepts `POST {base_url}/audio/transcriptions`.
@@ -203,7 +240,7 @@ VIBE_STICK_ASR_API_KEY=your-groq-key
The legacy aliases `VIBE_STICK_GROQ_API_KEY`, `VIBE_STICK_GROQ_MODEL`, and `VIBE_STICK_GROQ_LANGUAGE` remain supported.
-### ASR option 3: local command (offline)
+### ASR option 4: local command (offline)
```sh
VIBE_STICK_TRANSCRIBE_CMD=/path/to/transcribe-command
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 7933c29..33011ab 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -10,6 +10,10 @@ VibeStick 把 M5Stack StickS3 变成一个桌面 AI agent 小终端:显示状
VibeStick 面向 M5Stack StickS3,不是 M5Stack 官方项目。Codex、Claude 等第三方 agent 名称只用于说明本地兼容工具和集成。
+本分支支持由 Bridge 在服务器端调用 Sub2-Usage,缓存 Codex 与 Claude 的 5H/7D 剩余额度后再发送给 StickS3。Token 不进入固件;固件会优先显示接口的 `account_name`,字段暂缺时显示账号编号。
+
+当前固件与 Bridge 的完整更新记录:[VibeStick 0.1.6](docs/releases/0.1.6.md)。
+
## 开始前的准备
- [ ] M5 StickS3|一根 USB-C 数据线|一台电脑(最好是Mac)
@@ -22,6 +26,28 @@ VibeStick 面向 M5Stack StickS3,不是 M5Stack 官方项目。Codex、Claude
你可以手动执行,也可以交给 AI 编程 agent,例如 Claude Code 和 Codex。
+### Windows bridge 快速启动
+
+Windows 支持运行 Python bridge、读取本机 Codex / Claude 状态、接收 StickS3 上传的音频,以及把转写结果粘贴到当前窗口。macOS HUD 和 Mac 本机麦克风回退仍然仅限 macOS;Windows 使用 StickS3 自带麦克风上传的音频。
+
+在 PowerShell 中运行:
+
+```powershell
+git clone https://github.com/GaryGaryyy/VibeStick.git
+cd VibeStick
+.\scripts\setup.ps1
+```
+
+然后填写 `firmware\sticks3\include\vibe_stick_secrets.h` 中的 Wi-Fi 信息,并在 `.env` 中填写 ASR API key。启动 bridge:
+
+```powershell
+.\scripts\dev.ps1
+```
+
+Windows 防火墙首次询问时,需要允许专用网络访问,否则 StickS3 无法连接电脑的 8765 端口。可在浏览器打开 `http://127.0.0.1:8765/health` 检查 bridge;看到 `"ok": true` 表示服务已启动。
+
+固件仍需 ESP-IDF v5.5.x。Windows 可使用 Espressif 官方 ESP-IDF Installation Manager 安装工具链,然后在 ESP-IDF PowerShell 中进入 `firmware\sticks3` 执行 `idf.py build flash`。
+
> 说明:标 👤 的步骤是需要人亲自动手的物理操作,例如插线、长按/短按电源键、在系统设置里授权。AI agent 请按顺序执行 shell 步骤,执行到 👤 步骤时暂停,让用户完成后再继续。
1. 克隆仓库并创建本地配置文件:
@@ -167,7 +193,25 @@ open -e .env
- `VIBE_STICK_RECORDING_USE_MAC_MIC`:设为 `0` 可关闭 Mac 麦克风兜底。
- `VIBE_STICK_AUTO_ENTER`:设为 `1` 会在粘贴后自动按 Return。
-### ASR 方案 1:SiliconFlow(默认推荐)
+### StickS3 电源行为
+
+- 屏幕背光会在 30 秒后熄灭;这只是待机,不是关机。
+- 使用电池时,连续 60 分钟无操作会触发 PMIC 关机。关机前先释放 Wi-Fi、音频、LCD 和未使用的 IMU 电源轨;若 PMIC 没有真正切断 ESP32-S3 电源,则自动进入深度睡眠兜底。
+- 双击侧面的复位/电源键才是硬件关机。长按会进入下载模式,等待刷机期间仍会耗电。
+
+### ASR 方案 1:局域网 FunASR(最低延迟)
+
+StickS3 录音结束后,bridge 会把 WAV 直接发送到局域网 FunASR WebSocket 服务。无需 API Key,并可从配套接口定期读取热词和标准化规则。
+
+```sh
+VIBE_STICK_ASR_PROVIDER=local-funasr
+VIBE_STICK_ASR_BASE_URL=ws://192.168.31.100:10095
+VIBE_STICK_FUNASR_HOTWORDS_URL=http://192.168.31.100:10096/api/hotwords
+VIBE_STICK_FUNASR_CHUNK_DELAY_MS=2
+VIBE_STICK_ASR_LANGUAGE=zh
+```
+
+### ASR 方案 2:SiliconFlow
```sh
VIBE_STICK_ASR_PROVIDER=openai-compatible
@@ -181,7 +225,7 @@ VIBE_STICK_ASR_ATTEMPTS=2
使用云端 ASR 时,音频会离开本机 Mac。
-### ASR 方案 2:任意 OpenAI 兼容服务
+### ASR 方案 3:任意 OpenAI 兼容服务
只要服务支持 `POST {base_url}/audio/transcriptions` 即可。
@@ -201,7 +245,7 @@ VIBE_STICK_ASR_API_KEY=your-groq-key
旧别名 `VIBE_STICK_GROQ_API_KEY`、`VIBE_STICK_GROQ_MODEL`、`VIBE_STICK_GROQ_LANGUAGE` 仍然支持。
-### ASR 方案 3:本地命令(离线)
+### ASR 方案 4:本地命令(离线)
```sh
VIBE_STICK_TRANSCRIBE_CMD=/path/to/transcribe-command
diff --git a/bridge/deploy/unraid/apply-sub2-update.sh b/bridge/deploy/unraid/apply-sub2-update.sh
new file mode 100644
index 0000000..fbc15ea
--- /dev/null
+++ b/bridge/deploy/unraid/apply-sub2-update.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+set -euo pipefail
+
+if [[ -z "${SUB2_TOKEN:-}" ]]; then
+ echo "SUB2_TOKEN is required" >&2
+ exit 2
+fi
+
+APP=/mnt/user/appdata/FunASR-2pass/gateway
+TEMPLATE=/boot/config/plugins/dockerMan/templates-user/my-Vibe-ASR-Gateway.xml
+WORK=/www/wwwroot/work
+TIMESTAMP=$(date +%Y%m%d-%H%M%S)
+BACKUP="$WORK/backups/vibestick-sub2/$TIMESTAMP"
+RELEASE="$WORK/vibestick-sub2-release"
+ARCHIVE="$WORK/vibestick-sub2-release.tar.gz"
+
+backup_file() {
+ local source=$1
+ if [[ -f "$source" ]]; then
+ local destination="$BACKUP$source"
+ mkdir -p "$(dirname "$destination")"
+ cp -a "$source" "$destination"
+ fi
+}
+
+mkdir -p "$BACKUP"
+backup_file "$APP/src/vibe_stick/__init__.py"
+backup_file "$APP/src/vibe_stick/protocol/state.py"
+backup_file "$APP/src/vibe_stick/providers/base.py"
+backup_file "$APP/src/vibe_stick/server/app.py"
+backup_file "$APP/start.sh"
+backup_file "$TEMPLATE"
+
+rm -rf "$RELEASE"
+mkdir -p "$RELEASE"
+tar -xzf "$ARCHIVE" -C "$RELEASE"
+
+install -d "$APP/src/vibe_stick/usage"
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/__init__.py" "$APP/src/vibe_stick/__init__.py"
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/protocol/state.py" "$APP/src/vibe_stick/protocol/state.py"
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/providers/base.py" "$APP/src/vibe_stick/providers/base.py"
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/server/app.py" "$APP/src/vibe_stick/server/app.py"
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/usage/__init__.py" "$APP/src/vibe_stick/usage/__init__.py"
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/usage/sub2.py" "$APP/src/vibe_stick/usage/sub2.py"
+install -m 0755 "$RELEASE/bridge/deploy/unraid/start.sh" "$APP/start.sh"
+
+umask 077
+printf '%s\n' \
+ "VIBE_STICK_SUB2_USAGE_TOKEN=$SUB2_TOKEN" \
+ 'VIBE_STICK_SUB2_USAGE_BASE_URL=https://88api.ai/sub2-usage' \
+ 'VIBE_STICK_SUB2_CODEX_ACCOUNT_ID=52' \
+ 'VIBE_STICK_SUB2_CLAUDE_ACCOUNT_ID=29' \
+ 'VIBE_STICK_SUB2_USAGE_INTERVAL_SECONDS=60' \
+ > "$APP/sub2-usage.env"
+
+if ! grep -q 'VIBE_STICK_SUB2_USAGE_TOKEN' "$TEMPLATE"; then
+ awk -v token="$SUB2_TOKEN" '
+ { print }
+ /Target="VIBE_STICK_BRIDGE_TOKEN"/ {
+ print " " token ""
+ print " https://88api.ai/sub2-usage"
+ print " 52"
+ print " 29"
+ print " 60"
+ }
+ ' "$TEMPLATE" > "$WORK/my-Vibe-ASR-Gateway.xml.new"
+ install -m 0600 "$WORK/my-Vibe-ASR-Gateway.xml.new" "$TEMPLATE"
+ rm -f "$WORK/my-Vibe-ASR-Gateway.xml.new"
+else
+ sed -i -E "s#^(.*Target=\"VIBE_STICK_SUB2_USAGE_TOKEN\"[^>]*>)[^<]*()#\1${SUB2_TOKEN}\2#" "$TEMPLATE"
+fi
+
+docker restart Vibe-ASR-Gateway >/dev/null
+sleep 6
+echo "backup=$BACKUP"
+printf 'health='
+curl -fsS http://127.0.0.1:8765/health
+printf '\n'
+
+rm -rf "$RELEASE"
+rm -f "$ARCHIVE" "$WORK/apply-sub2-update.sh"
diff --git a/bridge/deploy/unraid/apply-task-events-update.sh b/bridge/deploy/unraid/apply-task-events-update.sh
new file mode 100644
index 0000000..0d719db
--- /dev/null
+++ b/bridge/deploy/unraid/apply-task-events-update.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -euo pipefail
+
+APP=/mnt/user/appdata/FunASR-2pass/gateway
+WORK=/www/wwwroot/work
+TIMESTAMP=$(date +%Y%m%d-%H%M%S)
+BACKUP="$WORK/backups/vibestick-task-events/$TIMESTAMP"
+RELEASE="$WORK/vibestick-task-events-release"
+ARCHIVE="$WORK/vibestick-task-events-release.tar.gz"
+
+backup_file() {
+ local source=$1
+ if [[ -f "$source" ]]; then
+ local destination="$BACKUP$source"
+ mkdir -p "$(dirname "$destination")"
+ cp -a "$source" "$destination"
+ fi
+}
+
+mkdir -p "$BACKUP"
+backup_file "$APP/src/vibe_stick/__init__.py"
+backup_file "$APP/src/vibe_stick/paste/input_relay.py"
+backup_file "$APP/src/vibe_stick/server/app.py"
+
+rm -rf "$RELEASE"
+mkdir -p "$RELEASE"
+tar -xzf "$ARCHIVE" -C "$RELEASE"
+
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/__init__.py" \
+ "$APP/src/vibe_stick/__init__.py"
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/paste/input_relay.py" \
+ "$APP/src/vibe_stick/paste/input_relay.py"
+install -m 0644 "$RELEASE/bridge/src/vibe_stick/server/app.py" \
+ "$APP/src/vibe_stick/server/app.py"
+
+docker restart Vibe-ASR-Gateway >/dev/null
+sleep 6
+echo "backup=$BACKUP"
+printf 'health='
+curl -fsS http://127.0.0.1:8765/health
+printf '\n'
+
+rm -rf "$RELEASE"
+rm -f "$ARCHIVE" "$WORK/apply-task-events-update.sh"
diff --git a/bridge/deploy/unraid/my-Vibe-ASR-Gateway.xml b/bridge/deploy/unraid/my-Vibe-ASR-Gateway.xml
new file mode 100644
index 0000000..d965043
--- /dev/null
+++ b/bridge/deploy/unraid/my-Vibe-ASR-Gateway.xml
@@ -0,0 +1,46 @@
+
+
+ Vibe-ASR-Gateway
+ python:3.12-alpine
+ https://hub.docker.com/_/python
+ bridge
+
+ sh
+ false
+ https://github.com/GaryGaryyy/VibeStick/issues
+ https://github.com/GaryGaryyy/VibeStick
+ VibeStick S3 audio gateway, FunASR hotword API, and persistent Windows text-input relay. Keeps the established local Windows ASR path unchanged.
+ Tools:Utilities
+
+
+ /state/plugins/dynamix.docker.manager/images/Vibe-ASR-Gateway-icon.png
+ --restart unless-stopped
+ /config/gateway/start.sh
+
+
+
+ FunASR-2pass must be reachable at ws://192.168.31.100:10095. Intended for trusted LAN use.
+ 8765
+ 10096
+ 10097
+ /mnt/user/appdata/FunASR-2pass
+ __VIBE_STICK_BRIDGE_TOKEN__
+
+ https://88api.ai/sub2-usage
+ 52
+ 29
+ 60
+ local-funasr
+ ws://192.168.31.100:10095
+ paraformer-zh-streaming
+ zh
+ http://127.0.0.1:10096/api/hotwords
+ 10097
+ /config/gateway/data
+ 0.55
+ 8000000
+ 4
+ 125
+ /config/hotwords.txt
+ 0
+
diff --git a/bridge/deploy/unraid/start.sh b/bridge/deploy/unraid/start.sh
new file mode 100644
index 0000000..e428a75
--- /dev/null
+++ b/bridge/deploy/unraid/start.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -eu
+
+if [ -f /config/gateway/sub2-usage.env ]; then
+ set -a
+ . /config/gateway/sub2-usage.env
+ set +a
+fi
+
+export PYTHONPATH="/config/gateway/src:/config/gateway/vendor${PYTHONPATH:+:$PYTHONPATH}"
+exec python /config/gateway/supervisor.py
diff --git a/bridge/deploy/unraid/supervisor.py b/bridge/deploy/unraid/supervisor.py
new file mode 100644
index 0000000..e04c350
--- /dev/null
+++ b/bridge/deploy/unraid/supervisor.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+import os
+import signal
+import subprocess
+import sys
+import time
+
+
+def main() -> int:
+ environment = os.environ.copy()
+ processes = [
+ subprocess.Popen([sys.executable, "/config/hotword-api.py"], env=environment),
+ subprocess.Popen(
+ [
+ sys.executable,
+ "-m",
+ "vibe_stick",
+ "--host",
+ "0.0.0.0",
+ "--port",
+ "8765",
+ ],
+ env=environment,
+ ),
+ ]
+
+ stopping = False
+
+ def stop_all(_signum: int | None = None, _frame: object | None = None) -> None:
+ nonlocal stopping
+ if stopping:
+ return
+ stopping = True
+ for process in processes:
+ if process.poll() is None:
+ process.terminate()
+
+ signal.signal(signal.SIGTERM, stop_all)
+ signal.signal(signal.SIGINT, stop_all)
+
+ try:
+ while True:
+ for process in processes:
+ return_code = process.poll()
+ if return_code is not None:
+ stop_all()
+ return return_code
+ time.sleep(0.25)
+ finally:
+ stop_all()
+ deadline = time.monotonic() + 5
+ for process in processes:
+ if process.poll() is None:
+ remaining = max(0.0, deadline - time.monotonic())
+ try:
+ process.wait(timeout=remaining)
+ except subprocess.TimeoutExpired:
+ process.kill()
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/bridge/pyproject.toml b/bridge/pyproject.toml
index 55fdd28..fad3852 100644
--- a/bridge/pyproject.toml
+++ b/bridge/pyproject.toml
@@ -1,9 +1,9 @@
[project]
name = "vibestick"
-version = "0.1.4"
+version = "0.1.6"
description = "Local VibeStick Bridge service for VibeStick."
requires-python = ">=3.11"
-dependencies = []
+dependencies = ["websockets>=15,<17"]
[project.scripts]
vibestick-bridge = "vibe_stick.__main__:main"
diff --git a/bridge/src/vibe_stick/__init__.py b/bridge/src/vibe_stick/__init__.py
index d4d67b3..f0f15ce 100644
--- a/bridge/src/vibe_stick/__init__.py
+++ b/bridge/src/vibe_stick/__init__.py
@@ -1,3 +1,3 @@
"""VibeStick Bridge for VibeStick."""
-__version__ = "0.1.4"
+__version__ = "0.1.6"
diff --git a/bridge/src/vibe_stick/audio/recorder.py b/bridge/src/vibe_stick/audio/recorder.py
index 591d4d0..0586f9c 100644
--- a/bridge/src/vibe_stick/audio/recorder.py
+++ b/bridge/src/vibe_stick/audio/recorder.py
@@ -3,6 +3,7 @@
import json
import math
import os
+import platform
import signal
import struct
import subprocess
@@ -15,9 +16,10 @@
from typing import Any
from vibe_stick.audio.transcriber import TranscriptionAdapter
+from vibe_stick.audio.streaming import FunASRStreamingSession
from vibe_stick.config.paths import RECORDINGS_DIR
from vibe_stick.desktop.hud import hide_hud, show_hud
-from vibe_stick.paste.input_injector import MacPasteInjector
+from vibe_stick.paste.input_injector import PasteInjector
MIN_AUDIO_DURATION_SECONDS = 0.7
MIN_AUDIO_RMS = 120.0
@@ -63,10 +65,11 @@ class AudioMetrics:
class RecordingController:
"""project-owned push-to-talk session boundary."""
- def __init__(self, path: Path) -> None:
+ def __init__(self, path: Path, paste_injector: PasteInjector | None = None) -> None:
self.path = path
self.transcriber = TranscriptionAdapter()
- self.paste_injector = MacPasteInjector()
+ self.paste_injector = paste_injector or PasteInjector()
+ self.streaming = FunASRStreamingSession(self.paste_injector)
self.audio_recorder = MacMicRecorder()
self.session = self._load()
@@ -84,8 +87,12 @@ def start(self, request: dict[str, Any] | None = None) -> RecordingSession:
)
use_mac_mic = "sticks3" not in requested_source.lower()
if not use_mac_mic:
- self.session.audio_source = "sticks3_pcm"
- self.session.message = "Waiting for StickS3 audio upload"
+ if self.streaming.start(self.session.session_id):
+ self.session.audio_source = "sticks3_pcm_stream"
+ self.session.message = "StickS3 streaming transcription started"
+ else:
+ self.session.audio_source = "sticks3_pcm"
+ self.session.message = "Streaming unavailable; waiting for complete StickS3 audio"
show_hud("listening")
mic_result = self.audio_recorder.start(self.session.session_id) if use_mac_mic else None
@@ -106,6 +113,8 @@ def start(self, request: dict[str, Any] | None = None) -> RecordingSession:
hook = _run_command_hook("VIBE_STICK_RECORDING_START_CMD", self.session.to_jsonable(), timeout=15)
if hook is not None and not hook[0]:
self.audio_recorder.stop()
+ if self.session.audio_source == "sticks3_pcm_stream":
+ self.streaming.abort(rollback=True)
self.session.active = False
self.session.stopped_at = datetime.now().isoformat(timespec="seconds")
self.session.status = "start_failed"
@@ -114,6 +123,27 @@ def start(self, request: dict[str, Any] | None = None) -> RecordingSession:
self._save()
return self.session
+ def attach_pcm_chunk(self, pcm: bytes, *, session_id: str = "") -> RecordingSession:
+ session_id = _clean_session_id(session_id)
+ if not pcm:
+ self.session.status = "audio_failed"
+ self.session.message = "Streaming audio chunk was empty"
+ elif not self.session.active or session_id != self.session.session_id:
+ self.session.status = "audio_failed"
+ self.session.message = "Streaming audio session did not match the active recording"
+ elif self.session.audio_source != "sticks3_pcm_stream" or not self.streaming.active:
+ self.session.status = "stream_unavailable"
+ self.session.message = "StickS3 streaming session is unavailable"
+ elif not self.streaming.append(pcm):
+ self.session.status = "audio_failed"
+ self.session.message = "StickS3 streaming audio could not be forwarded"
+ else:
+ self.session.status = "recording"
+ self.session.message = "StickS3 streaming audio received"
+ if self.session.status != "recording":
+ self._save()
+ return self.session
+
def attach_pcm(
self,
pcm: bytes,
@@ -123,6 +153,8 @@ def attach_pcm(
channels: int = 1,
bits_per_sample: int = 16,
) -> RecordingSession:
+ if self.session.audio_source == "sticks3_pcm_stream":
+ self.streaming.abort(rollback=True)
if not pcm:
self.session.status = "audio_failed"
self.session.message = "Uploaded audio was empty"
@@ -173,6 +205,30 @@ def stop(self, request: dict[str, Any] | None = None) -> RecordingSession:
self.session.active = False
self.session.stopped_at = datetime.now().isoformat(timespec="seconds")
explicit_text = str(request.get("text") or request.get("transcript") or "")
+ if self.session.audio_source == "sticks3_pcm_stream":
+ stream_result = self.streaming.stop()
+ if stream_result.audio:
+ self.session.audio_file = str(
+ self._write_sticks3_wav(self.session.session_id, stream_result.audio)
+ )
+ self.session.transcript = stream_result.text
+ self.session.transcript_source = "local-funasr-stream"
+ self.session.pasted = stream_result.success and stream_result.input_success
+ if self.session.pasted:
+ self.session.status = "pasted"
+ self.session.message = stream_result.message
+ hide_hud(delay_seconds=0.5)
+ elif stream_result.text:
+ self.session.status = "paste_failed"
+ self.session.message = stream_result.message
+ show_hud("failed", hold_seconds=1.8)
+ else:
+ self.session.status = "audio_skipped"
+ self.session.message = stream_result.message
+ show_hud("unclear", hold_seconds=1.8)
+ self._save_stop_result()
+ return self.session
+
mic_stop = self.audio_recorder.stop()
if mic_stop is not None:
ok, audio_file, message = mic_stop
@@ -294,6 +350,31 @@ def stop(self, request: dict[str, Any] | None = None) -> RecordingSession:
self._save_stop_result()
return self.session
+ def abort_stale_stream(self, message: str) -> RecordingSession:
+ """Close an orphaned StickS3 stream without rewriting entered text."""
+ if self.session.audio_source == "sticks3_pcm_stream":
+ self.streaming.abort(rollback=False)
+ elif self.session.active:
+ self.audio_recorder.stop()
+ self.session.active = False
+ self.session.stopped_at = datetime.now().isoformat(timespec="seconds")
+ self.session.status = "stream_timeout"
+ self.session.message = message
+ self.session.pasted = False
+ self._save_stop_result()
+ return self.session
+
+ @staticmethod
+ def _write_sticks3_wav(session_id: str, pcm: bytes) -> Path:
+ RECORDINGS_DIR.mkdir(parents=True, exist_ok=True)
+ audio_file = RECORDINGS_DIR / f"{session_id or uuid.uuid4().hex}.wav"
+ with wave.open(str(audio_file), "wb") as wav:
+ wav.setnchannels(1)
+ wav.setsampwidth(2)
+ wav.setframerate(16000)
+ wav.writeframes(pcm)
+ return audio_file
+
def _load(self) -> RecordingSession:
try:
data = json.loads(self.path.read_text())
@@ -446,6 +527,8 @@ def __init__(self) -> None:
self.audio_file: Path | None = None
def start(self, session_id: str) -> tuple[bool, Path | None, str] | None:
+ if platform.system() != "Darwin":
+ return None
if os.environ.get("VIBE_STICK_RECORDING_USE_MAC_MIC", "1").strip().lower() in {"0", "false", "no", "off"}:
return None
if self.process and self.process.poll() is None:
diff --git a/bridge/src/vibe_stick/audio/streaming.py b/bridge/src/vibe_stick/audio/streaming.py
new file mode 100644
index 0000000..53fddc3
--- /dev/null
+++ b/bridge/src/vibe_stick/audio/streaming.py
@@ -0,0 +1,588 @@
+from __future__ import annotations
+
+import json
+import math
+import os
+import re
+import struct
+import threading
+import time
+from dataclasses import dataclass
+from typing import Any
+
+from vibe_stick.audio.transcriber import (
+ FUNASR_AUDIO_CHUNK_BYTES,
+ _apply_funasr_normalizations,
+ _load_asr_config,
+ _load_funasr_hotwords,
+ _merge_funasr_text,
+)
+from vibe_stick.paste.input_injector import PasteInjector
+
+SPEECH_RMS_THRESHOLD = 900.0
+SPEECH_START_BLOCKS = 2
+MINIMUM_SPEECH_BLOCKS = 3
+PRE_ROLL_BLOCKS = 5
+LIVE_FINAL_MIN_SECONDS = 0.16
+LIVE_FINAL_QUIET_SECONDS = 0.10
+_PUNCTUATION = ",。!?;:,.!?;:"
+_SPOKEN_DECIMAL_AS_TIME_RE = re.compile(
+ r"(? tuple[int, str]:
+ if current == previous:
+ return 0, ""
+ common = 0
+ for old_character, new_character in zip(previous, current):
+ if old_character != new_character:
+ break
+ common += 1
+ return len(previous) - common, current[common:]
+
+
+def locked_live_input_target(previous: str, current: str, locked_length: int) -> str:
+ """Preserve calibrated segments while allowing only the active tail to change."""
+ locked_length = min(max(0, locked_length), len(previous))
+ if locked_length == 0 or previous[:locked_length] == current[:locked_length]:
+ return current
+ return previous[:locked_length] + current[min(locked_length, len(current)) :]
+
+
+def committed_segment_delta(previous: str, current: str, fallback: str) -> str:
+ if current.startswith(previous):
+ return current[len(previous) :]
+ return fallback.strip()
+
+
+def spoken_format_command(text: str) -> str | None:
+ """Return a deterministic edit for a standalone spoken format command."""
+ command = text.strip(" \t\r\n,。!?;:,.!?;:")
+ return _SPOKEN_FORMAT_COMMANDS.get(command)
+
+
+def strip_trailing_url_period(text: str) -> str:
+ """URLs are identifiers, so remove sentence periods emitted by ASR."""
+ stripped = text.rstrip()
+ if not stripped.endswith(("。", ".")):
+ return text
+ without_period = stripped[:-1]
+ return without_period if _URL_AT_END_RE.search(without_period) else text
+
+
+def normalize_stream_text(
+ text: str,
+ normalizations: dict[str, str],
+ *,
+ sentence_final: bool = False,
+) -> str:
+ text = _apply_funasr_normalizations(text, normalizations).strip()
+ for pattern, replacement in _TECHNICAL_PHRASE_REPAIRS:
+ text = pattern.sub(replacement, text)
+ text = re.sub(
+ r"断流(\d+)\s*s动(?:自动)?回收会话",
+ r"断流\1 秒自动回收会话",
+ text,
+ flags=re.IGNORECASE,
+ )
+ text = re.sub(r"(?<=\d)\s*(?:ms|毫秒)(?=\D|$)", " ms", text, flags=re.IGNORECASE)
+ text = re.sub(r"(?<=\d)\s*s(?=(?:自动|断流|后|内|$))", " 秒", text, flags=re.IGNORECASE)
+ # Explicit symbol words are unambiguous. Requiring the user to say the
+ # symbol name avoids guessing whether an ordinary “到” means an arrow.
+ text = text.replace("箭头", "→")
+ text = re.sub(r"(?<=\d)冒号(?=\d)", ":", text)
+ text = _SPOKEN_DECIMAL_AS_TIME_RE.sub(
+ lambda match: f"{match.group(1)}.{int(match.group(2)) + int(match.group(3))} 秒",
+ text,
+ )
+ # These clause starters are reliable punctuation cues in dictated
+ # technical prose. Do not touch them at the beginning of a sentence.
+ text = re.sub(
+ r"(?<=[\u4e00-\u9fffA-Za-z0-9])(?=(?:松开后|按下后|录音结束后|识别完成后|停顿后))",
+ ",",
+ text,
+ )
+ text = re.sub(r"(刷入|完成|退出)(?=改为)", r"\1。", text)
+ text = re.sub(r"(不是[^,。!?\n]{1,16}问题)(?=已(?:处理|修复|解决|完成))", r"\1。", text)
+ text = re.sub(r"(自动补逗号)(?=结束时)", r"\1,", text)
+ text = re.sub(r"(补句号)(?=修正)", r"\1。", text)
+ text = strip_trailing_url_period(text)
+ if sentence_final and text and text[-1] not in _PUNCTUATION and not _URL_AT_END_RE.search(text):
+ text += "。"
+ return text
+
+
+def join_calibrated_segment(base: str, segment: str) -> str:
+ segment = segment.strip()
+ if not base:
+ return segment
+ if not segment:
+ return base
+ if base[-1] in _PUNCTUATION and segment[0] in _PUNCTUATION:
+ segment = segment.lstrip(",,。.")
+ return base if not segment else base + segment
+ if base[-1] not in _PUNCTUATION and segment[0] not in _PUNCTUATION:
+ return base + "," + segment
+ return _join_text(base, segment)
+
+
+def novel_stream_suffix(existing: str, incoming: str, overlap_limit: int = 96) -> str:
+ if not existing or not incoming:
+ return incoming
+ for overlap in range(min(len(existing), len(incoming), overlap_limit), 0, -1):
+ if existing.endswith(incoming[:overlap]):
+ return incoming[overlap:]
+ return incoming
+
+
+def _join_text(left: str, right: str) -> str:
+ if not left:
+ return right
+ if not right:
+ return left
+ needs_space = left[-1].isascii() and left[-1].isalnum() and right[0].isascii() and right[0].isalnum()
+ return left + (" " if needs_space else "") + right
+
+
+def _pcm16_rms(block: bytes) -> float:
+ usable = len(block) - (len(block) % 2)
+ if usable <= 0:
+ return 0.0
+ samples = struct.unpack(f"<{usable // 2}h", block[:usable])
+ return math.sqrt(sum(sample * sample for sample in samples) / len(samples))
+
+
+class FunASRStreamingSession:
+ """One StickS3 push-to-talk stream backed by a persistent FunASR socket."""
+
+ def __init__(self, injector: PasteInjector) -> None:
+ self.injector = injector
+ self._lock = threading.RLock()
+ self._send_lock = threading.Lock()
+ self._stop_event = threading.Event()
+ self._final_event = threading.Event()
+ self._receiver: threading.Thread | None = None
+ self._websocket: Any = None
+ self._session_id = ""
+ self._target_window = 0
+ self._audio = bytearray()
+ self._pending = bytearray()
+ self._pre_roll: list[bytes] = []
+ self._speech_started = False
+ self._consecutive_speech = 0
+ self._speech_blocks = 0
+ self._committed_text = ""
+ self._online_text = ""
+ self._display_text = ""
+ self._display_segment_start = 0
+ self._locked_prefix_length = 0
+ self._applied_text = ""
+ self._dedupe_next_online = False
+ self._dedupe_reference = ""
+ self._normalizations: dict[str, str] = {}
+ self._input_success = True
+ self._error = ""
+ self._stop_sent_at = 0.0
+ self._last_post_stop_result_at = 0.0
+
+ @property
+ def active(self) -> bool:
+ return bool(self._websocket and self._session_id and not self._stop_event.is_set())
+
+ def start(self, session_id: str, *, connector=None) -> bool: # noqa: ANN001
+ self.abort(rollback=True)
+ self._error = ""
+ config = _load_asr_config()
+ if config.get("provider") != "local-funasr" or not config.get("base_url"):
+ self._error = "Streaming requires the local FunASR provider"
+ return False
+ if connector is None:
+ try:
+ from websockets.sync.client import connect as connector
+ except ImportError:
+ self._error = "The websockets package is unavailable"
+ return False
+
+ hotwords, self._normalizations = _load_funasr_hotwords()
+ begin_session = getattr(self.injector, "begin_session", None)
+ if begin_session:
+ relay_start = begin_session(session_id)
+ if not relay_start.success:
+ self._error = relay_start.message
+ return False
+ try:
+ websocket = connector(
+ config["base_url"],
+ subprotocols=["binary"],
+ ping_interval=None,
+ max_size=None,
+ open_timeout=4,
+ close_timeout=2,
+ )
+ websocket.send(
+ json.dumps(
+ {
+ "mode": "2pass",
+ "chunk_size": [5, 10, 5],
+ "chunk_interval": 10,
+ "encoder_chunk_look_back": 4,
+ "decoder_chunk_look_back": 0,
+ "audio_fs": 16000,
+ "wav_name": f"sticks3-{session_id}",
+ "wav_format": "pcm",
+ "is_speaking": True,
+ "hotwords": json.dumps(hotwords, ensure_ascii=False) if hotwords else "",
+ "itn": True,
+ },
+ ensure_ascii=False,
+ )
+ )
+ except (OSError, TimeoutError, ValueError) as exc:
+ end_session = getattr(self.injector, "end_session", None)
+ if end_session:
+ end_session(session_id)
+ self._error = f"Could not connect to FunASR: {exc}"
+ return False
+
+ with self._lock:
+ self._websocket = websocket
+ self._session_id = session_id
+ self._target_window = self.injector.capture_target()
+ self._stop_event.clear()
+ self._final_event.clear()
+ self._receiver = threading.Thread(
+ target=self._receive_loop,
+ name="vibestick-funasr-stream",
+ daemon=True,
+ )
+ self._receiver.start()
+ return True
+
+ def append(self, pcm: bytes) -> bool:
+ if not pcm or not self.active:
+ return False
+ with self._lock:
+ self._audio.extend(pcm)
+ self._pending.extend(pcm)
+ blocks: list[bytes] = []
+ while len(self._pending) >= FUNASR_AUDIO_CHUNK_BYTES:
+ blocks.append(bytes(self._pending[:FUNASR_AUDIO_CHUNK_BYTES]))
+ del self._pending[:FUNASR_AUDIO_CHUNK_BYTES]
+ try:
+ for block in blocks:
+ self._feed_audio_block(block)
+ return True
+ except (OSError, TimeoutError, RuntimeError) as exc:
+ self._error = f"FunASR audio send failed: {exc}"
+ return False
+
+ def stop(self) -> StreamingResult:
+ websocket = self._websocket
+ if not websocket:
+ return StreamingResult("", bytes(self._audio), False, False, self._error or "Stream is inactive")
+
+ try:
+ with self._lock:
+ tail = bytes(self._pending)
+ self._pending.clear()
+ if tail and self._speech_started:
+ self._send(tail)
+ with self._lock:
+ self._stop_sent_at = time.monotonic()
+ # FunASR can mark completed phrase segments final while the
+ # speaker is still talking. Only a result after release should
+ # satisfy the final-calibration wait.
+ self._final_event.clear()
+ self._send(json.dumps({"is_speaking": False}))
+ self._wait_for_live_final()
+ except (OSError, TimeoutError, RuntimeError) as exc:
+ self._error = f"FunASR stop failed: {exc}"
+
+ self._stop_event.set()
+ try:
+ websocket.close()
+ except (OSError, RuntimeError):
+ pass
+ receiver = self._receiver
+ if receiver and receiver is not threading.current_thread():
+ receiver.join(timeout=1.0)
+
+ with self._lock:
+ text = self._display_text or _join_text(self._committed_text, self._online_text)
+ audio = bytes(self._audio)
+ enough_speech = self._speech_blocks >= MINIMUM_SPEECH_BLOCKS
+ input_success = self._input_success
+ if enough_speech and text:
+ format_edit = spoken_format_command(text)
+ text = (
+ format_edit
+ if format_edit is not None
+ else normalize_stream_text(text, self._normalizations, sentence_final=True)
+ )
+ if text != self._applied_text:
+ final_edit = self._publish(text)
+ input_success = input_success and final_edit
+ if not enough_speech:
+ if self._applied_text:
+ removal = self.injector.edit(
+ len(self._applied_text),
+ "",
+ target_window=self._target_window,
+ )
+ input_success = input_success and removal.success
+ text = ""
+ success = bool(text and enough_speech and input_success)
+ message = self._error or (
+ "Streaming transcript entered" if success else "No clear streaming transcript"
+ )
+ result = StreamingResult(text, audio, success, input_success, message)
+ end_session = getattr(self.injector, "end_session", None)
+ if end_session:
+ end_session(self._session_id)
+ self._reset_runtime()
+ return result
+
+ def abort(self, *, rollback: bool = False) -> None:
+ websocket = self._websocket
+ session_id = self._session_id
+ self._stop_event.set()
+ if rollback and self._applied_text:
+ self.injector.edit(
+ len(self._applied_text),
+ "",
+ target_window=self._target_window,
+ )
+ if websocket:
+ try:
+ websocket.close()
+ except (OSError, RuntimeError):
+ pass
+ receiver = self._receiver
+ if receiver and receiver is not threading.current_thread():
+ receiver.join(timeout=1.0)
+ end_session = getattr(self.injector, "end_session", None)
+ if session_id and end_session:
+ end_session(session_id)
+ self._reset_runtime()
+
+ def _feed_audio_block(self, block: bytes) -> None:
+ level = _pcm16_rms(block)
+ is_speech = level >= SPEECH_RMS_THRESHOLD
+ if is_speech:
+ self._speech_blocks += 1
+ if self._speech_started:
+ self._send(block)
+ return
+
+ self._pre_roll.append(block)
+ if len(self._pre_roll) > PRE_ROLL_BLOCKS:
+ del self._pre_roll[0]
+ self._consecutive_speech = self._consecutive_speech + 1 if is_speech else 0
+ if self._consecutive_speech < SPEECH_START_BLOCKS:
+ return
+ self._speech_started = True
+ for ready_block in self._pre_roll:
+ self._send(ready_block)
+ self._pre_roll.clear()
+
+ def _send(self, payload: object) -> None:
+ websocket = self._websocket
+ if not websocket:
+ raise RuntimeError("Stream is inactive")
+ with self._send_lock:
+ websocket.send(payload)
+
+ def _receive_loop(self) -> None:
+ while not self._stop_event.is_set():
+ try:
+ raw = self._websocket.recv(timeout=0.2)
+ except TimeoutError:
+ continue
+ except Exception as exc: # The websocket library has version-specific close exceptions.
+ if not self._stop_event.is_set():
+ self._error = f"FunASR receive failed: {exc}"
+ break
+ try:
+ payload = json.loads(raw)
+ except (TypeError, ValueError, json.JSONDecodeError):
+ continue
+ if not isinstance(payload, dict):
+ continue
+ self._handle_result(payload)
+ if payload.get("is_final"):
+ self._final_event.set()
+
+ def _handle_result(self, payload: dict[str, Any]) -> None:
+ text = str(payload.get("text") or "").strip()
+ if not text:
+ return
+ mode = str(payload.get("mode") or "")
+ with self._lock:
+ if self._stop_sent_at:
+ self._last_post_stop_result_at = time.monotonic()
+ if mode == "2pass-online":
+ self._online_text += text
+ starts_new_segment = self._dedupe_next_online
+ visible_suffix = (
+ novel_stream_suffix(self._dedupe_reference, text)
+ if starts_new_segment
+ else text
+ )
+ if starts_new_segment and visible_suffix == text:
+ visible_suffix = novel_stream_suffix(self._display_text, text)
+ self._dedupe_next_online = False
+ self._dedupe_reference = ""
+ if not visible_suffix:
+ return
+ target = (
+ _join_text(self._display_text, visible_suffix)
+ if starts_new_segment
+ else self._display_text + visible_suffix
+ )
+ target = normalize_stream_text(target, self._normalizations)
+ self._display_text = target
+ else:
+ previous_committed = self._committed_text
+ self._committed_text = _merge_funasr_text(previous_committed, text)
+ self._online_text = ""
+ is_release_final = bool(self._stop_sent_at and payload.get("is_final"))
+ if is_release_final:
+ corrected = normalize_stream_text(
+ self._committed_text,
+ self._normalizations,
+ )
+ target = locked_live_input_target(
+ self._applied_text,
+ corrected,
+ self._locked_prefix_length,
+ )
+ self._display_text = target
+ else:
+ corrected_segment = committed_segment_delta(
+ previous_committed,
+ self._committed_text,
+ text,
+ )
+ if not corrected_segment:
+ return
+ target = join_calibrated_segment(
+ self._display_text[: self._display_segment_start],
+ corrected_segment,
+ )
+ target = normalize_stream_text(
+ target,
+ self._normalizations,
+ )
+ self._display_text = target
+ self._display_segment_start = len(target)
+ self._locked_prefix_length = len(target)
+ self._dedupe_next_online = True
+ self._dedupe_reference = text
+ self._publish(target)
+
+ def _publish(self, target: str) -> bool:
+ delete_count, suffix = live_input_edit(self._applied_text, target)
+ if not delete_count and not suffix:
+ return True
+ result = self.injector.edit(
+ delete_count,
+ suffix,
+ target_window=self._target_window,
+ )
+ with self._lock:
+ self._input_success = self._input_success and result.success
+ if result.success:
+ self._applied_text = target
+ elif not self._error:
+ self._error = result.message
+ return result.success
+
+ def _reset_runtime(self) -> None:
+ self._stop_event.set()
+ self._websocket = None
+ self._receiver = None
+ self._session_id = ""
+ self._target_window = 0
+ self._audio.clear()
+ self._pending.clear()
+ self._pre_roll.clear()
+ self._speech_started = False
+ self._consecutive_speech = 0
+ self._speech_blocks = 0
+ self._committed_text = ""
+ self._online_text = ""
+ self._display_text = ""
+ self._display_segment_start = 0
+ self._locked_prefix_length = 0
+ self._applied_text = ""
+ self._dedupe_next_online = False
+ self._dedupe_reference = ""
+ self._normalizations = {}
+ self._input_success = True
+ self._stop_sent_at = 0.0
+ self._last_post_stop_result_at = 0.0
+
+ def _wait_for_live_final(self) -> None:
+ with self._lock:
+ stop_sent_at = self._stop_sent_at
+ deadline = stop_sent_at + _final_wait_seconds()
+ minimum_finish = stop_sent_at + LIVE_FINAL_MIN_SECONDS
+ while not self._final_event.is_set():
+ now = time.monotonic()
+ if now >= deadline:
+ return
+ with self._lock:
+ last_result = self._last_post_stop_result_at
+ if (
+ now >= minimum_finish
+ and last_result >= stop_sent_at
+ and now - last_result >= LIVE_FINAL_QUIET_SECONDS
+ ):
+ return
+ self._final_event.wait(timeout=min(0.04, max(0.0, deadline - now)))
+
+
+def _final_wait_seconds() -> float:
+ raw = os.environ.get("VIBE_STICK_STREAM_FINAL_WAIT_SECONDS", "0.55")
+ try:
+ value = float(raw)
+ except ValueError:
+ value = 0.55
+ return max(LIVE_FINAL_MIN_SECONDS, min(3.0, value))
diff --git a/bridge/src/vibe_stick/audio/transcriber.py b/bridge/src/vibe_stick/audio/transcriber.py
index d8196a4..2b39ba3 100644
--- a/bridge/src/vibe_stick/audio/transcriber.py
+++ b/bridge/src/vibe_stick/audio/transcriber.py
@@ -7,6 +7,7 @@
import tomllib
import urllib.error
import urllib.request
+import wave
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@@ -17,6 +18,10 @@
GROQ_ASR_BASE_URL = "https://api.groq.com/openai/v1"
DEFAULT_ASR_MODEL = "whisper-large-v3-turbo"
DEFAULT_ASR_LANGUAGE = "zh"
+DEFAULT_FUNASR_BASE_URL = "ws://127.0.0.1:10095"
+FUNASR_AUDIO_CHUNK_BYTES = 1920
+
+_hotword_cache: tuple[float, dict[str, int], dict[str, str]] | None = None
@dataclass
@@ -109,7 +114,10 @@ def _transcribe_with_configured_asr(self, session_payload: dict[str, Any]) -> Tr
)
config = _load_asr_config()
- if config.get("provider") not in {"groq", "openai-compatible"} or not config.get("api_key"):
+ provider = config.get("provider")
+ if provider == "local-funasr" and config.get("base_url"):
+ return _transcribe_funasr(audio_file, config)
+ if provider not in {"groq", "openai-compatible"} or not config.get("api_key"):
return TranscriptionResult(
success=False,
message="No transcription adapter configured",
@@ -194,6 +202,10 @@ def _config_from_generic_env() -> dict[str, str]:
base_url = base_url or GROQ_ASR_BASE_URL
model = model or os.environ.get("VIBE_STICK_GROQ_MODEL", DEFAULT_ASR_MODEL).strip()
language = language or os.environ.get("VIBE_STICK_GROQ_LANGUAGE", DEFAULT_ASR_LANGUAGE).strip()
+ elif provider == "local-funasr":
+ base_url = base_url or DEFAULT_FUNASR_BASE_URL
+ model = model or "paraformer-zh-streaming"
+ language = language or DEFAULT_ASR_LANGUAGE
else:
model = model or DEFAULT_ASR_MODEL
language = language or DEFAULT_ASR_LANGUAGE
@@ -220,6 +232,10 @@ def _config_from_toml(data: dict[str, Any]) -> dict[str, str]:
base_url = base_url or GROQ_ASR_BASE_URL
model = str(data.get("groq_model") or model or DEFAULT_ASR_MODEL).strip()
language = str(data.get("groq_language") or language or DEFAULT_ASR_LANGUAGE).strip()
+ elif provider == "local-funasr":
+ base_url = base_url or DEFAULT_FUNASR_BASE_URL
+ model = model or "paraformer-zh-streaming"
+ language = language or DEFAULT_ASR_LANGUAGE
elif provider == "openai-compatible":
model = model or DEFAULT_ASR_MODEL
language = language or DEFAULT_ASR_LANGUAGE
@@ -253,6 +269,8 @@ def _asr_config(
def _normalize_asr_provider(raw: object) -> str:
value = str(raw or "").strip().lower()
+ if value in {"funasr", "local-funasr", "local_funasr"}:
+ return "local-funasr"
if value in {"groq", "openai-compatible"}:
return value
return ""
@@ -309,7 +327,7 @@ def _transcribe_openai_compatible_once(
headers={
"Authorization": f"Bearer {config['api_key']}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
- "User-Agent": "VibeStick/0.1 macOS",
+ "User-Agent": "VibeStick/0.1",
"Connection": "close",
},
)
@@ -350,7 +368,200 @@ def _transcription_url(base_url: str) -> str:
def _asr_label(provider: str) -> str:
- return "Groq" if provider == "groq" else "OpenAI-compatible"
+ if provider == "groq":
+ return "Groq"
+ if provider == "local-funasr":
+ return "Local FunASR"
+ return "OpenAI-compatible"
+
+
+def _transcribe_funasr(audio_file: Path, config: dict[str, str]) -> TranscriptionResult:
+ last_result = TranscriptionResult(
+ success=False,
+ message="Local FunASR transcription failed",
+ source="local-funasr",
+ )
+ for attempt in range(1, _asr_attempt_count() + 1):
+ result = _transcribe_funasr_once(audio_file, config, attempt)
+ if result.success:
+ return result
+ last_result = result
+ if attempt < _asr_attempt_count():
+ time.sleep(min(0.5, 0.15 * attempt))
+ return last_result
+
+
+def _transcribe_funasr_once(
+ audio_file: Path,
+ config: dict[str, str],
+ attempt: int,
+ connector=None, # noqa: ANN001
+) -> TranscriptionResult:
+ try:
+ with wave.open(str(audio_file), "rb") as wav_file:
+ channels = wav_file.getnchannels()
+ sample_width = wav_file.getsampwidth()
+ sample_rate = wav_file.getframerate()
+ compression = wav_file.getcomptype()
+ audio = wav_file.readframes(wav_file.getnframes())
+ except (OSError, EOFError, wave.Error) as exc:
+ return TranscriptionResult(
+ success=False,
+ message=f"Could not read WAV audio file: {exc}",
+ source="local-funasr",
+ )
+ if channels != 1 or sample_width != 2 or compression != "NONE":
+ return TranscriptionResult(
+ success=False,
+ message="Local FunASR requires mono PCM16 WAV audio",
+ source="local-funasr",
+ )
+ if not audio:
+ return TranscriptionResult(
+ success=False,
+ message="Local FunASR received an empty recording",
+ source="local-funasr",
+ )
+
+ if connector is None:
+ try:
+ from websockets.sync.client import connect as connector
+ except ImportError:
+ return TranscriptionResult(
+ success=False,
+ message="Local FunASR requires the 'websockets' package",
+ source="local-funasr",
+ )
+
+ hotwords, normalizations = _load_funasr_hotwords()
+ start_message = {
+ "mode": "2pass",
+ "chunk_size": [5, 10, 5],
+ "chunk_interval": 10,
+ "encoder_chunk_look_back": 4,
+ "decoder_chunk_look_back": 0,
+ "audio_fs": sample_rate,
+ "wav_name": audio_file.name,
+ "wav_format": "pcm",
+ "is_speaking": True,
+ "hotwords": json.dumps(hotwords, ensure_ascii=False) if hotwords else "",
+ "itn": True,
+ }
+ timeout = _asr_timeout_seconds()
+ chunk_delay = _funasr_chunk_delay_seconds()
+ messages: list[dict[str, Any]] = []
+ try:
+ with connector(
+ config["base_url"],
+ subprotocols=["binary"],
+ ping_interval=None,
+ max_size=None,
+ open_timeout=min(8, timeout),
+ close_timeout=3,
+ ) as websocket:
+ websocket.send(json.dumps(start_message, ensure_ascii=False))
+ for offset in range(0, len(audio), FUNASR_AUDIO_CHUNK_BYTES):
+ websocket.send(audio[offset : offset + FUNASR_AUDIO_CHUNK_BYTES])
+ if chunk_delay:
+ time.sleep(chunk_delay)
+ websocket.send(json.dumps({"is_speaking": False}))
+
+ deadline = time.monotonic() + timeout
+ while time.monotonic() < deadline:
+ raw_message = websocket.recv(timeout=max(0.05, deadline - time.monotonic()))
+ payload = json.loads(raw_message)
+ if isinstance(payload, dict):
+ messages.append(payload)
+ if payload.get("is_final"):
+ break
+ except (OSError, TimeoutError, ValueError, json.JSONDecodeError) as exc:
+ return TranscriptionResult(
+ success=False,
+ message=f"Local FunASR transcription failed on attempt {attempt}: {exc}",
+ source="local-funasr",
+ )
+
+ transcript = _funasr_final_text(messages)
+ transcript = _apply_funasr_normalizations(transcript, normalizations)
+ if not transcript:
+ return TranscriptionResult(
+ success=False,
+ message="Local FunASR returned no transcript",
+ source="local-funasr",
+ )
+ return TranscriptionResult(
+ text=transcript,
+ success=True,
+ message="Transcript supplied by Local FunASR ASR",
+ source="local-funasr",
+ )
+
+
+def _funasr_final_text(messages: list[dict[str, Any]]) -> str:
+ offline = ""
+ online = ""
+ for message in messages:
+ text = str(message.get("text") or "").strip()
+ if not text:
+ continue
+ if message.get("mode") == "2pass-offline":
+ offline = _merge_funasr_text(offline, text)
+ elif message.get("mode") == "2pass-online":
+ online += text
+ return (offline or online).strip()
+
+
+def _merge_funasr_text(previous: str, current: str) -> str:
+ if not previous or current.startswith(previous):
+ return current
+ if previous.startswith(current):
+ return previous
+ max_overlap = min(len(previous), len(current))
+ for size in range(max_overlap, 0, -1):
+ if previous[-size:] == current[:size]:
+ return previous + current[size:]
+ return previous + current
+
+
+def _load_funasr_hotwords() -> tuple[dict[str, int], dict[str, str]]:
+ global _hotword_cache
+ url = os.environ.get("VIBE_STICK_FUNASR_HOTWORDS_URL", "").strip()
+ if not url:
+ return {}, {}
+ now = time.monotonic()
+ if _hotword_cache and now - _hotword_cache[0] < 300:
+ return _hotword_cache[1], _hotword_cache[2]
+ try:
+ with urllib.request.urlopen(url, timeout=2) as response:
+ payload = json.loads(response.read().decode("utf-8"))
+ hotwords = {
+ str(word): int(weight)
+ for word, weight in dict(payload.get("hotwords") or {}).items()
+ }
+ normalizations = {
+ str(source): str(target)
+ for source, target in dict(payload.get("normalizations") or {}).items()
+ }
+ _hotword_cache = (now, hotwords, normalizations)
+ return hotwords, normalizations
+ except (OSError, TimeoutError, ValueError, json.JSONDecodeError):
+ return {}, {}
+
+
+def _apply_funasr_normalizations(text: str, normalizations: dict[str, str]) -> str:
+ for source in sorted(normalizations, key=len, reverse=True):
+ text = text.replace(source, normalizations[source])
+ text = text.replace(source.lower(), normalizations[source])
+ return text
+
+
+def _funasr_chunk_delay_seconds() -> float:
+ raw = os.environ.get("VIBE_STICK_FUNASR_CHUNK_DELAY_MS", "2")
+ try:
+ delay_ms = float(raw)
+ except ValueError:
+ delay_ms = 2.0
+ return max(0.0, min(60.0, delay_ms)) / 1000.0
def _discard_http_error_body(exc: urllib.error.HTTPError) -> None:
diff --git a/bridge/src/vibe_stick/codex/local_observer.py b/bridge/src/vibe_stick/codex/local_observer.py
index ee99181..925e6f7 100644
--- a/bridge/src/vibe_stick/codex/local_observer.py
+++ b/bridge/src/vibe_stick/codex/local_observer.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
+import platform
import subprocess
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
@@ -198,12 +199,20 @@ def _is_newer(value: datetime, other: datetime | None) -> bool:
def _codex_process_running() -> bool:
+ command = ["ps", "-axo", "command="]
+ if platform.system() == "Windows":
+ command = [
+ "powershell.exe", "-NoProfile", "-NonInteractive", "-Command",
+ "[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); Get-Process | ForEach-Object { $_.ProcessName }",
+ ]
try:
result = subprocess.run(
- ["ps", "-axo", "command="],
+ command,
check=False,
capture_output=True,
text=True,
+ encoding="utf-8",
+ errors="replace",
timeout=2,
)
except (OSError, subprocess.TimeoutExpired):
@@ -211,12 +220,17 @@ def _codex_process_running() -> bool:
if result.returncode != 0:
return False
- for line in result.stdout.splitlines():
- lower = line.lower()
+ for line in (result.stdout or "").splitlines():
+ lower = line.lower().replace("\\", "/")
if "/applications/codex.app/" in lower:
return True
if "codex app-server" in lower:
return True
+ executable = lower.split()[0].strip('"').rsplit("/", 1)[-1]
+ if executable in {"codex", "codex.exe", "codex.cmd"}:
+ return True
+ if "codex.exe" in lower or "codex.cmd" in lower:
+ return True
return False
diff --git a/bridge/src/vibe_stick/config/paths.py b/bridge/src/vibe_stick/config/paths.py
index 4e6679e..be9d18d 100644
--- a/bridge/src/vibe_stick/config/paths.py
+++ b/bridge/src/vibe_stick/config/paths.py
@@ -1,11 +1,25 @@
from __future__ import annotations
+import os
+import platform
from pathlib import Path
-APP_SUPPORT_DIR = (
- Path.home() / "Library" / "Application Support" / "VibeStick"
-)
+def _app_support_dir() -> Path:
+ override = os.environ.get("VIBE_STICK_APP_SUPPORT_DIR", "").strip()
+ if override:
+ return Path(override).expanduser()
+ system = platform.system()
+ if system == "Windows":
+ base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
+ return Path(base) / "VibeStick" if base else Path.home() / "AppData" / "Local" / "VibeStick"
+ if system == "Darwin":
+ return Path.home() / "Library" / "Application Support" / "VibeStick"
+ data_home = os.environ.get("XDG_DATA_HOME", "").strip()
+ return (Path(data_home).expanduser() if data_home else Path.home() / ".local" / "share") / "VibeStick"
+
+
+APP_SUPPORT_DIR = _app_support_dir()
STATE_PATH = APP_SUPPORT_DIR / "state.json"
QUOTA_PATH = APP_SUPPORT_DIR / "quota.json"
CLAUDE_QUOTA_PATH = APP_SUPPORT_DIR / "claude-quota.json"
diff --git a/bridge/src/vibe_stick/paste/input_injector.py b/bridge/src/vibe_stick/paste/input_injector.py
index 73c2909..a115637 100644
--- a/bridge/src/vibe_stick/paste/input_injector.py
+++ b/bridge/src/vibe_stick/paste/input_injector.py
@@ -3,22 +3,179 @@
import platform
import subprocess
import time
+import ctypes
+import ctypes.wintypes
from dataclasses import dataclass
+INPUT_KEYBOARD = 1
+KEYEVENTF_KEYUP = 0x0002
+KEYEVENTF_UNICODE = 0x0004
+
+
+class KEYBDINPUT(ctypes.Structure):
+ _fields_ = [
+ ("wVk", ctypes.wintypes.WORD),
+ ("wScan", ctypes.wintypes.WORD),
+ ("dwFlags", ctypes.wintypes.DWORD),
+ ("time", ctypes.wintypes.DWORD),
+ ("dwExtraInfo", ctypes.c_size_t),
+ ]
+
+
+class MOUSEINPUT(ctypes.Structure):
+ _fields_ = [
+ ("dx", ctypes.wintypes.LONG),
+ ("dy", ctypes.wintypes.LONG),
+ ("mouseData", ctypes.wintypes.DWORD),
+ ("dwFlags", ctypes.wintypes.DWORD),
+ ("time", ctypes.wintypes.DWORD),
+ ("dwExtraInfo", ctypes.c_size_t),
+ ]
+
+
+class HARDWAREINPUT(ctypes.Structure):
+ _fields_ = [
+ ("uMsg", ctypes.wintypes.DWORD),
+ ("wParamL", ctypes.wintypes.WORD),
+ ("wParamH", ctypes.wintypes.WORD),
+ ]
+
+
+class INPUTUNION(ctypes.Union):
+ # INPUT is a tagged union. Even though streaming text only uses `ki`, the
+ # mouse member must be present so ctypes gives INPUT the Win32 ABI size
+ # (40 bytes on 64-bit Windows). A keyboard-only union is 32 bytes and
+ # SendInput rejects every event with ERROR_INVALID_PARAMETER.
+ _fields_ = [
+ ("mi", MOUSEINPUT),
+ ("ki", KEYBDINPUT),
+ ("hi", HARDWAREINPUT),
+ ]
+
+
+class INPUT(ctypes.Structure):
+ _anonymous_ = ("data",)
+ _fields_ = [("type", ctypes.wintypes.DWORD), ("data", INPUTUNION)]
+
+
@dataclass
class PasteResult:
success: bool
message: str
-class MacPasteInjector:
+class PasteInjector:
+ def begin_session(self, session_id: str) -> PasteResult:
+ del session_id
+ return PasteResult(True, "Local input session started")
+
+ def end_session(self, session_id: str = "") -> PasteResult:
+ del session_id
+ return PasteResult(True, "Local input session ended")
+
+ def capture_target(self) -> int:
+ if platform.system() != "Windows":
+ return 0
+ try:
+ ctypes.windll.user32.GetForegroundWindow.restype = ctypes.wintypes.HWND
+ return int(ctypes.windll.user32.GetForegroundWindow() or 0)
+ except (AttributeError, OSError):
+ return 0
+
+ def edit(self, delete_count: int, text: str, *, target_window: int = 0) -> PasteResult:
+ if platform.system() != "Windows":
+ return PasteResult(False, "Streaming input is only available on Windows")
+ try:
+ user32 = ctypes.windll.user32
+ if target_window:
+ if not user32.IsWindow(target_window):
+ return PasteResult(False, "The original input window is no longer available")
+ if int(user32.GetForegroundWindow() or 0) != target_window:
+ user32.SetForegroundWindow(target_window)
+
+ inputs: list[INPUT] = []
+ for _ in range(max(0, delete_count)):
+ inputs.append(INPUT(type=INPUT_KEYBOARD, ki=KEYBDINPUT(0x08, 0, 0, 0, 0)))
+ inputs.append(
+ INPUT(type=INPUT_KEYBOARD, ki=KEYBDINPUT(0x08, 0, KEYEVENTF_KEYUP, 0, 0))
+ )
+ utf16 = text.encode("utf-16-le")
+ for offset in range(0, len(utf16), 2):
+ code_unit = int.from_bytes(utf16[offset : offset + 2], "little")
+ if code_unit in (0x000A, 0x000D):
+ inputs.append(INPUT(type=INPUT_KEYBOARD, ki=KEYBDINPUT(0x0D, 0, 0, 0, 0)))
+ inputs.append(
+ INPUT(type=INPUT_KEYBOARD, ki=KEYBDINPUT(0x0D, 0, KEYEVENTF_KEYUP, 0, 0))
+ )
+ continue
+ inputs.append(
+ INPUT(type=INPUT_KEYBOARD, ki=KEYBDINPUT(0, code_unit, KEYEVENTF_UNICODE, 0, 0))
+ )
+ inputs.append(
+ INPUT(
+ type=INPUT_KEYBOARD,
+ ki=KEYBDINPUT(0, code_unit, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, 0),
+ )
+ )
+ if not inputs:
+ return PasteResult(True, "No streaming edit was needed")
+ user32.SendInput.argtypes = [
+ ctypes.wintypes.UINT,
+ ctypes.POINTER(INPUT),
+ ctypes.c_int,
+ ]
+ user32.SendInput.restype = ctypes.wintypes.UINT
+ input_array = (INPUT * len(inputs))(*inputs)
+ sent = int(user32.SendInput(len(inputs), input_array, ctypes.sizeof(INPUT)))
+ if sent != len(inputs):
+ error = int(ctypes.windll.kernel32.GetLastError())
+ return PasteResult(
+ False,
+ f"Streaming input sent {sent}/{len(inputs)} events (Win32 error {error})",
+ )
+ return PasteResult(True, "Streaming text updated in the focused app")
+ except (AttributeError, OSError) as exc:
+ return PasteResult(False, f"Windows streaming input failed: {exc}")
+
+ def press_enter(self) -> PasteResult:
+ system = platform.system()
+ if system == "Windows":
+ try:
+ user32 = ctypes.windll.user32
+ key_up = 0x0002
+ vk_return = 0x0D
+ user32.keybd_event(vk_return, 0, 0, 0)
+ user32.keybd_event(vk_return, 0, key_up, 0)
+ except (AttributeError, OSError) as exc:
+ return PasteResult(False, f"Windows Enter key failed: {exc}")
+ return PasteResult(True, "Pressed Enter in the focused app")
+ if system == "Darwin":
+ try:
+ result = subprocess.run(
+ ["osascript", "-e", 'tell application "System Events" to key code 36'],
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=5,
+ )
+ except (OSError, subprocess.TimeoutExpired) as exc:
+ return PasteResult(False, f"macOS Enter key failed: {exc}")
+ if result.returncode != 0:
+ message = (result.stderr or result.stdout or "macOS Enter key failed").strip()
+ return PasteResult(False, message)
+ return PasteResult(True, "Pressed Enter in the focused app")
+ return PasteResult(False, "Automatic Enter is only available on macOS and Windows")
+
def paste(self, text: str, press_enter: bool = False) -> PasteResult:
text = text.strip()
if not text:
return PasteResult(False, "No text to paste")
- if platform.system() != "Darwin":
- return PasteResult(False, "Automatic paste is only available on macOS")
+ system = platform.system()
+ if system == "Windows":
+ return self._paste_windows(text, press_enter)
+ if system != "Darwin":
+ return PasteResult(False, "Automatic paste is only available on macOS and Windows")
previous_text = self._read_clipboard()
set_result = self._set_clipboard(text)
@@ -47,6 +204,104 @@ def paste(self, text: str, press_enter: bool = False) -> PasteResult:
return PasteResult(False, message)
return PasteResult(True, "Pasted into the focused app")
+ def _paste_windows(self, text: str, press_enter: bool) -> PasteResult:
+ previous_text = self._read_windows_clipboard()
+ set_result = self._set_windows_clipboard(text)
+ if not set_result.success:
+ return set_result
+ try:
+ user32 = ctypes.windll.user32
+ key_up = 0x0002
+ vk_control = 0x11
+ vk_v = 0x56
+ vk_return = 0x0D
+ user32.keybd_event(vk_control, 0, 0, 0)
+ user32.keybd_event(vk_v, 0, 0, 0)
+ user32.keybd_event(vk_v, 0, key_up, 0)
+ user32.keybd_event(vk_control, 0, key_up, 0)
+ if press_enter:
+ time.sleep(0.12)
+ user32.keybd_event(vk_return, 0, 0, 0)
+ user32.keybd_event(vk_return, 0, key_up, 0)
+ time.sleep(0.2)
+ except (AttributeError, OSError) as exc:
+ return PasteResult(False, f"Windows paste failed: {exc}")
+ finally:
+ if previous_text is not None:
+ self._set_windows_clipboard(previous_text)
+ return PasteResult(True, "Pasted into the focused app")
+
+ def _read_windows_clipboard(self) -> str | None:
+ user32 = ctypes.windll.user32
+ kernel32 = ctypes.windll.kernel32
+ user32.GetClipboardData.restype = ctypes.c_void_p
+ kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
+ kernel32.GlobalLock.restype = ctypes.c_void_p
+ kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
+ cf_unicode_text = 13
+ if not self._open_windows_clipboard(user32):
+ return None
+ try:
+ handle = user32.GetClipboardData(cf_unicode_text)
+ if not handle:
+ return None
+ pointer = kernel32.GlobalLock(handle)
+ if not pointer:
+ return None
+ try:
+ return ctypes.wstring_at(pointer)
+ finally:
+ kernel32.GlobalUnlock(handle)
+ finally:
+ user32.CloseClipboard()
+
+ def _set_windows_clipboard(self, text: str) -> PasteResult:
+ user32 = ctypes.windll.user32
+ kernel32 = ctypes.windll.kernel32
+ kernel32.GlobalAlloc.argtypes = [ctypes.c_uint, ctypes.c_size_t]
+ kernel32.GlobalAlloc.restype = ctypes.c_void_p
+ kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
+ kernel32.GlobalLock.restype = ctypes.c_void_p
+ kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
+ kernel32.GlobalFree.argtypes = [ctypes.c_void_p]
+ user32.SetClipboardData.argtypes = [ctypes.c_uint, ctypes.c_void_p]
+ user32.SetClipboardData.restype = ctypes.c_void_p
+ cf_unicode_text = 13
+ gmem_moveable = 0x0002
+ data = (text + "\0").encode("utf-16-le")
+ if not self._open_windows_clipboard(user32):
+ return PasteResult(False, "Could not open the Windows clipboard")
+ handle = None
+ try:
+ if not user32.EmptyClipboard():
+ return PasteResult(False, "Could not clear the Windows clipboard")
+ handle = kernel32.GlobalAlloc(gmem_moveable, len(data))
+ if not handle:
+ return PasteResult(False, "Could not allocate Windows clipboard memory")
+ pointer = kernel32.GlobalLock(handle)
+ if not pointer:
+ return PasteResult(False, "Could not lock Windows clipboard memory")
+ try:
+ ctypes.memmove(pointer, data, len(data))
+ finally:
+ kernel32.GlobalUnlock(handle)
+ if not user32.SetClipboardData(cf_unicode_text, handle):
+ return PasteResult(False, "Could not update the Windows clipboard")
+ handle = None # Clipboard owns the allocation after SetClipboardData succeeds.
+ return PasteResult(True, "Clipboard updated")
+ finally:
+ if handle:
+ kernel32.GlobalFree(handle)
+ user32.CloseClipboard()
+
+ @staticmethod
+ def _open_windows_clipboard(user32: object) -> bool:
+ for _ in range(10):
+ if user32.OpenClipboard(None):
+ return True
+ time.sleep(0.02)
+ return False
+
def _read_clipboard(self) -> str | None:
try:
result = subprocess.run(
@@ -78,3 +333,7 @@ def _set_clipboard(self, text: str) -> PasteResult:
message = (result.stderr or "Clipboard write failed").strip()
return PasteResult(False, message)
return PasteResult(True, "Clipboard updated")
+
+
+# Backwards-compatible name for callers and third-party imports.
+MacPasteInjector = PasteInjector
diff --git a/bridge/src/vibe_stick/paste/input_relay.py b/bridge/src/vibe_stick/paste/input_relay.py
new file mode 100644
index 0000000..663ae94
--- /dev/null
+++ b/bridge/src/vibe_stick/paste/input_relay.py
@@ -0,0 +1,238 @@
+from __future__ import annotations
+
+import json
+import threading
+import uuid
+from dataclasses import dataclass, field
+from typing import Any, Callable
+
+from vibe_stick.paste.input_injector import PasteResult
+
+
+@dataclass
+class _PendingRequest:
+ event: threading.Event = field(default_factory=threading.Event)
+ response: dict[str, Any] | None = None
+
+
+class InputRelayHub:
+ """Thread-safe bridge between the Unraid service and one Windows client."""
+
+ def __init__(self, token: str) -> None:
+ self.token = token
+ self._lock = threading.RLock()
+ self._send_lock = threading.Lock()
+ self._connection: Any = None
+ self._pending: dict[str, _PendingRequest] = {}
+ self._server: Any = None
+ self._thread: threading.Thread | None = None
+ self._provider_status_handler: Callable[[dict[str, Any]], None] | None = None
+ self._device_status_provider: Callable[[], list[dict[str, Any]]] | None = None
+
+ @property
+ def connected(self) -> bool:
+ with self._lock:
+ return self._connection is not None
+
+ def start(self, host: str, port: int) -> None:
+ if self._thread and self._thread.is_alive():
+ return
+ self._thread = threading.Thread(
+ target=self._serve,
+ args=(host, port),
+ name="vibestick-input-relay",
+ daemon=True,
+ )
+ self._thread.start()
+
+ def set_provider_status_handler(
+ self,
+ handler: Callable[[dict[str, Any]], None] | None,
+ ) -> None:
+ """Accept authenticated task status reports from the Windows client."""
+
+ with self._lock:
+ self._provider_status_handler = handler
+
+ def set_device_status_provider(
+ self,
+ provider: Callable[[], list[dict[str, Any]]] | None,
+ ) -> None:
+ """Expose current Lot device status to newly connected Windows clients."""
+
+ with self._lock:
+ self._device_status_provider = provider
+
+ def _serve(self, host: str, port: int) -> None:
+ from websockets.sync.server import serve
+
+ with serve(
+ self._handle_connection,
+ host,
+ port,
+ ping_interval=20,
+ ping_timeout=20,
+ max_size=256_000,
+ ) as server:
+ self._server = server
+ print(f"VibeStick input relay listening on ws://{host}:{port}", flush=True)
+ server.serve_forever()
+
+ def _handle_connection(self, connection: Any) -> None:
+ from websockets.exceptions import ConnectionClosed
+
+ try:
+ raw = connection.recv(timeout=5)
+ hello = json.loads(raw) if isinstance(raw, str) else {}
+ if (
+ not isinstance(hello, dict)
+ or hello.get("type") != "hello"
+ or hello.get("role") != "windows"
+ or str(hello.get("token") or "") != self.token
+ ):
+ connection.close(code=1008, reason="unauthorized")
+ return
+ with self._lock:
+ previous = self._connection
+ self._connection = connection
+ if previous and previous is not connection:
+ try:
+ previous.close(code=1012, reason="replaced")
+ except OSError:
+ pass
+ with self._lock:
+ device_status_provider = self._device_status_provider
+ devices = device_status_provider() if device_status_provider is not None else []
+ connection.send(json.dumps({"type": "hello_ack", "ok": True, "devices": devices}))
+ try:
+ for message in connection:
+ if not isinstance(message, str):
+ continue
+ try:
+ payload = json.loads(message)
+ except json.JSONDecodeError:
+ continue
+ if not isinstance(payload, dict):
+ continue
+ if payload.get("type") == "provider_status":
+ with self._lock:
+ handler = self._provider_status_handler
+ if handler is not None:
+ try:
+ handler(payload)
+ except Exception as exc:
+ print(
+ f"Windows provider status rejected error={exc.__class__.__name__}",
+ flush=True,
+ )
+ continue
+ if payload.get("type") != "ack":
+ continue
+ request_id = str(payload.get("request_id") or "")
+ with self._lock:
+ pending = self._pending.get(request_id)
+ if pending:
+ pending.response = payload
+ pending.event.set()
+ except (ConnectionClosed, OSError):
+ # Client restarts, sleep and network changes are routine. The
+ # outer listener accepts the reconnect; don't emit a traceback.
+ pass
+ finally:
+ with self._lock:
+ if self._connection is connection:
+ self._connection = None
+ pending_requests = list(self._pending.values())
+ for pending in pending_requests:
+ pending.event.set()
+
+ def request(self, event_type: str, *, timeout: float = 1.5, **payload: Any) -> PasteResult:
+ request_id = uuid.uuid4().hex
+ pending = _PendingRequest()
+ with self._lock:
+ connection = self._connection
+ if connection is None:
+ return PasteResult(False, "Windows input client is not connected")
+ self._pending[request_id] = pending
+ message = {"type": event_type, "request_id": request_id, **payload}
+ try:
+ with self._send_lock:
+ connection.send(json.dumps(message, ensure_ascii=False))
+ except (OSError, RuntimeError) as exc:
+ with self._lock:
+ self._pending.pop(request_id, None)
+ return PasteResult(False, f"Windows input relay send failed: {exc}")
+ if not pending.event.wait(timeout):
+ with self._lock:
+ self._pending.pop(request_id, None)
+ return PasteResult(False, "Windows input client did not acknowledge the request")
+ with self._lock:
+ self._pending.pop(request_id, None)
+ response = pending.response or {}
+ return PasteResult(
+ bool(response.get("success")),
+ str(response.get("message") or "Windows input request failed"),
+ )
+
+ def send_event(self, payload: dict[str, Any]) -> bool:
+ with self._lock:
+ connection = self._connection
+ if connection is None:
+ return False
+ try:
+ with self._send_lock:
+ connection.send(json.dumps(payload, ensure_ascii=False))
+ return True
+ except (OSError, RuntimeError):
+ return False
+
+
+class RelayPasteInjector:
+ def __init__(self, hub: InputRelayHub) -> None:
+ self.hub = hub
+ self.session_id = ""
+
+ def begin_session(self, session_id: str) -> PasteResult:
+ result = self.hub.request("session_start", session_id=session_id, timeout=2.0)
+ if result.success:
+ self.session_id = session_id
+ return result
+
+ def end_session(self, session_id: str = "") -> PasteResult:
+ active_session = session_id or self.session_id
+ if not active_session:
+ return PasteResult(True, "No relay session was active")
+ result = self.hub.request("session_end", session_id=active_session, timeout=1.0)
+ if active_session == self.session_id:
+ self.session_id = ""
+ return result
+
+ def capture_target(self) -> int:
+ return 1 if self.session_id else 0
+
+ def edit(self, delete_count: int, text: str, *, target_window: int = 0) -> PasteResult:
+ del target_window
+ if not self.session_id:
+ return PasteResult(False, "No Windows relay session is active")
+ return self.hub.request(
+ "edit",
+ session_id=self.session_id,
+ delete_count=max(0, int(delete_count)),
+ text=text,
+ )
+
+ def press_enter(self) -> PasteResult:
+ return self.hub.request("press_enter")
+
+ def paste(self, text: str, press_enter: bool = False) -> PasteResult:
+ session_id = uuid.uuid4().hex
+ started = self.begin_session(session_id)
+ if not started.success:
+ return started
+ try:
+ result = self.edit(0, text)
+ if result.success and press_enter:
+ result = self.press_enter()
+ return result
+ finally:
+ self.end_session(session_id)
diff --git a/bridge/src/vibe_stick/protocol/state.py b/bridge/src/vibe_stick/protocol/state.py
index 72747a9..e7701b1 100644
--- a/bridge/src/vibe_stick/protocol/state.py
+++ b/bridge/src/vibe_stick/protocol/state.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from dataclasses import asdict, dataclass
+from dataclasses import asdict, dataclass, field
from datetime import datetime
from enum import StrEnum
from typing import Any
@@ -31,6 +31,7 @@ class CodexState:
quota_7d_remaining: int | None = None
quota_updated_at: str = ""
quota_stale: bool = False
+ account_name: str = ""
@dataclass
@@ -44,6 +45,7 @@ class ProviderState:
quota_7d_remaining: int | None = None
quota_updated_at: str = ""
quota_stale: bool = False
+ account_name: str = ""
@dataclass
@@ -63,6 +65,7 @@ class VibeStickState:
provider: ProviderState
codex: CodexState
alert: AlertState
+ providers: dict[str, ProviderState] = field(default_factory=dict)
def to_jsonable(self) -> dict[str, Any]:
data = asdict(self)
@@ -70,6 +73,8 @@ def to_jsonable(self) -> dict[str, Any]:
data["provider"]["status"] = self.provider.status.value
data["codex"]["status"] = self.codex.status.value
data["alert"]["type"] = self.alert.type.value
+ for provider_id, provider in self.providers.items():
+ data["providers"][provider_id]["status"] = provider.status.value
return data
@@ -83,6 +88,7 @@ def event_id(prefix: str) -> str:
def state_from_dict(data: dict[str, Any]) -> VibeStickState:
provider_data = data.get("provider", {})
+ providers_data = data.get("providers", {})
codex_data = data.get("codex", {})
codex_data = codex_data if isinstance(codex_data, dict) else {}
alert_data = data.get("alert", {})
@@ -102,12 +108,18 @@ def state_from_dict(data: dict[str, Any]) -> VibeStickState:
quota_7d_remaining=codex_data.get("quota_7d_remaining"),
quota_updated_at=str(codex_data.get("quota_updated_at") or ""),
quota_stale=bool(codex_data.get("quota_stale", False)),
+ account_name=str(codex_data.get("account_name") or ""),
),
alert=AlertState(
event_id=str(alert_data.get("event_id") or ""),
type=AlertType(alert_data.get("type", AlertType.NONE.value)),
message=str(alert_data.get("message") or ""),
),
+ providers=_providers_from_dict(
+ providers_data if isinstance(providers_data, dict) else {},
+ provider_state,
+ codex_data,
+ ),
)
@@ -123,6 +135,7 @@ def _provider_state_from_dict(provider_data: dict[str, Any], codex_data: dict[st
quota_7d_remaining=provider_data.get("quota_7d_remaining"),
quota_updated_at=str(provider_data.get("quota_updated_at") or ""),
quota_stale=bool(provider_data.get("quota_stale", False)),
+ account_name=str(provider_data.get("account_name") or ""),
)
return ProviderState(
@@ -135,9 +148,28 @@ def _provider_state_from_dict(provider_data: dict[str, Any], codex_data: dict[st
quota_7d_remaining=codex_data.get("quota_7d_remaining"),
quota_updated_at=str(codex_data.get("quota_updated_at") or ""),
quota_stale=bool(codex_data.get("quota_stale", False)),
+ account_name=str(codex_data.get("account_name") or ""),
)
+def _providers_from_dict(
+ providers_data: dict[str, Any],
+ active_provider: ProviderState,
+ codex_data: dict[str, Any],
+) -> dict[str, ProviderState]:
+ providers: dict[str, ProviderState] = {}
+ for provider_id, raw_provider in providers_data.items():
+ if not isinstance(raw_provider, dict):
+ continue
+ normalized = dict(raw_provider)
+ normalized.setdefault("id", str(provider_id))
+ providers[str(provider_id)] = _provider_state_from_dict(normalized, codex_data)
+ providers.setdefault(active_provider.id, active_provider)
+ if "codex" not in providers and codex_data:
+ providers["codex"] = _provider_state_from_dict({}, codex_data)
+ return providers
+
+
def default_state() -> VibeStickState:
codex = CodexState(
status=AgentStatus.RUNNING,
@@ -146,6 +178,7 @@ def default_state() -> VibeStickState:
quota_7d_remaining=None,
quota_updated_at="",
quota_stale=False,
+ account_name="",
)
return VibeStickState(
time=now_time_text(),
@@ -163,7 +196,9 @@ def default_state() -> VibeStickState:
quota_7d_remaining=codex.quota_7d_remaining,
quota_updated_at=codex.quota_updated_at,
quota_stale=codex.quota_stale,
+ account_name=codex.account_name,
),
codex=codex,
alert=AlertState(event_id="", type=AlertType.NONE, message=""),
+ providers={},
)
diff --git a/bridge/src/vibe_stick/providers/base.py b/bridge/src/vibe_stick/providers/base.py
index cf61fd8..917d89e 100644
--- a/bridge/src/vibe_stick/providers/base.py
+++ b/bridge/src/vibe_stick/providers/base.py
@@ -22,6 +22,7 @@ class ProviderObservation:
alert_message: str
alert_event_id: str
latest_event_timestamp: datetime | None = None
+ account_name: str = ""
class Provider(Protocol):
diff --git a/bridge/src/vibe_stick/providers/claude.py b/bridge/src/vibe_stick/providers/claude.py
index 753cbcb..b7a5b55 100644
--- a/bridge/src/vibe_stick/providers/claude.py
+++ b/bridge/src/vibe_stick/providers/claude.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
+import platform
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
@@ -122,12 +123,20 @@ def _is_latest(ts: datetime) -> bool:
def _claude_process_running() -> bool:
+ command = ["ps", "-axo", "command="]
+ if platform.system() == "Windows":
+ command = [
+ "powershell.exe", "-NoProfile", "-NonInteractive", "-Command",
+ "[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); Get-Process | ForEach-Object { $_.ProcessName }",
+ ]
try:
result = subprocess.run(
- ["ps", "-axo", "command="],
+ command,
check=False,
capture_output=True,
text=True,
+ encoding="utf-8",
+ errors="replace",
timeout=2,
)
except (OSError, subprocess.TimeoutExpired):
@@ -135,14 +144,18 @@ def _claude_process_running() -> bool:
if result.returncode != 0:
return False
- for line in result.stdout.splitlines():
+ for line in (result.stdout or "").splitlines():
lower = line.strip().lower()
if not lower:
continue
if "claude.app/contents/macos/claude" in lower:
return True
- executable = lower.split()[0].rsplit("/", 1)[-1]
- if executable == "claude":
+ executable = lower.split()[0].strip('"').replace("\\", "/").rsplit("/", 1)[-1]
+ if executable in {"claude", "claude.exe", "claude.cmd"}:
+ return True
+ if "claude.exe" in lower or "claude.cmd" in lower:
+ return True
+ if "@anthropic-ai/claude-code" in lower or "claude-code/cli.js" in lower.replace("\\", "/"):
return True
return False
diff --git a/bridge/src/vibe_stick/server/app.py b/bridge/src/vibe_stick/server/app.py
index 9df0f96..4a5592c 100644
--- a/bridge/src/vibe_stick/server/app.py
+++ b/bridge/src/vibe_stick/server/app.py
@@ -35,12 +35,25 @@
from vibe_stick.providers.base import ProviderObservation
from vibe_stick.providers.claude import observe_claude
from vibe_stick.providers.codex import observe_codex
+from vibe_stick.paste.input_relay import InputRelayHub, RelayPasteInjector
+from vibe_stick.usage.sub2 import DEFAULT_BASE_URL as SUB2_DEFAULT_BASE_URL
+from vibe_stick.usage.sub2 import Sub2UsageSnapshot, fetch_sub2_usage
MANUAL_STATUS_SECONDS = 60
BRIDGE_NAME = "vibestick-bridge"
DEFAULT_MAX_RECORDING_AUDIO_BYTES = 2_000_000
DEFAULT_CLAUDE_USAGE_INTERVAL_SECONDS = 300
MIN_CLAUDE_USAGE_INTERVAL_SECONDS = 30
+DEFAULT_RECORDING_IDLE_TIMEOUT_SECONDS = 4.0
+DEFAULT_RECORDING_MAX_SECONDS = 125.0
+DEFAULT_SUB2_USAGE_INTERVAL_SECONDS = 60
+MIN_SUB2_USAGE_INTERVAL_SECONDS = 30
+DEFAULT_SUB2_CODEX_ACCOUNT_ID = 52
+DEFAULT_SUB2_CLAUDE_ACCOUNT_ID = 29
+REMOTE_PROVIDER_STATUS_TTL_SECONDS = 30.0
+LOT_DEVICE_TTL_SECONDS = 10.0
+LOT_DEVICE_WATCHDOG_INTERVAL_SECONDS = 1.0
+LOT_DEVICE_STATUS_TYPE = "device_status"
PLACEHOLDER_BRIDGE_TOKENS = {
"change-this-shared-token",
"paste-generated-token-here",
@@ -50,9 +63,10 @@
class BridgeStateStore:
- def __init__(self) -> None:
+ def __init__(self, relay_hub: InputRelayHub | None = None) -> None:
ensure_app_support()
self._lock = threading.RLock()
+ self._recording_lock = threading.RLock()
self._project_root = _resolve_project_root()
self._manual_status_until = 0.0
self._state = self._load_state()
@@ -62,12 +76,65 @@ def __init__(self) -> None:
self._claude_quota = _claude_quota_from_state(self._state)
self._claude_usage_last_attempt = 0.0
self._claude_usage_last_success = 0.0
+ self._sub2_usage_token = os.environ.get("VIBE_STICK_SUB2_USAGE_TOKEN", "").strip()
+ self._sub2_usage_base_url = (
+ os.environ.get("VIBE_STICK_SUB2_USAGE_BASE_URL", "").strip() or SUB2_DEFAULT_BASE_URL
+ )
+ self._sub2_usage_interval = _sub2_usage_interval_seconds()
+ self._sub2_usage_accounts = {
+ "codex": _sub2_account_id("VIBE_STICK_SUB2_CODEX_ACCOUNT_ID", DEFAULT_SUB2_CODEX_ACCOUNT_ID),
+ "claude": _sub2_account_id("VIBE_STICK_SUB2_CLAUDE_ACCOUNT_ID", DEFAULT_SUB2_CLAUDE_ACCOUNT_ID),
+ }
+ self._sub2_usage_snapshots: dict[str, Sub2UsageSnapshot] = {}
+ self._sub2_usage_snapshot_times: dict[str, float] = {}
+ self._sub2_usage_refresh_event = threading.Event()
+ self._remote_provider_observations: dict[str, ProviderObservation] = {}
+ self._remote_provider_seen_at: dict[str, float] = {}
+ self._relay_hub = relay_hub
+ self._lot_devices: dict[str, dict[str, Any]] = {}
+ self._lot_device_last_signature = ""
quota = load_quota(QUOTA_PATH)
self._state.codex.quota_5h_remaining = quota.quota_5h_remaining
self._state.codex.quota_7d_remaining = quota.quota_7d_remaining
self._state.codex.quota_updated_at = quota.quota_updated_at
self._state.codex.quota_stale = quota.quota_stale
- self.recording = RecordingController(RECORDING_PATH)
+ relay_injector = RelayPasteInjector(relay_hub) if relay_hub else None
+ self.recording = RecordingController(RECORDING_PATH, paste_injector=relay_injector)
+ if self.recording.session.active:
+ self.recording.abort_stale_stream("Gateway restarted during an active recording")
+ self._recording_started_monotonic = 0.0
+ self._recording_last_activity_monotonic = 0.0
+ self._recording_idle_timeout_seconds = _positive_float_env(
+ "VIBE_STICK_RECORDING_IDLE_TIMEOUT_SECONDS",
+ DEFAULT_RECORDING_IDLE_TIMEOUT_SECONDS,
+ minimum=2.0,
+ maximum=30.0,
+ )
+ self._recording_max_seconds = _positive_float_env(
+ "VIBE_STICK_RECORDING_MAX_SECONDS",
+ DEFAULT_RECORDING_MAX_SECONDS,
+ minimum=10.0,
+ maximum=300.0,
+ )
+ threading.Thread(
+ target=self._recording_watchdog_loop,
+ name="vibestick-recording-watchdog",
+ daemon=True,
+ ).start()
+ if self._sub2_usage_token:
+ threading.Thread(
+ target=self._sub2_usage_loop,
+ name="vibestick-sub2-usage",
+ daemon=True,
+ ).start()
+ if relay_hub is not None:
+ relay_hub.set_provider_status_handler(self.update_provider_status)
+ relay_hub.set_device_status_provider(self.current_lot_devices)
+ threading.Thread(
+ target=self._lot_device_watchdog_loop,
+ name="vibestick-lot-device-watchdog",
+ daemon=True,
+ ).start()
hide_hud()
def get_state(self) -> VibeStickState:
@@ -87,6 +154,7 @@ def update_from_event(self, event: dict[str, Any]) -> VibeStickState:
elif event_name == "button_double":
self.refresh_quota_locked()
elif event_name == "button_short":
+ self.recording.paste_injector.press_enter()
self._state.alert = AlertState(event_id="", type=AlertType.NONE, message="")
self._save_state_locked()
return self._state
@@ -98,6 +166,8 @@ def refresh_quota(self) -> VibeStickState:
return self._state
def refresh_quota_locked(self) -> None:
+ if self._sub2_usage_token:
+ self._sub2_usage_refresh_event.set()
if self._state.active_provider == "claude":
self._refresh_claude_usage_locked(force=True)
self._state.provider = _provider_state_from_observation(
@@ -111,8 +181,115 @@ def refresh_quota_locked(self) -> None:
if self._state.active_provider == "codex":
self._state.provider = _provider_state_from_observation(codex_observation)
+ def update_provider_status(self, payload: dict[str, Any]) -> None:
+ provider_id = str(payload.get("provider") or "").strip().lower()
+ if provider_id not in {"codex", "claude"}:
+ raise ValueError("Unsupported provider status")
+ try:
+ status = AgentStatus(str(payload.get("status") or "UNKNOWN").upper())
+ except ValueError:
+ status = AgentStatus.UNKNOWN
+ alert_type = str(payload.get("alert_type") or "NONE").upper()
+ if alert_type not in {item.value for item in AlertType}:
+ alert_type = AlertType.NONE.value
+ observation = ProviderObservation(
+ provider_id=provider_id,
+ display_name="Codex" if provider_id == "codex" else "Claude",
+ online=True,
+ status=status,
+ project=str(payload.get("project") or "vibestick")[:128],
+ quota_5h_remaining=None,
+ quota_7d_remaining=None,
+ quota_updated_at="",
+ quota_stale=False,
+ alert_type=alert_type,
+ alert_message=str(payload.get("alert_message") or "")[:512],
+ alert_event_id=str(payload.get("alert_event_id") or "")[:160],
+ )
+ with self._lock:
+ self._remote_provider_observations[provider_id] = observation
+ self._remote_provider_seen_at[provider_id] = time.monotonic()
+
+ def mark_lot_device_seen(self, headers: Any, remote_addr: str = "") -> None:
+ firmware_name = str(headers.get("X-Vibe-Stick-Firmware-Name", "") or "").strip()
+ firmware_version = str(headers.get("X-Vibe-Stick-Firmware-Version", "") or "").strip()
+ transport = str(headers.get("X-Vibe-Stick-Firmware-Transport", "") or "").strip()
+ if not firmware_name and not firmware_version and not transport:
+ return
+ device_id = _lot_device_id(firmware_name, remote_addr)
+ display_name = _lot_device_display_name(firmware_name)
+ now = time.monotonic()
+ with self._lock:
+ self._lot_devices[device_id] = {
+ "id": device_id,
+ "name": display_name,
+ "firmware_name": firmware_name,
+ "firmware_version": firmware_version,
+ "transport": transport,
+ "remote_addr": remote_addr,
+ "online": True,
+ "last_seen": now,
+ }
+ self._publish_lot_device_status_if_changed()
+
+ def current_lot_devices(self) -> list[dict[str, Any]]:
+ with self._lock:
+ return self._online_lot_devices_locked(time.monotonic())
+
+ def _lot_device_watchdog_loop(self) -> None:
+ while True:
+ time.sleep(LOT_DEVICE_WATCHDOG_INTERVAL_SECONDS)
+ self._publish_lot_device_status_if_changed()
+
+ def _online_lot_devices_locked(self, now: float) -> list[dict[str, Any]]:
+ devices: list[dict[str, Any]] = []
+ stale_ids: list[str] = []
+ for device_id, device in self._lot_devices.items():
+ last_seen = float(device.get("last_seen") or 0.0)
+ if now - last_seen <= LOT_DEVICE_TTL_SECONDS:
+ devices.append(
+ {
+ "id": device_id,
+ "name": str(device.get("name") or "Stick S3"),
+ "online": True,
+ "firmware_name": str(device.get("firmware_name") or ""),
+ "firmware_version": str(device.get("firmware_version") or ""),
+ "transport": str(device.get("transport") or ""),
+ "last_seen_age_seconds": round(max(0.0, now - last_seen), 2),
+ }
+ )
+ else:
+ stale_ids.append(device_id)
+ for device_id in stale_ids:
+ self._lot_devices.pop(device_id, None)
+ devices.sort(key=lambda item: str(item.get("name") or item.get("id") or ""))
+ return devices
+
+ def _publish_lot_device_status_if_changed(self) -> None:
+ relay_hub = self._relay_hub
+ if relay_hub is None:
+ return
+ with self._lock:
+ devices = self._online_lot_devices_locked(time.monotonic())
+ signature = json.dumps(
+ [(device.get("id"), device.get("name")) for device in devices],
+ ensure_ascii=False,
+ sort_keys=True,
+ )
+ if signature == self._lot_device_last_signature:
+ return
+ self._lot_device_last_signature = signature
+ relay_hub.send_event({"type": LOT_DEVICE_STATUS_TYPE, "devices": devices})
+
def start_recording(self, request: dict[str, Any] | None = None) -> dict[str, Any]:
- session = self.recording.start(request)
+ with self._recording_lock:
+ session = self.recording.start(request)
+ now = time.monotonic()
+ if session.active and session.audio_source == "sticks3_pcm_stream":
+ self._recording_started_monotonic = now
+ self._recording_last_activity_monotonic = now
+ else:
+ self._clear_recording_watchdog_locked()
with self._lock:
self._state.alert = AlertState(
event_id="",
@@ -120,11 +297,17 @@ def start_recording(self, request: dict[str, Any] | None = None) -> dict[str, An
message="",
)
self._save_state_locked()
- return {"recording": session.to_jsonable(), "state": self.get_state().to_jsonable()}
+ # Provider state is polled separately by the StickS3. Keeping it off
+ # the start path lets the first audio frame leave immediately.
+ return {"recording": session.to_jsonable()}
def stop_recording(self, request: dict[str, Any] | None = None) -> dict[str, Any]:
- session = self.recording.stop(request)
- return {"recording": session.to_jsonable(), "state": self.get_state().to_jsonable()}
+ with self._recording_lock:
+ session = self.recording.stop(request)
+ self._clear_recording_watchdog_locked()
+ # Streaming text has already been injected; provider observation here
+ # only makes button release feel slower.
+ return {"recording": session.to_jsonable()}
def upload_recording_audio(
self,
@@ -135,18 +318,84 @@ def upload_recording_audio(
channels: int = 1,
bits_per_sample: int = 16,
) -> dict[str, Any]:
- session = self.recording.attach_pcm(
- pcm,
- session_id=session_id,
- sample_rate=sample_rate,
- channels=channels,
- bits_per_sample=bits_per_sample,
- )
- return {"recording": session.to_jsonable(), "state": self.get_state().to_jsonable()}
+ with self._recording_lock:
+ session = self.recording.attach_pcm(
+ pcm,
+ session_id=session_id,
+ sample_rate=sample_rate,
+ channels=channels,
+ bits_per_sample=bits_per_sample,
+ )
+ self._recording_last_activity_monotonic = time.monotonic()
+ return {"recording": session.to_jsonable()}
+
+ def upload_recording_chunk(self, pcm: bytes, *, session_id: str = "") -> dict[str, Any]:
+ with self._recording_lock:
+ session = self.recording.attach_pcm_chunk(pcm, session_id=session_id)
+ if session.active and session.status == "recording":
+ self._recording_last_activity_monotonic = time.monotonic()
+ # This endpoint runs several times per second. Provider observation is
+ # unrelated to audio ingestion and can add enough latency to break the
+ # live-input cadence, so return only the recording acknowledgement.
+ return {"recording": session.to_jsonable()}
+
+ def _clear_recording_watchdog_locked(self) -> None:
+ self._recording_started_monotonic = 0.0
+ self._recording_last_activity_monotonic = 0.0
+
+ def _recording_watchdog_loop(self) -> None:
+ while True:
+ time.sleep(0.5)
+ with self._recording_lock:
+ session = self.recording.session
+ if not session.active or session.audio_source != "sticks3_pcm_stream":
+ continue
+ now = time.monotonic()
+ idle_seconds = now - self._recording_last_activity_monotonic
+ age_seconds = now - self._recording_started_monotonic
+ if self._recording_started_monotonic <= 0:
+ continue
+ if age_seconds >= self._recording_max_seconds:
+ reason = f"Recording exceeded {self._recording_max_seconds:.0f} seconds"
+ elif idle_seconds >= self._recording_idle_timeout_seconds:
+ reason = f"StickS3 audio stopped for {idle_seconds:.1f} seconds"
+ else:
+ continue
+ print(f"recording watchdog abort session={session.session_id} reason={reason}", flush=True)
+ self.recording.abort_stale_stream(reason)
+ self._clear_recording_watchdog_locked()
+
+ def _sub2_usage_loop(self) -> None:
+ while True:
+ refreshed: dict[str, Sub2UsageSnapshot] = {}
+ for provider_id, account_id in self._sub2_usage_accounts.items():
+ try:
+ refreshed[provider_id] = fetch_sub2_usage(
+ provider_id,
+ account_id,
+ self._sub2_usage_token,
+ base_url=self._sub2_usage_base_url,
+ timeout=10.0,
+ )
+ except Exception as exc:
+ print(
+ f"Sub2-Usage refresh failed provider={provider_id} error={exc.__class__.__name__}",
+ flush=True,
+ )
+ if refreshed:
+ refreshed_at = time.monotonic()
+ with self._lock:
+ self._sub2_usage_snapshots.update(refreshed)
+ for provider_id in refreshed:
+ self._sub2_usage_snapshot_times[provider_id] = refreshed_at
+ self._sub2_usage_refresh_event.wait(self._sub2_usage_interval)
+ self._sub2_usage_refresh_event.clear()
def _refresh_providers_locked(self) -> None:
codex_observation = observe_codex(self._project_root)
claude_observation = observe_claude(self._project_root)
+ codex_observation = self._remote_provider_observation("codex", codex_observation)
+ claude_observation = self._remote_provider_observation("claude", claude_observation)
self._apply_codex_quota(codex_observation)
if time.monotonic() < self._manual_status_until:
@@ -163,16 +412,40 @@ def _refresh_providers_locked(self) -> None:
if active_provider == "claude":
self._refresh_claude_usage_locked(force=False)
- active_observation = self._apply_claude_quota(claude_observation)
- else:
- active_observation = codex_observation
+ claude_observation = self._apply_claude_quota(claude_observation)
+ self._apply_sub2_usage(codex_observation)
+ self._apply_sub2_usage(claude_observation)
+ observations = {
+ "codex": codex_observation,
+ "claude": claude_observation,
+ }
+ active_observation = observations[active_provider]
self._state.codex = _codex_state_from_observation(codex_observation)
self._state.provider = _provider_state_from_observation(active_observation)
+ self._state.providers = {
+ provider_id: _provider_state_from_observation(observation)
+ for provider_id, observation in observations.items()
+ }
self._apply_alert_from_observation(
_select_alert_observation(active_observation, codex_observation, claude_observation)
)
+ def _remote_provider_observation(
+ self,
+ provider_id: str,
+ fallback: ProviderObservation,
+ ) -> ProviderObservation:
+ seen_at = self._remote_provider_seen_at.get(provider_id, 0.0)
+ observation = self._remote_provider_observations.get(provider_id)
+ if (
+ observation is None
+ or seen_at <= 0
+ or time.monotonic() - seen_at > REMOTE_PROVIDER_STATUS_TTL_SECONDS
+ ):
+ return fallback
+ return observation
+
def _apply_alert_from_observation(self, observation: ProviderObservation) -> None:
try:
alert_type = AlertType(observation.alert_type)
@@ -243,6 +516,20 @@ def _apply_claude_quota(self, observation: ProviderObservation) -> ProviderObser
observation.quota_stale = quota.quota_stale
return observation
+ def _apply_sub2_usage(self, observation: ProviderObservation) -> None:
+ snapshot = self._sub2_usage_snapshots.get(observation.provider_id)
+ if snapshot is None:
+ return
+ observation.account_name = snapshot.account_name or f"{snapshot.account_id}#"
+ observation.quota_5h_remaining = snapshot.quota_5h_remaining
+ observation.quota_7d_remaining = snapshot.quota_7d_remaining
+ observation.quota_updated_at = snapshot.quota_updated_at
+ refreshed_at = self._sub2_usage_snapshot_times.get(observation.provider_id, 0.0)
+ observation.quota_stale = (
+ refreshed_at <= 0
+ or time.monotonic() - refreshed_at > self._sub2_usage_interval * 3
+ )
+
def _current_claude_quota(self) -> QuotaSnapshot:
if (
self._claude_quota.quota_5h_remaining is None
@@ -291,6 +578,7 @@ class VibeStickHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
if self.path == "/state":
+ store.mark_lot_device_seen(self.headers, str(self.client_address[0]))
self._send_json(_with_bridge_metadata(store.get_state().to_jsonable()))
elif self.path == "/health":
self._send_json(
@@ -308,6 +596,8 @@ def do_POST(self) -> None:
if parsed.path in _protected_paths() and not self._is_authorized():
self._send_error(HTTPStatus.UNAUTHORIZED, "Unauthorized")
return
+ if parsed.path in _protected_paths():
+ store.mark_lot_device_seen(self.headers, str(self.client_address[0]))
if parsed.path == "/event":
body = self._read_json_body()
@@ -338,6 +628,22 @@ def do_POST(self) -> None:
bits_per_sample=_int_header(self.headers.get("X-Vibe-Stick-Bits-Per-Sample"), 16),
)
)
+ elif parsed.path == "/recording/chunk":
+ query = parse_qs(parsed.query)
+ content_length = self._content_length()
+ if content_length > 32_000:
+ self._send_error(
+ HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
+ "Streaming audio chunk exceeds 32000 bytes",
+ )
+ return
+ pcm = self._read_raw_body(content_length)
+ self._send_json(
+ store.upload_recording_chunk(
+ pcm,
+ session_id=_first(query, "session_id"),
+ )
+ )
elif parsed.path == "/recording/stop":
body = self._read_json_body()
self._send_json(store.stop_recording(body))
@@ -401,7 +707,11 @@ def _send_error(self, status: HTTPStatus, message: str) -> None:
def run_server(host: str, port: int) -> None:
_enforce_bind_security(host)
- store = BridgeStateStore()
+ relay_port = _input_relay_port()
+ relay_hub = InputRelayHub(_bridge_token()) if relay_port else None
+ store = BridgeStateStore(relay_hub)
+ if relay_hub:
+ relay_hub.start(host, relay_port)
server = ThreadingHTTPServer((host, port), make_handler(store))
if not _bridge_token():
print(
@@ -418,10 +728,27 @@ def _protected_paths() -> set[str]:
"/quota/refresh",
"/recording/start",
"/recording/audio",
+ "/recording/chunk",
"/recording/stop",
}
+def _lot_device_display_name(firmware_name: str) -> str:
+ normalized = "".join(ch for ch in firmware_name.lower() if ch.isalnum())
+ if not normalized or "vibestick" in normalized or "sticks3" in normalized:
+ return "Stick S3"
+ return firmware_name.strip() or "Stick S3"
+
+
+def _lot_device_id(firmware_name: str, remote_addr: str) -> str:
+ normalized = "".join(ch for ch in firmware_name.lower() if ch.isalnum())
+ if "vibestick" in normalized or "sticks3" in normalized:
+ return "sticks3"
+ if normalized:
+ return normalized
+ return f"device-{remote_addr}"
+
+
def _bridge_token() -> str:
token = os.environ.get("VIBE_STICK_BRIDGE_TOKEN", "").strip()
if token.lower() in PLACEHOLDER_BRIDGE_TOKENS:
@@ -461,6 +788,17 @@ def _max_recording_audio_bytes() -> int:
return max(256_000, min(8_000_000, value))
+def _input_relay_port() -> int:
+ raw = os.environ.get("VIBE_STICK_INPUT_RELAY_PORT", "").strip()
+ if not raw:
+ return 0
+ try:
+ value = int(raw)
+ except ValueError:
+ return 0
+ return value if 1 <= value <= 65535 else 0
+
+
def _resolve_project_root() -> Path:
configured = os.environ.get("VIBE_STICK_PROJECT_ROOT", "").strip()
root = Path(configured).expanduser() if configured else Path.cwd()
@@ -511,6 +849,24 @@ def _configured_provider() -> str:
return value if value in {"codex", "claude", "auto"} else "auto"
+def _sub2_usage_interval_seconds() -> int:
+ try:
+ value = int(os.environ.get("VIBE_STICK_SUB2_USAGE_INTERVAL_SECONDS", ""))
+ except ValueError:
+ value = DEFAULT_SUB2_USAGE_INTERVAL_SECONDS
+ if value <= 0:
+ value = DEFAULT_SUB2_USAGE_INTERVAL_SECONDS
+ return max(MIN_SUB2_USAGE_INTERVAL_SECONDS, value)
+
+
+def _sub2_account_id(name: str, default: int) -> int:
+ try:
+ value = int(os.environ.get(name, ""))
+ except ValueError:
+ value = default
+ return value if value > 0 else default
+
+
def _select_active_provider(
configured: str,
last_active: str,
@@ -570,6 +926,22 @@ def _claude_usage_interval_seconds() -> int:
return max(MIN_CLAUDE_USAGE_INTERVAL_SECONDS, value)
+def _positive_float_env(
+ name: str,
+ default: float,
+ *,
+ minimum: float,
+ maximum: float,
+) -> float:
+ try:
+ value = float(os.environ.get(name, ""))
+ except ValueError:
+ value = default
+ if value <= 0:
+ value = default
+ return max(minimum, min(maximum, value))
+
+
def _codex_state_from_observation(observation: ProviderObservation) -> CodexState:
return CodexState(
status=observation.status,
@@ -578,6 +950,7 @@ def _codex_state_from_observation(observation: ProviderObservation) -> CodexStat
quota_7d_remaining=observation.quota_7d_remaining,
quota_updated_at=observation.quota_updated_at,
quota_stale=observation.quota_stale,
+ account_name=observation.account_name,
)
@@ -592,6 +965,7 @@ def _provider_state_from_observation(observation: ProviderObservation) -> Provid
quota_7d_remaining=observation.quota_7d_remaining,
quota_updated_at=observation.quota_updated_at,
quota_stale=observation.quota_stale,
+ account_name=observation.account_name,
)
diff --git a/bridge/src/vibe_stick/usage/__init__.py b/bridge/src/vibe_stick/usage/__init__.py
new file mode 100644
index 0000000..20e9c2d
--- /dev/null
+++ b/bridge/src/vibe_stick/usage/__init__.py
@@ -0,0 +1 @@
+"""External subscription usage integrations."""
diff --git a/bridge/src/vibe_stick/usage/sub2.py b/bridge/src/vibe_stick/usage/sub2.py
new file mode 100644
index 0000000..bf61f09
--- /dev/null
+++ b/bridge/src/vibe_stick/usage/sub2.py
@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass
+from datetime import datetime
+from typing import Any
+from urllib.error import HTTPError, URLError
+from urllib.request import Request, urlopen
+
+
+DEFAULT_BASE_URL = "https://88api.ai/sub2-usage"
+PROVIDER_PATHS = {
+ "codex": "gpt-plus",
+ "claude": "claude",
+}
+
+
+class Sub2UsageError(RuntimeError):
+ """Raised when a Sub2-Usage snapshot cannot be fetched or parsed."""
+
+
+@dataclass(frozen=True)
+class Sub2UsageSnapshot:
+ provider_id: str
+ account_id: int
+ account_name: str
+ quota_5h_remaining: int
+ quota_7d_remaining: int
+ quota_updated_at: str
+
+
+def fetch_sub2_usage(
+ provider_id: str,
+ account_id: int,
+ token: str,
+ *,
+ base_url: str = DEFAULT_BASE_URL,
+ timeout: float = 10.0,
+) -> Sub2UsageSnapshot:
+ provider_path = PROVIDER_PATHS.get(provider_id)
+ if provider_path is None:
+ raise Sub2UsageError(f"unsupported provider: {provider_id}")
+ if account_id <= 0:
+ raise Sub2UsageError("account id must be positive")
+ if not token.strip():
+ raise Sub2UsageError("missing Sub2-Usage token")
+
+ url = f"{base_url.rstrip('/')}/{provider_path}/{account_id}"
+ request = Request(
+ url,
+ headers={
+ "Accept": "application/json",
+ "Authorization": f"Bearer {token.strip()}",
+ "User-Agent": "VibeStick-Bridge/0.1",
+ },
+ method="GET",
+ )
+ try:
+ with urlopen(request, timeout=timeout) as response:
+ payload = json.loads(response.read().decode("utf-8"))
+ except (HTTPError, URLError, TimeoutError, OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
+ raise Sub2UsageError(f"request failed: {exc.__class__.__name__}") from exc
+
+ return parse_sub2_usage(provider_id, account_id, payload)
+
+
+def parse_sub2_usage(
+ provider_id: str,
+ account_id: int,
+ payload: Any,
+) -> Sub2UsageSnapshot:
+ if not isinstance(payload, dict):
+ raise Sub2UsageError("response must be a JSON object")
+ windows = payload.get("windows")
+ if not isinstance(windows, dict):
+ raise Sub2UsageError("response is missing windows")
+
+ five_hour = windows.get("five_hour")
+ seven_day = windows.get("seven_day")
+ if not isinstance(five_hour, dict) or not isinstance(seven_day, dict):
+ raise Sub2UsageError("response is missing usage windows")
+
+ response_account_id = _positive_int(payload.get("account_id")) or account_id
+ account_name = str(payload.get("account_name") or "").strip()
+ return Sub2UsageSnapshot(
+ provider_id=provider_id,
+ account_id=response_account_id,
+ account_name=account_name,
+ quota_5h_remaining=_remaining_percent(five_hour.get("remaining_percent"), "five_hour"),
+ quota_7d_remaining=_remaining_percent(seven_day.get("remaining_percent"), "seven_day"),
+ quota_updated_at=datetime.now().strftime("%H:%M"),
+ )
+
+
+def _remaining_percent(value: Any, field: str) -> int:
+ try:
+ parsed = float(value)
+ except (TypeError, ValueError) as exc:
+ raise Sub2UsageError(f"invalid {field} remaining_percent") from exc
+ return max(0, min(100, int(round(parsed))))
+
+
+def _positive_int(value: Any) -> int:
+ try:
+ parsed = int(value)
+ except (TypeError, ValueError):
+ return 0
+ return parsed if parsed > 0 else 0
diff --git a/docs/releases/0.1.6.md b/docs/releases/0.1.6.md
new file mode 100644
index 0000000..b094963
--- /dev/null
+++ b/docs/releases/0.1.6.md
@@ -0,0 +1,110 @@
+# VibeStick 0.1.6 集成更新记录
+
+- 发布日期:2026-07-15
+- 适用设备:M5Stack StickS3
+- 配套 Bridge:0.1.6
+- Windows 远程输入协议:兼容客户端 1.14.2 及以上
+
+## 更新目标
+
+这一版把 StickS3 从“录完再上传”的语音遥控器,改造成低延迟的局域网流式语音输入终端,并补齐用量显示、任务提醒、电源管理与 Windows 远程输入链路。典型链路如下:
+
+```text
+StickS3 麦克风
+ -> Unraid 上的 VibeStick Bridge / FunASR
+ -> Windows 输入中继
+ -> 当前光标所在应用
+```
+
+API Token、ASR Key 和账户凭据只保存在 Bridge/服务器环境变量中,不编译进固件,也不写入仓库。
+
+## StickS3 操作方式
+
+- 正面键单击:唤醒屏幕并向当前电脑输入发送 Enter。
+- 正面键长按:开始流式语音输入;松开后立即停止采集并执行尾段校准。
+- 正面键双击:主动刷新 Codex/Claude 用量;屏幕刚被唤醒时第一次操作只用于唤醒。
+- 侧键单击:切换 Codex/Claude,并立即刷新当前账户与剩余额度。
+- 拿起设备:通过 BMI270 运动检测自动亮屏。
+- 翻转放置:自动关闭屏幕背光;拿起后恢复显示。
+
+## 主要变化
+
+### 1. 局域网流式 ASR
+
+- StickS3 录音期间持续分块上传 PCM,不再等整段录音结束后才提交。
+- Bridge 新增 FunASR 流式会话、热词读取和结果分段处理。
+- 松开按键后,音频上传和最终校准进入独立任务,避免界面长期卡在“正在聆听”。
+- 增加 GPIO 松开检测、断流回收、最长录音和事件队列保护,降低无线抖动导致的录音卡死概率。
+- 稳定流式模式锁定已确认前文,只校正当前活动尾段,避免整段文字反复回退、重复输入或被最终结果覆盖。
+- 增加静音/噪声门控、尾部幻觉过滤、英文开发术语和网址标准化能力。
+
+### 2. Windows 输入中继
+
+- Bridge 增加出站输入中继协议,可把 S3 的临时结果和最终结果发送到 Windows 当前光标位置。
+- 支持本机与远程桌面副客户端架构,不要求修改 RDP 配置。
+- 输入校正采用稳定尾段替换,保留已经锁定的长文本前文。
+- Windows 客户端源码不在本仓库;本仓库保存协议端、Bridge 端和自动化测试。
+
+### 3. Codex / Claude 用量与账户名
+
+- Bridge 在服务器端轮询 Sub2-Usage,读取 5 小时、7 天剩余百分比和 `account_name`。
+- Token 只由服务器环境变量提供;StickS3 只接收整理后的状态快照。
+- 固件首页用账户名替代原来的“在线/离线”文字,并保留最近一次成功结果作为短时缓存。
+- 双击正面键或单击侧键切换服务商时可主动刷新。
+
+### 4. 按键发送与任务提示音
+
+- 恢复原固件正面键单击发送 Enter 的行为;屏幕熄灭时也不会再吞掉这次发送操作。
+- 恢复 Codex/Claude 完成、需要确认和错误三类提示音。
+- 提示音在录音期间延后播放,只有播放成功后才消费事件,避免音频编解码器忙时漏报。
+- Bridge 可接收 Windows 客户端上报的任务状态,再经现有状态接口传给 StickS3。
+
+### 5. 电源与屏幕管理
+
+- 接入 BMI270 IMU,用于翻转熄屏、移动唤醒和拿起亮屏。
+- 电池供电且连续 60 分钟无操作时自动关机;录音中、任务运行中、USB 供电或充电时不触发。
+- 关机前停止音频、IMU 和 Wi-Fi,释放扬声器、LCD 与传感器电源轨。
+- 清除原固件的 LDO 保持状态,再向 M5PM1 PMIC 发送关机命令,避免“屏幕黑了但外设仍耗电”。
+- 如果 PMIC 未能完全切断主电源,ESP32-S3 自动进入深度睡眠作为低功耗兜底。
+
+### 6. 部署与维护
+
+- 增加 Unraid Docker 模板、启动脚本和 supervisor,使 FunASR、Bridge 与辅助接口可作为一个容器维护。
+- 增加 Windows PowerShell 安装/开发脚本。
+- 固件使用更大的单应用分区,以容纳流式、IMU、中文字体与电源管理代码。
+- 中文字体子集补充账户名所需字形;自定义音源只记录来源说明,不把第三方原声音频提交到仓库。
+
+## 配置摘要
+
+Bridge 端按需配置:
+
+```env
+VIBE_STICK_ASR_PROVIDER=local-funasr
+VIBE_STICK_ASR_BASE_URL=ws://:10095
+VIBE_STICK_SUB2_USAGE_TOKEN=
+VIBE_STICK_SUB2_USAGE_BASE_URL=https://88api.ai/sub2-usage
+VIBE_STICK_SUB2_CODEX_ACCOUNT_ID=
+VIBE_STICK_SUB2_CLAUDE_ACCOUNT_ID=
+```
+
+真实值应写入未跟踪的 `.env` 或 Unraid Docker 变量。不要把 Token、Wi-Fi 密码或 API Key 提交到 Git。
+
+## 验证记录
+
+- Bridge/Python 自动化测试:90 项通过。
+- Windows 配套客户端测试:19 项通过(客户端仓库外验证)。
+- 语音回放回归:26/26 通过(本地测试素材不进入仓库)。
+- StickS3 固件:ESP-IDF/PlatformIO 构建成功并已在真机刷写验证。
+- 构建占用:RAM 约 26.6%,应用 Flash 约 65.8%。
+- 真机已验证:流式输入、按键松开退出、账户名/额度、单击 Enter、任务提示音、移动亮屏和 PMIC 关机路径。
+
+## 已知限制
+
+- 当前固件应用分区余量约 34.2%,后续新增字体或大型资源前仍应评估空间。
+- “最终校准不改写已锁定前文”优先保证输入稳定;很早以前的错字应通过热词/标准化规则在流式阶段解决。
+- 真正断电仍受硬件 PMIC 与电池板设计约束;本版同时提供 PMIC 关机和深度睡眠兜底。
+- Windows 图形客户端属于配套项目,本仓库只包含与其通信的服务端协议实现。
+
+## 回滚与备份
+
+本次源码以独立 Git 分支、提交和版本标签保存。需要回滚时可检出本版本之前的 `v0.1.4`,或使用本次发布提交重新构建 0.1.6。运行环境中的真实密钥和服务器数据不属于 Git 备份范围,应继续由 Unraid/AppData 的独立备份策略管理。
diff --git a/firmware/sticks3/dependencies.lock b/firmware/sticks3/dependencies.lock
index 5d084dc..18de3c8 100644
--- a/firmware/sticks3/dependencies.lock
+++ b/firmware/sticks3/dependencies.lock
@@ -1,4 +1,18 @@
dependencies:
+ espressif/bmi270:
+ component_hash: a234ece0bea3595c45bb75f80449b823b366ca677aed6e3d4c35f14c90c4ace5
+ dependencies:
+ - name: idf
+ require: private
+ version: '>=5.2'
+ - name: espressif/sensor_hub
+ registry_url: https://components.espressif.com
+ require: public
+ version: ^0.1.4
+ source:
+ registry_url: https://components.espressif.com/
+ type: service
+ version: 1.1.0
espressif/button:
component_hash: ad64de1d1b8aa139564810a8519b2f1ac275a560a512479ee94c1b6983469d4b
dependencies:
@@ -14,7 +28,7 @@ dependencies:
type: service
version: 4.2.0
espressif/cmake_utilities:
- component_hash: 05165f30922b422b4b90c08845e6d449329b97370fbd06309803d8cb539d79e3
+ component_hash: 351350613ceafba240b761b4ea991e0f231ac7a9f59a9ee901f751bddc0bb18f
dependencies:
- name: idf
require: private
@@ -22,7 +36,7 @@ dependencies:
source:
registry_url: https://components.espressif.com
type: service
- version: 1.1.1
+ version: 0.5.3
espressif/esp_codec_dev:
component_hash: 896c67360ae1f8dedaf8a53dfe48c015317fa455f061a5165307892296990028
dependencies:
@@ -33,10 +47,42 @@ dependencies:
registry_url: https://components.espressif.com/
type: service
version: 1.5.10
+ espressif/i2c_bus:
+ component_hash: 483112c1619c54c38d0b734fb25f1678733267858a77b7af2f48cc7db6dbbd84
+ dependencies:
+ - name: espressif/cmake_utilities
+ registry_url: https://components.espressif.com
+ require: private
+ version: '*'
+ - name: idf
+ require: private
+ version: '>=4.0'
+ source:
+ registry_url: https://components.espressif.com/
+ type: service
+ version: 1.5.2
+ espressif/sensor_hub:
+ component_hash: ea30db551c34aee2862482abf8b3164dd3855ecb4b4659b1e6878455c9cb9e22
+ dependencies:
+ - name: espressif/cmake_utilities
+ registry_url: https://components.espressif.com
+ require: private
+ version: 0.*
+ - name: espressif/i2c_bus
+ registry_url: https://components.espressif.com
+ require: public
+ version: 1.*
+ - name: idf
+ require: private
+ version: '>=4.4'
+ source:
+ registry_url: https://components.espressif.com/
+ type: service
+ version: 0.1.5
idf:
source:
type: idf
- version: 5.5.1
+ version: 5.5.0
lvgl/lvgl:
component_hash: 2409fdc84e9766d2a18afd84b051aef3f838348136cdb1c10ac2e8bcdb012bf6
dependencies: []
@@ -45,9 +91,10 @@ dependencies:
type: service
version: 9.2.0
direct_dependencies:
+- espressif/bmi270
- espressif/button
- espressif/esp_codec_dev
- lvgl/lvgl
-manifest_hash: 0a75e552b8ec3c0f9aba37aab09da16f82f0bc1a64efd8ca5e40476f563598f8
+manifest_hash: 0d961453551635fe630863dc7b0655df21c5fd120ed8f242b069173865313191
target: esp32s3
version: 2.0.0
diff --git a/firmware/sticks3/generated/vibe_stick_cn_16.c b/firmware/sticks3/generated/vibe_stick_cn_16.c
index 2785ab8..c3c00b8 100644
--- a/firmware/sticks3/generated/vibe_stick_cn_16.c
+++ b/firmware/sticks3/generated/vibe_stick_cn_16.c
@@ -83,15 +83,30 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x0, 0xcc, 0x9a, 0x0, 0xd, 0x70, 0x0,
0x0, 0x9, 0xe1, 0xc, 0x70, 0xad, 0x0, 0x0,
0x0, 0x8f, 0x50, 0x1, 0xec, 0xe1, 0x0, 0x0,
- 0x1b, 0xe4, 0x0, 0x3, 0xdf, 0xc2, 0x0, 0x0,
+ 0x1b, 0xf4, 0x0, 0x3, 0xdf, 0xc3, 0x0, 0x0,
0x2c, 0x20, 0x4, 0xaf, 0x82, 0x9f, 0xa5, 0x0,
0x0, 0x0, 0xde, 0x82, 0x0, 0x3, 0x9e, 0xf2,
0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x20,
+ /* U+540A "吊" */
+ 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x10, 0x0,
+ 0xf3, 0x11, 0x11, 0x11, 0x4f, 0x10, 0x0, 0xf1,
+ 0x0, 0x0, 0x0, 0x2f, 0x10, 0x0, 0xf1, 0x0,
+ 0x0, 0x0, 0x2f, 0x10, 0x0, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0x10, 0x0, 0x22, 0x22, 0xe7, 0x22,
+ 0x22, 0x0, 0x0, 0x0, 0x0, 0xd6, 0x0, 0x0,
+ 0x0, 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0xf, 0x42, 0x22, 0xe8, 0x22, 0x22, 0xf3, 0xf,
+ 0x20, 0x0, 0xd6, 0x0, 0x0, 0xf3, 0xf, 0x20,
+ 0x0, 0xd6, 0x0, 0x0, 0xf3, 0xf, 0x20, 0x0,
+ 0xd6, 0x1, 0x23, 0xf2, 0xf, 0x20, 0x0, 0xd6,
+ 0x3, 0xee, 0xb0, 0x1, 0x0, 0x0, 0xd6, 0x0,
+ 0x0, 0x0,
+
/* U+542C "听" */
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0,
0x0, 0x0, 0x0, 0x0, 0x38, 0xde, 0x20, 0xef,
- 0xff, 0xe0, 0xbb, 0xff, 0xb6, 0x10, 0xe, 0x52,
+ 0xff, 0xe0, 0xbb, 0xfe, 0xb6, 0x10, 0xe, 0x52,
0x5e, 0xf, 0x82, 0x0, 0x0, 0x0, 0xe3, 0x3,
0xe0, 0xf3, 0x0, 0x0, 0x0, 0xe, 0x30, 0x3e,
0xf, 0x30, 0x0, 0x0, 0x0, 0xe3, 0x3, 0xe0,
@@ -121,6 +136,22 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0xd2, 0x0, 0x0, 0xb6, 0x14, 0x41, 0x0,
0x0, 0x0, 0x0, 0x0, 0xb6, 0x0, 0x0, 0x0,
+ /* U+54E5 "哥" */
+ 0xc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xb0, 0x0,
+ 0x0, 0xd, 0xdd, 0xdd, 0xb0, 0x7, 0xb0, 0x0,
+ 0x0, 0xf, 0x10, 0x4, 0xd0, 0x7, 0xb0, 0x0,
+ 0x0, 0xf, 0x10, 0x4, 0xd0, 0x7, 0xb0, 0x0,
+ 0x0, 0xd, 0xdd, 0xdd, 0xb3, 0x4a, 0xb0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x6, 0xba, 0x30, 0x0,
+ 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xc0, 0x0,
+ 0x0, 0x2e, 0xee, 0xee, 0xd0, 0x7, 0xb0, 0x0,
+ 0x0, 0x3f, 0x0, 0x3, 0xe0, 0x7, 0xb0, 0x0,
+ 0x0, 0x3f, 0x0, 0x4, 0xe0, 0x7, 0xb0, 0x0,
+ 0x0, 0x3f, 0xee, 0xee, 0xd1, 0x18, 0xb0, 0x0,
+ 0x0, 0x18, 0x0, 0x0, 0xc, 0xfe, 0x60, 0x0,
+
/* U+5728 "在" */
0x0, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x1, 0xf4, 0x0, 0x0, 0x0, 0x0,
@@ -130,7 +161,7 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x0, 0xe7, 0x0, 0x5, 0xa0, 0x0, 0x0,
0x0, 0x9, 0xd0, 0x0, 0x6, 0xc0, 0x0, 0x0,
0x0, 0x6f, 0x50, 0x0, 0x6, 0xc0, 0x0, 0x0,
- 0x7, 0xff, 0x40, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0x6, 0xff, 0x40, 0xff, 0xff, 0xff, 0xff, 0x60,
0x3f, 0x6e, 0x40, 0x22, 0x28, 0xd2, 0x22, 0x10,
0x3, 0xe, 0x40, 0x0, 0x6, 0xc0, 0x0, 0x0,
0x0, 0xe, 0x40, 0x0, 0x6, 0xc0, 0x0, 0x0,
@@ -138,12 +169,29 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0xe, 0x41, 0x11, 0x17, 0xc1, 0x11, 0x10,
0x0, 0xe, 0x4a, 0xff, 0xff, 0xff, 0xff, 0xf0,
+ /* U+5927 "大" */
+ 0x0, 0x0, 0x0, 0xa, 0xa0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xa, 0xa0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xb, 0x80, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xc, 0x70, 0x0, 0x0, 0x0,
+ 0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1,
+ 0x4, 0x44, 0x44, 0x6f, 0xf6, 0x44, 0x44, 0x40,
+ 0x0, 0x0, 0x0, 0x7e, 0xd8, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0xc9, 0x6e, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x4, 0xf3, 0xf, 0x70, 0x0, 0x0,
+ 0x0, 0x0, 0xd, 0xa0, 0x6, 0xf2, 0x0, 0x0,
+ 0x0, 0x0, 0xae, 0x10, 0x0, 0xcd, 0x10, 0x0,
+ 0x0, 0xa, 0xf3, 0x0, 0x0, 0x1d, 0xd1, 0x0,
+ 0x5, 0xee, 0x30, 0x0, 0x0, 0x1, 0xdf, 0x70,
+ 0x1e, 0x91, 0x0, 0x0, 0x0, 0x0, 0x8, 0xd1,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+
/* U+5931 "失" */
0x0, 0x5, 0xa0, 0xb, 0x80, 0x0, 0x0, 0x0,
0x0, 0xc, 0x80, 0xb, 0x80, 0x0, 0x0, 0x0,
0x0, 0x2f, 0xed, 0xdf, 0xed, 0xdd, 0xdb, 0x0,
0x0, 0xba, 0x55, 0x5c, 0xb5, 0x55, 0x54, 0x0,
- 0x6, 0xe1, 0x0, 0xb, 0x80, 0x0, 0x0, 0x0,
+ 0x7, 0xe1, 0x0, 0xb, 0x80, 0x0, 0x0, 0x0,
0xa, 0x60, 0x0, 0xc, 0x70, 0x0, 0x0, 0x0,
0x3, 0x33, 0x33, 0x3d, 0x93, 0x33, 0x33, 0x30,
0x2f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2,
@@ -207,16 +255,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+5F85 "待" */
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x8, 0xb0, 0x0, 0x7, 0xb0, 0x0, 0x0,
- 0x0, 0x6e, 0x20, 0x22, 0x28, 0xc2, 0x22, 0x0,
- 0x8, 0xe3, 0x0, 0xff, 0xff, 0xff, 0xff, 0x60,
- 0x1c, 0x20, 0x80, 0x0, 0x7, 0xb0, 0x0, 0x0,
- 0x0, 0x9, 0xc2, 0x22, 0x28, 0xc2, 0x22, 0x20,
- 0x0, 0x5f, 0x3e, 0xff, 0xff, 0xff, 0xff, 0xf3,
+ 0x0, 0x6, 0xd0, 0x0, 0x7, 0xb0, 0x0, 0x0,
+ 0x0, 0x4f, 0x30, 0x22, 0x28, 0xc2, 0x22, 0x0,
+ 0x8, 0xf4, 0x0, 0xff, 0xff, 0xff, 0xff, 0x60,
+ 0x1c, 0x20, 0x81, 0x0, 0x7, 0xb0, 0x0, 0x0,
+ 0x0, 0x8, 0xd2, 0x22, 0x28, 0xc2, 0x22, 0x20,
+ 0x0, 0x4f, 0x3e, 0xff, 0xff, 0xff, 0xff, 0xf3,
0x5, 0xff, 0x10, 0x0, 0x0, 0x3, 0xf0, 0x0,
0x4f, 0x8f, 0x11, 0x22, 0x22, 0x25, 0xf2, 0x20,
0x17, 0xf, 0x1b, 0xff, 0xff, 0xff, 0xff, 0xf1,
- 0x0, 0xf, 0x10, 0x26, 0x0, 0x3, 0xf0, 0x0,
+ 0x0, 0xf, 0x10, 0x27, 0x0, 0x3, 0xf0, 0x0,
0x0, 0xf, 0x10, 0x1d, 0x70, 0x3, 0xf0, 0x0,
0x0, 0xf, 0x10, 0x3, 0xf2, 0x3, 0xf0, 0x0,
0x0, 0xf, 0x10, 0x0, 0x31, 0x25, 0xf0, 0x0,
@@ -232,15 +280,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x0, 0x1d, 0xb0, 0x6, 0xf4, 0x0, 0x0,
0x0, 0x5, 0xeb, 0x9d, 0x30, 0x7f, 0x91, 0x0,
0x18, 0xee, 0x60, 0x7, 0xf5, 0x4, 0xdf, 0xb1,
- 0x9, 0x50, 0x0, 0x18, 0x41, 0x0, 0x4, 0x70,
+ 0x9, 0x50, 0x0, 0x18, 0x40, 0x0, 0x4, 0x70,
0x0, 0x73, 0x86, 0xb, 0xb0, 0x0, 0xb5, 0x0,
0x0, 0xe3, 0xa7, 0x0, 0xca, 0x0, 0x6d, 0x0,
0x4, 0xe0, 0xa7, 0x0, 0x15, 0x8, 0x4e, 0x50,
- 0xc, 0x80, 0xaa, 0x11, 0x11, 0x2d, 0x58, 0xc0,
+ 0xc, 0x80, 0xaa, 0x11, 0x11, 0x3d, 0x58, 0xc0,
0x7, 0x10, 0x4e, 0xff, 0xff, 0xfc, 0x2, 0x60,
/* U+6210 "成" */
- 0x0, 0x0, 0x0, 0x0, 0x4f, 0xb, 0x81, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x4f, 0xd, 0x91, 0x0,
0x0, 0x0, 0x0, 0x0, 0x3f, 0x0, 0x6d, 0x10,
0x0, 0xee, 0xee, 0xee, 0xff, 0xfe, 0xef, 0xe3,
0x0, 0xf7, 0x33, 0x33, 0x4f, 0x53, 0x33, 0x30,
@@ -253,7 +302,7 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x4, 0xf1, 0x23, 0xf2, 0x4, 0xf8, 0x0, 0x82,
0x8, 0xc1, 0xee, 0xa0, 0x5f, 0xce, 0x0, 0xd3,
0xe, 0x70, 0x0, 0x1a, 0xe4, 0xd, 0xb5, 0xf1,
- 0x3e, 0x0, 0x0, 0x4c, 0x20, 0x2, 0xbf, 0x80,
+ 0x3e, 0x0, 0x0, 0x4c, 0x10, 0x2, 0xbf, 0x80,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
/* U+672A "未" */
@@ -265,28 +314,28 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x0, 0x0, 0xa, 0x90, 0x0, 0x0, 0x0,
0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1,
0x3, 0x33, 0x33, 0xbf, 0xfb, 0x33, 0x33, 0x30,
- 0x0, 0x0, 0x6, 0xec, 0xbe, 0x60, 0x0, 0x0,
+ 0x0, 0x0, 0x6, 0xdc, 0xbd, 0x60, 0x0, 0x0,
0x0, 0x0, 0x7e, 0x2a, 0x92, 0xe7, 0x0, 0x0,
0x0, 0xa, 0xe2, 0xa, 0x90, 0x2e, 0xa0, 0x0,
- 0x5, 0xec, 0x20, 0xa, 0x90, 0x2, 0xde, 0x50,
- 0x2e, 0x70, 0x0, 0xa, 0x90, 0x0, 0x8, 0xe2,
+ 0x5, 0xec, 0x20, 0xa, 0x90, 0x2, 0xce, 0x50,
+ 0x2e, 0x70, 0x0, 0xa, 0x90, 0x0, 0x7, 0xe2,
0x0, 0x0, 0x0, 0xa, 0x90, 0x0, 0x0, 0x0,
/* U+677E "松" */
0x0, 0x9, 0x90, 0x0, 0xf, 0xff, 0xf3, 0x0,
0x0, 0x9, 0x90, 0x0, 0x73, 0x11, 0xc8, 0x0,
- 0x0, 0x9, 0x90, 0x2, 0xf1, 0x0, 0x7d, 0x0,
- 0x1f, 0xff, 0xff, 0xa7, 0xc0, 0x0, 0x1f, 0x20,
- 0x1, 0x1e, 0x91, 0x1d, 0x70, 0x0, 0xc, 0x90,
- 0x0, 0x2f, 0x90, 0x7e, 0x1, 0x40, 0x4, 0xf3,
- 0x0, 0x8f, 0xf4, 0xb6, 0x7, 0xf0, 0x0, 0xa5,
- 0x0, 0xda, 0xbe, 0x30, 0xb, 0xa0, 0x0, 0x0,
- 0x5, 0xb9, 0x96, 0x80, 0xf, 0x50, 0x10, 0x0,
- 0xd, 0x49, 0x90, 0x0, 0x3f, 0x10, 0xe5, 0x0,
- 0x2b, 0x9, 0x90, 0x0, 0x8a, 0x0, 0x6e, 0x0,
- 0x1, 0x9, 0x90, 0x0, 0xd5, 0x0, 0x1e, 0x80,
- 0x0, 0x9, 0x90, 0x8c, 0xfe, 0xef, 0xff, 0xf0,
- 0x0, 0x9, 0x90, 0x46, 0x54, 0x32, 0x10, 0xc4,
+ 0x0, 0x9, 0x90, 0x2, 0xf1, 0x0, 0x6d, 0x0,
+ 0x1f, 0xff, 0xff, 0xa8, 0xc0, 0x0, 0x1f, 0x20,
+ 0x1, 0x1e, 0x91, 0x1e, 0x60, 0x0, 0xb, 0xa0,
+ 0x0, 0x3f, 0xa0, 0x8d, 0x2, 0x40, 0x3, 0xf4,
+ 0x0, 0x8f, 0xf5, 0xa5, 0x7, 0xe0, 0x0, 0x84,
+ 0x0, 0xda, 0xbe, 0x40, 0xb, 0x90, 0x0, 0x0,
+ 0x6, 0xa9, 0x95, 0x70, 0xf, 0x50, 0x20, 0x0,
+ 0xe, 0x39, 0x90, 0x0, 0x4f, 0x0, 0xe6, 0x0,
+ 0x29, 0x9, 0x90, 0x0, 0x9a, 0x0, 0x5e, 0x10,
+ 0x0, 0x9, 0x90, 0x0, 0xe5, 0x23, 0x4e, 0x90,
+ 0x0, 0x9, 0x90, 0xaf, 0xff, 0xfe, 0xdc, 0xf1,
+ 0x0, 0x9, 0x90, 0x23, 0x20, 0x0, 0x0, 0xc4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
/* U+6B63 "正" */
@@ -305,12 +354,30 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x2d, 0xdf, 0xdd, 0xdd, 0xfd, 0xdd, 0xdd, 0xd2,
0x5, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x51,
+ /* U+6BDB "毛" */
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x37, 0xbf, 0xd1, 0x0, 0x1,
+ 0x47, 0x9c, 0xff, 0xb8, 0x30, 0x0, 0x7, 0xfd,
+ 0xb8, 0xad, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x5, 0xd0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0,
+ 0x7e, 0x79, 0xbe, 0xff, 0x0, 0x2a, 0xce, 0xff,
+ 0xfb, 0x86, 0x42, 0x0, 0x2, 0x86, 0x41, 0x6d,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0xd0,
+ 0x0, 0x24, 0x79, 0x0, 0x2, 0x46, 0xbf, 0xce,
+ 0xff, 0xdb, 0x81, 0xbf, 0xfe, 0xcc, 0xe5, 0x31,
+ 0x0, 0x0, 0x2, 0x20, 0x0, 0x5d, 0x0, 0x0,
+ 0x0, 0x30, 0x0, 0x0, 0x5, 0xd0, 0x0, 0x0,
+ 0xa, 0x80, 0x0, 0x0, 0x4f, 0x43, 0x22, 0x24,
+ 0xe5, 0x0, 0x0, 0x0, 0xbf, 0xff, 0xff, 0xfa,
+ 0x0,
+
/* U+6E05 "清" */
- 0x3, 0xda, 0x47, 0xee, 0xef, 0xfe, 0xee, 0xc0,
- 0x0, 0x18, 0x70, 0x0, 0x8, 0xa0, 0x0, 0x0,
+ 0x3, 0xa2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x1, 0xaf, 0x77, 0xee, 0xef, 0xfe, 0xee, 0xc0,
+ 0x0, 0x5, 0x70, 0x0, 0x8, 0xa0, 0x0, 0x0,
0x0, 0x0, 0x2, 0xff, 0xff, 0xff, 0xff, 0x70,
- 0x1, 0x0, 0x0, 0x0, 0x8, 0xa0, 0x0, 0x0,
- 0x2f, 0xa2, 0xb, 0xbb, 0xbe, 0xeb, 0xbb, 0xb4,
+ 0x0, 0x0, 0x0, 0x0, 0x8, 0xa0, 0x0, 0x0,
+ 0x2f, 0xa1, 0xb, 0xbb, 0xbe, 0xeb, 0xbb, 0xb4,
0x2, 0xbe, 0x3, 0x33, 0x33, 0x33, 0x33, 0x31,
0x0, 0x2, 0x0, 0x8e, 0xee, 0xee, 0xee, 0x20,
0x0, 0x0, 0x0, 0x98, 0x0, 0x0, 0xf, 0x20,
@@ -323,10 +390,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+72B6 "状" */
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x0, 0xf2, 0x0, 0xc, 0x61, 0x50, 0x0,
+ 0x0, 0x0, 0xf2, 0x0, 0xc, 0x61, 0x60, 0x0,
0x1, 0x0, 0xf2, 0x0, 0xc, 0x60, 0xc7, 0x0,
- 0xe, 0x50, 0xf2, 0x0, 0xc, 0x60, 0x1e, 0x30,
- 0x4, 0xf4, 0xf2, 0x0, 0xc, 0x60, 0x2, 0x0,
+ 0xe, 0x60, 0xf2, 0x0, 0xc, 0x60, 0x2e, 0x30,
+ 0x3, 0xf4, 0xf2, 0x0, 0xc, 0x60, 0x2, 0x0,
0x0, 0x87, 0xf7, 0xee, 0xef, 0xfe, 0xee, 0xe0,
0x0, 0x0, 0xf3, 0x33, 0x4f, 0xc3, 0x33, 0x30,
0x0, 0x1, 0xf2, 0x0, 0x1f, 0xe0, 0x0, 0x0,
@@ -340,10 +407,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+77E5 "知" */
0x0, 0x21, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0xe, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2,
+ 0xd, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2,
0xf4, 0x22, 0x21, 0x6e, 0xee, 0xee, 0x40, 0x7f,
0xff, 0xff, 0x96, 0xc3, 0x33, 0xf4, 0xe, 0x51,
- 0xf1, 0x0, 0x6b, 0x0, 0xe, 0x46, 0xc0, 0x1f,
+ 0xf1, 0x0, 0x6b, 0x0, 0xe, 0x47, 0xc0, 0x1f,
0x10, 0x6, 0xb0, 0x0, 0xe4, 0x2, 0x1, 0xf1,
0x0, 0x6b, 0x0, 0xe, 0x46, 0xff, 0xff, 0xff,
0xf7, 0xb0, 0x0, 0xe4, 0x12, 0x26, 0xf2, 0x22,
@@ -357,7 +424,7 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+786E "确" */
0x0, 0x0, 0x0, 0x0, 0x9, 0x60, 0x0, 0x1,
- 0xff, 0xff, 0xfe, 0x1, 0xf5, 0x0, 0x0, 0x1,
+ 0xff, 0xff, 0xfe, 0x2, 0xf5, 0x0, 0x0, 0x1,
0x4f, 0x21, 0x10, 0x8f, 0xff, 0xf4, 0x0, 0x6,
0xd0, 0x0, 0x1f, 0x60, 0x3d, 0x0, 0x0, 0x9a,
0x0, 0xc, 0xd2, 0x2c, 0x82, 0x20, 0xc, 0x70,
@@ -382,28 +449,28 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xf2, 0x0,
0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x0, 0x0,
0x1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20,
- 0x1, 0xf2, 0x0, 0xe6, 0x6, 0x70, 0x2f, 0x20,
+ 0x1, 0xf2, 0x0, 0xd6, 0x6, 0x70, 0x2f, 0x20,
0x1, 0xf1, 0x2, 0xf1, 0x14, 0xf3, 0x1f, 0x20,
- 0x1, 0xf5, 0xff, 0xfe, 0xdc, 0xbd, 0x1f, 0x20,
- 0x1, 0xf1, 0x10, 0x0, 0x0, 0x5, 0x3f, 0x20,
+ 0x1, 0xf4, 0xef, 0xfe, 0xdc, 0xcd, 0x1f, 0x20,
+ 0x1, 0xf1, 0x21, 0x0, 0x0, 0x5, 0x3f, 0x20,
0x1, 0xf1, 0x0, 0x0, 0x0, 0xe, 0xfc, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
/* U+7EBF "线" */
- 0x0, 0x4, 0xa0, 0x0, 0x8, 0xa3, 0x70, 0x0,
- 0x0, 0xb, 0x90, 0x0, 0x8, 0xa1, 0x8e, 0x20,
- 0x0, 0x2f, 0x10, 0x0, 0x7, 0xa0, 0x15, 0x30,
- 0x0, 0xb7, 0x7, 0x26, 0x8c, 0xff, 0xec, 0x70,
- 0x6, 0xd0, 0x4e, 0x18, 0x89, 0xd1, 0x0, 0x0,
- 0xf, 0xff, 0xf5, 0x0, 0x4, 0xe0, 0x14, 0x70,
- 0x3, 0x1a, 0xa0, 0x3, 0x6a, 0xfe, 0xfc, 0x91,
- 0x0, 0x6c, 0x0, 0x3d, 0xa8, 0xf4, 0x0, 0x50,
- 0x6, 0xfb, 0xce, 0x30, 0x0, 0xc6, 0x8, 0xc0,
- 0xc, 0xa6, 0x30, 0x0, 0x0, 0x8a, 0x9d, 0x10,
- 0x0, 0x0, 0x3, 0x10, 0x0, 0x6f, 0xc1, 0x0,
- 0x2, 0x6a, 0xed, 0x40, 0x19, 0xff, 0x60, 0x53,
- 0xd, 0xa6, 0x10, 0x4b, 0xe8, 0x15, 0xf5, 0xb5,
- 0x0, 0x0, 0x0, 0x48, 0x10, 0x0, 0x7e, 0xd0,
+ 0x0, 0x8, 0x50, 0x0, 0xd, 0x57, 0x50, 0x0,
+ 0x0, 0x1f, 0x40, 0x0, 0xd, 0x52, 0xbb, 0x0,
+ 0x0, 0x7b, 0x0, 0x0, 0xc, 0x50, 0x26, 0x10,
+ 0x1, 0xe3, 0x18, 0x16, 0x9e, 0xef, 0xeb, 0x30,
+ 0xa, 0x80, 0x9b, 0x19, 0x7c, 0x80, 0x0, 0x0,
+ 0x5f, 0xff, 0xe1, 0x0, 0x9, 0x90, 0x25, 0x50,
+ 0x3, 0x1e, 0x50, 0x14, 0x6c, 0xff, 0xeb, 0x70,
+ 0x0, 0xb8, 0x0, 0x7c, 0x99, 0xe1, 0x1, 0x40,
+ 0xa, 0xfa, 0xcd, 0x0, 0x1, 0xf1, 0xd, 0x80,
+ 0x2e, 0xa6, 0x30, 0x0, 0x0, 0xd6, 0xca, 0x0,
+ 0x0, 0x0, 0x4, 0x0, 0x0, 0xbf, 0x80, 0x0,
+ 0x4, 0x8c, 0xfb, 0x0, 0x3c, 0xef, 0x20, 0x70,
+ 0x3d, 0x94, 0x0, 0x7d, 0xd6, 0xa, 0xc5, 0xe0,
+ 0x0, 0x0, 0x0, 0x75, 0x0, 0x1, 0xbf, 0x90,
/* U+8046 "聆" */
0x1f, 0xff, 0xff, 0xfd, 0x0, 0xcc, 0x20, 0x0,
@@ -422,13 +489,13 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x0, 0x4, 0xd0, 0x3, 0xe0, 0x0, 0x0,
/* U+884C "行" */
- 0x0, 0x3, 0xc1, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x1e, 0x80, 0x3f, 0xff, 0xff, 0xff, 0xa0,
- 0x2, 0xdb, 0x0, 0x2, 0x22, 0x22, 0x22, 0x10,
- 0x2e, 0xb0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x8, 0x0, 0xca, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x7, 0xf1, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x4f, 0x70, 0xef, 0xff, 0xff, 0xff, 0xf1,
+ 0x0, 0x1, 0x91, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xc, 0xb0, 0x3f, 0xff, 0xff, 0xff, 0xa0,
+ 0x1, 0xcc, 0x0, 0x2, 0x22, 0x22, 0x22, 0x10,
+ 0x2e, 0xb0, 0x11, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x9, 0x0, 0xbb, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x7, 0xf2, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x3f, 0x70, 0xef, 0xff, 0xff, 0xff, 0xf1,
0x4, 0xff, 0x30, 0x11, 0x11, 0x19, 0xb1, 0x10,
0x4f, 0xae, 0x30, 0x0, 0x0, 0x8, 0xa0, 0x0,
0x17, 0xe, 0x30, 0x0, 0x0, 0x8, 0xa0, 0x0,
@@ -458,7 +525,7 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+8BC6 "识" */
0x1, 0xb1, 0x0, 0x2, 0x22, 0x22, 0x22, 0x0,
- 0x0, 0xae, 0x20, 0xf, 0xff, 0xff, 0xff, 0x40,
+ 0x0, 0x9e, 0x20, 0xf, 0xff, 0xff, 0xff, 0x40,
0x0, 0xa, 0xb0, 0xf, 0x30, 0x0, 0xe, 0x40,
0x0, 0x0, 0x10, 0xf, 0x30, 0x0, 0xe, 0x40,
0x3, 0x33, 0x0, 0xf, 0x30, 0x0, 0xe, 0x40,
@@ -469,65 +536,65 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0xe, 0x30, 0x0, 0x51, 0x0, 0x50, 0x0,
0x0, 0xe, 0x38, 0x13, 0xf1, 0x0, 0xd7, 0x0,
0x0, 0xf, 0xec, 0x1a, 0xa0, 0x0, 0x5f, 0x10,
- 0x0, 0x5f, 0xa0, 0x4f, 0x30, 0x0, 0xd, 0x80,
- 0x0, 0x87, 0x0, 0xe8, 0x0, 0x0, 0x6, 0xe0,
+ 0x0, 0x5f, 0x90, 0x4f, 0x30, 0x0, 0xd, 0x80,
+ 0x0, 0x86, 0x0, 0xe8, 0x0, 0x0, 0x6, 0xe0,
0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x10,
/* U+8D25 "败" */
0x0, 0x0, 0x0, 0x0, 0x3, 0x50, 0x0, 0x0,
- 0xc, 0xff, 0xff, 0xe0, 0x9, 0x80, 0x0, 0x0,
+ 0xc, 0xff, 0xff, 0xe0, 0xa, 0x80, 0x0, 0x0,
0xc, 0x41, 0x13, 0xe0, 0xe, 0x40, 0x0, 0x0,
0xc, 0x32, 0x91, 0xe0, 0x2f, 0xdd, 0xdd, 0xc0,
0xc, 0x33, 0xc1, 0xe0, 0x8b, 0x33, 0x7c, 0x30,
0xc, 0x33, 0xc1, 0xe0, 0xe5, 0x0, 0x79, 0x0,
0xc, 0x33, 0xc1, 0xe8, 0xf8, 0x0, 0xa6, 0x0,
0xc, 0x34, 0xc1, 0xe5, 0x6d, 0x0, 0xd2, 0x0,
- 0xc, 0x34, 0xb1, 0xe0, 0xb, 0x42, 0xe0, 0x0,
- 0xc, 0x36, 0xa1, 0xe0, 0x3, 0xc9, 0x80, 0x0,
+ 0xc, 0x34, 0xb1, 0xe0, 0xd, 0x42, 0xe0, 0x0,
+ 0xc, 0x36, 0xa1, 0xe0, 0x5, 0xc9, 0x70, 0x0,
0x9, 0x2a, 0x61, 0xa0, 0x0, 0xcf, 0x10, 0x0,
- 0x0, 0x1f, 0x58, 0x0, 0x0, 0xbf, 0x30, 0x0,
- 0x0, 0xa9, 0xd, 0x50, 0x8, 0xd7, 0xe2, 0x0,
- 0x1b, 0xd1, 0x3, 0xe2, 0xbe, 0x10, 0x7f, 0x70,
+ 0x0, 0x1e, 0x49, 0x0, 0x0, 0xbf, 0x30, 0x0,
+ 0x0, 0xa9, 0xc, 0x60, 0x8, 0xd7, 0xe2, 0x0,
+ 0x1b, 0xd0, 0x3, 0xe2, 0xae, 0x10, 0x7f, 0x70,
0x4a, 0x10, 0x0, 0x44, 0xa1, 0x0, 0x4, 0xa0,
/* U+8FD0 "运" */
- 0x1, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x2, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x2, 0xcd, 0x20, 0xdf, 0xff, 0xff, 0xff, 0x20,
0x0, 0x9, 0x90, 0x12, 0x22, 0x22, 0x22, 0x0,
- 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0xd, 0xc2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x9f, 0x2e, 0xff, 0xff, 0xff, 0xff, 0xf5,
+ 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xd, 0xb2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0xae, 0x2e, 0xff, 0xff, 0xff, 0xff, 0xf5,
0x0, 0x5, 0x2, 0x22, 0xe9, 0x22, 0x22, 0x20,
0x0, 0x0, 0x0, 0x2, 0xf2, 0x5, 0x50, 0x0,
- 0x4f, 0xff, 0x0, 0x7, 0xc0, 0x3, 0xf2, 0x0,
- 0x1, 0x4f, 0x0, 0xd, 0x50, 0x0, 0x8d, 0x0,
+ 0x4f, 0xff, 0x0, 0x7, 0xc0, 0x4, 0xf2, 0x0,
+ 0x1, 0x4f, 0x0, 0xd, 0x50, 0x0, 0x8c, 0x0,
0x0, 0x2f, 0x1, 0x5f, 0x67, 0x89, 0xbf, 0x60,
0x0, 0x2f, 0xb, 0xfe, 0xcb, 0x98, 0x68, 0xe0,
0x0, 0x9f, 0x80, 0x0, 0x0, 0x0, 0x0, 0x60,
0x1c, 0xe5, 0xec, 0x63, 0x11, 0x11, 0x12, 0x31,
- 0x3d, 0x10, 0x17, 0xbe, 0xff, 0xff, 0xff, 0xf2,
+ 0x3c, 0x10, 0x17, 0xbe, 0xff, 0xff, 0xff, 0xf2,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
/* U+9001 "送" */
0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
- 0x3, 0xf7, 0x0, 0x4, 0x6f, 0xff, 0xc0, 0x0,
+ 0x3, 0xf7, 0x0, 0x5, 0x6f, 0xff, 0xc0, 0x0,
0x0, 0x2d, 0xa0, 0x3f, 0x20, 0x1, 0xe6, 0x0,
- 0x0, 0x1, 0x11, 0xd8, 0x0, 0x0, 0x4f, 0x60,
+ 0x1, 0x1, 0x11, 0xd8, 0x0, 0x0, 0x4f, 0x60,
0xd, 0xb2, 0xd, 0xac, 0xdd, 0xdd, 0xd8, 0xf5,
- 0x1, 0xaf, 0x24, 0x2, 0x29, 0xb2, 0x20, 0x20,
+ 0x0, 0xaf, 0x24, 0x2, 0x29, 0xb2, 0x20, 0x20,
0x0, 0x5, 0x0, 0x0, 0x7, 0xa0, 0x0, 0x0,
0x0, 0x0, 0x6, 0xbb, 0xbd, 0xeb, 0xbb, 0xa0,
0x4f, 0xff, 0x3, 0x66, 0x6d, 0xf8, 0x66, 0x50,
0x1, 0x4f, 0x0, 0x0, 0x2f, 0xaa, 0x0, 0x0,
0x0, 0x2f, 0x0, 0x1, 0xc9, 0xc, 0x80, 0x0,
- 0x0, 0x2f, 0x0, 0x6e, 0x90, 0x2, 0xdc, 0x50,
- 0x0, 0x9f, 0x84, 0xc4, 0x0, 0x0, 0x6, 0xb0,
+ 0x0, 0x2f, 0x0, 0x6e, 0x90, 0x2, 0xec, 0x50,
+ 0x0, 0x9f, 0x84, 0xc4, 0x0, 0x0, 0x8, 0xc0,
0x1c, 0xe5, 0xec, 0x73, 0x11, 0x11, 0x12, 0x21,
- 0x3d, 0x10, 0x17, 0xbe, 0xff, 0xff, 0xff, 0xf4,
+ 0x3c, 0x10, 0x17, 0xbe, 0xff, 0xff, 0xff, 0xf4,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
/* U+9519 "错" */
- 0x0, 0x69, 0x0, 0x0, 0x2d, 0x0, 0xf1, 0x0,
+ 0x0, 0x59, 0x0, 0x0, 0x2d, 0x0, 0xf1, 0x0,
0x0, 0xd9, 0x44, 0x21, 0x3e, 0x11, 0xf3, 0x10,
0x8, 0xed, 0xdd, 0x5c, 0xff, 0xff, 0xff, 0xe0,
0x3e, 0x20, 0x0, 0x0, 0x2d, 0x0, 0xf1, 0x0,
@@ -554,33 +621,37 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 98, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = -1},
{.bitmap_index = 196, .adv_w = 256, .box_w = 15, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 309, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 437, .adv_w = 256, .box_w = 15, .box_h = 15, .ofs_x = 1, .ofs_y = -1},
- {.bitmap_index = 550, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 662, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 782, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 902, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 1022, .adv_w = 256, .box_w = 15, .box_h = 14, .ofs_x = 1, .ofs_y = -1},
- {.bitmap_index = 1127, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 1247, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 1375, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 1487, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 1607, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 1719, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 1839, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 1951, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 2063, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 2183, .adv_w = 256, .box_w = 15, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 2303, .adv_w = 256, .box_w = 15, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 2423, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 2543, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 2655, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 2767, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 2895, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 3015, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 3135, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -1},
- {.bitmap_index = 3255, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 3383, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
- {.bitmap_index = 3511, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1}
+ {.bitmap_index = 437, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = -1},
+ {.bitmap_index = 535, .adv_w = 256, .box_w = 15, .box_h = 15, .ofs_x = 1, .ofs_y = -1},
+ {.bitmap_index = 648, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 760, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 872, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 992, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 1112, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 1232, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 1352, .adv_w = 256, .box_w = 15, .box_h = 14, .ofs_x = 1, .ofs_y = -1},
+ {.bitmap_index = 1457, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 1577, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 1705, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 1817, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 1945, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 2057, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 2177, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 2289, .adv_w = 256, .box_w = 15, .box_h = 15, .ofs_x = 1, .ofs_y = -1},
+ {.bitmap_index = 2402, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 2522, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 2642, .adv_w = 256, .box_w = 15, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 2762, .adv_w = 256, .box_w = 15, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 2882, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 3002, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 3114, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 3226, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 3354, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 3474, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 3594, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -1},
+ {.bitmap_index = 3714, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 3842, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
+ {.bitmap_index = 3970, .adv_w = 256, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -1}
};
/*---------------------
@@ -588,10 +659,11 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
*--------------------*/
static const uint16_t unicode_list_0[] = {
- 0x0, 0x3cd, 0x3fe, 0x5a4, 0x5ff, 0x650, 0x8fb, 0xb04,
- 0xd5f, 0xfc5, 0x10d3, 0x1158, 0x11d4, 0x13e3, 0x18fd, 0x1951,
- 0x1d36, 0x1fd8, 0x2489, 0x29b8, 0x2a41, 0x2b8e, 0x3092, 0x3219,
- 0x3a1f, 0x3d77, 0x3d99, 0x3ef8, 0x41a3, 0x41d4, 0x46ec
+ 0x0, 0x3cd, 0x3fe, 0x5a4, 0x5dd, 0x5ff, 0x650, 0x6b8,
+ 0x8fb, 0xafa, 0xb04, 0xd5f, 0xfc5, 0x10d3, 0x1158, 0x11d4,
+ 0x13e3, 0x18fd, 0x1951, 0x1d36, 0x1dae, 0x1fd8, 0x2489, 0x29b8,
+ 0x2a41, 0x2b8e, 0x3092, 0x3219, 0x3a1f, 0x3d77, 0x3d99, 0x3ef8,
+ 0x41a3, 0x41d4, 0x46ec
};
/*Collect the unicode lists and glyph_id offsets*/
@@ -599,7 +671,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
{
.range_start = 20013, .range_length = 18157, .glyph_id_start = 1,
- .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 31, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
+ .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 35, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
}
};
diff --git a/firmware/sticks3/include/vibe_audio.h b/firmware/sticks3/include/vibe_audio.h
index 7482691..d1f8d9e 100644
--- a/firmware/sticks3/include/vibe_audio.h
+++ b/firmware/sticks3/include/vibe_audio.h
@@ -19,7 +19,10 @@ typedef enum {
esp_err_t vibe_audio_init(void);
esp_err_t vibe_audio_start(void);
esp_err_t vibe_audio_stop(void);
+esp_err_t vibe_audio_shutdown(void);
esp_err_t vibe_audio_play_sound(agent_sound_t sound);
bool vibe_audio_is_recording(void);
const uint8_t *vibe_audio_data(size_t *len);
+esp_err_t vibe_audio_chunk(size_t offset, size_t max_len,
+ const uint8_t **data, size_t *len);
void vibe_audio_clear(void);
diff --git a/firmware/sticks3/include/vibe_board.h b/firmware/sticks3/include/vibe_board.h
index d5726d6..0544ee3 100644
--- a/firmware/sticks3/include/vibe_board.h
+++ b/firmware/sticks3/include/vibe_board.h
@@ -11,3 +11,5 @@ esp_err_t vibe_board_battery_level(int *level_percent);
esp_err_t vibe_board_battery_charging(bool *charging);
esp_err_t vibe_board_usb_powered(bool *usb_powered);
esp_err_t vibe_board_speaker_set_enabled(bool enabled);
+esp_err_t vibe_board_prepare_shutdown(void);
+esp_err_t vibe_board_shutdown(void);
diff --git a/firmware/sticks3/include/vibe_imu.h b/firmware/sticks3/include/vibe_imu.h
new file mode 100644
index 0000000..15a1c9b
--- /dev/null
+++ b/firmware/sticks3/include/vibe_imu.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include
+
+#include "esp_err.h"
+
+esp_err_t vibe_imu_init(void);
+bool vibe_imu_is_ready(void);
+esp_err_t vibe_imu_read_acceleration(float *x_g, float *y_g, float *z_g);
+esp_err_t vibe_imu_shutdown(void);
diff --git a/firmware/sticks3/include/vibe_stick_config.h b/firmware/sticks3/include/vibe_stick_config.h
index cc033ff..d387b45 100644
--- a/firmware/sticks3/include/vibe_stick_config.h
+++ b/firmware/sticks3/include/vibe_stick_config.h
@@ -2,15 +2,32 @@
#define VIBE_STICK_DEVICE_NAME "VibeStick"
#define FIRMWARE_NAME "vibestick"
-#define FIRMWARE_VERSION "0.1.4"
+#define FIRMWARE_VERSION "0.1.6"
#define TRANSPORT "HTTP"
#define VIBE_STICK_STATE_PATH "/state"
#define VIBE_STICK_EVENT_PATH "/event"
#define VIBE_STICK_QUOTA_REFRESH_PATH "/quota/refresh"
#define VIBE_STICK_RECORDING_START_PATH "/recording/start"
#define VIBE_STICK_RECORDING_AUDIO_PATH "/recording/audio"
+#define VIBE_STICK_RECORDING_CHUNK_PATH "/recording/chunk"
#define VIBE_STICK_RECORDING_STOP_PATH "/recording/stop"
#define VIBE_STICK_STATE_POLL_MS 2000
+#define VIBE_STICK_SCREEN_IDLE_MS 30000
+#define VIBE_STICK_SCREEN_EVENT_WAKE_MS 20000
+#define VIBE_STICK_AUTO_POWER_OFF_MS (60LL * 60LL * 1000LL)
+#define VIBE_STICK_IMU_POLL_MS 100
+#define VIBE_STICK_FACE_DOWN_HOLD_MS 800
+#define VIBE_STICK_FACE_UP_HOLD_MS 500
+#define VIBE_STICK_FACE_DOWN_Z_THRESHOLD_G (-0.72f)
+#define VIBE_STICK_FACE_UP_Z_THRESHOLD_G 0.60f
+#define VIBE_STICK_FACE_DOWN_XY_LIMIT_G 0.70f
+#define VIBE_STICK_PICKUP_DELTA_G 0.08f
+#define VIBE_STICK_PICKUP_STRONG_DELTA_G 0.18f
+#define VIBE_STICK_PICKUP_CONFIRM_SAMPLES 2
+#define VIBE_STICK_RECORDING_CHUNK_INTERVAL_MS 120
+#define VIBE_STICK_RECORDING_CHUNK_BYTES 3840
+#define VIBE_STICK_RECORDING_RELEASE_DEBOUNCE_MS 80
+#define VIBE_STICK_RECORDING_MAX_MS 115000
#if __has_include("vibe_stick_secrets.h")
#include "vibe_stick_secrets.h"
diff --git a/firmware/sticks3/original_sound_sources/SOURCES.md b/firmware/sticks3/original_sound_sources/SOURCES.md
new file mode 100644
index 0000000..86ffa11
--- /dev/null
+++ b/firmware/sticks3/original_sound_sources/SOURCES.md
@@ -0,0 +1,12 @@
+# Original sound sources
+
+These files are downloaded recordings, not AI-generated or voice-cloned audio.
+
+| Firmware event | Local source file | Source | Notes |
+| --- | --- | --- | --- |
+| Done | `01-aolige-original.mp3` | https://www.ear0.com/sound/37257 | Ear0 sound 37257; the source page marks it CC0. |
+| Approval | _pending_ | _pending_ | The selected `快点吧,我等到花儿都谢了` clip has not yet been found as a trustworthy standalone download. The firmware uses its original tone for now. |
+| Error | `03-wanle-babiqule-original.mp3` | https://www.myinstants.com/en/instant/wan-liao-bbqliao-6592/ | User-uploaded original recording. The complete downloaded file is retained; the firmware uses only its first 3 seconds. Licensing is not stated on the page, so use it personally unless you obtain permission. |
+
+Generated firmware assets live separately under `src/sounds/` and are 16 kHz,
+mono, signed 16-bit PCM conversions of these source files.
diff --git a/firmware/sticks3/partitions_singleapp_large.csv b/firmware/sticks3/partitions_singleapp_large.csv
new file mode 100644
index 0000000..90a7b25
--- /dev/null
+++ b/firmware/sticks3/partitions_singleapp_large.csv
@@ -0,0 +1,4 @@
+# Name, Type, SubType, Offset, Size, Flags
+nvs, data, nvs, , 0x6000,
+phy_init, data, phy, , 0x1000,
+factory, app, factory, , 1500K,
diff --git a/firmware/sticks3/platformio.ini b/firmware/sticks3/platformio.ini
index ac637a1..a294ca5 100644
--- a/firmware/sticks3/platformio.ini
+++ b/firmware/sticks3/platformio.ini
@@ -3,11 +3,13 @@ src_dir = src
include_dir = include
[env:sticks3]
-platform = espressif32
+platform = espressif32 @ 6.12.0
board = esp32-s3-devkitc-1
framework = espidf
monitor_speed = 115200
board_build.flash_size = 8MB
+board_build.partitions = partitions_singleapp_large.csv
board_upload.flash_size = 8MB
+board_upload.maximum_size = 1536000
build_flags =
- -D VIBE_STICK_FIRMWARE_VERSION=\"0.1.1\"
+ -D VIBE_STICK_FIRMWARE_VERSION=\"0.1.6\"
diff --git a/firmware/sticks3/sdkconfig.sticks3 b/firmware/sticks3/sdkconfig.sticks3
new file mode 100644
index 0000000..6588624
--- /dev/null
+++ b/firmware/sticks3/sdkconfig.sticks3
@@ -0,0 +1,2888 @@
+#
+# Automatically generated file. DO NOT EDIT.
+# Espressif IoT Development Framework (ESP-IDF) 5.5.0 Project Configuration
+#
+CONFIG_SOC_ADC_SUPPORTED=y
+CONFIG_SOC_UART_SUPPORTED=y
+CONFIG_SOC_PCNT_SUPPORTED=y
+CONFIG_SOC_PHY_SUPPORTED=y
+CONFIG_SOC_WIFI_SUPPORTED=y
+CONFIG_SOC_TWAI_SUPPORTED=y
+CONFIG_SOC_GDMA_SUPPORTED=y
+CONFIG_SOC_UHCI_SUPPORTED=y
+CONFIG_SOC_AHB_GDMA_SUPPORTED=y
+CONFIG_SOC_GPTIMER_SUPPORTED=y
+CONFIG_SOC_LCDCAM_SUPPORTED=y
+CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED=y
+CONFIG_SOC_LCDCAM_RGB_LCD_SUPPORTED=y
+CONFIG_SOC_MCPWM_SUPPORTED=y
+CONFIG_SOC_DEDICATED_GPIO_SUPPORTED=y
+CONFIG_SOC_CACHE_SUPPORT_WRAP=y
+CONFIG_SOC_ULP_SUPPORTED=y
+CONFIG_SOC_ULP_FSM_SUPPORTED=y
+CONFIG_SOC_RISCV_COPROC_SUPPORTED=y
+CONFIG_SOC_BT_SUPPORTED=y
+CONFIG_SOC_USB_OTG_SUPPORTED=y
+CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED=y
+CONFIG_SOC_CCOMP_TIMER_SUPPORTED=y
+CONFIG_SOC_ASYNC_MEMCPY_SUPPORTED=y
+CONFIG_SOC_SUPPORTS_SECURE_DL_MODE=y
+CONFIG_SOC_EFUSE_KEY_PURPOSE_FIELD=y
+CONFIG_SOC_EFUSE_SUPPORTED=y
+CONFIG_SOC_SDMMC_HOST_SUPPORTED=y
+CONFIG_SOC_RTC_FAST_MEM_SUPPORTED=y
+CONFIG_SOC_RTC_SLOW_MEM_SUPPORTED=y
+CONFIG_SOC_RTC_MEM_SUPPORTED=y
+CONFIG_SOC_PSRAM_DMA_CAPABLE=y
+CONFIG_SOC_XT_WDT_SUPPORTED=y
+CONFIG_SOC_I2S_SUPPORTED=y
+CONFIG_SOC_RMT_SUPPORTED=y
+CONFIG_SOC_SDM_SUPPORTED=y
+CONFIG_SOC_GPSPI_SUPPORTED=y
+CONFIG_SOC_LEDC_SUPPORTED=y
+CONFIG_SOC_I2C_SUPPORTED=y
+CONFIG_SOC_SYSTIMER_SUPPORTED=y
+CONFIG_SOC_SUPPORT_COEXISTENCE=y
+CONFIG_SOC_TEMP_SENSOR_SUPPORTED=y
+CONFIG_SOC_AES_SUPPORTED=y
+CONFIG_SOC_MPI_SUPPORTED=y
+CONFIG_SOC_SHA_SUPPORTED=y
+CONFIG_SOC_HMAC_SUPPORTED=y
+CONFIG_SOC_DIG_SIGN_SUPPORTED=y
+CONFIG_SOC_FLASH_ENC_SUPPORTED=y
+CONFIG_SOC_SECURE_BOOT_SUPPORTED=y
+CONFIG_SOC_MEMPROT_SUPPORTED=y
+CONFIG_SOC_TOUCH_SENSOR_SUPPORTED=y
+CONFIG_SOC_BOD_SUPPORTED=y
+CONFIG_SOC_CLK_TREE_SUPPORTED=y
+CONFIG_SOC_MPU_SUPPORTED=y
+CONFIG_SOC_WDT_SUPPORTED=y
+CONFIG_SOC_SPI_FLASH_SUPPORTED=y
+CONFIG_SOC_RNG_SUPPORTED=y
+CONFIG_SOC_LIGHT_SLEEP_SUPPORTED=y
+CONFIG_SOC_DEEP_SLEEP_SUPPORTED=y
+CONFIG_SOC_LP_PERIPH_SHARE_INTERRUPT=y
+CONFIG_SOC_PM_SUPPORTED=y
+CONFIG_SOC_SIMD_INSTRUCTION_SUPPORTED=y
+CONFIG_SOC_XTAL_SUPPORT_40M=y
+CONFIG_SOC_APPCPU_HAS_CLOCK_GATING_BUG=y
+CONFIG_SOC_ADC_RTC_CTRL_SUPPORTED=y
+CONFIG_SOC_ADC_DIG_CTRL_SUPPORTED=y
+CONFIG_SOC_ADC_ARBITER_SUPPORTED=y
+CONFIG_SOC_ADC_DIG_IIR_FILTER_SUPPORTED=y
+CONFIG_SOC_ADC_MONITOR_SUPPORTED=y
+CONFIG_SOC_ADC_DMA_SUPPORTED=y
+CONFIG_SOC_ADC_PERIPH_NUM=2
+CONFIG_SOC_ADC_MAX_CHANNEL_NUM=10
+CONFIG_SOC_ADC_ATTEN_NUM=4
+CONFIG_SOC_ADC_DIGI_CONTROLLER_NUM=2
+CONFIG_SOC_ADC_PATT_LEN_MAX=24
+CONFIG_SOC_ADC_DIGI_MIN_BITWIDTH=12
+CONFIG_SOC_ADC_DIGI_MAX_BITWIDTH=12
+CONFIG_SOC_ADC_DIGI_RESULT_BYTES=4
+CONFIG_SOC_ADC_DIGI_DATA_BYTES_PER_CONV=4
+CONFIG_SOC_ADC_DIGI_IIR_FILTER_NUM=2
+CONFIG_SOC_ADC_DIGI_MONITOR_NUM=2
+CONFIG_SOC_ADC_SAMPLE_FREQ_THRES_HIGH=83333
+CONFIG_SOC_ADC_SAMPLE_FREQ_THRES_LOW=611
+CONFIG_SOC_ADC_RTC_MIN_BITWIDTH=12
+CONFIG_SOC_ADC_RTC_MAX_BITWIDTH=12
+CONFIG_SOC_ADC_CALIBRATION_V1_SUPPORTED=y
+CONFIG_SOC_ADC_SELF_HW_CALI_SUPPORTED=y
+CONFIG_SOC_ADC_SHARED_POWER=y
+CONFIG_SOC_APB_BACKUP_DMA=y
+CONFIG_SOC_BROWNOUT_RESET_SUPPORTED=y
+CONFIG_SOC_CACHE_WRITEBACK_SUPPORTED=y
+CONFIG_SOC_CACHE_FREEZE_SUPPORTED=y
+CONFIG_SOC_CACHE_ACS_INVALID_STATE_ON_PANIC=y
+CONFIG_SOC_CPU_CORES_NUM=2
+CONFIG_SOC_CPU_INTR_NUM=32
+CONFIG_SOC_CPU_HAS_FPU=y
+CONFIG_SOC_HP_CPU_HAS_MULTIPLE_CORES=y
+CONFIG_SOC_CPU_BREAKPOINTS_NUM=2
+CONFIG_SOC_CPU_WATCHPOINTS_NUM=2
+CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=0x40
+CONFIG_SOC_SIMD_PREFERRED_DATA_ALIGNMENT=16
+CONFIG_SOC_DS_SIGNATURE_MAX_BIT_LEN=4096
+CONFIG_SOC_DS_KEY_PARAM_MD_IV_LENGTH=16
+CONFIG_SOC_DS_KEY_CHECK_MAX_WAIT_US=1100
+CONFIG_SOC_AHB_GDMA_VERSION=1
+CONFIG_SOC_GDMA_NUM_GROUPS_MAX=1
+CONFIG_SOC_GDMA_PAIRS_PER_GROUP=5
+CONFIG_SOC_GDMA_PAIRS_PER_GROUP_MAX=5
+CONFIG_SOC_AHB_GDMA_SUPPORT_PSRAM=y
+CONFIG_SOC_GPIO_PORT=1
+CONFIG_SOC_GPIO_PIN_COUNT=49
+CONFIG_SOC_GPIO_SUPPORT_PIN_GLITCH_FILTER=y
+CONFIG_SOC_GPIO_FILTER_CLK_SUPPORT_APB=y
+CONFIG_SOC_GPIO_SUPPORT_RTC_INDEPENDENT=y
+CONFIG_SOC_GPIO_SUPPORT_FORCE_HOLD=y
+CONFIG_SOC_GPIO_VALID_GPIO_MASK=0x1FFFFFFFFFFFF
+CONFIG_SOC_GPIO_IN_RANGE_MAX=48
+CONFIG_SOC_GPIO_OUT_RANGE_MAX=48
+CONFIG_SOC_GPIO_VALID_DIGITAL_IO_PAD_MASK=0x0001FFFFFC000000
+CONFIG_SOC_GPIO_CLOCKOUT_BY_IO_MUX=y
+CONFIG_SOC_GPIO_CLOCKOUT_CHANNEL_NUM=3
+CONFIG_SOC_GPIO_SUPPORT_HOLD_IO_IN_DSLP=y
+CONFIG_SOC_DEDIC_GPIO_OUT_CHANNELS_NUM=8
+CONFIG_SOC_DEDIC_GPIO_IN_CHANNELS_NUM=8
+CONFIG_SOC_DEDIC_GPIO_OUT_AUTO_ENABLE=y
+CONFIG_SOC_I2C_NUM=2
+CONFIG_SOC_HP_I2C_NUM=2
+CONFIG_SOC_I2C_FIFO_LEN=32
+CONFIG_SOC_I2C_CMD_REG_NUM=8
+CONFIG_SOC_I2C_SUPPORT_SLAVE=y
+CONFIG_SOC_I2C_SUPPORT_HW_CLR_BUS=y
+CONFIG_SOC_I2C_SUPPORT_XTAL=y
+CONFIG_SOC_I2C_SUPPORT_RTC=y
+CONFIG_SOC_I2C_SUPPORT_10BIT_ADDR=y
+CONFIG_SOC_I2C_SLAVE_SUPPORT_BROADCAST=y
+CONFIG_SOC_I2C_SLAVE_SUPPORT_I2CRAM_ACCESS=y
+CONFIG_SOC_I2C_SLAVE_CAN_GET_STRETCH_CAUSE=y
+CONFIG_SOC_I2S_NUM=2
+CONFIG_SOC_I2S_HW_VERSION_2=y
+CONFIG_SOC_I2S_SUPPORTS_XTAL=y
+CONFIG_SOC_I2S_SUPPORTS_PLL_F160M=y
+CONFIG_SOC_I2S_SUPPORTS_PCM=y
+CONFIG_SOC_I2S_SUPPORTS_PDM=y
+CONFIG_SOC_I2S_SUPPORTS_PDM_TX=y
+CONFIG_SOC_I2S_SUPPORTS_PCM2PDM=y
+CONFIG_SOC_I2S_SUPPORTS_PDM_RX=y
+CONFIG_SOC_I2S_SUPPORTS_PDM2PCM=y
+CONFIG_SOC_I2S_PDM_MAX_TX_LINES=2
+CONFIG_SOC_I2S_PDM_MAX_RX_LINES=4
+CONFIG_SOC_I2S_SUPPORTS_TDM=y
+CONFIG_SOC_LEDC_SUPPORT_APB_CLOCK=y
+CONFIG_SOC_LEDC_SUPPORT_XTAL_CLOCK=y
+CONFIG_SOC_LEDC_TIMER_NUM=4
+CONFIG_SOC_LEDC_CHANNEL_NUM=8
+CONFIG_SOC_LEDC_TIMER_BIT_WIDTH=14
+CONFIG_SOC_LEDC_SUPPORT_FADE_STOP=y
+CONFIG_SOC_MCPWM_GROUPS=2
+CONFIG_SOC_MCPWM_TIMERS_PER_GROUP=3
+CONFIG_SOC_MCPWM_OPERATORS_PER_GROUP=3
+CONFIG_SOC_MCPWM_COMPARATORS_PER_OPERATOR=2
+CONFIG_SOC_MCPWM_GENERATORS_PER_OPERATOR=2
+CONFIG_SOC_MCPWM_TRIGGERS_PER_OPERATOR=2
+CONFIG_SOC_MCPWM_GPIO_FAULTS_PER_GROUP=3
+CONFIG_SOC_MCPWM_CAPTURE_TIMERS_PER_GROUP=y
+CONFIG_SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER=3
+CONFIG_SOC_MCPWM_GPIO_SYNCHROS_PER_GROUP=3
+CONFIG_SOC_MCPWM_SWSYNC_CAN_PROPAGATE=y
+CONFIG_SOC_MMU_LINEAR_ADDRESS_REGION_NUM=1
+CONFIG_SOC_MMU_PERIPH_NUM=1
+CONFIG_SOC_MPU_MIN_REGION_SIZE=0x20000000
+CONFIG_SOC_MPU_REGIONS_MAX_NUM=8
+CONFIG_SOC_PCNT_GROUPS=1
+CONFIG_SOC_PCNT_UNITS_PER_GROUP=4
+CONFIG_SOC_PCNT_CHANNELS_PER_UNIT=2
+CONFIG_SOC_PCNT_THRES_POINT_PER_UNIT=2
+CONFIG_SOC_RMT_GROUPS=1
+CONFIG_SOC_RMT_TX_CANDIDATES_PER_GROUP=4
+CONFIG_SOC_RMT_RX_CANDIDATES_PER_GROUP=4
+CONFIG_SOC_RMT_CHANNELS_PER_GROUP=8
+CONFIG_SOC_RMT_MEM_WORDS_PER_CHANNEL=48
+CONFIG_SOC_RMT_SUPPORT_RX_PINGPONG=y
+CONFIG_SOC_RMT_SUPPORT_RX_DEMODULATION=y
+CONFIG_SOC_RMT_SUPPORT_TX_ASYNC_STOP=y
+CONFIG_SOC_RMT_SUPPORT_TX_LOOP_COUNT=y
+CONFIG_SOC_RMT_SUPPORT_TX_LOOP_AUTO_STOP=y
+CONFIG_SOC_RMT_SUPPORT_TX_SYNCHRO=y
+CONFIG_SOC_RMT_SUPPORT_TX_CARRIER_DATA_ONLY=y
+CONFIG_SOC_RMT_SUPPORT_XTAL=y
+CONFIG_SOC_RMT_SUPPORT_RC_FAST=y
+CONFIG_SOC_RMT_SUPPORT_APB=y
+CONFIG_SOC_RMT_SUPPORT_DMA=y
+CONFIG_SOC_LCD_I80_SUPPORTED=y
+CONFIG_SOC_LCD_RGB_SUPPORTED=y
+CONFIG_SOC_LCD_I80_BUSES=1
+CONFIG_SOC_LCD_RGB_PANELS=1
+CONFIG_SOC_LCD_I80_BUS_WIDTH=16
+CONFIG_SOC_LCD_RGB_DATA_WIDTH=16
+CONFIG_SOC_LCD_SUPPORT_RGB_YUV_CONV=y
+CONFIG_SOC_LCDCAM_I80_NUM_BUSES=1
+CONFIG_SOC_LCDCAM_I80_BUS_WIDTH=16
+CONFIG_SOC_LCDCAM_RGB_NUM_PANELS=1
+CONFIG_SOC_LCDCAM_RGB_DATA_WIDTH=16
+CONFIG_SOC_RTC_CNTL_CPU_PD_DMA_BUS_WIDTH=128
+CONFIG_SOC_RTC_CNTL_CPU_PD_REG_FILE_NUM=549
+CONFIG_SOC_RTC_CNTL_TAGMEM_PD_DMA_BUS_WIDTH=128
+CONFIG_SOC_RTCIO_PIN_COUNT=22
+CONFIG_SOC_RTCIO_INPUT_OUTPUT_SUPPORTED=y
+CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y
+CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y
+CONFIG_SOC_LP_IO_CLOCK_IS_INDEPENDENT=y
+CONFIG_SOC_SDM_GROUPS=1
+CONFIG_SOC_SDM_CHANNELS_PER_GROUP=8
+CONFIG_SOC_SDM_CLK_SUPPORT_APB=y
+CONFIG_SOC_SPI_PERIPH_NUM=3
+CONFIG_SOC_SPI_MAX_CS_NUM=6
+CONFIG_SOC_SPI_MAXIMUM_BUFFER_SIZE=64
+CONFIG_SOC_SPI_SUPPORT_DDRCLK=y
+CONFIG_SOC_SPI_SLAVE_SUPPORT_SEG_TRANS=y
+CONFIG_SOC_SPI_SUPPORT_CD_SIG=y
+CONFIG_SOC_SPI_SUPPORT_CONTINUOUS_TRANS=y
+CONFIG_SOC_SPI_SUPPORT_SLAVE_HD_VER2=y
+CONFIG_SOC_SPI_SUPPORT_CLK_APB=y
+CONFIG_SOC_SPI_SUPPORT_CLK_XTAL=y
+CONFIG_SOC_SPI_PERIPH_SUPPORT_CONTROL_DUMMY_OUT=y
+CONFIG_SOC_MEMSPI_IS_INDEPENDENT=y
+CONFIG_SOC_SPI_MAX_PRE_DIVIDER=16
+CONFIG_SOC_SPI_SUPPORT_OCT=y
+CONFIG_SOC_SPI_SCT_SUPPORTED=y
+CONFIG_SOC_SPI_SCT_REG_NUM=14
+CONFIG_SOC_SPI_SCT_BUFFER_NUM_MAX=y
+CONFIG_SOC_SPI_SCT_CONF_BITLEN_MAX=0x3FFFA
+CONFIG_SOC_MEMSPI_SRC_FREQ_120M_SUPPORTED=y
+CONFIG_SOC_MEMSPI_SRC_FREQ_80M_SUPPORTED=y
+CONFIG_SOC_MEMSPI_SRC_FREQ_40M_SUPPORTED=y
+CONFIG_SOC_MEMSPI_SRC_FREQ_20M_SUPPORTED=y
+CONFIG_SOC_SPIRAM_SUPPORTED=y
+CONFIG_SOC_SPIRAM_XIP_SUPPORTED=y
+CONFIG_SOC_SYSTIMER_COUNTER_NUM=2
+CONFIG_SOC_SYSTIMER_ALARM_NUM=3
+CONFIG_SOC_SYSTIMER_BIT_WIDTH_LO=32
+CONFIG_SOC_SYSTIMER_BIT_WIDTH_HI=20
+CONFIG_SOC_SYSTIMER_FIXED_DIVIDER=y
+CONFIG_SOC_SYSTIMER_INT_LEVEL=y
+CONFIG_SOC_SYSTIMER_ALARM_MISS_COMPENSATE=y
+CONFIG_SOC_TIMER_GROUPS=2
+CONFIG_SOC_TIMER_GROUP_TIMERS_PER_GROUP=2
+CONFIG_SOC_TIMER_GROUP_COUNTER_BIT_WIDTH=54
+CONFIG_SOC_TIMER_GROUP_SUPPORT_XTAL=y
+CONFIG_SOC_TIMER_GROUP_SUPPORT_APB=y
+CONFIG_SOC_TIMER_GROUP_TOTAL_TIMERS=4
+CONFIG_SOC_LP_TIMER_BIT_WIDTH_LO=32
+CONFIG_SOC_LP_TIMER_BIT_WIDTH_HI=16
+CONFIG_SOC_TOUCH_SENSOR_VERSION=2
+CONFIG_SOC_TOUCH_SENSOR_NUM=15
+CONFIG_SOC_TOUCH_MIN_CHAN_ID=1
+CONFIG_SOC_TOUCH_MAX_CHAN_ID=14
+CONFIG_SOC_TOUCH_SUPPORT_BENCHMARK=y
+CONFIG_SOC_TOUCH_SUPPORT_SLEEP_WAKEUP=y
+CONFIG_SOC_TOUCH_SUPPORT_WATERPROOF=y
+CONFIG_SOC_TOUCH_SUPPORT_PROX_SENSING=y
+CONFIG_SOC_TOUCH_SUPPORT_DENOISE_CHAN=y
+CONFIG_SOC_TOUCH_PROXIMITY_CHANNEL_NUM=3
+CONFIG_SOC_TOUCH_PROXIMITY_MEAS_DONE_SUPPORTED=y
+CONFIG_SOC_TOUCH_SAMPLE_CFG_NUM=1
+CONFIG_SOC_TWAI_CONTROLLER_NUM=1
+CONFIG_SOC_TWAI_MASK_FILTER_NUM=1
+CONFIG_SOC_TWAI_CLK_SUPPORT_APB=y
+CONFIG_SOC_TWAI_BRP_MIN=2
+CONFIG_SOC_TWAI_BRP_MAX=16384
+CONFIG_SOC_TWAI_SUPPORTS_RX_STATUS=y
+CONFIG_SOC_UART_NUM=3
+CONFIG_SOC_UART_HP_NUM=3
+CONFIG_SOC_UART_FIFO_LEN=128
+CONFIG_SOC_UART_BITRATE_MAX=5000000
+CONFIG_SOC_UART_SUPPORT_FSM_TX_WAIT_SEND=y
+CONFIG_SOC_UART_SUPPORT_WAKEUP_INT=y
+CONFIG_SOC_UART_SUPPORT_APB_CLK=y
+CONFIG_SOC_UART_SUPPORT_RTC_CLK=y
+CONFIG_SOC_UART_SUPPORT_XTAL_CLK=y
+CONFIG_SOC_UART_WAKEUP_SUPPORT_ACTIVE_THRESH_MODE=y
+CONFIG_SOC_UHCI_NUM=1
+CONFIG_SOC_USB_OTG_PERIPH_NUM=1
+CONFIG_SOC_SHA_DMA_MAX_BUFFER_SIZE=3968
+CONFIG_SOC_SHA_SUPPORT_DMA=y
+CONFIG_SOC_SHA_SUPPORT_RESUME=y
+CONFIG_SOC_SHA_GDMA=y
+CONFIG_SOC_SHA_SUPPORT_SHA1=y
+CONFIG_SOC_SHA_SUPPORT_SHA224=y
+CONFIG_SOC_SHA_SUPPORT_SHA256=y
+CONFIG_SOC_SHA_SUPPORT_SHA384=y
+CONFIG_SOC_SHA_SUPPORT_SHA512=y
+CONFIG_SOC_SHA_SUPPORT_SHA512_224=y
+CONFIG_SOC_SHA_SUPPORT_SHA512_256=y
+CONFIG_SOC_SHA_SUPPORT_SHA512_T=y
+CONFIG_SOC_MPI_MEM_BLOCKS_NUM=4
+CONFIG_SOC_MPI_OPERATIONS_NUM=3
+CONFIG_SOC_RSA_MAX_BIT_LEN=4096
+CONFIG_SOC_AES_SUPPORT_DMA=y
+CONFIG_SOC_AES_GDMA=y
+CONFIG_SOC_AES_SUPPORT_AES_128=y
+CONFIG_SOC_AES_SUPPORT_AES_256=y
+CONFIG_SOC_PM_SUPPORT_EXT0_WAKEUP=y
+CONFIG_SOC_PM_SUPPORT_EXT1_WAKEUP=y
+CONFIG_SOC_PM_SUPPORT_EXT_WAKEUP=y
+CONFIG_SOC_PM_SUPPORT_WIFI_WAKEUP=y
+CONFIG_SOC_PM_SUPPORT_BT_WAKEUP=y
+CONFIG_SOC_PM_SUPPORT_TOUCH_SENSOR_WAKEUP=y
+CONFIG_SOC_PM_SUPPORT_CPU_PD=y
+CONFIG_SOC_PM_SUPPORT_TAGMEM_PD=y
+CONFIG_SOC_PM_SUPPORT_RTC_PERIPH_PD=y
+CONFIG_SOC_PM_SUPPORT_RC_FAST_PD=y
+CONFIG_SOC_PM_SUPPORT_VDDSDIO_PD=y
+CONFIG_SOC_PM_SUPPORT_MAC_BB_PD=y
+CONFIG_SOC_PM_SUPPORT_MODEM_PD=y
+CONFIG_SOC_CONFIGURABLE_VDDSDIO_SUPPORTED=y
+CONFIG_SOC_PM_SUPPORT_DEEPSLEEP_CHECK_STUB_ONLY=y
+CONFIG_SOC_PM_CPU_RETENTION_BY_RTCCNTL=y
+CONFIG_SOC_PM_MODEM_RETENTION_BY_BACKUPDMA=y
+CONFIG_SOC_PM_MODEM_PD_BY_SW=y
+CONFIG_SOC_CLK_RC_FAST_D256_SUPPORTED=y
+CONFIG_SOC_RTC_SLOW_CLK_SUPPORT_RC_FAST_D256=y
+CONFIG_SOC_CLK_RC_FAST_SUPPORT_CALIBRATION=y
+CONFIG_SOC_CLK_XTAL32K_SUPPORTED=y
+CONFIG_SOC_CLK_LP_FAST_SUPPORT_XTAL_D2=y
+CONFIG_SOC_EFUSE_DIS_DOWNLOAD_ICACHE=y
+CONFIG_SOC_EFUSE_DIS_DOWNLOAD_DCACHE=y
+CONFIG_SOC_EFUSE_HARD_DIS_JTAG=y
+CONFIG_SOC_EFUSE_DIS_USB_JTAG=y
+CONFIG_SOC_EFUSE_SOFT_DIS_JTAG=y
+CONFIG_SOC_EFUSE_DIS_DIRECT_BOOT=y
+CONFIG_SOC_EFUSE_DIS_ICACHE=y
+CONFIG_SOC_EFUSE_BLOCK9_KEY_PURPOSE_QUIRK=y
+CONFIG_SOC_SECURE_BOOT_V2_RSA=y
+CONFIG_SOC_EFUSE_SECURE_BOOT_KEY_DIGESTS=3
+CONFIG_SOC_EFUSE_REVOKE_BOOT_KEY_DIGESTS=y
+CONFIG_SOC_SUPPORT_SECURE_BOOT_REVOKE_KEY=y
+CONFIG_SOC_FLASH_ENCRYPTED_XTS_AES_BLOCK_MAX=64
+CONFIG_SOC_FLASH_ENCRYPTION_XTS_AES=y
+CONFIG_SOC_FLASH_ENCRYPTION_XTS_AES_OPTIONS=y
+CONFIG_SOC_FLASH_ENCRYPTION_XTS_AES_128=y
+CONFIG_SOC_FLASH_ENCRYPTION_XTS_AES_256=y
+CONFIG_SOC_MEMPROT_CPU_PREFETCH_PAD_SIZE=16
+CONFIG_SOC_MEMPROT_MEM_ALIGN_SIZE=256
+CONFIG_SOC_PHY_DIG_REGS_MEM_SIZE=21
+CONFIG_SOC_MAC_BB_PD_MEM_SIZE=192
+CONFIG_SOC_WIFI_LIGHT_SLEEP_CLK_WIDTH=12
+CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_WAIT_IDLE=y
+CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_SUSPEND=y
+CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_RESUME=y
+CONFIG_SOC_SPI_MEM_SUPPORT_SW_SUSPEND=y
+CONFIG_SOC_SPI_MEM_SUPPORT_FLASH_OPI_MODE=y
+CONFIG_SOC_SPI_MEM_SUPPORT_TIMING_TUNING=y
+CONFIG_SOC_SPI_MEM_SUPPORT_CONFIG_GPIO_BY_EFUSE=y
+CONFIG_SOC_SPI_MEM_SUPPORT_WRAP=y
+CONFIG_SOC_MEMSPI_TIMING_TUNING_BY_MSPI_DELAY=y
+CONFIG_SOC_MEMSPI_CORE_CLK_SHARED_WITH_PSRAM=y
+CONFIG_SOC_SPI_MEM_SUPPORT_CACHE_32BIT_ADDR_MAP=y
+CONFIG_SOC_COEX_HW_PTI=y
+CONFIG_SOC_EXTERNAL_COEX_LEADER_TX_LINE=y
+CONFIG_SOC_SDMMC_USE_GPIO_MATRIX=y
+CONFIG_SOC_SDMMC_NUM_SLOTS=2
+CONFIG_SOC_SDMMC_SUPPORT_XTAL_CLOCK=y
+CONFIG_SOC_SDMMC_DELAY_PHASE_NUM=4
+CONFIG_SOC_TEMPERATURE_SENSOR_SUPPORT_FAST_RC=y
+CONFIG_SOC_WIFI_HW_TSF=y
+CONFIG_SOC_WIFI_FTM_SUPPORT=y
+CONFIG_SOC_WIFI_GCMP_SUPPORT=y
+CONFIG_SOC_WIFI_WAPI_SUPPORT=y
+CONFIG_SOC_WIFI_CSI_SUPPORT=y
+CONFIG_SOC_WIFI_MESH_SUPPORT=y
+CONFIG_SOC_WIFI_SUPPORT_VARIABLE_BEACON_WINDOW=y
+CONFIG_SOC_WIFI_PHY_NEEDS_USB_WORKAROUND=y
+CONFIG_SOC_BLE_SUPPORTED=y
+CONFIG_SOC_BLE_MESH_SUPPORTED=y
+CONFIG_SOC_BLE_50_SUPPORTED=y
+CONFIG_SOC_BLE_DEVICE_PRIVACY_SUPPORTED=y
+CONFIG_SOC_BLUFI_SUPPORTED=y
+CONFIG_SOC_ULP_HAS_ADC=y
+CONFIG_SOC_PHY_COMBO_MODULE=y
+CONFIG_IDF_CMAKE=y
+CONFIG_IDF_TOOLCHAIN="gcc"
+CONFIG_IDF_TOOLCHAIN_GCC=y
+CONFIG_IDF_TARGET_ARCH_XTENSA=y
+CONFIG_IDF_TARGET_ARCH="xtensa"
+CONFIG_IDF_TARGET="esp32s3"
+CONFIG_IDF_INIT_VERSION="5.5.0"
+CONFIG_IDF_TARGET_ESP32S3=y
+CONFIG_IDF_FIRMWARE_CHIP_ID=0x0009
+
+#
+# Build type
+#
+CONFIG_APP_BUILD_TYPE_APP_2NDBOOT=y
+# CONFIG_APP_BUILD_TYPE_RAM is not set
+CONFIG_APP_BUILD_GENERATE_BINARIES=y
+CONFIG_APP_BUILD_BOOTLOADER=y
+CONFIG_APP_BUILD_USE_FLASH_SECTIONS=y
+# CONFIG_APP_REPRODUCIBLE_BUILD is not set
+# CONFIG_APP_NO_BLOBS is not set
+# end of Build type
+
+#
+# Bootloader config
+#
+
+#
+# Bootloader manager
+#
+CONFIG_BOOTLOADER_COMPILE_TIME_DATE=y
+CONFIG_BOOTLOADER_PROJECT_VER=1
+# end of Bootloader manager
+
+#
+# Application Rollback
+#
+# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set
+# end of Application Rollback
+
+#
+# Bootloader Rollback
+#
+# end of Bootloader Rollback
+
+CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x0
+CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
+# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set
+# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set
+# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_NONE is not set
+
+#
+# Log
+#
+CONFIG_BOOTLOADER_LOG_VERSION_1=y
+CONFIG_BOOTLOADER_LOG_VERSION=1
+# CONFIG_BOOTLOADER_LOG_LEVEL_NONE is not set
+# CONFIG_BOOTLOADER_LOG_LEVEL_ERROR is not set
+# CONFIG_BOOTLOADER_LOG_LEVEL_WARN is not set
+CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y
+# CONFIG_BOOTLOADER_LOG_LEVEL_DEBUG is not set
+# CONFIG_BOOTLOADER_LOG_LEVEL_VERBOSE is not set
+CONFIG_BOOTLOADER_LOG_LEVEL=3
+
+#
+# Format
+#
+# CONFIG_BOOTLOADER_LOG_COLORS is not set
+CONFIG_BOOTLOADER_LOG_TIMESTAMP_SOURCE_CPU_TICKS=y
+# end of Format
+
+#
+# Settings
+#
+CONFIG_BOOTLOADER_LOG_MODE_TEXT_EN=y
+CONFIG_BOOTLOADER_LOG_MODE_TEXT=y
+# end of Settings
+# end of Log
+
+#
+# Serial Flash Configurations
+#
+# CONFIG_BOOTLOADER_FLASH_DC_AWARE is not set
+CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y
+# end of Serial Flash Configurations
+
+CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y
+# CONFIG_BOOTLOADER_FACTORY_RESET is not set
+# CONFIG_BOOTLOADER_APP_TEST is not set
+CONFIG_BOOTLOADER_REGION_PROTECTION_ENABLE=y
+CONFIG_BOOTLOADER_WDT_ENABLE=y
+# CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set
+CONFIG_BOOTLOADER_WDT_TIME_MS=9000
+# CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP is not set
+# CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON is not set
+# CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set
+CONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0
+# CONFIG_BOOTLOADER_CUSTOM_RESERVE_RTC is not set
+# end of Bootloader config
+
+#
+# Security features
+#
+CONFIG_SECURE_BOOT_V2_RSA_SUPPORTED=y
+CONFIG_SECURE_BOOT_V2_PREFERRED=y
+# CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT is not set
+# CONFIG_SECURE_BOOT is not set
+# CONFIG_SECURE_FLASH_ENC_ENABLED is not set
+CONFIG_SECURE_ROM_DL_MODE_ENABLED=y
+# end of Security features
+
+#
+# Application manager
+#
+CONFIG_APP_COMPILE_TIME_DATE=y
+# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set
+# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set
+# CONFIG_APP_PROJECT_VER_FROM_CONFIG is not set
+CONFIG_APP_RETRIEVE_LEN_ELF_SHA=9
+# end of Application manager
+
+CONFIG_ESP_ROM_HAS_CRC_LE=y
+CONFIG_ESP_ROM_HAS_CRC_BE=y
+CONFIG_ESP_ROM_HAS_MZ_CRC32=y
+CONFIG_ESP_ROM_HAS_JPEG_DECODE=y
+CONFIG_ESP_ROM_UART_CLK_IS_XTAL=y
+CONFIG_ESP_ROM_HAS_RETARGETABLE_LOCKING=y
+CONFIG_ESP_ROM_USB_OTG_NUM=3
+CONFIG_ESP_ROM_USB_SERIAL_DEVICE_NUM=4
+CONFIG_ESP_ROM_HAS_ERASE_0_REGION_BUG=y
+CONFIG_ESP_ROM_HAS_ENCRYPTED_WRITES_USING_LEGACY_DRV=y
+CONFIG_ESP_ROM_GET_CLK_FREQ=y
+CONFIG_ESP_ROM_HAS_HAL_WDT=y
+CONFIG_ESP_ROM_NEEDS_SWSETUP_WORKAROUND=y
+CONFIG_ESP_ROM_HAS_LAYOUT_TABLE=y
+CONFIG_ESP_ROM_HAS_SPI_FLASH=y
+CONFIG_ESP_ROM_HAS_SPI_FLASH_MMAP=y
+CONFIG_ESP_ROM_HAS_ETS_PRINTF_BUG=y
+CONFIG_ESP_ROM_HAS_NEWLIB=y
+CONFIG_ESP_ROM_HAS_NEWLIB_NANO_FORMAT=y
+CONFIG_ESP_ROM_HAS_NEWLIB_32BIT_TIME=y
+CONFIG_ESP_ROM_NEEDS_SET_CACHE_MMU_SIZE=y
+CONFIG_ESP_ROM_RAM_APP_NEEDS_MMU_INIT=y
+CONFIG_ESP_ROM_HAS_FLASH_COUNT_PAGES_BUG=y
+CONFIG_ESP_ROM_HAS_CACHE_SUSPEND_WAITI_BUG=y
+CONFIG_ESP_ROM_HAS_CACHE_WRITEBACK_BUG=y
+CONFIG_ESP_ROM_HAS_SW_FLOAT=y
+CONFIG_ESP_ROM_HAS_VERSION=y
+CONFIG_ESP_ROM_SUPPORT_DEEP_SLEEP_WAKEUP_STUB=y
+CONFIG_ESP_ROM_HAS_OUTPUT_PUTC_FUNC=y
+CONFIG_ESP_ROM_CONSOLE_OUTPUT_SECONDARY=y
+
+#
+# Boot ROM Behavior
+#
+CONFIG_BOOT_ROM_LOG_ALWAYS_ON=y
+# CONFIG_BOOT_ROM_LOG_ALWAYS_OFF is not set
+# CONFIG_BOOT_ROM_LOG_ON_GPIO_HIGH is not set
+# CONFIG_BOOT_ROM_LOG_ON_GPIO_LOW is not set
+# end of Boot ROM Behavior
+
+#
+# Serial flasher config
+#
+# CONFIG_ESPTOOLPY_NO_STUB is not set
+# CONFIG_ESPTOOLPY_OCT_FLASH is not set
+CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT=y
+# CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set
+# CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set
+CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
+# CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set
+CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y
+CONFIG_ESPTOOLPY_FLASHMODE="dio"
+# CONFIG_ESPTOOLPY_FLASHFREQ_120M is not set
+CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
+# CONFIG_ESPTOOLPY_FLASHFREQ_40M is not set
+# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set
+CONFIG_ESPTOOLPY_FLASHFREQ="80m"
+# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
+# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
+# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
+CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
+# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set
+# CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set
+# CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set
+# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set
+CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
+# CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE is not set
+CONFIG_ESPTOOLPY_BEFORE_RESET=y
+# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set
+CONFIG_ESPTOOLPY_BEFORE="default_reset"
+CONFIG_ESPTOOLPY_AFTER_RESET=y
+# CONFIG_ESPTOOLPY_AFTER_NORESET is not set
+CONFIG_ESPTOOLPY_AFTER="hard_reset"
+CONFIG_ESPTOOLPY_MONITOR_BAUD=115200
+# end of Serial flasher config
+
+#
+# Partition Table
+#
+# CONFIG_PARTITION_TABLE_SINGLE_APP is not set
+CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
+# CONFIG_PARTITION_TABLE_TWO_OTA is not set
+# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set
+# CONFIG_PARTITION_TABLE_CUSTOM is not set
+CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
+CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp_large.csv"
+CONFIG_PARTITION_TABLE_OFFSET=0x8000
+CONFIG_PARTITION_TABLE_MD5=y
+# end of Partition Table
+
+#
+# Compiler options
+#
+CONFIG_COMPILER_OPTIMIZATION_DEBUG=y
+# CONFIG_COMPILER_OPTIMIZATION_SIZE is not set
+# CONFIG_COMPILER_OPTIMIZATION_PERF is not set
+# CONFIG_COMPILER_OPTIMIZATION_NONE is not set
+CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y
+# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set
+# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set
+CONFIG_COMPILER_ASSERT_NDEBUG_EVALUATE=y
+CONFIG_COMPILER_FLOAT_LIB_FROM_GCCLIB=y
+CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=2
+# CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT is not set
+CONFIG_COMPILER_HIDE_PATHS_MACROS=y
+# CONFIG_COMPILER_CXX_EXCEPTIONS is not set
+# CONFIG_COMPILER_CXX_RTTI is not set
+CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y
+# CONFIG_COMPILER_STACK_CHECK_MODE_NORM is not set
+# CONFIG_COMPILER_STACK_CHECK_MODE_STRONG is not set
+# CONFIG_COMPILER_STACK_CHECK_MODE_ALL is not set
+# CONFIG_COMPILER_NO_MERGE_CONSTANTS is not set
+# CONFIG_COMPILER_WARN_WRITE_STRINGS is not set
+CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=y
+# CONFIG_COMPILER_DISABLE_GCC12_WARNINGS is not set
+# CONFIG_COMPILER_DISABLE_GCC13_WARNINGS is not set
+# CONFIG_COMPILER_DISABLE_GCC14_WARNINGS is not set
+# CONFIG_COMPILER_DUMP_RTL_FILES is not set
+CONFIG_COMPILER_RT_LIB_GCCLIB=y
+CONFIG_COMPILER_RT_LIB_NAME="gcc"
+CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y
+# CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE is not set
+# CONFIG_COMPILER_STATIC_ANALYZER is not set
+# end of Compiler options
+
+#
+# Component config
+#
+
+#
+# Application Level Tracing
+#
+# CONFIG_APPTRACE_DEST_JTAG is not set
+CONFIG_APPTRACE_DEST_NONE=y
+# CONFIG_APPTRACE_DEST_UART1 is not set
+# CONFIG_APPTRACE_DEST_UART2 is not set
+# CONFIG_APPTRACE_DEST_USB_CDC is not set
+CONFIG_APPTRACE_DEST_UART_NONE=y
+CONFIG_APPTRACE_UART_TASK_PRIO=1
+CONFIG_APPTRACE_LOCK_ENABLE=y
+# end of Application Level Tracing
+
+#
+# Bluetooth
+#
+# CONFIG_BT_ENABLED is not set
+
+#
+# Common Options
+#
+# CONFIG_BT_BLE_LOG_SPI_OUT_ENABLED is not set
+# end of Common Options
+# end of Bluetooth
+
+#
+# Console Library
+#
+# CONFIG_CONSOLE_SORTED_HELP is not set
+# end of Console Library
+
+#
+# Driver Configurations
+#
+
+#
+# Legacy TWAI Driver Configurations
+#
+# CONFIG_TWAI_SKIP_LEGACY_CONFLICT_CHECK is not set
+CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM=y
+# end of Legacy TWAI Driver Configurations
+
+#
+# Legacy ADC Driver Configuration
+#
+# CONFIG_ADC_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_ADC_SKIP_LEGACY_CONFLICT_CHECK is not set
+
+#
+# Legacy ADC Calibration Configuration
+#
+# CONFIG_ADC_CALI_SUPPRESS_DEPRECATE_WARN is not set
+# end of Legacy ADC Calibration Configuration
+# end of Legacy ADC Driver Configuration
+
+#
+# Legacy MCPWM Driver Configurations
+#
+# CONFIG_MCPWM_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_MCPWM_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy MCPWM Driver Configurations
+
+#
+# Legacy Timer Group Driver Configurations
+#
+# CONFIG_GPTIMER_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_GPTIMER_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy Timer Group Driver Configurations
+
+#
+# Legacy RMT Driver Configurations
+#
+# CONFIG_RMT_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_RMT_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy RMT Driver Configurations
+
+#
+# Legacy I2S Driver Configurations
+#
+# CONFIG_I2S_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_I2S_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy I2S Driver Configurations
+
+#
+# Legacy I2C Driver Configurations
+#
+# CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy I2C Driver Configurations
+
+#
+# Legacy PCNT Driver Configurations
+#
+# CONFIG_PCNT_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_PCNT_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy PCNT Driver Configurations
+
+#
+# Legacy SDM Driver Configurations
+#
+# CONFIG_SDM_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_SDM_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy SDM Driver Configurations
+
+#
+# Legacy Temperature Sensor Driver Configurations
+#
+# CONFIG_TEMP_SENSOR_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_TEMP_SENSOR_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy Temperature Sensor Driver Configurations
+
+#
+# Legacy Touch Sensor Driver Configurations
+#
+# CONFIG_TOUCH_SUPPRESS_DEPRECATE_WARN is not set
+# CONFIG_TOUCH_SKIP_LEGACY_CONFLICT_CHECK is not set
+# end of Legacy Touch Sensor Driver Configurations
+# end of Driver Configurations
+
+#
+# eFuse Bit Manager
+#
+# CONFIG_EFUSE_CUSTOM_TABLE is not set
+# CONFIG_EFUSE_VIRTUAL is not set
+CONFIG_EFUSE_MAX_BLK_LEN=256
+# end of eFuse Bit Manager
+
+#
+# ESP-TLS
+#
+CONFIG_ESP_TLS_USING_MBEDTLS=y
+# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set
+CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y
+# CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS is not set
+# CONFIG_ESP_TLS_SERVER_SESSION_TICKETS is not set
+# CONFIG_ESP_TLS_SERVER_CERT_SELECT_HOOK is not set
+# CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set
+# CONFIG_ESP_TLS_PSK_VERIFICATION is not set
+# CONFIG_ESP_TLS_INSECURE is not set
+# end of ESP-TLS
+
+#
+# ADC and ADC Calibration
+#
+# CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM is not set
+# CONFIG_ADC_CONTINUOUS_ISR_IRAM_SAFE is not set
+# CONFIG_ADC_CONTINUOUS_FORCE_USE_ADC2_ON_C3_S3 is not set
+# CONFIG_ADC_ENABLE_DEBUG_LOG is not set
+# end of ADC and ADC Calibration
+
+#
+# Wireless Coexistence
+#
+CONFIG_ESP_COEX_ENABLED=y
+# CONFIG_ESP_COEX_EXTERNAL_COEXIST_ENABLE is not set
+# CONFIG_ESP_COEX_GPIO_DEBUG is not set
+# end of Wireless Coexistence
+
+#
+# Common ESP-related
+#
+CONFIG_ESP_ERR_TO_NAME_LOOKUP=y
+# end of Common ESP-related
+
+#
+# ESP-Driver:GPIO Configurations
+#
+# CONFIG_GPIO_CTRL_FUNC_IN_IRAM is not set
+# end of ESP-Driver:GPIO Configurations
+
+#
+# ESP-Driver:GPTimer Configurations
+#
+CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM=y
+# CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM is not set
+# CONFIG_GPTIMER_ISR_CACHE_SAFE is not set
+CONFIG_GPTIMER_OBJ_CACHE_SAFE=y
+# CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set
+# end of ESP-Driver:GPTimer Configurations
+
+#
+# ESP-Driver:I2C Configurations
+#
+# CONFIG_I2C_ISR_IRAM_SAFE is not set
+# CONFIG_I2C_ENABLE_DEBUG_LOG is not set
+# CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2 is not set
+CONFIG_I2C_MASTER_ISR_HANDLER_IN_IRAM=y
+# end of ESP-Driver:I2C Configurations
+
+#
+# ESP-Driver:I2S Configurations
+#
+# CONFIG_I2S_ISR_IRAM_SAFE is not set
+# CONFIG_I2S_ENABLE_DEBUG_LOG is not set
+# end of ESP-Driver:I2S Configurations
+
+#
+# ESP-Driver:LEDC Configurations
+#
+# CONFIG_LEDC_CTRL_FUNC_IN_IRAM is not set
+# end of ESP-Driver:LEDC Configurations
+
+#
+# ESP-Driver:MCPWM Configurations
+#
+CONFIG_MCPWM_ISR_HANDLER_IN_IRAM=y
+# CONFIG_MCPWM_ISR_CACHE_SAFE is not set
+# CONFIG_MCPWM_CTRL_FUNC_IN_IRAM is not set
+CONFIG_MCPWM_OBJ_CACHE_SAFE=y
+# CONFIG_MCPWM_ENABLE_DEBUG_LOG is not set
+# end of ESP-Driver:MCPWM Configurations
+
+#
+# ESP-Driver:PCNT Configurations
+#
+# CONFIG_PCNT_CTRL_FUNC_IN_IRAM is not set
+# CONFIG_PCNT_ISR_IRAM_SAFE is not set
+# CONFIG_PCNT_ENABLE_DEBUG_LOG is not set
+# end of ESP-Driver:PCNT Configurations
+
+#
+# ESP-Driver:RMT Configurations
+#
+CONFIG_RMT_ENCODER_FUNC_IN_IRAM=y
+CONFIG_RMT_TX_ISR_HANDLER_IN_IRAM=y
+CONFIG_RMT_RX_ISR_HANDLER_IN_IRAM=y
+# CONFIG_RMT_RECV_FUNC_IN_IRAM is not set
+# CONFIG_RMT_TX_ISR_CACHE_SAFE is not set
+# CONFIG_RMT_RX_ISR_CACHE_SAFE is not set
+CONFIG_RMT_OBJ_CACHE_SAFE=y
+# CONFIG_RMT_ENABLE_DEBUG_LOG is not set
+# CONFIG_RMT_ISR_IRAM_SAFE is not set
+# end of ESP-Driver:RMT Configurations
+
+#
+# ESP-Driver:Sigma Delta Modulator Configurations
+#
+# CONFIG_SDM_CTRL_FUNC_IN_IRAM is not set
+# CONFIG_SDM_ENABLE_DEBUG_LOG is not set
+# end of ESP-Driver:Sigma Delta Modulator Configurations
+
+#
+# ESP-Driver:SPI Configurations
+#
+# CONFIG_SPI_MASTER_IN_IRAM is not set
+CONFIG_SPI_MASTER_ISR_IN_IRAM=y
+# CONFIG_SPI_SLAVE_IN_IRAM is not set
+CONFIG_SPI_SLAVE_ISR_IN_IRAM=y
+# end of ESP-Driver:SPI Configurations
+
+#
+# ESP-Driver:Touch Sensor Configurations
+#
+# CONFIG_TOUCH_CTRL_FUNC_IN_IRAM is not set
+# CONFIG_TOUCH_ISR_IRAM_SAFE is not set
+# CONFIG_TOUCH_ENABLE_DEBUG_LOG is not set
+# CONFIG_TOUCH_SKIP_FSM_CHECK is not set
+# end of ESP-Driver:Touch Sensor Configurations
+
+#
+# ESP-Driver:Temperature Sensor Configurations
+#
+# CONFIG_TEMP_SENSOR_ENABLE_DEBUG_LOG is not set
+# end of ESP-Driver:Temperature Sensor Configurations
+
+#
+# ESP-Driver:TWAI Configurations
+#
+# CONFIG_TWAI_ISR_IN_IRAM is not set
+# CONFIG_TWAI_ISR_CACHE_SAFE is not set
+# CONFIG_TWAI_ENABLE_DEBUG_LOG is not set
+# end of ESP-Driver:TWAI Configurations
+
+#
+# ESP-Driver:UART Configurations
+#
+# CONFIG_UART_ISR_IN_IRAM is not set
+# end of ESP-Driver:UART Configurations
+
+#
+# ESP-Driver:UHCI Configurations
+#
+# CONFIG_UHCI_ISR_HANDLER_IN_IRAM is not set
+# CONFIG_UHCI_ISR_CACHE_SAFE is not set
+# CONFIG_UHCI_ENABLE_DEBUG_LOG is not set
+# end of ESP-Driver:UHCI Configurations
+
+#
+# ESP-Driver:USB Serial/JTAG Configuration
+#
+CONFIG_USJ_ENABLE_USB_SERIAL_JTAG=y
+# end of ESP-Driver:USB Serial/JTAG Configuration
+
+#
+# Ethernet
+#
+CONFIG_ETH_ENABLED=y
+CONFIG_ETH_USE_SPI_ETHERNET=y
+# CONFIG_ETH_SPI_ETHERNET_DM9051 is not set
+# CONFIG_ETH_SPI_ETHERNET_W5500 is not set
+# CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL is not set
+# CONFIG_ETH_USE_OPENETH is not set
+# CONFIG_ETH_TRANSMIT_MUTEX is not set
+# end of Ethernet
+
+#
+# Event Loop Library
+#
+# CONFIG_ESP_EVENT_LOOP_PROFILING is not set
+CONFIG_ESP_EVENT_POST_FROM_ISR=y
+CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR=y
+# end of Event Loop Library
+
+#
+# GDB Stub
+#
+CONFIG_ESP_GDBSTUB_ENABLED=y
+# CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME is not set
+CONFIG_ESP_GDBSTUB_SUPPORT_TASKS=y
+CONFIG_ESP_GDBSTUB_MAX_TASKS=32
+# end of GDB Stub
+
+#
+# ESP HID
+#
+CONFIG_ESPHID_TASK_SIZE_BT=2048
+CONFIG_ESPHID_TASK_SIZE_BLE=4096
+# end of ESP HID
+
+#
+# ESP HTTP client
+#
+CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y
+# CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH is not set
+# CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH is not set
+# CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT is not set
+CONFIG_ESP_HTTP_CLIENT_EVENT_POST_TIMEOUT=2000
+# end of ESP HTTP client
+
+#
+# HTTP Server
+#
+CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
+CONFIG_HTTPD_MAX_URI_LEN=512
+CONFIG_HTTPD_ERR_RESP_NO_DELAY=y
+CONFIG_HTTPD_PURGE_BUF_LEN=32
+# CONFIG_HTTPD_LOG_PURGE_DATA is not set
+# CONFIG_HTTPD_WS_SUPPORT is not set
+# CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set
+CONFIG_HTTPD_SERVER_EVENT_POST_TIMEOUT=2000
+# end of HTTP Server
+
+#
+# ESP HTTPS OTA
+#
+# CONFIG_ESP_HTTPS_OTA_DECRYPT_CB is not set
+# CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP is not set
+CONFIG_ESP_HTTPS_OTA_EVENT_POST_TIMEOUT=2000
+# end of ESP HTTPS OTA
+
+#
+# ESP HTTPS server
+#
+# CONFIG_ESP_HTTPS_SERVER_ENABLE is not set
+CONFIG_ESP_HTTPS_SERVER_EVENT_POST_TIMEOUT=2000
+# CONFIG_ESP_HTTPS_SERVER_CERT_SELECT_HOOK is not set
+# end of ESP HTTPS server
+
+#
+# Hardware Settings
+#
+
+#
+# Chip revision
+#
+CONFIG_ESP32S3_REV_MIN_0=y
+# CONFIG_ESP32S3_REV_MIN_1 is not set
+# CONFIG_ESP32S3_REV_MIN_2 is not set
+CONFIG_ESP32S3_REV_MIN_FULL=0
+CONFIG_ESP_REV_MIN_FULL=0
+
+#
+# Maximum Supported ESP32-S3 Revision (Rev v0.99)
+#
+CONFIG_ESP32S3_REV_MAX_FULL=99
+CONFIG_ESP_REV_MAX_FULL=99
+CONFIG_ESP_EFUSE_BLOCK_REV_MIN_FULL=0
+CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL=199
+
+#
+# Maximum Supported ESP32-S3 eFuse Block Revision (eFuse Block Rev v1.99)
+#
+# end of Chip revision
+
+#
+# MAC Config
+#
+CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_STA=y
+CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_AP=y
+CONFIG_ESP_MAC_ADDR_UNIVERSE_BT=y
+CONFIG_ESP_MAC_ADDR_UNIVERSE_ETH=y
+CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES_FOUR=y
+CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES=4
+# CONFIG_ESP32S3_UNIVERSAL_MAC_ADDRESSES_TWO is not set
+CONFIG_ESP32S3_UNIVERSAL_MAC_ADDRESSES_FOUR=y
+CONFIG_ESP32S3_UNIVERSAL_MAC_ADDRESSES=4
+# CONFIG_ESP_MAC_USE_CUSTOM_MAC_AS_BASE_MAC is not set
+# end of MAC Config
+
+#
+# Sleep Config
+#
+CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND=y
+CONFIG_ESP_SLEEP_PSRAM_LEAKAGE_WORKAROUND=y
+CONFIG_ESP_SLEEP_MSPI_NEED_ALL_IO_PU=y
+CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y
+CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND=y
+CONFIG_ESP_SLEEP_WAIT_FLASH_READY_EXTRA_DELAY=2000
+# CONFIG_ESP_SLEEP_CACHE_SAFE_ASSERTION is not set
+# CONFIG_ESP_SLEEP_DEBUG is not set
+CONFIG_ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS=y
+# end of Sleep Config
+
+#
+# RTC Clock Config
+#
+CONFIG_RTC_CLK_SRC_INT_RC=y
+# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set
+# CONFIG_RTC_CLK_SRC_EXT_OSC is not set
+# CONFIG_RTC_CLK_SRC_INT_8MD256 is not set
+CONFIG_RTC_CLK_CAL_CYCLES=1024
+# end of RTC Clock Config
+
+#
+# Peripheral Control
+#
+CONFIG_ESP_PERIPH_CTRL_FUNC_IN_IRAM=y
+CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM=y
+# end of Peripheral Control
+
+#
+# GDMA Configurations
+#
+CONFIG_GDMA_CTRL_FUNC_IN_IRAM=y
+CONFIG_GDMA_ISR_HANDLER_IN_IRAM=y
+CONFIG_GDMA_OBJ_DRAM_SAFE=y
+# CONFIG_GDMA_ENABLE_DEBUG_LOG is not set
+# CONFIG_GDMA_ISR_IRAM_SAFE is not set
+# end of GDMA Configurations
+
+#
+# Main XTAL Config
+#
+CONFIG_XTAL_FREQ_40=y
+CONFIG_XTAL_FREQ=40
+# end of Main XTAL Config
+
+#
+# Power Supplier
+#
+
+#
+# Brownout Detector
+#
+CONFIG_ESP_BROWNOUT_DET=y
+CONFIG_ESP_BROWNOUT_DET_LVL_SEL_7=y
+# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_6 is not set
+# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_5 is not set
+# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_4 is not set
+# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_3 is not set
+# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_2 is not set
+# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_1 is not set
+CONFIG_ESP_BROWNOUT_DET_LVL=7
+CONFIG_ESP_BROWNOUT_USE_INTR=y
+# end of Brownout Detector
+# end of Power Supplier
+
+CONFIG_ESP_SPI_BUS_LOCK_ISR_FUNCS_IN_IRAM=y
+CONFIG_ESP_INTR_IN_IRAM=y
+# end of Hardware Settings
+
+#
+# ESP-Driver:LCD Controller Configurations
+#
+# CONFIG_LCD_ENABLE_DEBUG_LOG is not set
+# CONFIG_LCD_RGB_ISR_IRAM_SAFE is not set
+# CONFIG_LCD_RGB_RESTART_IN_VSYNC is not set
+# end of ESP-Driver:LCD Controller Configurations
+
+#
+# ESP-MM: Memory Management Configurations
+#
+# CONFIG_ESP_MM_CACHE_MSYNC_C2M_CHUNKED_OPS is not set
+# end of ESP-MM: Memory Management Configurations
+
+#
+# ESP NETIF Adapter
+#
+CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120
+# CONFIG_ESP_NETIF_PROVIDE_CUSTOM_IMPLEMENTATION is not set
+CONFIG_ESP_NETIF_TCPIP_LWIP=y
+# CONFIG_ESP_NETIF_LOOPBACK is not set
+CONFIG_ESP_NETIF_USES_TCPIP_WITH_BSD_API=y
+CONFIG_ESP_NETIF_REPORT_DATA_TRAFFIC=y
+# CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS is not set
+# CONFIG_ESP_NETIF_L2_TAP is not set
+# CONFIG_ESP_NETIF_BRIDGE_EN is not set
+# CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF is not set
+# end of ESP NETIF Adapter
+
+#
+# Partition API Configuration
+#
+# end of Partition API Configuration
+
+#
+# PHY
+#
+CONFIG_ESP_PHY_ENABLED=y
+CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE=y
+# CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION is not set
+CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=20
+CONFIG_ESP_PHY_MAX_TX_POWER=20
+# CONFIG_ESP_PHY_REDUCE_TX_POWER is not set
+CONFIG_ESP_PHY_ENABLE_USB=y
+# CONFIG_ESP_PHY_ENABLE_CERT_TEST is not set
+CONFIG_ESP_PHY_RF_CAL_PARTIAL=y
+# CONFIG_ESP_PHY_RF_CAL_NONE is not set
+# CONFIG_ESP_PHY_RF_CAL_FULL is not set
+CONFIG_ESP_PHY_CALIBRATION_MODE=0
+# CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set
+# CONFIG_ESP_PHY_RECORD_USED_TIME is not set
+CONFIG_ESP_PHY_IRAM_OPT=y
+# end of PHY
+
+#
+# Power Management
+#
+CONFIG_PM_SLEEP_FUNC_IN_IRAM=y
+# CONFIG_PM_ENABLE is not set
+CONFIG_PM_SLP_IRAM_OPT=y
+CONFIG_PM_POWER_DOWN_CPU_IN_LIGHT_SLEEP=y
+CONFIG_PM_RESTORE_CACHE_TAGMEM_AFTER_LIGHT_SLEEP=y
+# end of Power Management
+
+#
+# ESP PSRAM
+#
+CONFIG_SPIRAM=y
+
+#
+# SPI RAM config
+#
+# CONFIG_SPIRAM_MODE_QUAD is not set
+CONFIG_SPIRAM_MODE_OCT=y
+CONFIG_SPIRAM_TYPE_AUTO=y
+# CONFIG_SPIRAM_TYPE_ESPPSRAM64 is not set
+CONFIG_SPIRAM_CLK_IO=30
+CONFIG_SPIRAM_CS_IO=26
+# CONFIG_SPIRAM_XIP_FROM_PSRAM is not set
+# CONFIG_SPIRAM_FETCH_INSTRUCTIONS is not set
+# CONFIG_SPIRAM_RODATA is not set
+CONFIG_SPIRAM_SPEED_80M=y
+# CONFIG_SPIRAM_SPEED_40M is not set
+CONFIG_SPIRAM_SPEED=80
+# CONFIG_SPIRAM_ECC_ENABLE is not set
+CONFIG_SPIRAM_BOOT_HW_INIT=y
+CONFIG_SPIRAM_BOOT_INIT=y
+CONFIG_SPIRAM_PRE_CONFIGURE_MEMORY_PROTECTION=y
+# CONFIG_SPIRAM_IGNORE_NOTFOUND is not set
+# CONFIG_SPIRAM_USE_MEMMAP is not set
+# CONFIG_SPIRAM_USE_CAPS_ALLOC is not set
+CONFIG_SPIRAM_USE_MALLOC=y
+CONFIG_SPIRAM_MEMTEST=y
+CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384
+# CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP is not set
+CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768
+# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY is not set
+# CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY is not set
+# end of SPI RAM config
+# end of ESP PSRAM
+
+#
+# ESP Ringbuf
+#
+# CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH is not set
+# end of ESP Ringbuf
+
+#
+# ESP-ROM
+#
+CONFIG_ESP_ROM_PRINT_IN_IRAM=y
+# end of ESP-ROM
+
+#
+# ESP Security Specific
+#
+# end of ESP Security Specific
+
+#
+# ESP System Settings
+#
+# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set
+# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
+
+#
+# Cache config
+#
+CONFIG_ESP32S3_INSTRUCTION_CACHE_16KB=y
+# CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB is not set
+CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE=0x4000
+# CONFIG_ESP32S3_INSTRUCTION_CACHE_4WAYS is not set
+CONFIG_ESP32S3_INSTRUCTION_CACHE_8WAYS=y
+CONFIG_ESP32S3_ICACHE_ASSOCIATED_WAYS=8
+# CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_16B is not set
+CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_32B=y
+CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_SIZE=32
+# CONFIG_ESP32S3_DATA_CACHE_16KB is not set
+CONFIG_ESP32S3_DATA_CACHE_32KB=y
+# CONFIG_ESP32S3_DATA_CACHE_64KB is not set
+CONFIG_ESP32S3_DATA_CACHE_SIZE=0x8000
+# CONFIG_ESP32S3_DATA_CACHE_4WAYS is not set
+CONFIG_ESP32S3_DATA_CACHE_8WAYS=y
+CONFIG_ESP32S3_DCACHE_ASSOCIATED_WAYS=8
+# CONFIG_ESP32S3_DATA_CACHE_LINE_16B is not set
+CONFIG_ESP32S3_DATA_CACHE_LINE_32B=y
+# CONFIG_ESP32S3_DATA_CACHE_LINE_64B is not set
+CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE=32
+# end of Cache config
+
+#
+# Memory
+#
+# CONFIG_ESP32S3_RTCDATA_IN_FAST_MEM is not set
+# CONFIG_ESP32S3_USE_FIXED_STATIC_RAM_SIZE is not set
+# end of Memory
+
+#
+# Trace memory
+#
+# CONFIG_ESP32S3_TRAX is not set
+CONFIG_ESP32S3_TRACEMEM_RESERVE_DRAM=0x0
+# end of Trace memory
+
+# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set
+CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y
+# CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set
+# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set
+CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=0
+CONFIG_ESP_SYSTEM_RTC_FAST_MEM_AS_HEAP_DEPCHECK=y
+CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=y
+
+#
+# Memory protection
+#
+CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y
+CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=y
+# end of Memory protection
+
+CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32
+CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304
+CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584
+CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y
+# CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set
+# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set
+CONFIG_ESP_MAIN_TASK_AFFINITY=0x0
+CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048
+CONFIG_ESP_CONSOLE_UART_DEFAULT=y
+# CONFIG_ESP_CONSOLE_USB_CDC is not set
+# CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG is not set
+# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set
+# CONFIG_ESP_CONSOLE_NONE is not set
+# CONFIG_ESP_CONSOLE_SECONDARY_NONE is not set
+CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y
+CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y
+CONFIG_ESP_CONSOLE_UART=y
+CONFIG_ESP_CONSOLE_UART_NUM=0
+CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0
+CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
+CONFIG_ESP_INT_WDT=y
+CONFIG_ESP_INT_WDT_TIMEOUT_MS=300
+CONFIG_ESP_INT_WDT_CHECK_CPU1=y
+CONFIG_ESP_TASK_WDT_EN=y
+CONFIG_ESP_TASK_WDT_INIT=y
+# CONFIG_ESP_TASK_WDT_PANIC is not set
+CONFIG_ESP_TASK_WDT_TIMEOUT_S=5
+CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
+CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y
+# CONFIG_ESP_PANIC_HANDLER_IRAM is not set
+# CONFIG_ESP_DEBUG_STUBS_ENABLE is not set
+CONFIG_ESP_DEBUG_OCDAWARE=y
+CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_4=y
+CONFIG_ESP_SYSTEM_BBPLL_RECALIB=y
+# end of ESP System Settings
+
+#
+# IPC (Inter-Processor Call)
+#
+CONFIG_ESP_IPC_TASK_STACK_SIZE=1280
+CONFIG_ESP_IPC_USES_CALLERS_PRIORITY=y
+CONFIG_ESP_IPC_ISR_ENABLE=y
+# end of IPC (Inter-Processor Call)
+
+#
+# ESP Timer (High Resolution Timer)
+#
+CONFIG_ESP_TIMER_IN_IRAM=y
+# CONFIG_ESP_TIMER_PROFILING is not set
+CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y
+CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y
+CONFIG_ESP_TIMER_TASK_STACK_SIZE=3584
+CONFIG_ESP_TIMER_INTERRUPT_LEVEL=1
+# CONFIG_ESP_TIMER_SHOW_EXPERIMENTAL is not set
+CONFIG_ESP_TIMER_TASK_AFFINITY=0x0
+CONFIG_ESP_TIMER_TASK_AFFINITY_CPU0=y
+CONFIG_ESP_TIMER_ISR_AFFINITY_CPU0=y
+# CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD is not set
+CONFIG_ESP_TIMER_IMPL_SYSTIMER=y
+# end of ESP Timer (High Resolution Timer)
+
+#
+# Wi-Fi
+#
+CONFIG_ESP_WIFI_ENABLED=y
+CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10
+CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
+# CONFIG_ESP_WIFI_STATIC_TX_BUFFER is not set
+CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y
+CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1
+CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32
+CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y
+# CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER is not set
+CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0
+CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=5
+# CONFIG_ESP_WIFI_CSI_ENABLED is not set
+CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y
+CONFIG_ESP_WIFI_TX_BA_WIN=6
+CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y
+CONFIG_ESP_WIFI_RX_BA_WIN=6
+CONFIG_ESP_WIFI_NVS_ENABLED=y
+CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0=y
+# CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1 is not set
+CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752
+CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32
+CONFIG_ESP_WIFI_IRAM_OPT=y
+# CONFIG_ESP_WIFI_EXTRA_IRAM_OPT is not set
+CONFIG_ESP_WIFI_RX_IRAM_OPT=y
+CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y
+CONFIG_ESP_WIFI_ENABLE_SAE_PK=y
+CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y
+CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y
+CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA=y
+# CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set
+CONFIG_ESP_WIFI_SLP_DEFAULT_MIN_ACTIVE_TIME=50
+# CONFIG_ESP_WIFI_BSS_MAX_IDLE_SUPPORT is not set
+CONFIG_ESP_WIFI_SLP_DEFAULT_MAX_ACTIVE_TIME=10
+CONFIG_ESP_WIFI_SLP_DEFAULT_WAIT_BROADCAST_DATA_TIME=15
+# CONFIG_ESP_WIFI_FTM_ENABLE is not set
+CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=y
+# CONFIG_ESP_WIFI_GCMP_SUPPORT is not set
+CONFIG_ESP_WIFI_GMAC_SUPPORT=y
+CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y
+# CONFIG_ESP_WIFI_SLP_BEACON_LOST_OPT is not set
+CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=7
+CONFIG_ESP_WIFI_MBEDTLS_CRYPTO=y
+CONFIG_ESP_WIFI_MBEDTLS_TLS_CLIENT=y
+# CONFIG_ESP_WIFI_WAPI_PSK is not set
+# CONFIG_ESP_WIFI_SUITE_B_192 is not set
+# CONFIG_ESP_WIFI_11KV_SUPPORT is not set
+# CONFIG_ESP_WIFI_MBO_SUPPORT is not set
+# CONFIG_ESP_WIFI_DPP_SUPPORT is not set
+# CONFIG_ESP_WIFI_11R_SUPPORT is not set
+# CONFIG_ESP_WIFI_WPS_SOFTAP_REGISTRAR is not set
+
+#
+# WPS Configuration Options
+#
+# CONFIG_ESP_WIFI_WPS_STRICT is not set
+# CONFIG_ESP_WIFI_WPS_PASSPHRASE is not set
+# end of WPS Configuration Options
+
+# CONFIG_ESP_WIFI_DEBUG_PRINT is not set
+# CONFIG_ESP_WIFI_TESTING_OPTIONS is not set
+CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=y
+# CONFIG_ESP_WIFI_ENT_FREE_DYNAMIC_BUFFER is not set
+# end of Wi-Fi
+
+#
+# Core dump
+#
+# CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH is not set
+# CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set
+CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y
+# end of Core dump
+
+#
+# FAT Filesystem support
+#
+CONFIG_FATFS_VOLUME_COUNT=2
+CONFIG_FATFS_LFN_NONE=y
+# CONFIG_FATFS_LFN_HEAP is not set
+# CONFIG_FATFS_LFN_STACK is not set
+# CONFIG_FATFS_SECTOR_512 is not set
+CONFIG_FATFS_SECTOR_4096=y
+# CONFIG_FATFS_CODEPAGE_DYNAMIC is not set
+CONFIG_FATFS_CODEPAGE_437=y
+# CONFIG_FATFS_CODEPAGE_720 is not set
+# CONFIG_FATFS_CODEPAGE_737 is not set
+# CONFIG_FATFS_CODEPAGE_771 is not set
+# CONFIG_FATFS_CODEPAGE_775 is not set
+# CONFIG_FATFS_CODEPAGE_850 is not set
+# CONFIG_FATFS_CODEPAGE_852 is not set
+# CONFIG_FATFS_CODEPAGE_855 is not set
+# CONFIG_FATFS_CODEPAGE_857 is not set
+# CONFIG_FATFS_CODEPAGE_860 is not set
+# CONFIG_FATFS_CODEPAGE_861 is not set
+# CONFIG_FATFS_CODEPAGE_862 is not set
+# CONFIG_FATFS_CODEPAGE_863 is not set
+# CONFIG_FATFS_CODEPAGE_864 is not set
+# CONFIG_FATFS_CODEPAGE_865 is not set
+# CONFIG_FATFS_CODEPAGE_866 is not set
+# CONFIG_FATFS_CODEPAGE_869 is not set
+# CONFIG_FATFS_CODEPAGE_932 is not set
+# CONFIG_FATFS_CODEPAGE_936 is not set
+# CONFIG_FATFS_CODEPAGE_949 is not set
+# CONFIG_FATFS_CODEPAGE_950 is not set
+CONFIG_FATFS_CODEPAGE=437
+CONFIG_FATFS_FS_LOCK=0
+CONFIG_FATFS_TIMEOUT_MS=10000
+CONFIG_FATFS_PER_FILE_CACHE=y
+CONFIG_FATFS_ALLOC_PREFER_EXTRAM=y
+# CONFIG_FATFS_USE_FASTSEEK is not set
+CONFIG_FATFS_USE_STRFUNC_NONE=y
+# CONFIG_FATFS_USE_STRFUNC_WITHOUT_CRLF_CONV is not set
+# CONFIG_FATFS_USE_STRFUNC_WITH_CRLF_CONV is not set
+CONFIG_FATFS_VFS_FSTAT_BLKSIZE=0
+# CONFIG_FATFS_IMMEDIATE_FSYNC is not set
+# CONFIG_FATFS_USE_LABEL is not set
+CONFIG_FATFS_LINK_LOCK=y
+# CONFIG_FATFS_USE_DYN_BUFFERS is not set
+
+#
+# File system free space calculation behavior
+#
+CONFIG_FATFS_DONT_TRUST_FREE_CLUSTER_CNT=0
+CONFIG_FATFS_DONT_TRUST_LAST_ALLOC=0
+# end of File system free space calculation behavior
+# end of FAT Filesystem support
+
+#
+# FreeRTOS
+#
+
+#
+# Kernel
+#
+# CONFIG_FREERTOS_SMP is not set
+# CONFIG_FREERTOS_UNICORE is not set
+CONFIG_FREERTOS_HZ=1000
+# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set
+# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set
+CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y
+CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1
+CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536
+# CONFIG_FREERTOS_USE_IDLE_HOOK is not set
+# CONFIG_FREERTOS_USE_TICK_HOOK is not set
+CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16
+# CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY is not set
+CONFIG_FREERTOS_USE_TIMERS=y
+CONFIG_FREERTOS_TIMER_SERVICE_TASK_NAME="Tmr Svc"
+# CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU0 is not set
+# CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU1 is not set
+CONFIG_FREERTOS_TIMER_TASK_NO_AFFINITY=y
+CONFIG_FREERTOS_TIMER_SERVICE_TASK_CORE_AFFINITY=0x7FFFFFFF
+CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1
+CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048
+CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10
+CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0
+CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1
+# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set
+# CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES is not set
+# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set
+# CONFIG_FREERTOS_USE_APPLICATION_TASK_TAG is not set
+# end of Kernel
+
+#
+# Port
+#
+CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=y
+# CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is not set
+CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS=y
+# CONFIG_FREERTOS_TASK_PRE_DELETION_HOOK is not set
+# CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP is not set
+CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=y
+CONFIG_FREERTOS_ISR_STACKSIZE=1536
+CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y
+# CONFIG_FREERTOS_FPU_IN_ISR is not set
+CONFIG_FREERTOS_TICK_SUPPORT_SYSTIMER=y
+CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL1=y
+# CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set
+CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y
+# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set
+# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set
+# end of Port
+
+#
+# Extra
+#
+CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y
+# end of Extra
+
+CONFIG_FREERTOS_PORT=y
+CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF
+CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y
+CONFIG_FREERTOS_DEBUG_OCDAWARE=y
+CONFIG_FREERTOS_ENABLE_TASK_SNAPSHOT=y
+CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH=y
+CONFIG_FREERTOS_NUMBER_OF_CORES=2
+CONFIG_FREERTOS_IN_IRAM=y
+# end of FreeRTOS
+
+#
+# Hardware Abstraction Layer (HAL) and Low Level (LL)
+#
+CONFIG_HAL_ASSERTION_EQUALS_SYSTEM=y
+# CONFIG_HAL_ASSERTION_DISABLE is not set
+# CONFIG_HAL_ASSERTION_SILENT is not set
+# CONFIG_HAL_ASSERTION_ENABLE is not set
+CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=2
+CONFIG_HAL_WDT_USE_ROM_IMPL=y
+# end of Hardware Abstraction Layer (HAL) and Low Level (LL)
+
+#
+# Heap memory debugging
+#
+CONFIG_HEAP_POISONING_DISABLED=y
+# CONFIG_HEAP_POISONING_LIGHT is not set
+# CONFIG_HEAP_POISONING_COMPREHENSIVE is not set
+CONFIG_HEAP_TRACING_OFF=y
+# CONFIG_HEAP_TRACING_STANDALONE is not set
+# CONFIG_HEAP_TRACING_TOHOST is not set
+# CONFIG_HEAP_USE_HOOKS is not set
+# CONFIG_HEAP_TASK_TRACKING is not set
+# CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS is not set
+# CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH is not set
+# end of Heap memory debugging
+
+#
+# Log
+#
+CONFIG_LOG_VERSION_1=y
+# CONFIG_LOG_VERSION_2 is not set
+CONFIG_LOG_VERSION=1
+
+#
+# Log Level
+#
+# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set
+# CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set
+# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set
+CONFIG_LOG_DEFAULT_LEVEL_INFO=y
+# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set
+# CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set
+CONFIG_LOG_DEFAULT_LEVEL=3
+CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y
+# CONFIG_LOG_MAXIMUM_LEVEL_DEBUG is not set
+# CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE is not set
+CONFIG_LOG_MAXIMUM_LEVEL=3
+
+#
+# Level Settings
+#
+# CONFIG_LOG_MASTER_LEVEL is not set
+CONFIG_LOG_DYNAMIC_LEVEL_CONTROL=y
+# CONFIG_LOG_TAG_LEVEL_IMPL_NONE is not set
+# CONFIG_LOG_TAG_LEVEL_IMPL_LINKED_LIST is not set
+CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_AND_LINKED_LIST=y
+# CONFIG_LOG_TAG_LEVEL_CACHE_ARRAY is not set
+CONFIG_LOG_TAG_LEVEL_CACHE_BINARY_MIN_HEAP=y
+CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_SIZE=31
+# end of Level Settings
+# end of Log Level
+
+#
+# Format
+#
+# CONFIG_LOG_COLORS is not set
+CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y
+# CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set
+# end of Format
+
+#
+# Settings
+#
+CONFIG_LOG_MODE_TEXT_EN=y
+CONFIG_LOG_MODE_TEXT=y
+# end of Settings
+
+CONFIG_LOG_IN_IRAM=y
+# end of Log
+
+#
+# LWIP
+#
+CONFIG_LWIP_ENABLE=y
+CONFIG_LWIP_LOCAL_HOSTNAME="espressif"
+CONFIG_LWIP_TCPIP_TASK_PRIO=18
+# CONFIG_LWIP_TCPIP_CORE_LOCKING is not set
+# CONFIG_LWIP_CHECK_THREAD_SAFETY is not set
+CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y
+# CONFIG_LWIP_L2_TO_L3_COPY is not set
+# CONFIG_LWIP_IRAM_OPTIMIZATION is not set
+# CONFIG_LWIP_EXTRA_IRAM_OPTIMIZATION is not set
+CONFIG_LWIP_TIMERS_ONDEMAND=y
+CONFIG_LWIP_ND6=y
+# CONFIG_LWIP_FORCE_ROUTER_FORWARDING is not set
+CONFIG_LWIP_MAX_SOCKETS=10
+# CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set
+# CONFIG_LWIP_SO_LINGER is not set
+CONFIG_LWIP_SO_REUSE=y
+CONFIG_LWIP_SO_REUSE_RXTOALL=y
+# CONFIG_LWIP_SO_RCVBUF is not set
+# CONFIG_LWIP_NETBUF_RECVINFO is not set
+CONFIG_LWIP_IP_DEFAULT_TTL=64
+CONFIG_LWIP_IP4_FRAG=y
+CONFIG_LWIP_IP6_FRAG=y
+# CONFIG_LWIP_IP4_REASSEMBLY is not set
+# CONFIG_LWIP_IP6_REASSEMBLY is not set
+CONFIG_LWIP_IP_REASS_MAX_PBUFS=10
+# CONFIG_LWIP_IP_FORWARD is not set
+# CONFIG_LWIP_STATS is not set
+CONFIG_LWIP_ESP_GRATUITOUS_ARP=y
+CONFIG_LWIP_GARP_TMR_INTERVAL=60
+CONFIG_LWIP_ESP_MLDV6_REPORT=y
+CONFIG_LWIP_MLDV6_TMR_INTERVAL=40
+CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32
+CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y
+# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set
+# CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set
+# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set
+CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y
+# CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set
+CONFIG_LWIP_DHCP_OPTIONS_LEN=68
+CONFIG_LWIP_NUM_NETIF_CLIENT_DATA=0
+CONFIG_LWIP_DHCP_COARSE_TIMER_SECS=1
+
+#
+# DHCP server
+#
+CONFIG_LWIP_DHCPS=y
+CONFIG_LWIP_DHCPS_LEASE_UNIT=60
+CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8
+CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y
+CONFIG_LWIP_DHCPS_ADD_DNS=y
+# end of DHCP server
+
+# CONFIG_LWIP_AUTOIP is not set
+CONFIG_LWIP_IPV4=y
+CONFIG_LWIP_IPV6=y
+# CONFIG_LWIP_IPV6_AUTOCONFIG is not set
+CONFIG_LWIP_IPV6_NUM_ADDRESSES=3
+# CONFIG_LWIP_IPV6_FORWARD is not set
+# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set
+CONFIG_LWIP_NETIF_LOOPBACK=y
+CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8
+
+#
+# TCP
+#
+CONFIG_LWIP_MAX_ACTIVE_TCP=16
+CONFIG_LWIP_MAX_LISTENING_TCP=16
+CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y
+CONFIG_LWIP_TCP_MAXRTX=12
+CONFIG_LWIP_TCP_SYNMAXRTX=12
+CONFIG_LWIP_TCP_MSS=1440
+CONFIG_LWIP_TCP_TMR_INTERVAL=250
+CONFIG_LWIP_TCP_MSL=60000
+CONFIG_LWIP_TCP_FIN_WAIT_TIMEOUT=20000
+CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5760
+CONFIG_LWIP_TCP_WND_DEFAULT=5760
+CONFIG_LWIP_TCP_RECVMBOX_SIZE=6
+CONFIG_LWIP_TCP_ACCEPTMBOX_SIZE=6
+CONFIG_LWIP_TCP_QUEUE_OOSEQ=y
+CONFIG_LWIP_TCP_OOSEQ_TIMEOUT=6
+CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4
+# CONFIG_LWIP_TCP_SACK_OUT is not set
+CONFIG_LWIP_TCP_OVERSIZE_MSS=y
+# CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set
+# CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set
+CONFIG_LWIP_TCP_RTO_TIME=1500
+# end of TCP
+
+#
+# UDP
+#
+CONFIG_LWIP_MAX_UDP_PCBS=16
+CONFIG_LWIP_UDP_RECVMBOX_SIZE=6
+# end of UDP
+
+#
+# Checksums
+#
+# CONFIG_LWIP_CHECKSUM_CHECK_IP is not set
+# CONFIG_LWIP_CHECKSUM_CHECK_UDP is not set
+CONFIG_LWIP_CHECKSUM_CHECK_ICMP=y
+# end of Checksums
+
+CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=3072
+CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY=y
+# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0 is not set
+# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU1 is not set
+CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x7FFFFFFF
+CONFIG_LWIP_IPV6_MEMP_NUM_ND6_QUEUE=3
+CONFIG_LWIP_IPV6_ND6_NUM_NEIGHBORS=5
+CONFIG_LWIP_IPV6_ND6_NUM_PREFIXES=5
+CONFIG_LWIP_IPV6_ND6_NUM_ROUTERS=3
+CONFIG_LWIP_IPV6_ND6_NUM_DESTINATIONS=10
+# CONFIG_LWIP_PPP_SUPPORT is not set
+# CONFIG_LWIP_SLIP_SUPPORT is not set
+
+#
+# ICMP
+#
+CONFIG_LWIP_ICMP=y
+# CONFIG_LWIP_MULTICAST_PING is not set
+# CONFIG_LWIP_BROADCAST_PING is not set
+# end of ICMP
+
+#
+# LWIP RAW API
+#
+CONFIG_LWIP_MAX_RAW_PCBS=16
+# end of LWIP RAW API
+
+#
+# SNTP
+#
+CONFIG_LWIP_SNTP_MAX_SERVERS=1
+# CONFIG_LWIP_DHCP_GET_NTP_SRV is not set
+CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000
+CONFIG_LWIP_SNTP_STARTUP_DELAY=y
+CONFIG_LWIP_SNTP_MAXIMUM_STARTUP_DELAY=5000
+# end of SNTP
+
+#
+# DNS
+#
+CONFIG_LWIP_DNS_MAX_HOST_IP=1
+CONFIG_LWIP_DNS_MAX_SERVERS=3
+# CONFIG_LWIP_FALLBACK_DNS_SERVER_SUPPORT is not set
+# CONFIG_LWIP_DNS_SETSERVER_WITH_NETIF is not set
+# CONFIG_LWIP_USE_ESP_GETADDRINFO is not set
+# end of DNS
+
+CONFIG_LWIP_BRIDGEIF_MAX_PORTS=7
+CONFIG_LWIP_ESP_LWIP_ASSERT=y
+
+#
+# Hooks
+#
+# CONFIG_LWIP_HOOK_TCP_ISN_NONE is not set
+CONFIG_LWIP_HOOK_TCP_ISN_DEFAULT=y
+# CONFIG_LWIP_HOOK_TCP_ISN_CUSTOM is not set
+CONFIG_LWIP_HOOK_IP6_ROUTE_NONE=y
+# CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT is not set
+# CONFIG_LWIP_HOOK_IP6_ROUTE_CUSTOM is not set
+CONFIG_LWIP_HOOK_ND6_GET_GW_NONE=y
+# CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT is not set
+# CONFIG_LWIP_HOOK_ND6_GET_GW_CUSTOM is not set
+CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_NONE=y
+# CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_DEFAULT is not set
+# CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_CUSTOM is not set
+CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_NONE=y
+# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_DEFAULT is not set
+# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_CUSTOM is not set
+CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y
+# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set
+# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set
+CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_NONE=y
+# CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_CUSTOM is not set
+# CONFIG_LWIP_HOOK_IP6_INPUT_NONE is not set
+CONFIG_LWIP_HOOK_IP6_INPUT_DEFAULT=y
+# CONFIG_LWIP_HOOK_IP6_INPUT_CUSTOM is not set
+# end of Hooks
+
+# CONFIG_LWIP_DEBUG is not set
+# end of LWIP
+
+#
+# mbedTLS
+#
+CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y
+# CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC is not set
+# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set
+# CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set
+CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y
+CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
+CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
+# CONFIG_MBEDTLS_DYNAMIC_BUFFER is not set
+# CONFIG_MBEDTLS_DEBUG is not set
+
+#
+# mbedTLS v3.x related
+#
+# CONFIG_MBEDTLS_SSL_PROTO_TLS1_3 is not set
+# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH is not set
+# CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set
+# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set
+CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y
+CONFIG_MBEDTLS_PKCS7_C=y
+# end of mbedTLS v3.x related
+
+#
+# Certificate Bundle
+#
+CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
+CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y
+# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN is not set
+# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE is not set
+# CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE is not set
+# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEPRECATED_LIST is not set
+CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_MAX_CERTS=200
+# end of Certificate Bundle
+
+# CONFIG_MBEDTLS_ECP_RESTARTABLE is not set
+CONFIG_MBEDTLS_CMAC_C=y
+CONFIG_MBEDTLS_HARDWARE_AES=y
+CONFIG_MBEDTLS_AES_USE_INTERRUPT=y
+CONFIG_MBEDTLS_AES_INTERRUPT_LEVEL=0
+CONFIG_MBEDTLS_GCM_SUPPORT_NON_AES_CIPHER=y
+CONFIG_MBEDTLS_HARDWARE_MPI=y
+# CONFIG_MBEDTLS_LARGE_KEY_SOFTWARE_MPI is not set
+CONFIG_MBEDTLS_MPI_USE_INTERRUPT=y
+CONFIG_MBEDTLS_MPI_INTERRUPT_LEVEL=0
+CONFIG_MBEDTLS_HARDWARE_SHA=y
+CONFIG_MBEDTLS_ROM_MD5=y
+# CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN is not set
+# CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY is not set
+CONFIG_MBEDTLS_HAVE_TIME=y
+# CONFIG_MBEDTLS_PLATFORM_TIME_ALT is not set
+# CONFIG_MBEDTLS_HAVE_TIME_DATE is not set
+CONFIG_MBEDTLS_ECDSA_DETERMINISTIC=y
+CONFIG_MBEDTLS_SHA1_C=y
+CONFIG_MBEDTLS_SHA512_C=y
+# CONFIG_MBEDTLS_SHA3_C is not set
+CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y
+# CONFIG_MBEDTLS_TLS_SERVER_ONLY is not set
+# CONFIG_MBEDTLS_TLS_CLIENT_ONLY is not set
+# CONFIG_MBEDTLS_TLS_DISABLED is not set
+CONFIG_MBEDTLS_TLS_SERVER=y
+CONFIG_MBEDTLS_TLS_CLIENT=y
+CONFIG_MBEDTLS_TLS_ENABLED=y
+
+#
+# TLS Key Exchange Methods
+#
+# CONFIG_MBEDTLS_PSK_MODES is not set
+CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=y
+# end of TLS Key Exchange Methods
+
+CONFIG_MBEDTLS_SSL_RENEGOTIATION=y
+CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
+# CONFIG_MBEDTLS_SSL_PROTO_GMTSSL1_1 is not set
+# CONFIG_MBEDTLS_SSL_PROTO_DTLS is not set
+CONFIG_MBEDTLS_SSL_ALPN=y
+CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS=y
+CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS=y
+
+#
+# Symmetric Ciphers
+#
+CONFIG_MBEDTLS_AES_C=y
+# CONFIG_MBEDTLS_CAMELLIA_C is not set
+# CONFIG_MBEDTLS_DES_C is not set
+# CONFIG_MBEDTLS_BLOWFISH_C is not set
+# CONFIG_MBEDTLS_XTEA_C is not set
+CONFIG_MBEDTLS_CCM_C=y
+CONFIG_MBEDTLS_GCM_C=y
+# CONFIG_MBEDTLS_NIST_KW_C is not set
+# end of Symmetric Ciphers
+
+# CONFIG_MBEDTLS_RIPEMD160_C is not set
+
+#
+# Certificates
+#
+CONFIG_MBEDTLS_PEM_PARSE_C=y
+CONFIG_MBEDTLS_PEM_WRITE_C=y
+CONFIG_MBEDTLS_X509_CRL_PARSE_C=y
+CONFIG_MBEDTLS_X509_CSR_PARSE_C=y
+# end of Certificates
+
+CONFIG_MBEDTLS_ECP_C=y
+CONFIG_MBEDTLS_PK_PARSE_EC_EXTENDED=y
+CONFIG_MBEDTLS_PK_PARSE_EC_COMPRESSED=y
+# CONFIG_MBEDTLS_DHM_C is not set
+CONFIG_MBEDTLS_ECDH_C=y
+CONFIG_MBEDTLS_ECDSA_C=y
+# CONFIG_MBEDTLS_ECJPAKE_C is not set
+CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y
+CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y
+CONFIG_MBEDTLS_ECP_NIST_OPTIM=y
+# CONFIG_MBEDTLS_ECP_FIXED_POINT_OPTIM is not set
+# CONFIG_MBEDTLS_POLY1305_C is not set
+# CONFIG_MBEDTLS_CHACHA20_C is not set
+# CONFIG_MBEDTLS_HKDF_C is not set
+# CONFIG_MBEDTLS_THREADING_C is not set
+CONFIG_MBEDTLS_ERROR_STRINGS=y
+CONFIG_MBEDTLS_FS_IO=y
+# CONFIG_MBEDTLS_ALLOW_WEAK_CERTIFICATE_VERIFICATION is not set
+# end of mbedTLS
+
+#
+# ESP-MQTT Configurations
+#
+CONFIG_MQTT_PROTOCOL_311=y
+# CONFIG_MQTT_PROTOCOL_5 is not set
+CONFIG_MQTT_TRANSPORT_SSL=y
+CONFIG_MQTT_TRANSPORT_WEBSOCKET=y
+CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y
+# CONFIG_MQTT_MSG_ID_INCREMENTAL is not set
+# CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set
+# CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set
+# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set
+# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set
+# CONFIG_MQTT_CUSTOM_OUTBOX is not set
+# end of ESP-MQTT Configurations
+
+#
+# LibC
+#
+CONFIG_LIBC_NEWLIB=y
+CONFIG_LIBC_MISC_IN_IRAM=y
+CONFIG_LIBC_LOCKS_PLACE_IN_IRAM=y
+CONFIG_LIBC_STDOUT_LINE_ENDING_CRLF=y
+# CONFIG_LIBC_STDOUT_LINE_ENDING_LF is not set
+# CONFIG_LIBC_STDOUT_LINE_ENDING_CR is not set
+# CONFIG_LIBC_STDIN_LINE_ENDING_CRLF is not set
+# CONFIG_LIBC_STDIN_LINE_ENDING_LF is not set
+CONFIG_LIBC_STDIN_LINE_ENDING_CR=y
+# CONFIG_LIBC_NEWLIB_NANO_FORMAT is not set
+CONFIG_LIBC_TIME_SYSCALL_USE_RTC_HRT=y
+# CONFIG_LIBC_TIME_SYSCALL_USE_RTC is not set
+# CONFIG_LIBC_TIME_SYSCALL_USE_HRT is not set
+# CONFIG_LIBC_TIME_SYSCALL_USE_NONE is not set
+# end of LibC
+
+CONFIG_STDATOMIC_S32C1I_SPIRAM_WORKAROUND=y
+
+#
+# NVS
+#
+# CONFIG_NVS_ENCRYPTION is not set
+# CONFIG_NVS_ASSERT_ERROR_CHECK is not set
+# CONFIG_NVS_LEGACY_DUP_KEYS_COMPATIBILITY is not set
+# CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM is not set
+# end of NVS
+
+#
+# OpenThread
+#
+# CONFIG_OPENTHREAD_ENABLED is not set
+
+#
+# OpenThread Spinel
+#
+# CONFIG_OPENTHREAD_SPINEL_ONLY is not set
+# end of OpenThread Spinel
+# end of OpenThread
+
+#
+# Protocomm
+#
+CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0=y
+CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_1=y
+CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2=y
+CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_PATCH_VERSION=y
+# end of Protocomm
+
+#
+# PThreads
+#
+CONFIG_PTHREAD_TASK_PRIO_DEFAULT=5
+CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072
+CONFIG_PTHREAD_STACK_MIN=768
+CONFIG_PTHREAD_DEFAULT_CORE_NO_AFFINITY=y
+# CONFIG_PTHREAD_DEFAULT_CORE_0 is not set
+# CONFIG_PTHREAD_DEFAULT_CORE_1 is not set
+CONFIG_PTHREAD_TASK_CORE_DEFAULT=-1
+CONFIG_PTHREAD_TASK_NAME_DEFAULT="pthread"
+# end of PThreads
+
+#
+# MMU Config
+#
+CONFIG_MMU_PAGE_SIZE_64KB=y
+CONFIG_MMU_PAGE_MODE="64KB"
+CONFIG_MMU_PAGE_SIZE=0x10000
+# end of MMU Config
+
+#
+# Main Flash configuration
+#
+
+#
+# SPI Flash behavior when brownout
+#
+CONFIG_SPI_FLASH_BROWNOUT_RESET_XMC=y
+CONFIG_SPI_FLASH_BROWNOUT_RESET=y
+# end of SPI Flash behavior when brownout
+
+#
+# Optional and Experimental Features (READ DOCS FIRST)
+#
+
+#
+# Features here require specific hardware (READ DOCS FIRST!)
+#
+# CONFIG_SPI_FLASH_HPM_ENA is not set
+CONFIG_SPI_FLASH_HPM_AUTO=y
+# CONFIG_SPI_FLASH_HPM_DIS is not set
+CONFIG_SPI_FLASH_HPM_ON=y
+CONFIG_SPI_FLASH_HPM_DC_AUTO=y
+# CONFIG_SPI_FLASH_HPM_DC_DISABLE is not set
+# CONFIG_SPI_FLASH_AUTO_SUSPEND is not set
+CONFIG_SPI_FLASH_SUSPEND_TSUS_VAL_US=50
+# CONFIG_SPI_FLASH_FORCE_ENABLE_XMC_C_SUSPEND is not set
+# CONFIG_SPI_FLASH_FORCE_ENABLE_C6_H2_SUSPEND is not set
+CONFIG_SPI_FLASH_PLACE_FUNCTIONS_IN_IRAM=y
+# end of Optional and Experimental Features (READ DOCS FIRST)
+# end of Main Flash configuration
+
+#
+# SPI Flash driver
+#
+# CONFIG_SPI_FLASH_VERIFY_WRITE is not set
+# CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set
+CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y
+# CONFIG_SPI_FLASH_ROM_IMPL is not set
+CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS=y
+# CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS is not set
+# CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED is not set
+# CONFIG_SPI_FLASH_BYPASS_BLOCK_ERASE is not set
+CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y
+CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=20
+CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=1
+CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=8192
+# CONFIG_SPI_FLASH_SIZE_OVERRIDE is not set
+# CONFIG_SPI_FLASH_CHECK_ERASE_TIMEOUT_DISABLED is not set
+# CONFIG_SPI_FLASH_OVERRIDE_CHIP_DRIVER_LIST is not set
+
+#
+# Auto-detect flash chips
+#
+CONFIG_SPI_FLASH_VENDOR_XMC_SUPPORTED=y
+CONFIG_SPI_FLASH_VENDOR_GD_SUPPORTED=y
+CONFIG_SPI_FLASH_VENDOR_ISSI_SUPPORTED=y
+CONFIG_SPI_FLASH_VENDOR_MXIC_SUPPORTED=y
+CONFIG_SPI_FLASH_VENDOR_WINBOND_SUPPORTED=y
+CONFIG_SPI_FLASH_VENDOR_BOYA_SUPPORTED=y
+CONFIG_SPI_FLASH_VENDOR_TH_SUPPORTED=y
+CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP=y
+CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP=y
+CONFIG_SPI_FLASH_SUPPORT_GD_CHIP=y
+CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP=y
+CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP=y
+CONFIG_SPI_FLASH_SUPPORT_TH_CHIP=y
+CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP=y
+# end of Auto-detect flash chips
+
+CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE=y
+# end of SPI Flash driver
+
+#
+# SPIFFS Configuration
+#
+CONFIG_SPIFFS_MAX_PARTITIONS=3
+
+#
+# SPIFFS Cache Configuration
+#
+CONFIG_SPIFFS_CACHE=y
+CONFIG_SPIFFS_CACHE_WR=y
+# CONFIG_SPIFFS_CACHE_STATS is not set
+# end of SPIFFS Cache Configuration
+
+CONFIG_SPIFFS_PAGE_CHECK=y
+CONFIG_SPIFFS_GC_MAX_RUNS=10
+# CONFIG_SPIFFS_GC_STATS is not set
+CONFIG_SPIFFS_PAGE_SIZE=256
+CONFIG_SPIFFS_OBJ_NAME_LEN=32
+# CONFIG_SPIFFS_FOLLOW_SYMLINKS is not set
+CONFIG_SPIFFS_USE_MAGIC=y
+CONFIG_SPIFFS_USE_MAGIC_LENGTH=y
+CONFIG_SPIFFS_META_LENGTH=4
+CONFIG_SPIFFS_USE_MTIME=y
+
+#
+# Debug Configuration
+#
+# CONFIG_SPIFFS_DBG is not set
+# CONFIG_SPIFFS_API_DBG is not set
+# CONFIG_SPIFFS_GC_DBG is not set
+# CONFIG_SPIFFS_CACHE_DBG is not set
+# CONFIG_SPIFFS_CHECK_DBG is not set
+# CONFIG_SPIFFS_TEST_VISUALISATION is not set
+# end of Debug Configuration
+# end of SPIFFS Configuration
+
+#
+# TCP Transport
+#
+
+#
+# Websocket
+#
+CONFIG_WS_TRANSPORT=y
+CONFIG_WS_BUFFER_SIZE=1024
+# CONFIG_WS_DYNAMIC_BUFFER is not set
+# end of Websocket
+# end of TCP Transport
+
+#
+# Ultra Low Power (ULP) Co-processor
+#
+# CONFIG_ULP_COPROC_ENABLED is not set
+
+#
+# ULP Debugging Options
+#
+# end of ULP Debugging Options
+# end of Ultra Low Power (ULP) Co-processor
+
+#
+# Unity unit testing library
+#
+CONFIG_UNITY_ENABLE_FLOAT=y
+CONFIG_UNITY_ENABLE_DOUBLE=y
+# CONFIG_UNITY_ENABLE_64BIT is not set
+# CONFIG_UNITY_ENABLE_COLOR is not set
+CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y
+# CONFIG_UNITY_ENABLE_FIXTURE is not set
+# CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL is not set
+# CONFIG_UNITY_TEST_ORDER_BY_FILE_PATH_AND_LINE is not set
+# end of Unity unit testing library
+
+#
+# USB-OTG
+#
+CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE=256
+CONFIG_USB_HOST_HW_BUFFER_BIAS_BALANCED=y
+# CONFIG_USB_HOST_HW_BUFFER_BIAS_IN is not set
+# CONFIG_USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT is not set
+
+#
+# Hub Driver Configuration
+#
+
+#
+# Root Port configuration
+#
+CONFIG_USB_HOST_DEBOUNCE_DELAY_MS=250
+CONFIG_USB_HOST_RESET_HOLD_MS=30
+CONFIG_USB_HOST_RESET_RECOVERY_MS=30
+CONFIG_USB_HOST_SET_ADDR_RECOVERY_MS=10
+# end of Root Port configuration
+
+# CONFIG_USB_HOST_HUBS_SUPPORTED is not set
+# end of Hub Driver Configuration
+
+# CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK is not set
+CONFIG_USB_OTG_SUPPORTED=y
+# end of USB-OTG
+
+#
+# Virtual file system
+#
+CONFIG_VFS_SUPPORT_IO=y
+CONFIG_VFS_SUPPORT_DIR=y
+CONFIG_VFS_SUPPORT_SELECT=y
+CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y
+# CONFIG_VFS_SELECT_IN_RAM is not set
+CONFIG_VFS_SUPPORT_TERMIOS=y
+CONFIG_VFS_MAX_COUNT=8
+
+#
+# Host File System I/O (Semihosting)
+#
+CONFIG_VFS_SEMIHOSTFS_MAX_MOUNT_POINTS=1
+# end of Host File System I/O (Semihosting)
+
+CONFIG_VFS_INITIALIZE_DEV_NULL=y
+# end of Virtual file system
+
+#
+# Wear Levelling
+#
+# CONFIG_WL_SECTOR_SIZE_512 is not set
+CONFIG_WL_SECTOR_SIZE_4096=y
+CONFIG_WL_SECTOR_SIZE=4096
+# end of Wear Levelling
+
+#
+# Wi-Fi Provisioning Manager
+#
+CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16
+CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30
+CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
+# CONFIG_WIFI_PROV_STA_FAST_SCAN is not set
+# end of Wi-Fi Provisioning Manager
+
+#
+# IoT Button
+#
+CONFIG_BUTTON_PERIOD_TIME_MS=5
+CONFIG_BUTTON_DEBOUNCE_TICKS=2
+CONFIG_BUTTON_SHORT_PRESS_TIME_MS=180
+CONFIG_BUTTON_LONG_PRESS_TIME_MS=1500
+CONFIG_BUTTON_LONG_PRESS_HOLD_SERIAL_TIME_MS=20
+CONFIG_ADC_BUTTON_MAX_CHANNEL=3
+CONFIG_ADC_BUTTON_MAX_BUTTON_PER_CHANNEL=8
+CONFIG_ADC_BUTTON_SAMPLE_TIMES=1
+# end of IoT Button
+
+#
+# CMake Utilities
+#
+# CONFIG_CU_RELINKER_ENABLE is not set
+# CONFIG_CU_DIAGNOSTICS_COLOR_NEVER is not set
+CONFIG_CU_DIAGNOSTICS_COLOR_ALWAYS=y
+# CONFIG_CU_DIAGNOSTICS_COLOR_AUTO is not set
+# CONFIG_CU_GCC_LTO_ENABLE is not set
+# CONFIG_CU_GCC_STRING_1BYTE_ALIGN is not set
+# end of CMake Utilities
+
+#
+# Audio Codec Device Configuration
+#
+CONFIG_CODEC_ES8311_SUPPORT=y
+CONFIG_CODEC_ES7210_SUPPORT=y
+CONFIG_CODEC_ES7243_SUPPORT=y
+CONFIG_CODEC_ES7243E_SUPPORT=y
+CONFIG_CODEC_ES8156_SUPPORT=y
+CONFIG_CODEC_AW88298_SUPPORT=y
+CONFIG_CODEC_ES8389_SUPPORT=y
+CONFIG_CODEC_ES8374_SUPPORT=y
+CONFIG_CODEC_ES8388_SUPPORT=y
+CONFIG_CODEC_TAS5805M_SUPPORT=y
+# CONFIG_CODEC_ZL38063_SUPPORT is not set
+# CONFIG_CODEC_CJC8910_SUPPORT is not set
+# CONFIG_CODEC_DUMMY_SUPPORT is not set
+# CONFIG_CODEC_DATA_ADC_SUPPORT is not set
+# end of Audio Codec Device Configuration
+
+#
+# LVGL configuration
+#
+CONFIG_LV_CONF_SKIP=y
+# CONFIG_LV_CONF_MINIMAL is not set
+
+#
+# Color Settings
+#
+# CONFIG_LV_COLOR_DEPTH_32 is not set
+# CONFIG_LV_COLOR_DEPTH_24 is not set
+CONFIG_LV_COLOR_DEPTH_16=y
+# CONFIG_LV_COLOR_DEPTH_8 is not set
+# CONFIG_LV_COLOR_DEPTH_1 is not set
+CONFIG_LV_COLOR_DEPTH=16
+# end of Color Settings
+
+#
+# Memory Settings
+#
+CONFIG_LV_USE_BUILTIN_MALLOC=y
+# CONFIG_LV_USE_CLIB_MALLOC is not set
+# CONFIG_LV_USE_MICROPYTHON_MALLOC is not set
+# CONFIG_LV_USE_RTTHREAD_MALLOC is not set
+# CONFIG_LV_USE_CUSTOM_MALLOC is not set
+CONFIG_LV_USE_BUILTIN_STRING=y
+# CONFIG_LV_USE_CLIB_STRING is not set
+# CONFIG_LV_USE_CUSTOM_STRING is not set
+CONFIG_LV_USE_BUILTIN_SPRINTF=y
+# CONFIG_LV_USE_CLIB_SPRINTF is not set
+# CONFIG_LV_USE_CUSTOM_SPRINTF is not set
+CONFIG_LV_MEM_SIZE_KILOBYTES=64
+CONFIG_LV_MEM_POOL_EXPAND_SIZE_KILOBYTES=0
+CONFIG_LV_MEM_ADR=0x0
+# end of Memory Settings
+
+#
+# HAL Settings
+#
+CONFIG_LV_DEF_REFR_PERIOD=33
+CONFIG_LV_DPI_DEF=130
+# end of HAL Settings
+
+#
+# Operating System (OS)
+#
+CONFIG_LV_OS_NONE=y
+# CONFIG_LV_OS_PTHREAD is not set
+# CONFIG_LV_OS_FREERTOS is not set
+# CONFIG_LV_OS_CMSIS_RTOS2 is not set
+# CONFIG_LV_OS_RTTHREAD is not set
+# CONFIG_LV_OS_WINDOWS is not set
+# CONFIG_LV_OS_MQX is not set
+# CONFIG_LV_OS_CUSTOM is not set
+CONFIG_LV_USE_OS=0
+# end of Operating System (OS)
+
+#
+# Rendering Configuration
+#
+CONFIG_LV_DRAW_BUF_STRIDE_ALIGN=1
+CONFIG_LV_DRAW_BUF_ALIGN=4
+CONFIG_LV_DRAW_LAYER_SIMPLE_BUF_SIZE=24576
+CONFIG_LV_USE_DRAW_SW=y
+CONFIG_LV_DRAW_SW_SUPPORT_RGB565=y
+CONFIG_LV_DRAW_SW_SUPPORT_RGB565A8=y
+CONFIG_LV_DRAW_SW_SUPPORT_RGB888=y
+CONFIG_LV_DRAW_SW_SUPPORT_XRGB8888=y
+CONFIG_LV_DRAW_SW_SUPPORT_ARGB8888=y
+CONFIG_LV_DRAW_SW_SUPPORT_L8=y
+CONFIG_LV_DRAW_SW_SUPPORT_AL88=y
+CONFIG_LV_DRAW_SW_SUPPORT_A8=y
+CONFIG_LV_DRAW_SW_SUPPORT_I1=y
+CONFIG_LV_DRAW_SW_DRAW_UNIT_CNT=1
+# CONFIG_LV_USE_DRAW_ARM2D_SYNC is not set
+# CONFIG_LV_USE_NATIVE_HELIUM_ASM is not set
+CONFIG_LV_DRAW_SW_COMPLEX=y
+# CONFIG_LV_USE_DRAW_SW_COMPLEX_GRADIENTS is not set
+CONFIG_LV_DRAW_SW_SHADOW_CACHE_SIZE=0
+CONFIG_LV_DRAW_SW_CIRCLE_CACHE_SIZE=4
+CONFIG_LV_DRAW_SW_ASM_NONE=y
+# CONFIG_LV_DRAW_SW_ASM_NEON is not set
+# CONFIG_LV_DRAW_SW_ASM_HELIUM is not set
+# CONFIG_LV_DRAW_SW_ASM_CUSTOM is not set
+CONFIG_LV_USE_DRAW_SW_ASM=0
+# CONFIG_LV_USE_DRAW_VGLITE is not set
+# CONFIG_LV_USE_DRAW_PXP is not set
+# CONFIG_LV_USE_DRAW_DAVE2D is not set
+# CONFIG_LV_USE_DRAW_SDL is not set
+# CONFIG_LV_USE_DRAW_VG_LITE is not set
+# CONFIG_LV_USE_VECTOR_GRAPHIC is not set
+# end of Rendering Configuration
+
+#
+# Feature Configuration
+#
+
+#
+# Logging
+#
+# CONFIG_LV_USE_LOG is not set
+# end of Logging
+
+#
+# Asserts
+#
+CONFIG_LV_USE_ASSERT_NULL=y
+CONFIG_LV_USE_ASSERT_MALLOC=y
+# CONFIG_LV_USE_ASSERT_STYLE is not set
+# CONFIG_LV_USE_ASSERT_MEM_INTEGRITY is not set
+# CONFIG_LV_USE_ASSERT_OBJ is not set
+CONFIG_LV_ASSERT_HANDLER_INCLUDE="assert.h"
+# end of Asserts
+
+#
+# Debug
+#
+# CONFIG_LV_USE_REFR_DEBUG is not set
+# CONFIG_LV_USE_LAYER_DEBUG is not set
+# CONFIG_LV_USE_PARALLEL_DRAW_DEBUG is not set
+# end of Debug
+
+#
+# Others
+#
+# CONFIG_LV_ENABLE_GLOBAL_CUSTOM is not set
+CONFIG_LV_CACHE_DEF_SIZE=0
+CONFIG_LV_IMAGE_HEADER_CACHE_DEF_CNT=0
+CONFIG_LV_GRADIENT_MAX_STOPS=2
+CONFIG_LV_COLOR_MIX_ROUND_OFS=128
+# CONFIG_LV_OBJ_STYLE_CACHE is not set
+# CONFIG_LV_USE_OBJ_ID is not set
+# CONFIG_LV_USE_OBJ_PROPERTY is not set
+# end of Others
+# end of Feature Configuration
+
+#
+# Compiler Settings
+#
+# CONFIG_LV_BIG_ENDIAN_SYSTEM is not set
+CONFIG_LV_ATTRIBUTE_MEM_ALIGN_SIZE=1
+# CONFIG_LV_ATTRIBUTE_FAST_MEM_USE_IRAM is not set
+# CONFIG_LV_USE_FLOAT is not set
+# CONFIG_LV_USE_MATRIX is not set
+# CONFIG_LV_USE_PRIVATE_API is not set
+# end of Compiler Settings
+
+#
+# Font Usage
+#
+
+#
+# Enable built-in fonts
+#
+# CONFIG_LV_FONT_MONTSERRAT_8 is not set
+CONFIG_LV_FONT_MONTSERRAT_10=y
+CONFIG_LV_FONT_MONTSERRAT_12=y
+CONFIG_LV_FONT_MONTSERRAT_14=y
+CONFIG_LV_FONT_MONTSERRAT_16=y
+# CONFIG_LV_FONT_MONTSERRAT_18 is not set
+CONFIG_LV_FONT_MONTSERRAT_20=y
+# CONFIG_LV_FONT_MONTSERRAT_22 is not set
+# CONFIG_LV_FONT_MONTSERRAT_24 is not set
+# CONFIG_LV_FONT_MONTSERRAT_26 is not set
+# CONFIG_LV_FONT_MONTSERRAT_28 is not set
+# CONFIG_LV_FONT_MONTSERRAT_30 is not set
+# CONFIG_LV_FONT_MONTSERRAT_32 is not set
+# CONFIG_LV_FONT_MONTSERRAT_34 is not set
+# CONFIG_LV_FONT_MONTSERRAT_36 is not set
+# CONFIG_LV_FONT_MONTSERRAT_38 is not set
+# CONFIG_LV_FONT_MONTSERRAT_40 is not set
+# CONFIG_LV_FONT_MONTSERRAT_42 is not set
+# CONFIG_LV_FONT_MONTSERRAT_44 is not set
+# CONFIG_LV_FONT_MONTSERRAT_46 is not set
+# CONFIG_LV_FONT_MONTSERRAT_48 is not set
+# CONFIG_LV_FONT_MONTSERRAT_28_COMPRESSED is not set
+# CONFIG_LV_FONT_DEJAVU_16_PERSIAN_HEBREW is not set
+# CONFIG_LV_FONT_SIMSUN_14_CJK is not set
+# CONFIG_LV_FONT_SIMSUN_16_CJK is not set
+# CONFIG_LV_FONT_UNSCII_8 is not set
+# CONFIG_LV_FONT_UNSCII_16 is not set
+# end of Enable built-in fonts
+
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_8 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_10 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_12 is not set
+CONFIG_LV_FONT_DEFAULT_MONTSERRAT_14=y
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_16 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_18 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_20 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_22 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_24 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_26 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_28 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_30 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_32 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_34 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_36 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_38 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_40 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_42 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_44 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_46 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_48 is not set
+# CONFIG_LV_FONT_DEFAULT_MONTSERRAT_28_COMPRESSED is not set
+# CONFIG_LV_FONT_DEFAULT_DEJAVU_16_PERSIAN_HEBREW is not set
+# CONFIG_LV_FONT_DEFAULT_SIMSUN_14_CJK is not set
+# CONFIG_LV_FONT_DEFAULT_SIMSUN_16_CJK is not set
+# CONFIG_LV_FONT_DEFAULT_UNSCII_8 is not set
+# CONFIG_LV_FONT_DEFAULT_UNSCII_16 is not set
+# CONFIG_LV_FONT_FMT_TXT_LARGE is not set
+# CONFIG_LV_USE_FONT_COMPRESSED is not set
+CONFIG_LV_USE_FONT_PLACEHOLDER=y
+# end of Font Usage
+
+#
+# Text Settings
+#
+CONFIG_LV_TXT_ENC_UTF8=y
+# CONFIG_LV_TXT_ENC_ASCII is not set
+CONFIG_LV_TXT_BREAK_CHARS=" ,.;:-_)}"
+CONFIG_LV_TXT_LINE_BREAK_LONG_LEN=0
+# CONFIG_LV_USE_BIDI is not set
+# CONFIG_LV_USE_ARABIC_PERSIAN_CHARS is not set
+# end of Text Settings
+
+#
+# Widget Usage
+#
+CONFIG_LV_WIDGETS_HAS_DEFAULT_VALUE=y
+CONFIG_LV_USE_ANIMIMG=y
+CONFIG_LV_USE_ARC=y
+CONFIG_LV_USE_BAR=y
+CONFIG_LV_USE_BUTTON=y
+CONFIG_LV_USE_BUTTONMATRIX=y
+CONFIG_LV_USE_CALENDAR=y
+# CONFIG_LV_CALENDAR_WEEK_STARTS_MONDAY is not set
+CONFIG_LV_USE_CALENDAR_HEADER_ARROW=y
+CONFIG_LV_USE_CALENDAR_HEADER_DROPDOWN=y
+# CONFIG_LV_USE_CALENDAR_CHINESE is not set
+CONFIG_LV_USE_CANVAS=y
+CONFIG_LV_USE_CHART=y
+CONFIG_LV_USE_CHECKBOX=y
+CONFIG_LV_USE_DROPDOWN=y
+CONFIG_LV_USE_IMAGE=y
+CONFIG_LV_USE_IMAGEBUTTON=y
+CONFIG_LV_USE_KEYBOARD=y
+CONFIG_LV_USE_LABEL=y
+CONFIG_LV_LABEL_TEXT_SELECTION=y
+CONFIG_LV_LABEL_LONG_TXT_HINT=y
+CONFIG_LV_LABEL_WAIT_CHAR_COUNT=3
+CONFIG_LV_USE_LED=y
+CONFIG_LV_USE_LINE=y
+CONFIG_LV_USE_LIST=y
+CONFIG_LV_USE_MENU=y
+CONFIG_LV_USE_MSGBOX=y
+CONFIG_LV_USE_ROLLER=y
+CONFIG_LV_USE_SCALE=y
+CONFIG_LV_USE_SLIDER=y
+CONFIG_LV_USE_SPAN=y
+CONFIG_LV_SPAN_SNIPPET_STACK_SIZE=64
+CONFIG_LV_USE_SPINBOX=y
+CONFIG_LV_USE_SPINNER=y
+CONFIG_LV_USE_SWITCH=y
+CONFIG_LV_USE_TEXTAREA=y
+CONFIG_LV_TEXTAREA_DEF_PWD_SHOW_TIME=1500
+CONFIG_LV_USE_TABLE=y
+CONFIG_LV_USE_TABVIEW=y
+CONFIG_LV_USE_TILEVIEW=y
+CONFIG_LV_USE_WIN=y
+# end of Widget Usage
+
+#
+# Themes
+#
+CONFIG_LV_USE_THEME_DEFAULT=y
+# CONFIG_LV_THEME_DEFAULT_DARK is not set
+CONFIG_LV_THEME_DEFAULT_GROW=y
+CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=80
+CONFIG_LV_USE_THEME_SIMPLE=y
+# CONFIG_LV_USE_THEME_MONO is not set
+# end of Themes
+
+#
+# Layouts
+#
+CONFIG_LV_USE_FLEX=y
+CONFIG_LV_USE_GRID=y
+# end of Layouts
+
+#
+# 3rd Party Libraries
+#
+CONFIG_LV_FS_DEFAULT_DRIVE_LETTER=0
+# CONFIG_LV_USE_FS_STDIO is not set
+# CONFIG_LV_USE_FS_POSIX is not set
+# CONFIG_LV_USE_FS_WIN32 is not set
+# CONFIG_LV_USE_FS_FATFS is not set
+# CONFIG_LV_USE_FS_MEMFS is not set
+# CONFIG_LV_USE_FS_LITTLEFS is not set
+# CONFIG_LV_USE_FS_ARDUINO_ESP_LITTLEFS is not set
+# CONFIG_LV_USE_FS_ARDUINO_SD is not set
+# CONFIG_LV_USE_LODEPNG is not set
+# CONFIG_LV_USE_LIBPNG is not set
+# CONFIG_LV_USE_BMP is not set
+# CONFIG_LV_USE_TJPGD is not set
+# CONFIG_LV_USE_LIBJPEG_TURBO is not set
+# CONFIG_LV_USE_GIF is not set
+# CONFIG_LV_BIN_DECODER_RAM_LOAD is not set
+# CONFIG_LV_USE_RLE is not set
+# CONFIG_LV_USE_QRCODE is not set
+# CONFIG_LV_USE_BARCODE is not set
+# CONFIG_LV_USE_FREETYPE is not set
+# CONFIG_LV_USE_TINY_TTF is not set
+# CONFIG_LV_USE_RLOTTIE is not set
+# CONFIG_LV_USE_THORVG is not set
+# CONFIG_LV_USE_LZ4 is not set
+# CONFIG_LV_USE_FFMPEG is not set
+# end of 3rd Party Libraries
+
+#
+# Others
+#
+# CONFIG_LV_USE_SNAPSHOT is not set
+# CONFIG_LV_USE_SYSMON is not set
+# CONFIG_LV_USE_PROFILER is not set
+# CONFIG_LV_USE_MONKEY is not set
+# CONFIG_LV_USE_GRIDNAV is not set
+# CONFIG_LV_USE_FRAGMENT is not set
+# CONFIG_LV_USE_IMGFONT is not set
+CONFIG_LV_USE_OBSERVER=y
+# CONFIG_LV_USE_IME_PINYIN is not set
+# CONFIG_LV_USE_FILE_EXPLORER is not set
+# end of Others
+
+#
+# Devices
+#
+# CONFIG_LV_USE_SDL is not set
+# CONFIG_LV_USE_X11 is not set
+# CONFIG_LV_USE_WAYLAND is not set
+# CONFIG_LV_USE_LINUX_FBDEV is not set
+# CONFIG_LV_USE_NUTTX is not set
+# CONFIG_LV_USE_LINUX_DRM is not set
+# CONFIG_LV_USE_TFT_ESPI is not set
+# CONFIG_LV_USE_EVDEV is not set
+# CONFIG_LV_USE_LIBINPUT is not set
+# CONFIG_LV_USE_ST7735 is not set
+# CONFIG_LV_USE_ST7789 is not set
+# CONFIG_LV_USE_ST7796 is not set
+# CONFIG_LV_USE_ILI9341 is not set
+# CONFIG_LV_USE_GENERIC_MIPI is not set
+# CONFIG_LV_USE_RENESAS_GLCDC is not set
+# CONFIG_LV_USE_OPENGLES is not set
+# CONFIG_LV_USE_QNX is not set
+# end of Devices
+
+#
+# Examples
+#
+CONFIG_LV_BUILD_EXAMPLES=y
+# end of Examples
+
+#
+# Demos
+#
+# CONFIG_LV_USE_DEMO_WIDGETS is not set
+# CONFIG_LV_USE_DEMO_KEYPAD_AND_ENCODER is not set
+# CONFIG_LV_USE_DEMO_RENDER is not set
+# CONFIG_LV_USE_DEMO_SCROLL is not set
+# CONFIG_LV_USE_DEMO_STRESS is not set
+# CONFIG_LV_USE_DEMO_MUSIC is not set
+# CONFIG_LV_USE_DEMO_FLEX_LAYOUT is not set
+# CONFIG_LV_USE_DEMO_MULTILANG is not set
+# end of Demos
+# end of LVGL configuration
+# end of Component config
+
+# CONFIG_IDF_EXPERIMENTAL_FEATURES is not set
+
+# Deprecated options for backward compatibility
+# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set
+# CONFIG_NO_BLOBS is not set
+# CONFIG_APP_ROLLBACK_ENABLE is not set
+# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set
+# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set
+# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set
+CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y
+# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set
+# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set
+CONFIG_LOG_BOOTLOADER_LEVEL=3
+# CONFIG_FLASH_ENCRYPTION_ENABLED is not set
+# CONFIG_FLASHMODE_QIO is not set
+# CONFIG_FLASHMODE_QOUT is not set
+CONFIG_FLASHMODE_DIO=y
+# CONFIG_FLASHMODE_DOUT is not set
+CONFIG_MONITOR_BAUD=115200
+CONFIG_OPTIMIZATION_LEVEL_DEBUG=y
+CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y
+CONFIG_COMPILER_OPTIMIZATION_DEFAULT=y
+# CONFIG_OPTIMIZATION_LEVEL_RELEASE is not set
+# CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set
+CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y
+# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set
+# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set
+CONFIG_OPTIMIZATION_ASSERTION_LEVEL=2
+# CONFIG_CXX_EXCEPTIONS is not set
+CONFIG_STACK_CHECK_NONE=y
+# CONFIG_STACK_CHECK_NORM is not set
+# CONFIG_STACK_CHECK_STRONG is not set
+# CONFIG_STACK_CHECK_ALL is not set
+# CONFIG_WARN_WRITE_STRINGS is not set
+# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set
+CONFIG_ESP32_APPTRACE_DEST_NONE=y
+CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y
+# CONFIG_EXTERNAL_COEX_ENABLE is not set
+# CONFIG_ESP_WIFI_EXTERNAL_COEXIST_ENABLE is not set
+# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set
+# CONFIG_MCPWM_ISR_IRAM_SAFE is not set
+# CONFIG_EVENT_LOOP_PROFILING is not set
+CONFIG_POST_EVENTS_FROM_ISR=y
+CONFIG_POST_EVENTS_FROM_IRAM_ISR=y
+CONFIG_GDBSTUB_SUPPORT_TASKS=y
+CONFIG_GDBSTUB_MAX_TASKS=32
+# CONFIG_OTA_ALLOW_HTTP is not set
+CONFIG_ESP32S3_DEEP_SLEEP_WAKEUP_DELAY=2000
+CONFIG_ESP_SLEEP_DEEP_SLEEP_WAKEUP_DELAY=2000
+CONFIG_ESP32S3_RTC_CLK_SRC_INT_RC=y
+# CONFIG_ESP32S3_RTC_CLK_SRC_EXT_CRYS is not set
+# CONFIG_ESP32S3_RTC_CLK_SRC_EXT_OSC is not set
+# CONFIG_ESP32S3_RTC_CLK_SRC_INT_8MD256 is not set
+CONFIG_ESP32S3_RTC_CLK_CAL_CYCLES=1024
+CONFIG_PERIPH_CTRL_FUNC_IN_IRAM=y
+CONFIG_BROWNOUT_DET=y
+CONFIG_ESP32S3_BROWNOUT_DET=y
+CONFIG_BROWNOUT_DET_LVL_SEL_7=y
+CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_7=y
+# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set
+# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_6 is not set
+# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set
+# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_5 is not set
+# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set
+# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_4 is not set
+# CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set
+# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_3 is not set
+# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set
+# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_2 is not set
+# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set
+# CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_1 is not set
+CONFIG_BROWNOUT_DET_LVL=7
+CONFIG_ESP32S3_BROWNOUT_DET_LVL=7
+CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y
+CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y
+# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set
+CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20
+CONFIG_ESP32_PHY_MAX_TX_POWER=20
+# CONFIG_REDUCE_PHY_TX_POWER is not set
+# CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set
+CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y
+CONFIG_PM_POWER_DOWN_TAGMEM_IN_LIGHT_SLEEP=y
+CONFIG_ESP32S3_SPIRAM_SUPPORT=y
+CONFIG_DEFAULT_PSRAM_CLK_IO=30
+CONFIG_DEFAULT_PSRAM_CS_IO=26
+# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set
+# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160 is not set
+CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
+CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240
+CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32
+CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304
+CONFIG_MAIN_TASK_STACK_SIZE=3584
+CONFIG_CONSOLE_UART_DEFAULT=y
+# CONFIG_CONSOLE_UART_CUSTOM is not set
+# CONFIG_CONSOLE_UART_NONE is not set
+# CONFIG_ESP_CONSOLE_UART_NONE is not set
+CONFIG_CONSOLE_UART=y
+CONFIG_CONSOLE_UART_NUM=0
+CONFIG_CONSOLE_UART_BAUDRATE=115200
+CONFIG_INT_WDT=y
+CONFIG_INT_WDT_TIMEOUT_MS=300
+CONFIG_INT_WDT_CHECK_CPU1=y
+CONFIG_TASK_WDT=y
+CONFIG_ESP_TASK_WDT=y
+# CONFIG_TASK_WDT_PANIC is not set
+CONFIG_TASK_WDT_TIMEOUT_S=5
+CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
+CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y
+# CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set
+CONFIG_ESP32S3_DEBUG_OCDAWARE=y
+CONFIG_IPC_TASK_STACK_SIZE=1280
+CONFIG_TIMER_TASK_STACK_SIZE=3584
+CONFIG_ESP32_WIFI_ENABLED=y
+CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10
+CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32
+# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set
+CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y
+CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1
+CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32
+# CONFIG_ESP32_WIFI_CSI_ENABLED is not set
+CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y
+CONFIG_ESP32_WIFI_TX_BA_WIN=6
+CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y
+CONFIG_ESP32_WIFI_RX_BA_WIN=6
+CONFIG_ESP32_WIFI_NVS_ENABLED=y
+CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y
+# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set
+CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752
+CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32
+CONFIG_ESP32_WIFI_IRAM_OPT=y
+CONFIG_ESP32_WIFI_RX_IRAM_OPT=y
+CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y
+CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y
+CONFIG_WPA_MBEDTLS_CRYPTO=y
+CONFIG_WPA_MBEDTLS_TLS_CLIENT=y
+# CONFIG_WPA_WAPI_PSK is not set
+# CONFIG_WPA_SUITE_B_192 is not set
+# CONFIG_WPA_11KV_SUPPORT is not set
+# CONFIG_WPA_MBO_SUPPORT is not set
+# CONFIG_WPA_DPP_SUPPORT is not set
+# CONFIG_WPA_11R_SUPPORT is not set
+# CONFIG_WPA_WPS_SOFTAP_REGISTRAR is not set
+# CONFIG_WPA_WPS_STRICT is not set
+# CONFIG_WPA_DEBUG_PRINT is not set
+# CONFIG_WPA_TESTING_OPTIONS is not set
+# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set
+# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set
+CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y
+CONFIG_TIMER_TASK_PRIORITY=1
+CONFIG_TIMER_TASK_STACK_DEPTH=2048
+CONFIG_TIMER_QUEUE_LENGTH=10
+# CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set
+CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY=y
+# CONFIG_HAL_ASSERTION_SILIENT is not set
+# CONFIG_L2_TO_L3_COPY is not set
+CONFIG_ESP_GRATUITOUS_ARP=y
+CONFIG_GARP_TMR_INTERVAL=60
+CONFIG_TCPIP_RECVMBOX_SIZE=32
+CONFIG_TCP_MAXRTX=12
+CONFIG_TCP_SYNMAXRTX=12
+CONFIG_TCP_MSS=1440
+CONFIG_TCP_MSL=60000
+CONFIG_TCP_SND_BUF_DEFAULT=5760
+CONFIG_TCP_WND_DEFAULT=5760
+CONFIG_TCP_RECVMBOX_SIZE=6
+CONFIG_TCP_QUEUE_OOSEQ=y
+CONFIG_TCP_OVERSIZE_MSS=y
+# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set
+# CONFIG_TCP_OVERSIZE_DISABLE is not set
+CONFIG_UDP_RECVMBOX_SIZE=6
+CONFIG_TCPIP_TASK_STACK_SIZE=3072
+CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y
+# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set
+# CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set
+CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF
+# CONFIG_PPP_SUPPORT is not set
+CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y
+# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set
+# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set
+# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set
+# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set
+CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y
+# CONFIG_NEWLIB_NANO_FORMAT is not set
+CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y
+CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC_SYSTIMER=y
+CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC_FRC1=y
+# CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC is not set
+# CONFIG_ESP32S3_TIME_SYSCALL_USE_RTC is not set
+# CONFIG_NEWLIB_TIME_SYSCALL_USE_HRT is not set
+# CONFIG_ESP32S3_TIME_SYSCALL_USE_SYSTIMER is not set
+# CONFIG_ESP32S3_TIME_SYSCALL_USE_FRC1 is not set
+# CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set
+# CONFIG_ESP32S3_TIME_SYSCALL_USE_NONE is not set
+CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5
+CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072
+CONFIG_ESP32_PTHREAD_STACK_MIN=768
+CONFIG_ESP32_DEFAULT_PTHREAD_CORE_NO_AFFINITY=y
+# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_0 is not set
+# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_1 is not set
+CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1
+CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread"
+CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y
+# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set
+# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set
+CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y
+CONFIG_SUPPORT_TERMIOS=y
+CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1
+# End of deprecated options
diff --git a/firmware/sticks3/src/CMakeLists.txt b/firmware/sticks3/src/CMakeLists.txt
index f4a4405..d02ec18 100644
--- a/firmware/sticks3/src/CMakeLists.txt
+++ b/firmware/sticks3/src/CMakeLists.txt
@@ -1,5 +1,5 @@
idf_component_register(
- SRCS "main.c" "vibe_board.c" "vibe_audio.c" "../generated/vibe_stick_cn_16.c" "../generated/vibe_stick_ui_assets.c"
+ SRCS "main.c" "vibe_board.c" "vibe_audio.c" "vibe_imu.c" "../generated/vibe_stick_cn_16.c" "../generated/vibe_stick_ui_assets.c"
INCLUDE_DIRS "../include" "../generated"
- REQUIRES esp_wifi esp_http_client esp_event esp_netif nvs_flash json driver esp_timer esp_lcd esp_codec_dev
+ REQUIRES esp_wifi esp_http_client esp_event esp_netif nvs_flash json driver esp_timer esp_lcd esp_codec_dev bmi270
)
diff --git a/firmware/sticks3/src/idf_component.yml b/firmware/sticks3/src/idf_component.yml
index 1b582ef..34d5d42 100644
--- a/firmware/sticks3/src/idf_component.yml
+++ b/firmware/sticks3/src/idf_component.yml
@@ -1,4 +1,5 @@
dependencies:
espressif/button: "^4.1.6"
espressif/esp_codec_dev: "^1.3.4"
+ espressif/bmi270: "^1.0.2"
lvgl/lvgl: "9.2.0"
diff --git a/firmware/sticks3/src/main.c b/firmware/sticks3/src/main.c
index 86cf0c9..60216ab 100644
--- a/firmware/sticks3/src/main.c
+++ b/firmware/sticks3/src/main.c
@@ -1,10 +1,12 @@
#include
+#include
#include
#include
#include
#include "vibe_audio.h"
#include "vibe_board.h"
+#include "vibe_imu.h"
#include "vibe_stick_config.h"
#include "button_gpio.h"
#include "cJSON.h"
@@ -21,6 +23,7 @@
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_random.h"
+#include "esp_sleep.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
@@ -107,6 +110,7 @@ typedef struct {
typedef struct {
char status[24];
+ char account_name[48];
char project[40];
int quota_5h;
int quota_7d;
@@ -124,15 +128,41 @@ typedef struct {
static QueueHandle_t s_event_queue;
static SemaphoreHandle_t s_lvgl_lock;
+static SemaphoreHandle_t s_recording_upload_lock;
static bool s_wifi_connected;
+static bool s_wifi_initialized;
+static bool s_shutdown_started;
static bool s_recording_overlay_visible;
static bool s_long_press_active;
+static bool s_long_press_wake_only;
+static bool s_recording_stop_queued;
+static bool s_screen_on;
+static bool s_face_down;
+static bool s_state_wake_baseline_ready;
+static int64_t s_screen_awake_until_ms;
+static int64_t s_last_activity_ms;
+static int64_t s_last_imu_poll_ms;
+static int64_t s_face_down_candidate_since_ms;
+static int64_t s_face_up_candidate_since_ms;
+static float s_last_accel_x_g;
+static float s_last_accel_y_g;
+static float s_last_accel_z_g;
+static bool s_last_accel_valid;
+static int s_pickup_motion_samples;
+static char s_last_wake_status[PROVIDER_COUNT][24];
+static char s_last_wake_alert_event_id[56];
static char s_last_alert_event_id[56];
static char s_last_alert_type[24];
static bool s_alert_sound_baseline_ready;
static char s_recording_session_id[40];
+static size_t s_recording_uploaded_bytes;
+static int64_t s_recording_last_chunk_ms;
+static int64_t s_recording_started_ms;
+static int64_t s_recording_released_since_ms;
+static bool s_recording_streaming_active;
static lv_display_t *s_display;
+static esp_lcd_panel_handle_t s_panel;
static lv_obj_t *s_wifi_label;
static lv_obj_t *s_battery_label;
static lv_obj_t *s_battery_icon;
@@ -179,6 +209,7 @@ static agent_state_t s_state = {
static provider_display_state_t s_provider_states[PROVIDER_COUNT] = {
[PROVIDER_CODEX] = {
.status = "OFFLINE",
+ .account_name = "",
.project = "vibestick",
.quota_5h = 0,
.quota_7d = 0,
@@ -189,6 +220,7 @@ static provider_display_state_t s_provider_states[PROVIDER_COUNT] = {
},
[PROVIDER_CLAUDE] = {
.status = "OFFLINE",
+ .account_name = "",
.project = "vibestick",
.quota_5h = 0,
.quota_7d = 0,
@@ -237,13 +269,31 @@ static const lv_point_precise_t s_battery_bolt_points[] = {
static void render_state(void);
-static void queue_event(agent_event_type_t type)
+static bool queue_event(agent_event_type_t type)
{
if (!s_event_queue) {
- return;
+ return false;
}
agent_event_t event = {.type = type};
- (void)xQueueSend(s_event_queue, &event, 0);
+ if (xQueueSend(s_event_queue, &event, 0) != pdTRUE) {
+ ESP_LOGW(TAG, "event queue full; dropped type=%d", (int)type);
+ return false;
+ }
+ return true;
+}
+
+static void queue_recording_stop(const char *reason)
+{
+ if (s_recording_stop_queued) {
+ return;
+ }
+ s_recording_stop_queued = true;
+ s_long_press_active = false;
+ if (!queue_event(VIBE_STICK_EVENT_LONG_STOP)) {
+ s_recording_stop_queued = false;
+ return;
+ }
+ ESP_LOGI(TAG, "recording stop queued reason=%s", reason ? reason : "unknown");
}
static const agent_provider_config_t *provider_config(agent_provider_t provider)
@@ -394,6 +444,280 @@ static void set_backlight(uint8_t brightness)
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}
+static void set_screen_enabled(bool enabled)
+{
+ if (enabled) {
+ if (s_panel) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_lcd_panel_disp_on_off(s_panel, true));
+ }
+ set_backlight(LCD_BACKLIGHT_DEFAULT);
+ return;
+ }
+
+ set_backlight(0);
+ if (s_panel) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_lcd_panel_disp_on_off(s_panel, false));
+ }
+}
+
+static void sleep_screen(const char *reason)
+{
+ if (!s_screen_on) {
+ return;
+ }
+ set_screen_enabled(false);
+ s_screen_on = false;
+ ESP_LOGI(TAG, "screen off: %s", reason ? reason : "unknown");
+}
+
+static void wake_screen_for(int64_t duration_ms)
+{
+ const int64_t now_ms = esp_timer_get_time() / 1000;
+ set_screen_enabled(true);
+ s_screen_on = true;
+ s_screen_awake_until_ms = now_ms + duration_ms;
+ s_last_activity_ms = now_ms;
+}
+
+static bool wake_screen_for_interaction(void)
+{
+ const bool was_off = !s_screen_on;
+ wake_screen_for(VIBE_STICK_SCREEN_IDLE_MS);
+ return was_off;
+}
+
+static void maybe_sleep_screen(int64_t now_ms)
+{
+ if (!s_screen_on || s_recording_overlay_visible || vibe_audio_is_recording()) {
+ return;
+ }
+ if (now_ms >= s_screen_awake_until_ms) {
+ sleep_screen("idle standby");
+ }
+}
+
+static void maybe_update_face_down(int64_t now_ms)
+{
+ if (!vibe_imu_is_ready() ||
+ now_ms - s_last_imu_poll_ms < VIBE_STICK_IMU_POLL_MS) {
+ return;
+ }
+ s_last_imu_poll_ms = now_ms;
+
+ float x_g = 0.0f;
+ float y_g = 0.0f;
+ float z_g = 0.0f;
+ esp_err_t err = vibe_imu_read_acceleration(&x_g, &y_g, &z_g);
+ if (err != ESP_OK) {
+ ESP_LOGW(TAG, "BMI270 acceleration read failed: %s", esp_err_to_name(err));
+ s_face_down_candidate_since_ms = 0;
+ return;
+ }
+
+ float motion_delta_g = 0.0f;
+ if (s_last_accel_valid) {
+ const float dx = x_g - s_last_accel_x_g;
+ const float dy = y_g - s_last_accel_y_g;
+ const float dz = z_g - s_last_accel_z_g;
+ motion_delta_g = sqrtf(dx * dx + dy * dy + dz * dz);
+ }
+ s_last_accel_x_g = x_g;
+ s_last_accel_y_g = y_g;
+ s_last_accel_z_g = z_g;
+ s_last_accel_valid = true;
+
+ if (!s_screen_on && !s_recording_overlay_visible && !vibe_audio_is_recording()) {
+ if (motion_delta_g >= VIBE_STICK_PICKUP_DELTA_G) {
+ s_pickup_motion_samples++;
+ } else {
+ s_pickup_motion_samples = 0;
+ }
+ if (motion_delta_g >= VIBE_STICK_PICKUP_STRONG_DELTA_G ||
+ s_pickup_motion_samples >= VIBE_STICK_PICKUP_CONFIRM_SAMPLES) {
+ s_face_down = false;
+ s_face_down_candidate_since_ms = 0;
+ s_face_up_candidate_since_ms = 0;
+ s_pickup_motion_samples = 0;
+ wake_screen_for(VIBE_STICK_SCREEN_IDLE_MS);
+ ESP_LOGI(TAG, "pickup motion confirmed; screen wake delta=%.2f g",
+ motion_delta_g);
+ return;
+ }
+ } else {
+ s_pickup_motion_samples = 0;
+ }
+
+ const bool face_down_sample =
+ z_g <= VIBE_STICK_FACE_DOWN_Z_THRESHOLD_G &&
+ fabsf(x_g) <= VIBE_STICK_FACE_DOWN_XY_LIMIT_G &&
+ fabsf(y_g) <= VIBE_STICK_FACE_DOWN_XY_LIMIT_G;
+ const bool face_up_sample =
+ z_g >= VIBE_STICK_FACE_UP_Z_THRESHOLD_G &&
+ fabsf(x_g) <= VIBE_STICK_FACE_DOWN_XY_LIMIT_G &&
+ fabsf(y_g) <= VIBE_STICK_FACE_DOWN_XY_LIMIT_G;
+
+ if (s_face_down) {
+ if (!face_up_sample) {
+ s_face_up_candidate_since_ms = 0;
+ if (face_down_sample &&
+ !s_recording_overlay_visible && !vibe_audio_is_recording()) {
+ sleep_screen("face down");
+ }
+ return;
+ }
+
+ if (s_face_up_candidate_since_ms == 0) {
+ s_face_up_candidate_since_ms = now_ms;
+ return;
+ }
+ if (now_ms - s_face_up_candidate_since_ms < VIBE_STICK_FACE_UP_HOLD_MS) {
+ return;
+ }
+
+ s_face_down = false;
+ s_face_down_candidate_since_ms = 0;
+ s_face_up_candidate_since_ms = 0;
+ wake_screen_for(VIBE_STICK_SCREEN_IDLE_MS);
+ ESP_LOGI(TAG, "face-up confirmed; screen wake x=%.2f y=%.2f z=%.2f",
+ x_g, y_g, z_g);
+ return;
+ }
+
+ s_face_up_candidate_since_ms = 0;
+ if (!face_down_sample) {
+ s_face_down_candidate_since_ms = 0;
+ return;
+ }
+
+ if (s_face_down_candidate_since_ms == 0) {
+ s_face_down_candidate_since_ms = now_ms;
+ return;
+ }
+ if (now_ms - s_face_down_candidate_since_ms < VIBE_STICK_FACE_DOWN_HOLD_MS) {
+ return;
+ }
+
+ if (!s_face_down) {
+ s_face_down = true;
+ ESP_LOGI(TAG, "face-down mode active x=%.2f y=%.2f z=%.2f",
+ x_g, y_g, z_g);
+ }
+ if (!s_recording_overlay_visible && !vibe_audio_is_recording()) {
+ sleep_screen("face down");
+ }
+}
+
+static bool provider_is_busy(void)
+{
+ for (int i = 0; i < PROVIDER_COUNT; ++i) {
+ const char *status = s_provider_states[i].status;
+ if (strcmp(status, "RUNNING") == 0 || strcmp(status, "APPROVAL") == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static void enter_power_off(const char *reason)
+{
+ if (s_shutdown_started) {
+ return;
+ }
+ s_shutdown_started = true;
+ ESP_LOGI(TAG, "power off requested: %s", reason ? reason : "unknown");
+
+ sleep_screen("power off");
+
+ ESP_ERROR_CHECK_WITHOUT_ABORT(vibe_audio_shutdown());
+ ESP_ERROR_CHECK_WITHOUT_ABORT(vibe_imu_shutdown());
+ if (s_wifi_initialized) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
+ s_wifi_connected = false;
+ }
+
+ // Remove all peripheral power holds before the PMIC shutdown command.
+ // Failure here must not prevent the lower-power fallback below.
+ ESP_ERROR_CHECK_WITHOUT_ABORT(vibe_board_prepare_shutdown());
+ esp_err_t shutdown_err = vibe_board_shutdown();
+ if (shutdown_err != ESP_OK) {
+ ESP_LOGE(TAG, "M5PM1 shutdown command failed: %s", esp_err_to_name(shutdown_err));
+ }
+
+ // A successful PMIC command cuts ESP32-S3 power and never reaches this
+ // point. If the rail stays alive, deep sleep prevents the old behavior of
+ // returning to the Wi-Fi polling loop at full power. BtnA wakes fallback
+ // sleep when PMIC power-off wasn't available.
+ vTaskDelay(pdMS_TO_TICKS(750));
+ gpio_set_direction(PIN_BUTTON_FRONT, GPIO_MODE_INPUT);
+ gpio_set_pull_mode(PIN_BUTTON_FRONT, GPIO_PULLUP_ONLY);
+ ESP_ERROR_CHECK_WITHOUT_ABORT(
+ esp_sleep_enable_ext1_wakeup(1ULL << PIN_BUTTON_FRONT, ESP_EXT1_WAKEUP_ANY_LOW));
+ ESP_LOGW(TAG, "PMIC rail still active; entering deep-sleep fallback");
+ esp_deep_sleep_start();
+}
+
+static void maybe_auto_power_off(int64_t now_ms)
+{
+ if (VIBE_STICK_AUTO_POWER_OFF_MS <= 0 ||
+ now_ms - s_last_activity_ms < VIBE_STICK_AUTO_POWER_OFF_MS ||
+ s_state.usb_powered || s_state.battery_charging ||
+ s_recording_overlay_visible || vibe_audio_is_recording() ||
+ provider_is_busy()) {
+ return;
+ }
+
+ enter_power_off("60-minute idle timeout");
+}
+
+static bool status_wakes_screen(const char *status)
+{
+ return strcmp(status, "RUNNING") == 0 ||
+ strcmp(status, "DONE") == 0 ||
+ strcmp(status, "APPROVAL") == 0 ||
+ strcmp(status, "ERROR") == 0;
+}
+
+static void remember_screen_wake_baseline(void)
+{
+ for (int i = 0; i < PROVIDER_COUNT; ++i) {
+ strlcpy(s_last_wake_status[i], s_provider_states[i].status,
+ sizeof(s_last_wake_status[i]));
+ }
+ strlcpy(s_last_wake_alert_event_id, s_state.alert_event_id,
+ sizeof(s_last_wake_alert_event_id));
+ s_state_wake_baseline_ready = true;
+}
+
+static void maybe_wake_screen_for_state_change(void)
+{
+ if (!s_state_wake_baseline_ready) {
+ remember_screen_wake_baseline();
+ return;
+ }
+
+ bool should_wake = false;
+ for (int i = 0; i < PROVIDER_COUNT; ++i) {
+ const char *status = s_provider_states[i].status;
+ if (strcmp(s_last_wake_status[i], status) != 0 && status_wakes_screen(status)) {
+ should_wake = true;
+ }
+ }
+ if (s_state.alert_event_id[0] != '\0' &&
+ strcmp(s_last_wake_alert_event_id, s_state.alert_event_id) != 0 &&
+ strcmp(s_state.alert_type, "NONE") != 0) {
+ should_wake = true;
+ }
+
+ remember_screen_wake_baseline();
+ if (should_wake && !s_face_down) {
+ wake_screen_for(VIBE_STICK_SCREEN_EVENT_WAKE_MS);
+ ESP_LOGI(TAG, "screen wake for state change");
+ } else if (should_wake) {
+ ESP_LOGI(TAG, "screen wake suppressed while face down");
+ }
+}
+
static void init_backlight(void)
{
ledc_timer_config_t timer = {
@@ -457,6 +781,7 @@ static esp_err_t init_display(void)
ESP_RETURN_ON_ERROR(esp_lcd_panel_invert_color(panel, true), TAG, "panel invert");
ESP_RETURN_ON_ERROR(esp_lcd_panel_set_gap(panel, LCD_X_GAP, LCD_Y_GAP), TAG, "panel gap");
ESP_RETURN_ON_ERROR(esp_lcd_panel_disp_on_off(panel, true), TAG, "panel on");
+ s_panel = panel;
lv_init();
s_display = lv_display_create(LCD_H_RES, LCD_V_RES);
@@ -530,29 +855,6 @@ static void create_provider_icon(lv_obj_t *parent)
lv_obj_align(s_provider_icon, LV_ALIGN_TOP_LEFT, 18, 52);
}
-static const char *status_text_for(const char *status)
-{
- if (strcmp(status, "RUNNING") == 0) {
- return "运行中";
- }
- if (strcmp(status, "DONE") == 0) {
- return "已完成";
- }
- if (strcmp(status, "APPROVAL") == 0) {
- return "待确认";
- }
- if (strcmp(status, "ERROR") == 0) {
- return "出错";
- }
- if (strcmp(status, "OFFLINE") == 0) {
- return "离线";
- }
- if (strcmp(status, "IDLE") == 0 || strcmp(status, "UNKNOWN") == 0) {
- return "待命";
- }
- return "待命";
-}
-
static void set_battery_ui(int battery_value, bool charging, bool usb_powered)
{
if (battery_value < 0) {
@@ -671,7 +973,8 @@ static void create_ui(void)
s_provider_label = make_label(screen, "Codex", &lv_font_montserrat_16, lv_color_hex(0xf3f4f6), 60, LV_TEXT_ALIGN_LEFT);
lv_obj_align(s_provider_label, LV_ALIGN_TOP_LEFT, 72, 51);
- s_status_label = make_label(screen, "待命", FONT_CN, lv_color_hex(0xf3f4f6), 52, LV_TEXT_ALIGN_LEFT);
+ s_status_label = make_label(screen, "--", FONT_CN, lv_color_hex(0xf3f4f6), 52, LV_TEXT_ALIGN_LEFT);
+ lv_label_set_long_mode(s_status_label, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_align(s_status_label, LV_ALIGN_TOP_LEFT, 82, 73);
lv_obj_t *quota_wrap = make_plain_obj(screen, LCD_H_RES - 16, 104, lv_color_hex(0x0e1014), LV_OPA_COVER, 8);
@@ -798,7 +1101,10 @@ static void render_state(void)
}
lv_label_set_text(s_provider_label, provider->display_name);
lv_obj_set_style_text_color(s_provider_label, provider->implemented ? lv_color_hex(0xf3f4f6) : lv_color_hex(0xd7d9de), 0);
- lv_label_set_text(s_status_label, implemented ? status_text_for(display_state->status) : "待命");
+ lv_label_set_text(
+ s_status_label,
+ implemented && display_state->account_name[0] != '\0' ? display_state->account_name : "--"
+ );
set_status_color(provider, status_key);
set_quota_title(s_quota_5h_title_label, "5H", quota_stale);
set_quota_title(s_quota_7d_title_label, "7D", quota_stale);
@@ -867,7 +1173,7 @@ static void remember_alert_sound_baseline(void)
s_alert_sound_baseline_ready = true;
}
-static bool should_play_alert_sound(void)
+static bool alert_sound_event_is_new(void)
{
agent_sound_t ignored;
const bool target = sound_for_alert_type(s_state.alert_type, &ignored);
@@ -882,37 +1188,37 @@ static bool should_play_alert_sound(void)
return false;
}
- bool should_play = false;
if (s_state.alert_event_id[0] != '\0') {
- should_play = strcmp(s_last_alert_event_id, s_state.alert_event_id) != 0;
- } else {
- should_play = strcmp(s_last_alert_type, s_state.alert_type) != 0;
+ return strcmp(s_last_alert_event_id, s_state.alert_event_id) != 0;
}
- remember_alert_sound_baseline();
- return should_play;
+ return strcmp(s_last_alert_type, s_state.alert_type) != 0;
}
static void maybe_handle_alert(void)
{
agent_sound_t sound;
if (!sound_for_alert_type(s_state.alert_type, &sound)) {
- (void)should_play_alert_sound();
+ (void)alert_sound_event_is_new();
return;
}
- if (!should_play_alert_sound()) {
+ if (!alert_sound_event_is_new()) {
return;
}
if (s_recording_overlay_visible || vibe_audio_is_recording()) {
- ESP_LOGI(TAG, "skip alert sound while recording overlay is active type=%s",
+ // Do not consume the event: play it after recording releases the codec.
+ ESP_LOGI(TAG, "defer alert sound while recording overlay is active type=%s",
s_state.alert_type);
return;
}
esp_err_t err = vibe_audio_play_sound(sound);
if (err != ESP_OK) {
- ESP_LOGW(TAG, "alert sound skipped type=%s err=%s",
+ // Keep the event pending so a transient codec/bus conflict can retry.
+ ESP_LOGW(TAG, "alert sound deferred type=%s err=%s",
s_state.alert_type, esp_err_to_name(err));
+ return;
}
+ remember_alert_sound_baseline();
ESP_LOGI(TAG, "alert type=%s project=%s message=%s",
s_state.alert_type, s_state.project, s_state.alert_message);
}
@@ -984,8 +1290,8 @@ static esp_err_t http_request(const char *method, const char *path, const char *
return http_request_timeout(method, path, body, response, response_len, 2500);
}
-static esp_err_t http_post_binary(const char *path, const uint8_t *body, size_t body_len,
- char *response, int response_len)
+static esp_err_t http_post_binary_timeout(const char *path, const uint8_t *body, size_t body_len,
+ char *response, int response_len, int timeout_ms)
{
char url[192];
snprintf(url, sizeof(url), "http://%s:%d%s", VIBE_STICK_BRIDGE_HOST, VIBE_STICK_BRIDGE_PORT, path);
@@ -999,7 +1305,7 @@ static esp_err_t http_post_binary(const char *path, const uint8_t *body, size_t
}
esp_http_client_config_t config = {
.url = url,
- .timeout_ms = 20000,
+ .timeout_ms = timeout_ms,
.event_handler = http_event_handler,
.user_data = &capture,
};
@@ -1024,9 +1330,19 @@ static esp_err_t http_post_binary(const char *path, const uint8_t *body, size_t
ESP_LOGW(TAG, "http POST %s status=%d empty response", path, status_code);
}
esp_http_client_cleanup(client);
+ if (err == ESP_OK && (status_code < 200 || status_code >= 300)) {
+ ESP_LOGW(TAG, "http POST %s failed status=%d", path, status_code);
+ return ESP_FAIL;
+ }
return err;
}
+static esp_err_t http_post_binary(const char *path, const uint8_t *body, size_t body_len,
+ char *response, int response_len)
+{
+ return http_post_binary_timeout(path, body, body_len, response, response_len, 20000);
+}
+
static void copy_json_string(cJSON *root, const char *key, char *target, size_t target_len)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
@@ -1066,6 +1382,7 @@ static bool json_percent_value(cJSON *item, int *value)
static void parse_provider_fields(cJSON *source, provider_display_state_t *target)
{
copy_json_string(source, "status", target->status, sizeof(target->status));
+ copy_json_string(source, "account_name", target->account_name, sizeof(target->account_name));
copy_json_string(source, "project", target->project, sizeof(target->project));
copy_json_string(source, "quota_updated_at", target->quota_updated_at, sizeof(target->quota_updated_at));
@@ -1084,6 +1401,27 @@ static void parse_provider_fields(cJSON *source, provider_display_state_t *targe
target->quota_stale = cJSON_IsBool(stale) ? cJSON_IsTrue(stale) : false;
}
+static void parse_providers_json(cJSON *providers)
+{
+ for (size_t i = 0; i < sizeof(s_provider_configs) / sizeof(s_provider_configs[0]); ++i) {
+ const agent_provider_config_t *provider = &s_provider_configs[i];
+ cJSON *provider_json = cJSON_GetObjectItemCaseSensitive(providers, provider->key);
+ if (!cJSON_IsObject(provider_json)) {
+ continue;
+ }
+ provider_display_state_t *display_state = provider_display_state(provider->id);
+ parse_provider_fields(provider_json, display_state);
+ ESP_LOGI(TAG, "provider cache parsed key=%s account=%s q5=%s%d q7=%s%d stale=%d",
+ provider->key,
+ display_state->account_name,
+ display_state->quota_5h_valid ? "" : "invalid:",
+ display_state->quota_5h,
+ display_state->quota_7d_valid ? "" : "invalid:",
+ display_state->quota_7d,
+ display_state->quota_stale);
+ }
+}
+
static void parse_provider_json(cJSON *state_root, cJSON *provider)
{
char provider_key[16] = "";
@@ -1141,8 +1479,12 @@ static bool parse_state_json(const char *json)
s_state.wifi = cJSON_IsBool(wifi) ? cJSON_IsTrue(wifi) : s_state.wifi;
s_state.ble = cJSON_IsBool(ble) ? cJSON_IsTrue(ble) : s_state.ble;
+ cJSON *providers = cJSON_GetObjectItemCaseSensitive(state_root, "providers");
cJSON *provider = cJSON_GetObjectItemCaseSensitive(state_root, "provider");
cJSON *codex = cJSON_GetObjectItemCaseSensitive(state_root, "codex");
+ if (cJSON_IsObject(providers)) {
+ parse_providers_json(providers);
+ }
if (cJSON_IsObject(provider)) {
parse_provider_json(state_root, provider);
} else {
@@ -1168,7 +1510,7 @@ static bool parse_state_json(const char *json)
static void poll_state(void)
{
- char response[1536] = {0};
+ char response[3072] = {0};
int battery_level = 0;
if (vibe_board_battery_level(&battery_level) == ESP_OK) {
s_state.battery = battery_level;
@@ -1202,9 +1544,11 @@ static void poll_state(void)
provider_display_state_t *display_state = current_provider_display_state();
strlcpy(display_state->status, "OFFLINE", sizeof(display_state->status));
s_state.wifi = s_wifi_connected;
+ maybe_wake_screen_for_state_change();
render_state();
return;
}
+ maybe_wake_screen_for_state_change();
render_state();
maybe_handle_alert();
}
@@ -1217,6 +1561,7 @@ static void post_simple_event(const char *event_name, const char *path)
const char *target_path = path ? path : VIBE_STICK_EVENT_PATH;
esp_err_t err = http_request("POST", target_path, body, response, sizeof(response));
if (err == ESP_OK && response[0] != '\0' && parse_state_json(response)) {
+ maybe_wake_screen_for_state_change();
render_state();
}
}
@@ -1270,6 +1615,21 @@ static bool parse_recording_status(const char *json, char *status_text, size_t s
return ok;
}
+static bool parse_recording_streaming_active(const char *json)
+{
+ cJSON *root = cJSON_Parse(json);
+ if (!root) {
+ return false;
+ }
+ cJSON *recording = cJSON_GetObjectItemCaseSensitive(root, "recording");
+ cJSON *audio_source = cJSON_IsObject(recording) ?
+ cJSON_GetObjectItemCaseSensitive(recording, "audio_source") : NULL;
+ bool active = cJSON_IsString(audio_source) && audio_source->valuestring &&
+ strcmp(audio_source->valuestring, "sticks3_pcm_stream") == 0;
+ cJSON_Delete(root);
+ return active;
+}
+
static void generate_recording_session_id(char *session_id, size_t session_id_len)
{
if (session_id_len < 33) {
@@ -1308,18 +1668,115 @@ static void upload_recording_audio(void)
}
}
+static bool upload_recording_chunk(void)
+{
+ const uint8_t *chunk = NULL;
+ size_t chunk_len = 0;
+ esp_err_t audio_err = vibe_audio_chunk(s_recording_uploaded_bytes,
+ VIBE_STICK_RECORDING_CHUNK_BYTES,
+ &chunk, &chunk_len);
+ if (audio_err != ESP_OK || chunk_len == 0) {
+ return audio_err == ESP_OK;
+ }
+
+ char path[128];
+ snprintf(path, sizeof(path), "%s?session_id=%s&offset=%u",
+ VIBE_STICK_RECORDING_CHUNK_PATH, s_recording_session_id,
+ (unsigned)s_recording_uploaded_bytes);
+ char response[768] = {0};
+ esp_err_t err = http_post_binary_timeout(path, chunk, chunk_len,
+ response, sizeof(response), 800);
+ if (err != ESP_OK) {
+ ESP_LOGW(TAG, "streaming audio upload failed offset=%u len=%u: %s",
+ (unsigned)s_recording_uploaded_bytes, (unsigned)chunk_len,
+ esp_err_to_name(err));
+ return false;
+ }
+ char status[32] = {0};
+ if (parse_recording_status(response, status, sizeof(status)) &&
+ (strcmp(status, "stream_unavailable") == 0 ||
+ strcmp(status, "audio_failed") == 0)) {
+ ESP_LOGW(TAG, "bridge rejected streaming audio status=%s", status);
+ return false;
+ }
+ s_recording_uploaded_bytes += chunk_len;
+ return true;
+}
+
+static void maybe_upload_recording_chunk(int64_t now_ms)
+{
+ if (!s_recording_streaming_active || !vibe_audio_is_recording() ||
+ s_recording_session_id[0] == '\0' ||
+ now_ms - s_recording_last_chunk_ms < VIBE_STICK_RECORDING_CHUNK_INTERVAL_MS) {
+ return;
+ }
+ if (xSemaphoreTake(s_recording_upload_lock, 0) != pdTRUE) {
+ return;
+ }
+ if (!s_recording_streaming_active || !vibe_audio_is_recording() ||
+ now_ms - s_recording_last_chunk_ms < VIBE_STICK_RECORDING_CHUNK_INTERVAL_MS) {
+ xSemaphoreGive(s_recording_upload_lock);
+ return;
+ }
+ s_recording_last_chunk_ms = now_ms;
+ if (!upload_recording_chunk()) {
+ s_recording_streaming_active = false;
+ }
+ xSemaphoreGive(s_recording_upload_lock);
+}
+
+static bool flush_recording_chunks(void)
+{
+ while (s_recording_streaming_active) {
+ if (xSemaphoreTake(s_recording_upload_lock, portMAX_DELAY) != pdTRUE) {
+ return false;
+ }
+ size_t total = 0;
+ (void)vibe_audio_data(&total);
+ if (s_recording_uploaded_bytes >= total) {
+ xSemaphoreGive(s_recording_upload_lock);
+ return true;
+ }
+ if (!upload_recording_chunk()) {
+ s_recording_streaming_active = false;
+ xSemaphoreGive(s_recording_upload_lock);
+ return false;
+ }
+ xSemaphoreGive(s_recording_upload_lock);
+ }
+ return false;
+}
+
+static void recording_upload_task(void *arg)
+{
+ (void)arg;
+ while (true) {
+ maybe_upload_recording_chunk(esp_timer_get_time() / 1000);
+ vTaskDelay(pdMS_TO_TICKS(12));
+ }
+}
+
static void handle_recording_start(void)
{
generate_recording_session_id(s_recording_session_id, sizeof(s_recording_session_id));
if (s_recording_session_id[0] == '\0') {
ESP_LOGW(TAG, "recording start failed: no session id");
+ s_recording_started_ms = 0;
+ s_recording_stop_queued = false;
return;
}
+ s_recording_uploaded_bytes = 0;
+ s_recording_last_chunk_ms = esp_timer_get_time() / 1000;
+ s_recording_started_ms = s_recording_last_chunk_ms;
+ s_recording_released_since_ms = 0;
+ s_recording_streaming_active = false;
esp_err_t audio_err = vibe_audio_start();
if (audio_err != ESP_OK) {
ESP_LOGW(TAG, "hardware recording start failed: %s", esp_err_to_name(audio_err));
s_recording_session_id[0] = '\0';
+ s_recording_started_ms = 0;
+ s_recording_stop_queued = false;
return;
}
show_recording_overlay("正在聆听", "松开发送", true);
@@ -1338,6 +1795,9 @@ static void handle_recording_start(void)
strcmp(response_session_id, s_recording_session_id) != 0) {
ESP_LOGW(TAG, "bridge returned a different recording session id");
}
+ s_recording_streaming_active = parse_recording_streaming_active(response);
+ ESP_LOGI(TAG, "StickS3 streaming upload %s",
+ s_recording_streaming_active ? "enabled" : "unavailable; final upload fallback");
if (parse_state_json(response)) {
render_state();
}
@@ -1349,10 +1809,17 @@ static void handle_recording_start(void)
static void handle_recording_stop(void)
{
- show_recording_overlay("正在发送", "", true);
+ // Audio is already streamed while the button is held. Dismiss the
+ // listening overlay immediately; final correction continues in the
+ // background and only a real failure is surfaced to the user.
+ show_recording_overlay(NULL, NULL, false);
if (s_recording_session_id[0] == '\0') {
(void)vibe_audio_stop();
vibe_audio_clear();
+ s_recording_started_ms = 0;
+ s_recording_released_since_ms = 0;
+ s_recording_streaming_active = false;
+ s_recording_stop_queued = false;
poll_state();
show_recording_overlay(NULL, NULL, false);
return;
@@ -1363,10 +1830,12 @@ static void handle_recording_stop(void)
ESP_LOGW(TAG, "hardware recording stop failed: %s", esp_err_to_name(audio_err));
}
- upload_recording_audio();
+ bool streamed = s_recording_streaming_active && flush_recording_chunks();
+ if (!streamed) {
+ upload_recording_audio();
+ }
vibe_audio_clear();
- show_recording_overlay("正在识别", "", true);
const char *body = "{\"event\":\"button_long_stop\",\"source\":\"sticks3\",\"paste\":true}";
char response[1024] = {0};
esp_err_t err = http_request_timeout("POST", VIBE_STICK_RECORDING_STOP_PATH, body, response, sizeof(response), 30000);
@@ -1392,6 +1861,11 @@ static void handle_recording_stop(void)
vTaskDelay(pdMS_TO_TICKS(900));
}
s_recording_session_id[0] = '\0';
+ s_recording_uploaded_bytes = 0;
+ s_recording_started_ms = 0;
+ s_recording_released_since_ms = 0;
+ s_recording_streaming_active = false;
+ s_recording_stop_queued = false;
poll_state();
show_recording_overlay(NULL, NULL, false);
}
@@ -1405,7 +1879,9 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base,
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
s_wifi_connected = false;
- esp_wifi_connect();
+ if (!s_shutdown_started) {
+ esp_wifi_connect();
+ }
render_state();
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
s_wifi_connected = true;
@@ -1434,6 +1910,7 @@ static esp_err_t init_wifi(void)
ESP_RETURN_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_STA), TAG, "wifi mode");
ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &wifi_config), TAG, "wifi config");
ESP_RETURN_ON_ERROR(esp_wifi_start(), TAG, "wifi start");
+ s_wifi_initialized = true;
return ESP_OK;
}
@@ -1462,6 +1939,7 @@ static void button_long_start_cb(void *button_handle, void *usr_data)
{
(void)button_handle;
(void)usr_data;
+ s_recording_stop_queued = false;
s_long_press_active = true;
queue_event(VIBE_STICK_EVENT_LONG_START);
}
@@ -1470,9 +1948,8 @@ static void button_up_cb(void *button_handle, void *usr_data)
{
(void)button_handle;
(void)usr_data;
- if (s_long_press_active) {
- s_long_press_active = false;
- queue_event(VIBE_STICK_EVENT_LONG_STOP);
+ if (s_long_press_active || vibe_audio_is_recording()) {
+ queue_recording_stop("button_up");
}
}
@@ -1520,7 +1997,33 @@ static void app_task(void *arg)
int64_t last_poll = 0;
while (true) {
int64_t now_ms = esp_timer_get_time() / 1000;
- if (s_wifi_connected && now_ms - last_poll >= VIBE_STICK_STATE_POLL_MS) {
+ if (vibe_audio_is_recording()) {
+ if (gpio_get_level(PIN_BUTTON_FRONT) != 0) {
+ if (s_recording_released_since_ms == 0) {
+ s_recording_released_since_ms = now_ms;
+ } else if (!s_recording_stop_queued &&
+ now_ms - s_recording_released_since_ms >=
+ VIBE_STICK_RECORDING_RELEASE_DEBOUNCE_MS) {
+ queue_recording_stop("gpio_release_failsafe");
+ }
+ } else {
+ s_recording_released_since_ms = 0;
+ }
+ if (!s_recording_stop_queued && s_recording_started_ms > 0 &&
+ now_ms - s_recording_started_ms >= VIBE_STICK_RECORDING_MAX_MS) {
+ queue_recording_stop("maximum_duration");
+ }
+ } else {
+ s_recording_released_since_ms = 0;
+ }
+ maybe_update_face_down(now_ms);
+ maybe_sleep_screen(now_ms);
+ maybe_auto_power_off(now_ms);
+ // Provider/quota observation can be much slower than an audio frame.
+ // Never let the cosmetic state poll block real-time PCM uploads.
+ if (s_wifi_connected && !vibe_audio_is_recording() &&
+ !s_recording_streaming_active &&
+ now_ms - last_poll >= VIBE_STICK_STATE_POLL_MS) {
last_poll = now_ms;
poll_state();
}
@@ -1532,20 +2035,42 @@ static void app_task(void *arg)
poll_state();
break;
case VIBE_STICK_EVENT_SHORT_PRESS:
+ // The original firmware used one front-button click as Send/Enter.
+ // Wake the display too, but never consume that click just because
+ // the panel happened to be asleep.
+ (void)wake_screen_for_interaction();
post_simple_event("button_short", NULL);
break;
case VIBE_STICK_EVENT_DOUBLE_CLICK:
+ if (wake_screen_for_interaction()) {
+ break;
+ }
post_simple_event("button_double", VIBE_STICK_QUOTA_REFRESH_PATH);
poll_state();
break;
case VIBE_STICK_EVENT_LONG_START:
+ if (wake_screen_for_interaction()) {
+ s_long_press_wake_only = true;
+ break;
+ }
handle_recording_start();
break;
case VIBE_STICK_EVENT_LONG_STOP:
+ if (s_long_press_wake_only) {
+ s_long_press_wake_only = false;
+ wake_screen_for(VIBE_STICK_SCREEN_IDLE_MS);
+ break;
+ }
+ wake_screen_for(VIBE_STICK_SCREEN_IDLE_MS);
handle_recording_stop();
break;
case VIBE_STICK_EVENT_PROVIDER_NEXT:
+ if (wake_screen_for_interaction()) {
+ break;
+ }
switch_provider();
+ post_simple_event("button_double", VIBE_STICK_QUOTA_REFRESH_PATH);
+ poll_state();
break;
}
}
@@ -1564,9 +2089,12 @@ void app_main(void)
}
ESP_ERROR_CHECK_WITHOUT_ABORT(vibe_board_init_power());
+ ESP_ERROR_CHECK_WITHOUT_ABORT(vibe_imu_init());
s_event_queue = xQueueCreate(10, sizeof(agent_event_t));
s_lvgl_lock = xSemaphoreCreateMutex();
+ s_recording_upload_lock = xSemaphoreCreateMutex();
ESP_ERROR_CHECK(init_display());
+ wake_screen_for(VIBE_STICK_SCREEN_IDLE_MS);
lvgl_lock();
create_ui();
lvgl_unlock();
@@ -1574,5 +2102,6 @@ void app_main(void)
ESP_ERROR_CHECK(init_button());
ESP_ERROR_CHECK(vibe_audio_init());
ESP_ERROR_CHECK(init_wifi());
- xTaskCreate(app_task, "agent_app", 6144, NULL, 4, NULL);
+ xTaskCreate(recording_upload_task, "audio_upload", 6144, NULL, 5, NULL);
+ xTaskCreate(app_task, "agent_app", 8192, NULL, 4, NULL);
}
diff --git a/firmware/sticks3/src/vibe_audio.c b/firmware/sticks3/src/vibe_audio.c
index e9a819e..5e9b3ad 100644
--- a/firmware/sticks3/src/vibe_audio.c
+++ b/firmware/sticks3/src/vibe_audio.c
@@ -23,7 +23,7 @@
#define AUDIO_FRAME_MS 60
#define AUDIO_FRAME_SAMPLES ((VIBE_STICK_AUDIO_SAMPLE_RATE * AUDIO_FRAME_MS) / 1000)
-#define AUDIO_MAX_SECONDS 45
+#define AUDIO_MAX_SECONDS 120
#define AUDIO_MAX_BYTES (VIBE_STICK_AUDIO_SAMPLE_RATE * VIBE_STICK_AUDIO_CHANNELS * \
(VIBE_STICK_AUDIO_BITS_PER_SAMPLE / 8) * AUDIO_MAX_SECONDS)
#define TASK_EXIT_WAIT_MS 800
@@ -49,7 +49,7 @@ static const audio_codec_data_if_t *s_data_if;
static const audio_codec_gpio_if_t *s_gpio_if;
static const audio_codec_if_t *s_codec_if;
static uint8_t *s_audio_buffer;
-static size_t s_audio_len;
+static atomic_size_t s_audio_len;
static size_t s_audio_capacity;
static esp_err_t init_i2s(bool enable_tx, bool enable_rx)
@@ -336,15 +336,17 @@ static void audio_task(void *arg)
ESP_LOGW(TAG, "codec read failed: %s", esp_err_to_name(err));
continue;
}
- if (s_audio_len + sizeof(frame) <= s_audio_capacity) {
- memcpy(s_audio_buffer + s_audio_len, frame, sizeof(frame));
- s_audio_len += sizeof(frame);
+ size_t audio_len = atomic_load_explicit(&s_audio_len, memory_order_acquire);
+ if (audio_len + sizeof(frame) <= s_audio_capacity) {
+ memcpy(s_audio_buffer + audio_len, frame, sizeof(frame));
+ atomic_store_explicit(&s_audio_len, audio_len + sizeof(frame), memory_order_release);
} else {
dropped += sizeof(frame);
}
}
- ESP_LOGI(TAG, "recorded %u bytes dropped=%u", (unsigned)s_audio_len, (unsigned)dropped);
+ ESP_LOGI(TAG, "recorded %u bytes dropped=%u",
+ (unsigned)atomic_load(&s_audio_len), (unsigned)dropped);
release_session_resources();
s_audio_task = NULL;
vTaskDelete(NULL);
@@ -384,7 +386,7 @@ esp_err_t vibe_audio_start(void)
xSemaphoreGive(s_audio_mutex);
ESP_RETURN_ON_FALSE(false, ESP_ERR_NO_MEM, TAG, "audio buffer");
}
- s_audio_len = 0;
+ atomic_store(&s_audio_len, 0);
esp_err_t err = init_i2s(false, true);
if (err != ESP_OK) {
@@ -431,6 +433,19 @@ esp_err_t vibe_audio_stop(void)
return ESP_OK;
}
+esp_err_t vibe_audio_shutdown(void)
+{
+ esp_err_t stop_err = vibe_audio_stop();
+ if (s_audio_mutex && xSemaphoreTake(s_audio_mutex, pdMS_TO_TICKS(250)) == pdTRUE) {
+ release_session_resources();
+ vibe_audio_clear();
+ s_initialized = false;
+ xSemaphoreGive(s_audio_mutex);
+ }
+ ESP_ERROR_CHECK_WITHOUT_ABORT(vibe_board_speaker_set_enabled(false));
+ return stop_err;
+}
+
bool vibe_audio_is_recording(void)
{
return atomic_load(&s_running) || s_audio_task != NULL;
@@ -484,17 +499,34 @@ esp_err_t vibe_audio_play_sound(agent_sound_t sound)
const uint8_t *vibe_audio_data(size_t *len)
{
if (len) {
- *len = s_audio_len;
+ *len = atomic_load_explicit(&s_audio_len, memory_order_acquire);
}
return s_audio_buffer;
}
+esp_err_t vibe_audio_chunk(size_t offset, size_t max_len,
+ const uint8_t **data, size_t *len)
+{
+ ESP_RETURN_ON_FALSE(data != NULL && len != NULL, ESP_ERR_INVALID_ARG, TAG,
+ "null chunk output");
+ size_t total = atomic_load_explicit(&s_audio_len, memory_order_acquire);
+ if (!s_audio_buffer || offset >= total || max_len == 0) {
+ *data = NULL;
+ *len = 0;
+ return ESP_OK;
+ }
+ size_t available = total - offset;
+ *len = available < max_len ? available : max_len;
+ *data = s_audio_buffer + offset;
+ return ESP_OK;
+}
+
void vibe_audio_clear(void)
{
if (s_audio_buffer) {
heap_caps_free(s_audio_buffer);
s_audio_buffer = NULL;
}
- s_audio_len = 0;
+ atomic_store(&s_audio_len, 0);
s_audio_capacity = 0;
}
diff --git a/firmware/sticks3/src/vibe_board.c b/firmware/sticks3/src/vibe_board.c
index de05a92..e326f5e 100644
--- a/firmware/sticks3/src/vibe_board.c
+++ b/firmware/sticks3/src/vibe_board.c
@@ -13,6 +13,7 @@
#define M5PM1_REG_PWR_CFG 0x06
#define M5PM1_REG_HOLD_CFG 0x07
#define M5PM1_REG_I2C_CFG 0x09
+#define M5PM1_REG_SYS_CMD 0x0C
#define M5PM1_REG_GPIO_MODE 0x10
#define M5PM1_REG_GPIO_OUT 0x11
#define M5PM1_REG_GPIO_IN 0x12
@@ -36,6 +37,7 @@
#define M5PM1_GPIO_FUNC_MASK(pin) (0x03 << ((pin) * 2))
#define M5PM1_GPIO_FUNC_GPIO(pin) (0x00 << ((pin) * 2))
#define M5PM1_GPIO_FUNC_IRQ(pin) (0x01 << ((pin) * 2))
+#define M5PM1_SYS_CMD_SHUTDOWN 0xA1
#define I2C_FREQ_HZ 100000
static const char *TAG = "vibe_board";
@@ -136,8 +138,10 @@ esp_err_t vibe_board_init_power(void)
ESP_ERROR_CHECK_WITHOUT_ABORT(write_reg(M5PM1_REG_PWR_CFG,
(pwr_cfg | M5PM1_PWR_CFG_LDO_EN) &
~M5PM1_PWR_CFG_LED_CTRL));
+ // The firmware doesn't use IMU wake. Keeping LDO_HOLD set would leave the
+ // IMU rail powered after shutdown and slowly drain the battery.
ESP_ERROR_CHECK_WITHOUT_ABORT(write_reg(M5PM1_REG_HOLD_CFG,
- hold_cfg | M5PM1_HOLD_CFG_LDO_HOLD));
+ hold_cfg & ~M5PM1_HOLD_CFG_LDO_HOLD));
ESP_ERROR_CHECK_WITHOUT_ABORT(update_reg(M5PM1_REG_GPIO_FUNC0,
M5PM1_GPIO2_L3B_POWER_EN, 0));
ESP_ERROR_CHECK_WITHOUT_ABORT(update_reg(M5PM1_REG_GPIO_MODE,
@@ -161,7 +165,7 @@ esp_err_t vibe_board_init_power(void)
M5PM1_GPIO_FUNC_MASK(1),
M5PM1_GPIO_FUNC_IRQ(1)));
- ESP_LOGI(TAG, "power hold enabled");
+ ESP_LOGI(TAG, "active peripheral rails enabled; shutdown IMU hold disabled");
return ESP_OK;
}
@@ -237,3 +241,47 @@ esp_err_t vibe_board_speaker_set_enabled(bool enabled)
ESP_LOGI(TAG, "speaker amp %s", enabled ? "enabled" : "disabled");
return ESP_OK;
}
+
+esp_err_t vibe_board_prepare_shutdown(void)
+{
+ ESP_RETURN_ON_FALSE(s_pmic_dev != NULL, ESP_ERR_INVALID_STATE, TAG, "pmic missing");
+
+ esp_err_t first_error = ESP_OK;
+ esp_err_t err = vibe_board_speaker_set_enabled(false);
+ if (err != ESP_OK) {
+ first_error = err;
+ ESP_LOGW(TAG, "speaker power off failed: %s", esp_err_to_name(err));
+ }
+
+ // L3B supplies LCD, microphone and speaker. It isn't cascaded with the
+ // ESP rail, so explicitly turn it off before asking the PMIC to shut down.
+ err = update_reg(M5PM1_REG_GPIO_OUT, M5PM1_GPIO2_L3B_POWER_EN, 0);
+ if (err != ESP_OK && first_error == ESP_OK) {
+ first_error = err;
+ }
+
+ // L1 supplies the IMU. Clear its shutdown hold and switch it off because
+ // VibeStick doesn't use motion wake.
+ err = update_reg(M5PM1_REG_HOLD_CFG, M5PM1_HOLD_CFG_LDO_HOLD, 0);
+ if (err != ESP_OK && first_error == ESP_OK) {
+ first_error = err;
+ }
+ err = update_reg(M5PM1_REG_PWR_CFG, M5PM1_PWR_CFG_LDO_EN, 0);
+ if (err != ESP_OK && first_error == ESP_OK) {
+ first_error = err;
+ }
+
+ ESP_LOGI(TAG, "peripheral rails prepared for shutdown");
+ return first_error;
+}
+
+esp_err_t vibe_board_shutdown(void)
+{
+ ESP_RETURN_ON_FALSE(s_pmic_dev != NULL, ESP_ERR_INVALID_STATE, TAG, "pmic missing");
+
+ // Match the official M5PM1 driver: allow pending I2C work to settle, then
+ // write key 0xA plus command 0x1 to the system command register.
+ esp_rom_delay_us(120000);
+ ESP_LOGI(TAG, "requesting M5PM1 shutdown");
+ return write_reg(M5PM1_REG_SYS_CMD, M5PM1_SYS_CMD_SHUTDOWN);
+}
diff --git a/firmware/sticks3/src/vibe_imu.c b/firmware/sticks3/src/vibe_imu.c
new file mode 100644
index 0000000..28e8417
--- /dev/null
+++ b/firmware/sticks3/src/vibe_imu.c
@@ -0,0 +1,99 @@
+#include "vibe_imu.h"
+
+#include "bmi270.h"
+#include "driver/i2c_master.h"
+#include "esp_check.h"
+#include "esp_log.h"
+#include "vibe_board.h"
+
+#define BMI270_REG_PWR_CTRL 0x7D
+#define BMI270_PWR_CTRL_ACCEL_ONLY BIT(2)
+
+static const char *TAG = "vibe_imu";
+static bmi270_handle_t *s_imu;
+
+static esp_err_t create_at_address(uint8_t address)
+{
+ bmi270_driver_config_t driver_config = {
+ .addr = address,
+ .interface = BMI270_USE_I2C,
+ .i2c_bus = vibe_board_i2c_bus(),
+ };
+ return bmi270_create(&driver_config, &s_imu);
+}
+
+esp_err_t vibe_imu_init(void)
+{
+ if (s_imu) {
+ return ESP_OK;
+ }
+
+ ESP_RETURN_ON_FALSE(vibe_board_i2c_bus() != NULL, ESP_ERR_INVALID_STATE,
+ TAG, "board I2C bus is not ready");
+
+ esp_err_t err = create_at_address(BMI270_I2C_ADDRESS_L);
+ if (err != ESP_OK) {
+ s_imu = NULL;
+ err = create_at_address(BMI270_I2C_ADDRESS_H);
+ }
+ ESP_RETURN_ON_ERROR(err, TAG, "BMI270 not found");
+
+ const bmi270_config_t config = {
+ .acce_odr = BMI270_ACC_ODR_25_HZ,
+ .acce_range = BMI270_ACC_RANGE_2_G,
+ .gyro_odr = BMI270_GYR_ODR_25_HZ,
+ .gyro_range = BMI270_GYR_RANGE_250_DPS,
+ };
+ err = bmi270_start(s_imu, &config);
+ if (err != ESP_OK) {
+ bmi270_delete(s_imu);
+ s_imu = NULL;
+ return err;
+ }
+
+ // The public driver starts accelerometer, gyroscope and temperature
+ // together. Face-down detection only needs acceleration, so turn the
+ // other two blocks back off to reduce active battery use.
+ const uint8_t power_ctrl[] = {
+ BMI270_REG_PWR_CTRL,
+ BMI270_PWR_CTRL_ACCEL_ONLY,
+ };
+ err = i2c_master_transmit(s_imu->i2c_handle, power_ctrl,
+ sizeof(power_ctrl), 100);
+ if (err != ESP_OK) {
+ bmi270_stop(s_imu);
+ bmi270_delete(s_imu);
+ s_imu = NULL;
+ return err;
+ }
+
+ ESP_LOGI(TAG, "BMI270 ready in accelerometer-only mode");
+ return ESP_OK;
+}
+
+bool vibe_imu_is_ready(void)
+{
+ return s_imu != NULL;
+}
+
+esp_err_t vibe_imu_read_acceleration(float *x_g, float *y_g, float *z_g)
+{
+ ESP_RETURN_ON_FALSE(s_imu != NULL, ESP_ERR_INVALID_STATE, TAG,
+ "BMI270 is not ready");
+ return bmi270_get_acce_data(s_imu, x_g, y_g, z_g);
+}
+
+esp_err_t vibe_imu_shutdown(void)
+{
+ if (!s_imu) {
+ return ESP_OK;
+ }
+
+ esp_err_t result = bmi270_stop(s_imu);
+ esp_err_t delete_result = bmi270_delete(s_imu);
+ s_imu = NULL;
+ if (result != ESP_OK) {
+ return result;
+ }
+ return delete_result;
+}
diff --git a/scripts/dev.ps1 b/scripts/dev.ps1
new file mode 100644
index 0000000..98143a0
--- /dev/null
+++ b/scripts/dev.ps1
@@ -0,0 +1,32 @@
+[CmdletBinding()]
+param(
+ [string]$ListenAddress = "0.0.0.0",
+ [int]$Port = 8765
+)
+
+$ErrorActionPreference = "Stop"
+$RootDir = Split-Path -Parent $PSScriptRoot
+$EnvPath = Join-Path $RootDir ".env"
+
+if (Test-Path -LiteralPath $EnvPath) {
+ foreach ($line in [IO.File]::ReadAllLines($EnvPath, [Text.Encoding]::UTF8)) {
+ $trimmed = $line.Trim()
+ if (-not $trimmed -or $trimmed.StartsWith("#") -or -not $trimmed.Contains("=")) { continue }
+ $name, $value = $trimmed.Split("=", 2)
+ [Environment]::SetEnvironmentVariable($name.Trim(), $value.Trim().Trim('"'), "Process")
+ }
+}
+
+$placeholders = @("", "change-this-shared-token", "paste-generated-token-here", "changeme", "change-me")
+if ($ListenAddress -notin @("127.0.0.1", "localhost", "::1") -and $env:VIBE_STICK_BRIDGE_TOKEN -in $placeholders) {
+ throw "VIBE_STICK_BRIDGE_TOKEN is required when the bridge listens outside loopback. Run .\scripts\setup.ps1 first."
+}
+
+$env:PYTHONPATH = Join-Path $RootDir "bridge\src"
+Push-Location (Join-Path $RootDir "bridge")
+try {
+ & python -m vibe_stick --host $ListenAddress --port $Port
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+} finally {
+ Pop-Location
+}
diff --git a/scripts/setup.ps1 b/scripts/setup.ps1
new file mode 100644
index 0000000..1e7793b
--- /dev/null
+++ b/scripts/setup.ps1
@@ -0,0 +1,111 @@
+[CmdletBinding()]
+param()
+
+$ErrorActionPreference = "Stop"
+$RootDir = Split-Path -Parent $PSScriptRoot
+$EnvPath = Join-Path $RootDir ".env"
+$EnvExamplePath = Join-Path $RootDir ".env.example"
+$SecretsPath = Join-Path $RootDir "firmware\sticks3\include\vibe_stick_secrets.h"
+$SecretsExamplePath = Join-Path $RootDir "firmware\sticks3\include\vibe_stick_secrets.example.h"
+$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+
+function Read-Text([string]$Path) {
+ return [IO.File]::ReadAllText($Path, [Text.Encoding]::UTF8)
+}
+
+function Write-Text([string]$Path, [string]$Text) {
+ [IO.File]::WriteAllText($Path, $Text, $script:Utf8NoBom)
+}
+
+function Get-EnvValue([string]$Name) {
+ $match = [regex]::Match((Read-Text $script:EnvPath), "(?m)^$([regex]::Escape($Name))=(.*)$")
+ if ($match.Success) { return $match.Groups[1].Value.Trim().Trim('"') }
+ return ""
+}
+
+function Get-SecretValue([string]$Name) {
+ $pattern = '(?m)^#define\s+' + [regex]::Escape($Name) + '\s+"([^"]*)"'
+ $match = [regex]::Match((Read-Text $script:SecretsPath), $pattern)
+ if ($match.Success) { return $match.Groups[1].Value }
+ return ""
+}
+
+function Set-EnvValue([string]$Name, [string]$Value) {
+ $text = Read-Text $script:EnvPath
+ $pattern = "(?m)^$([regex]::Escape($Name))=.*$"
+ $replacement = "$Name=$Value"
+ if ([regex]::IsMatch($text, $pattern)) {
+ $text = [regex]::Replace($text, $pattern, { param($match) $replacement }, 1)
+ } else {
+ $text = $text.TrimEnd() + [Environment]::NewLine + $replacement + [Environment]::NewLine
+ }
+ Write-Text $script:EnvPath $text
+}
+
+function Set-SecretValue([string]$Name, [string]$Value) {
+ $text = Read-Text $script:SecretsPath
+ $pattern = '(?m)^#define\s+' + [regex]::Escape($Name) + '\s+"[^"]*"'
+ $replacement = "#define $Name `"$Value`""
+ $text = [regex]::Replace($text, $pattern, { param($match) $replacement }, 1)
+ Write-Text $script:SecretsPath $text
+}
+
+function Test-Placeholder([string]$Value) {
+ return [string]::IsNullOrWhiteSpace($Value) -or $Value -in @(
+ "change-this-shared-token", "paste-generated-token-here", "changeme", "change-me", "your-token"
+ )
+}
+
+if (-not (Test-Path -LiteralPath $EnvPath)) {
+ Copy-Item -LiteralPath $EnvExamplePath -Destination $EnvPath
+ Write-Host "Created .env from .env.example."
+} else {
+ Write-Host "Kept existing .env."
+}
+
+if (-not (Test-Path -LiteralPath $SecretsPath)) {
+ Copy-Item -LiteralPath $SecretsExamplePath -Destination $SecretsPath
+ Write-Host "Created firmware secrets from the example."
+} else {
+ Write-Host "Kept existing firmware secrets."
+}
+
+$envToken = Get-EnvValue "VIBE_STICK_BRIDGE_TOKEN"
+$secretToken = Get-SecretValue "VIBE_STICK_BRIDGE_TOKEN"
+if (-not (Test-Placeholder $envToken)) {
+ if (Test-Placeholder $secretToken) {
+ Set-SecretValue "VIBE_STICK_BRIDGE_TOKEN" $envToken
+ Write-Host "Copied the .env bridge token into firmware secrets."
+ } elseif ($envToken -ne $secretToken) {
+ Write-Warning "The .env and firmware bridge tokens differ; existing values were preserved."
+ }
+} elseif (-not (Test-Placeholder $secretToken)) {
+ Set-EnvValue "VIBE_STICK_BRIDGE_TOKEN" $secretToken
+ Write-Host "Copied the firmware bridge token into .env."
+} else {
+ $bytes = New-Object byte[] 32
+ $rng = [Security.Cryptography.RandomNumberGenerator]::Create()
+ try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }
+ $token = -join ($bytes | ForEach-Object { $_.ToString("x2") })
+ Set-EnvValue "VIBE_STICK_BRIDGE_TOKEN" $token
+ Set-SecretValue "VIBE_STICK_BRIDGE_TOKEN" $token
+ Write-Host "Generated one shared bridge token."
+}
+
+$lanAddress = Get-NetIPConfiguration -ErrorAction SilentlyContinue |
+ Where-Object { $_.IPv4DefaultGateway -and $_.IPv4Address.IPAddress -notlike "169.254.*" } |
+ ForEach-Object { $_.IPv4Address.IPAddress } |
+ Select-Object -First 1
+$bridgeHost = Get-SecretValue "VIBE_STICK_BRIDGE_HOST"
+if ($lanAddress -and ($bridgeHost -in @("", "127.0.0.1", "0.0.0.0", "192.168.1.10", "192.168.0.10", "10.0.0.10", "YOUR_MAC_IP", "your-mac-ip"))) {
+ Set-SecretValue "VIBE_STICK_BRIDGE_HOST" $lanAddress
+ Write-Host "Set firmware bridge host to detected Windows LAN IP: $lanAddress"
+} elseif ($lanAddress) {
+ Write-Host "Kept firmware bridge host $bridgeHost (detected LAN IP: $lanAddress)."
+} else {
+ Write-Warning "Could not detect a LAN IPv4 address; set VIBE_STICK_BRIDGE_HOST manually."
+}
+
+Write-Host ""
+Write-Host "Next: edit Wi-Fi values in firmware\sticks3\include\vibe_stick_secrets.h and ASR values in .env."
+Write-Host "Run .\scripts\dev.ps1 to start the Windows bridge."
diff --git a/tests/test_audio_streaming.py b/tests/test_audio_streaming.py
new file mode 100644
index 0000000..a3463f1
--- /dev/null
+++ b/tests/test_audio_streaming.py
@@ -0,0 +1,209 @@
+import json
+import queue
+import threading
+import time
+import unittest
+from unittest import mock
+
+from vibe_stick.audio.streaming import (
+ FUNASR_AUDIO_CHUNK_BYTES,
+ FunASRStreamingSession,
+ join_calibrated_segment,
+ locked_live_input_target,
+ normalize_stream_text,
+ spoken_format_command,
+)
+from vibe_stick.paste.input_injector import PasteResult
+
+
+class _FakeInjector:
+ def __init__(self) -> None:
+ self.text = ""
+ self.target = 77
+
+ def capture_target(self) -> int:
+ return self.target
+
+ def edit(self, delete_count: int, text: str, *, target_window: int = 0) -> PasteResult:
+ if target_window != self.target:
+ return PasteResult(False, "wrong target")
+ if delete_count:
+ self.text = self.text[:-delete_count]
+ self.text += text
+ return PasteResult(True, "ok")
+
+
+class _FakeSocket:
+ def __init__(self) -> None:
+ self.sent: list[object] = []
+ self.messages: queue.Queue[object] = queue.Queue()
+ self.closed = threading.Event()
+
+ def send(self, payload: object) -> None:
+ self.sent.append(payload)
+ if payload == '{"is_speaking": false}':
+ self.messages.put(
+ json.dumps(
+ {"mode": "2pass-offline", "text": "linux GitHub", "is_final": True}
+ )
+ )
+
+ def recv(self, timeout: float | None = None): # noqa: ANN201
+ try:
+ payload = self.messages.get(timeout=timeout)
+ except queue.Empty as exc:
+ raise TimeoutError from exc
+ if payload is None:
+ raise RuntimeError("closed")
+ return payload
+
+ def close(self) -> None:
+ self.closed.set()
+ self.messages.put(None)
+
+
+class AudioStreamingTests(unittest.TestCase):
+ def test_pause_segments_gain_punctuation_without_duplicate_commas(self) -> None:
+ first = join_calibrated_segment("第一段完成", "第二段开始")
+ second = join_calibrated_segment(first + ",", ",第三段开始")
+
+ self.assertEqual(first, "第一段完成,第二段开始")
+ self.assertEqual(second, "第一段完成,第二段开始,第三段开始")
+
+ def test_spoken_decimal_time_error_is_repaired_in_recording_context(self) -> None:
+ text = normalize_stream_text(
+ "21:20:04长录音重放测试松开后退格数为零",
+ {},
+ sentence_final=True,
+ )
+
+ self.assertEqual(text, "21.24 秒长录音重放测试,松开后退格数为零。")
+
+ def test_real_sticks3_segments_restore_words_and_punctuation(self) -> None:
+ normalizations = {
+ "推格": "退格",
+ "分段小整": "分段校正",
+ "停顿,后端落": "停顿后的段落",
+ "后端落": "段落",
+ }
+ raw_segments = [
+ "21:20:04长录音重放测试松开后推格数为零",
+ ",两个问题都已修复并重新刷入",
+ ",改为稳定分段小整停顿,后端落会锁定",
+ ]
+
+ result = ""
+ for raw_segment in raw_segments:
+ segment = normalize_stream_text(raw_segment, normalizations)
+ result = join_calibrated_segment(result, segment)
+ result = normalize_stream_text(result, normalizations, sentence_final=True)
+
+ self.assertEqual(
+ result,
+ "21.24 秒长录音重放测试,松开后退格数为零,"
+ "两个问题都已修复并重新刷入,改为稳定分段校正,停顿后的段落会锁定。",
+ )
+
+ def test_real_long_sticks3_technical_errors_are_repaired_in_context(self) -> None:
+ raw = (
+ "已经修复并重新刷写s三,现在已经退出聆听界面,并恢复在线,"
+ "更音是按键松开事件,偶发丢失设备一直上传音频,现在加入了"
+ "按键松开,时间正常停止直接读取gpio状态作为兜底松开80ms后自动停止"
+ "设备录音最长44秒,防止无线卡住,greatwall断流4s动收回绘画"
+ "最长保护50秒事件队列满十记录错误并允许重试"
+ )
+
+ result = normalize_stream_text(raw, {}, sentence_final=True)
+
+ self.assertIn("S3", result)
+ self.assertIn("根因是按键松开事件", result)
+ self.assertIn("按键松开事件正常停止", result)
+ self.assertIn("GPIO状态", result)
+ self.assertIn("80 ms后自动停止", result)
+ self.assertIn("防止无限卡住", result)
+ self.assertIn("Gateway断流4 秒自动回收会话", result)
+ self.assertIn("事件队列满时记录错误", result)
+ self.assertTrue(result.endswith("。"))
+
+ def test_sentence_final_does_not_add_period_after_url(self) -> None:
+ self.assertEqual(
+ normalize_stream_text("请打开fuzhuniu.net", {}, sentence_final=True),
+ "请打开fuzhuniu.net",
+ )
+
+ def test_existing_period_is_removed_after_url(self) -> None:
+ self.assertEqual(
+ normalize_stream_text("请打开fuzhuniu.net。", {}, sentence_final=True),
+ "请打开fuzhuniu.net",
+ )
+
+ def test_standalone_spoken_newline_becomes_real_newline(self) -> None:
+ self.assertEqual(spoken_format_command("换行。"), "\n")
+ self.assertEqual(spoken_format_command("另起一行"), "\n")
+ self.assertIsNone(spoken_format_command("这里讨论换行规则"))
+
+ def test_technical_prose_gains_reliable_clause_boundaries(self) -> None:
+ self.assertEqual(
+ normalize_stream_text(
+ "不是发音问题已处理根据语音停顿自动补逗号结束时补句号修正退格",
+ {},
+ sentence_final=True,
+ ),
+ "不是发音问题。已处理根据语音停顿自动补逗号,结束时补句号。修正退格。",
+ )
+
+ def test_explicit_spoken_symbols_are_deterministic(self) -> None:
+ self.assertEqual(
+ normalize_stream_text("21冒号20冒号04长录音箭头21.24秒", {}, sentence_final=True),
+ "21.24 秒长录音→21.24秒。",
+ )
+
+ def test_final_calibration_cannot_rewrite_locked_prefix(self) -> None:
+ previous = "第一段已经校准。当前尾段有错字"
+ corrected = "第一段被离线模型改坏。当前尾段已修正"
+ locked_length = len("第一段已经校准。")
+
+ target = locked_live_input_target(previous, corrected, locked_length)
+
+ self.assertTrue(target.startswith("第一段已经校准。"))
+ self.assertNotIn("第一段被离线模型改坏", target)
+
+ def test_sticks3_audio_is_entered_before_stream_stop(self) -> None:
+ injector = _FakeInjector()
+ socket = _FakeSocket()
+ session = FunASRStreamingSession(injector) # type: ignore[arg-type]
+
+ with mock.patch(
+ "vibe_stick.audio.streaming._load_asr_config",
+ return_value={"provider": "local-funasr", "base_url": "ws://funasr.test:10095"},
+ ), mock.patch(
+ "vibe_stick.audio.streaming._load_funasr_hotwords",
+ return_value=({}, {"linux": "Linux"}),
+ ):
+ self.assertTrue(session.start("session-1", connector=lambda *_args, **_kwargs: socket))
+
+ loud_block = (2000).to_bytes(2, "little", signed=True) * (FUNASR_AUDIO_CHUNK_BYTES // 2)
+ self.assertTrue(session.append(loud_block * 4))
+ socket.messages.put(json.dumps({"mode": "2pass-online", "text": "linux"}))
+ socket.messages.put(
+ json.dumps(
+ {"mode": "2pass-offline", "text": "linux GitHub", "is_final": True}
+ )
+ )
+
+ deadline = time.monotonic() + 1.0
+ while injector.text != "Linux GitHub" and time.monotonic() < deadline:
+ time.sleep(0.01)
+ self.assertEqual(injector.text, "Linux GitHub")
+
+ result = session.stop()
+ self.assertTrue(result.success)
+ self.assertTrue(result.input_success)
+ self.assertEqual(result.text, "Linux GitHub。")
+ self.assertEqual(injector.text, "Linux GitHub。")
+ self.assertTrue(any(isinstance(payload, bytes) for payload in socket.sent))
+ self.assertEqual(socket.sent[-1], '{"is_speaking": false}')
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_audio_transcriber.py b/tests/test_audio_transcriber.py
index 8e7206e..fc8067c 100644
--- a/tests/test_audio_transcriber.py
+++ b/tests/test_audio_transcriber.py
@@ -1,6 +1,7 @@
import os
import tempfile
import unittest
+import wave
from pathlib import Path
from unittest import mock
@@ -22,6 +23,85 @@ def read(self) -> bytes:
class TranscriberConfigTests(unittest.TestCase):
+ def test_local_funasr_config_does_not_require_api_key(self) -> None:
+ with mock.patch.dict(
+ os.environ,
+ {
+ "VIBE_STICK_ASR_PROVIDER": "local-funasr",
+ "VIBE_STICK_ASR_BASE_URL": "ws://192.168.31.100:10095",
+ },
+ clear=True,
+ ):
+ config = transcriber._load_asr_config()
+
+ self.assertEqual(config["provider"], "local-funasr")
+ self.assertEqual(config["base_url"], "ws://192.168.31.100:10095")
+ self.assertEqual(config["api_key"], "")
+
+ def test_local_funasr_sends_pcm_and_uses_offline_result(self) -> None:
+ class FakeSocket:
+ def __init__(self) -> None:
+ self.sent: list[object] = []
+ self.messages = iter(
+ [
+ '{"mode":"2pass-online","text":"Linux"}',
+ '{"mode":"2pass-offline","text":"linux GitHub","is_final":true}',
+ ]
+ )
+
+ def __enter__(self): # noqa: ANN204
+ return self
+
+ def __exit__(self, *exc: object) -> bool:
+ return False
+
+ def send(self, payload: object) -> None:
+ self.sent.append(payload)
+
+ def recv(self, timeout=None): # noqa: ANN001, ANN201
+ return next(self.messages)
+
+ socket = FakeSocket()
+
+ def connector(uri, **kwargs): # noqa: ANN001, ANN201
+ self.assertEqual(uri, "ws://funasr.test:10095")
+ self.assertEqual(kwargs["subprotocols"], ["binary"])
+ return socket
+
+ with tempfile.TemporaryDirectory() as tmp:
+ audio = Path(tmp) / "sample.wav"
+ with wave.open(str(audio), "wb") as wav_file:
+ wav_file.setnchannels(1)
+ wav_file.setsampwidth(2)
+ wav_file.setframerate(16000)
+ wav_file.writeframes(b"\x01\x00" * 1920)
+
+ with mock.patch.object(
+ transcriber,
+ "_load_funasr_hotwords",
+ return_value=({"linux": 35}, {"linux": "Linux"}),
+ ):
+ with mock.patch.object(transcriber, "_funasr_chunk_delay_seconds", return_value=0):
+ result = transcriber._transcribe_funasr_once(
+ audio,
+ {
+ "provider": "local-funasr",
+ "base_url": "ws://funasr.test:10095",
+ "api_key": "",
+ "model": "paraformer-zh-streaming",
+ "language": "zh",
+ },
+ attempt=1,
+ connector=connector,
+ )
+
+ self.assertTrue(result.success)
+ self.assertEqual(result.source, "local-funasr")
+ self.assertEqual(result.text, "Linux GitHub")
+ self.assertIsInstance(socket.sent[0], str)
+ self.assertTrue(any(isinstance(item, bytes) for item in socket.sent))
+ self.assertEqual(socket.sent[-1], '{"is_speaking": false}')
+
def test_load_asr_config_reads_vibestick_asr_file(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
diff --git a/tests/test_config_paths.py b/tests/test_config_paths.py
new file mode 100644
index 0000000..29edcc1
--- /dev/null
+++ b/tests/test_config_paths.py
@@ -0,0 +1,25 @@
+import os
+import unittest
+from pathlib import Path
+from unittest import mock
+
+from vibe_stick.config import paths
+
+
+class AppSupportPathTests(unittest.TestCase):
+ def test_explicit_override_wins(self) -> None:
+ with mock.patch.dict(os.environ, {"VIBE_STICK_APP_SUPPORT_DIR": "custom-data"}, clear=False):
+ self.assertEqual(paths._app_support_dir(), Path("custom-data"))
+
+ def test_windows_uses_local_app_data(self) -> None:
+ env = {"LOCALAPPDATA": r"C:\Users\tester\AppData\Local"}
+ with mock.patch.dict(os.environ, env, clear=True):
+ with mock.patch.object(paths.platform, "system", return_value="Windows"):
+ self.assertEqual(
+ paths._app_support_dir(),
+ Path(r"C:\Users\tester\AppData\Local") / "VibeStick",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_desktop_hud.py b/tests/test_desktop_hud.py
index 5f1faef..c20ca24 100644
--- a/tests/test_desktop_hud.py
+++ b/tests/test_desktop_hud.py
@@ -22,7 +22,7 @@ def test_show_hud_writes_vibestick_state(self) -> None:
):
hud.show_hud("listening")
- primary = json.loads(primary_state.read_text())
+ primary = json.loads(primary_state.read_text(encoding="utf-8"))
self.assertEqual(primary["status"], "listening")
self.assertEqual(primary["text"], "正在聆听")
diff --git a/tests/test_input_injector.py b/tests/test_input_injector.py
new file mode 100644
index 0000000..b336edc
--- /dev/null
+++ b/tests/test_input_injector.py
@@ -0,0 +1,17 @@
+import ctypes
+import platform
+import unittest
+
+from vibe_stick.paste.input_injector import INPUT
+
+
+class InputInjectorTests(unittest.TestCase):
+ def test_input_structure_matches_windows_abi(self) -> None:
+ if platform.system() != "Windows":
+ self.skipTest("Win32 ABI assertion")
+ expected = 40 if ctypes.sizeof(ctypes.c_void_p) == 8 else 28
+ self.assertEqual(ctypes.sizeof(INPUT), expected)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_input_relay.py b/tests/test_input_relay.py
new file mode 100644
index 0000000..19e8daf
--- /dev/null
+++ b/tests/test_input_relay.py
@@ -0,0 +1,119 @@
+import json
+import socket
+import threading
+import time
+import unittest
+
+from websockets.sync.client import connect
+
+from vibe_stick.paste.input_relay import InputRelayHub, RelayPasteInjector
+
+
+def _free_port() -> int:
+ with socket.socket() as probe:
+ probe.bind(("127.0.0.1", 0))
+ return int(probe.getsockname()[1])
+
+
+class InputRelayTests(unittest.TestCase):
+ def test_authenticated_windows_client_can_report_provider_status(self) -> None:
+ port = _free_port()
+ hub = InputRelayHub("test-token")
+ received: list[dict] = []
+ reported = threading.Event()
+
+ def handler(payload: dict) -> None:
+ received.append(payload)
+ reported.set()
+
+ hub.set_provider_status_handler(handler)
+ hub.start("127.0.0.1", port)
+ deadline = time.monotonic() + 2
+ while True:
+ try:
+ connection = connect(f"ws://127.0.0.1:{port}", open_timeout=0.2)
+ break
+ except OSError:
+ if time.monotonic() >= deadline:
+ raise
+ time.sleep(0.02)
+ with connection:
+ connection.send(json.dumps({"type": "hello", "role": "windows", "token": "test-token"}))
+ self.assertTrue(json.loads(connection.recv())["ok"])
+ connection.send(
+ json.dumps(
+ {
+ "type": "provider_status",
+ "provider": "codex",
+ "status": "DONE",
+ "alert_type": "DONE",
+ "alert_event_id": "evt-1",
+ }
+ )
+ )
+ self.assertTrue(reported.wait(2))
+
+ self.assertEqual(received[0]["alert_event_id"], "evt-1")
+
+ def test_windows_client_acknowledges_session_edits(self) -> None:
+ port = _free_port()
+ hub = InputRelayHub("test-token")
+ hub.start("127.0.0.1", port)
+ received: list[dict] = []
+ ready = threading.Event()
+
+ def client() -> None:
+ deadline = time.monotonic() + 2
+ while True:
+ try:
+ connection = connect(f"ws://127.0.0.1:{port}", open_timeout=0.2)
+ break
+ except OSError:
+ if time.monotonic() >= deadline:
+ raise
+ time.sleep(0.02)
+ with connection:
+ connection.send(
+ json.dumps(
+ {"type": "hello", "role": "windows", "token": "test-token"}
+ )
+ )
+ hello = json.loads(connection.recv())
+ self.assertTrue(hello["ok"])
+ ready.set()
+ for raw in connection:
+ event = json.loads(raw)
+ received.append(event)
+ connection.send(
+ json.dumps(
+ {
+ "type": "ack",
+ "request_id": event["request_id"],
+ "success": True,
+ "message": "ok",
+ }
+ )
+ )
+ if event["type"] == "session_end":
+ return
+
+ worker = threading.Thread(target=client, daemon=True)
+ worker.start()
+ self.assertTrue(ready.wait(2))
+
+ injector = RelayPasteInjector(hub)
+ self.assertTrue(injector.begin_session("session-1").success)
+ self.assertTrue(injector.edit(0, "你好").success)
+ self.assertTrue(injector.edit(1, "们").success)
+ self.assertTrue(injector.press_enter().success)
+ self.assertTrue(injector.end_session().success)
+ worker.join(timeout=2)
+
+ self.assertEqual(
+ [event["type"] for event in received],
+ ["session_start", "edit", "edit", "press_enter", "session_end"],
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_protocol_state.py b/tests/test_protocol_state.py
index 033cfda..a381310 100644
--- a/tests/test_protocol_state.py
+++ b/tests/test_protocol_state.py
@@ -57,6 +57,34 @@ def test_generic_provider_block_serializes_status_string(self) -> None:
self.assertEqual(payload["provider"]["id"], "claude")
self.assertEqual(payload["provider"]["status"], "ERROR")
+ def test_provider_cache_serializes_both_accounts(self) -> None:
+ state = default_state()
+ state.providers = {
+ "codex": ProviderState(
+ id="codex",
+ display_name="Codex",
+ status=AgentStatus.IDLE,
+ account_name="52#",
+ quota_5h_remaining=100,
+ quota_7d_remaining=55,
+ ),
+ "claude": ProviderState(
+ id="claude",
+ display_name="Claude",
+ status=AgentStatus.OFFLINE,
+ account_name="29#",
+ quota_5h_remaining=100,
+ quota_7d_remaining=80,
+ ),
+ }
+
+ payload = state.to_jsonable()
+
+ self.assertEqual(payload["providers"]["codex"]["account_name"], "52#")
+ self.assertEqual(payload["providers"]["claude"]["status"], "OFFLINE")
+ restored = state_from_dict(payload)
+ self.assertEqual(restored.providers["claude"].quota_7d_remaining, 80)
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_server_providers.py b/tests/test_server_providers.py
index d7b12a5..af373b0 100644
--- a/tests/test_server_providers.py
+++ b/tests/test_server_providers.py
@@ -1,4 +1,5 @@
import os
+import threading
import unittest
from datetime import datetime, timezone
from unittest import mock
@@ -7,6 +8,7 @@
from vibe_stick.codex.quota import QuotaSnapshot
from vibe_stick.providers.base import ProviderObservation
from vibe_stick.server import app
+from vibe_stick.usage.sub2 import Sub2UsageSnapshot
class ServerProviderTests(unittest.TestCase):
@@ -91,6 +93,52 @@ def test_claude_usage_interval_has_minimum(self) -> None:
with mock.patch.dict(os.environ, {"VIBE_STICK_CLAUDE_USAGE_INTERVAL_SECONDS": "90"}):
self.assertEqual(app._claude_usage_interval_seconds(), 90)
+ def test_sub2_usage_interval_has_minimum(self) -> None:
+ with mock.patch.dict(os.environ, {}, clear=True):
+ self.assertEqual(app._sub2_usage_interval_seconds(), 60)
+ with mock.patch.dict(os.environ, {"VIBE_STICK_SUB2_USAGE_INTERVAL_SECONDS": "5"}):
+ self.assertEqual(app._sub2_usage_interval_seconds(), 30)
+
+ def test_sub2_snapshot_overrides_quota_and_falls_back_to_account_id(self) -> None:
+ store = app.BridgeStateStore.__new__(app.BridgeStateStore)
+ store._sub2_usage_interval = 60
+ store._sub2_usage_snapshots = {
+ "codex": Sub2UsageSnapshot("codex", 52, "", 100, 55, "12:34")
+ }
+ store._sub2_usage_snapshot_times = {"codex": app.time.monotonic()}
+ observation = self._obs("codex")
+
+ store._apply_sub2_usage(observation)
+
+ self.assertEqual(observation.account_name, "52#")
+ self.assertEqual(observation.quota_5h_remaining, 100)
+ self.assertEqual(observation.quota_7d_remaining, 55)
+ self.assertFalse(observation.quota_stale)
+
+ def test_windows_provider_status_overrides_unraid_local_observer(self) -> None:
+ store = app.BridgeStateStore.__new__(app.BridgeStateStore)
+ store._lock = threading.RLock()
+ store._remote_provider_observations = {}
+ store._remote_provider_seen_at = {}
+ store.update_provider_status(
+ {
+ "provider": "claude",
+ "status": "DONE",
+ "project": "demo",
+ "alert_type": "DONE",
+ "alert_event_id": "evt_claude_done_1",
+ "alert_message": "Claude task completed",
+ }
+ )
+
+ observation = store._remote_provider_observation(
+ "claude", self._obs("claude", online=False, status=AgentStatus.OFFLINE)
+ )
+
+ self.assertTrue(observation.online)
+ self.assertEqual(observation.status, AgentStatus.DONE)
+ self.assertEqual(observation.alert_event_id, "evt_claude_done_1")
+
def test_failed_claude_usage_refresh_keeps_cached_quota_stale(self) -> None:
store = app.BridgeStateStore.__new__(app.BridgeStateStore)
store._claude_quota = QuotaSnapshot(66, 96, "09:40", False)
diff --git a/tests/test_sub2_usage.py b/tests/test_sub2_usage.py
new file mode 100644
index 0000000..5403034
--- /dev/null
+++ b/tests/test_sub2_usage.py
@@ -0,0 +1,47 @@
+import unittest
+
+from vibe_stick.usage.sub2 import Sub2UsageError, parse_sub2_usage
+
+
+class Sub2UsageTests(unittest.TestCase):
+ def test_parse_reads_remaining_windows_and_account_name(self) -> None:
+ snapshot = parse_sub2_usage(
+ "codex",
+ 52,
+ {
+ "account_id": 52,
+ "account_name": "Codex Main",
+ "windows": {
+ "five_hour": {"remaining_percent": 83.6},
+ "seven_day": {"remaining_percent": 55},
+ },
+ },
+ )
+
+ self.assertEqual(snapshot.account_name, "Codex Main")
+ self.assertEqual(snapshot.quota_5h_remaining, 84)
+ self.assertEqual(snapshot.quota_7d_remaining, 55)
+
+ def test_parse_allows_current_response_without_account_name(self) -> None:
+ snapshot = parse_sub2_usage(
+ "claude",
+ 29,
+ {
+ "account_id": 29,
+ "windows": {
+ "five_hour": {"remaining_percent": 100},
+ "seven_day": {"remaining_percent": 80},
+ },
+ },
+ )
+
+ self.assertEqual(snapshot.account_name, "")
+ self.assertEqual(snapshot.account_id, 29)
+
+ def test_parse_rejects_missing_usage_window(self) -> None:
+ with self.assertRaises(Sub2UsageError):
+ parse_sub2_usage("codex", 52, {"windows": {}})
+
+
+if __name__ == "__main__":
+ unittest.main()