| New file |
| | |
| | | # SmartLearning æ¥å£ææ¡£
|
| | |
|
| | | > æå¡é»è®¤å°åï¼`http://{host}:{port}`ï¼é»è®¤ `http://0.0.0.0:9500`ï¼ |
| | | > 交äºå¼ API ææ¡£ï¼å¯å¨æå¡åè®¿é® `/docs` |
| | | > åè®®çæ¬ï¼HTTP API v1 / MQTT snlp/v1
|
| | |
|
| | | ---
|
| | |
|
| | | ## ç®å½
|
| | |
|
| | | - [ä¸ãéç¨è¯´æ](#ä¸éç¨è¯´æ)
|
| | | - [äºãHTTP æ¥å£](#äºhttp-æ¥å£)
|
| | | - [2.1 ç³»ç»](#21-ç³»ç»)
|
| | | - [2.2 éæèµæº](#22-éæèµæº)
|
| | | - [2.3 åºæ¯è¯å«](#23-åºæ¯è¯å«)
|
| | | - [2.4 å¯¹è¯æµ](#24-å¯¹è¯æµ)
|
| | | - [2.5 课ç¨ä¸ä»»å¡](#25-课ç¨ä¸ä»»å¡)
|
| | | - [2.6 设å¤åå§å](#26-设å¤åå§å)
|
| | | - [2.7 设å¤ç®¡ç](#27-设å¤ç®¡ç)
|
| | | - [ä¸ãMQTT æ¥å£](#ä¸mqtt-æ¥å£)
|
| | | - [3.1 Topic è§å](#31-topic-è§å)
|
| | | - [3.2 æ¶æ¯éç¨æ ¼å¼](#32-æ¶æ¯éç¨æ ¼å¼)
|
| | | - [3.3 ç»ç«¯ä¸æ¥ï¼rptï¼](#33-ç»ç«¯ä¸æ¥rpt)
|
| | | - [3.4 äºç«¯ä¸åï¼cmdï¼](#34-äºç«¯ä¸åcmd)
|
| | | - [3.5 æå¡ç«¯èªå¨ä¸åæµç¨](#35-æå¡ç«¯èªå¨ä¸åæµç¨)
|
| | | - [åãæ¥å£éæ¥è¡¨](#忥å£éæ¥è¡¨)
|
| | |
|
| | | ---
|
| | |
|
| | | ## ä¸ãéç¨è¯´æ
|
| | |
|
| | | ### 1.1 Base URL
|
| | |
|
| | | | ç¯å¢ | å°å |
|
| | | |------|------|
|
| | | | æ¬å°å¼å | `http://localhost:9500` |
|
| | | | ç产é¨ç½² | ç± `.env` ä¸ `BASE_URL` é
ç½®ï¼ç»ç«¯ä¸è½½èµæºé¡»ä½¿ç¨è¯¥å°å |
|
| | |
|
| | | ### 1.2 请æ±å¤´
|
| | |
|
| | | JSON æ¥å£ç»ä¸ä½¿ç¨ï¼
|
| | |
|
| | | ```
|
| | | Content-Type: application/json
|
| | | ```
|
| | |
|
| | | SSE å¯¹è¯æµæ¥å£è¿åï¼
|
| | |
|
| | | ```
|
| | | Content-Type: text/event-stream
|
| | | ```
|
| | |
|
| | | ### 1.3 é误ååº
|
| | |
|
| | | FastAPI æ åéè¯¯æ ¼å¼ï¼
|
| | |
|
| | | ```json
|
| | | {
|
| | | "detail": "Course not found: course99"
|
| | | }
|
| | | ```
|
| | |
|
| | | å¸¸è§ HTTP ç¶æç ï¼
|
| | |
|
| | | | ç¶æç | 说æ |
|
| | | |--------|------|
|
| | | | 200 | æå |
|
| | | | 400 | 请æ±åæ°é误 |
|
| | | | 404 | èµæºä¸åå¨ |
|
| | | | 422 | 请æ±ä½æ ¡éªå¤±è´¥ |
|
| | |
|
| | | ---
|
| | |
|
| | | ## äºãHTTP æ¥å£
|
| | |
|
| | | ### 2.1 ç³»ç»
|
| | |
|
| | | #### GET /health
|
| | |
|
| | | å¥åº·æ£æ¥ï¼æ¥çæå¡ä¸ MQTT è¿æ¥ç¶æã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/health
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "status": "ok",
|
| | | "mqtt_connected": true
|
| | | }
|
| | | ```
|
| | |
|
| | | | åæ®µ | ç±»å | 说æ |
|
| | | |------|------|------|
|
| | | | status | string | æå¡ç¶æï¼åºå® `"ok"` |
|
| | | | mqtt_connected | boolean | MQTT Broker æ¯å¦å·²è¿æ¥ |
|
| | |
|
| | | ---
|
| | |
|
| | | ### 2.2 éæèµæº
|
| | |
|
| | | #### GET /static/{path}
|
| | |
|
| | | æç®¡ `data/` ç®å½ä¸çè¯¾ç¨æä»¶ï¼ä¾ç»ç«¯ HTTP ä¸è½½ã
|
| | |
|
| | | **URL è§åï¼**
|
| | |
|
| | | ```
|
| | | {BASE_URL}/static/{ç¸å¯¹è·¯å¾}
|
| | | ```
|
| | |
|
| | | **示ä¾ï¼**
|
| | |
|
| | | | èµæº | URL |
|
| | | |------|-----|
|
| | | | 课ç¨ç´¢å¼ | `http://localhost:9500/static/courses/index.json` |
|
| | | | 课ç¨é
ç½® | `http://localhost:9500/static/courses/course01/course.json` |
|
| | | | 课ç¨ä»»å¡å
| `http://localhost:9500/static/courses/course01/courseTask.json` |
|
| | | | é³é¢ | `http://localhost:9500/static/courses/course01/chapter01/resources/001.mp3` |
|
| | | | å°é¢ | `http://localhost:9500/static/courses/course01/chapter01/resources/001.png` |
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -O http://localhost:9500/static/courses/course01/courseTask.json
|
| | | ```
|
| | |
|
| | | **ååºï¼** å¯¹åºæä»¶çäºè¿å¶/JSON å
容ã
|
| | |
|
| | | ---
|
| | |
|
| | | ### 2.3 åºæ¯è¯å«
|
| | |
|
| | | #### POST /api/v1/scene/analyze
|
| | |
|
| | | æ ¹æ®è®¾å¤ä¸ä¸æè¿è¡åºæ¯è¯å«ï¼è¿åé®åçç¥ä¸å»ºè®®æå¾ã
|
| | |
|
| | | **请æ±ä½ï¼**
|
| | |
|
| | | | åæ®µ | ç±»å | å¿
å¡« | 说æ |
|
| | | |------|------|------|------|
|
| | | | timestamp | int | æ¯ | Unix æ¶é´æ³ |
|
| | | | audio_blob | string | æ¯ | Base64 é³é¢æå ä½ç¬¦ |
|
| | | | device_id | string | æ¯ | è®¾å¤ ID |
|
| | | | context_hints | object | å¦ | åºæ¯æç¤ºï¼å¦ `{"scene":"dining","confidence":0.9}` |
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -X POST http://localhost:9500/api/v1/scene/analyze \
|
| | | -H "Content-Type: application/json" \
|
| | | -d '{
|
| | | "timestamp": 1718365200,
|
| | | "audio_blob": "",
|
| | | "device_id": "SN-88293021",
|
| | | "context_hints": {
|
| | | "scene": "dining",
|
| | | "confidence": 0.9
|
| | | }
|
| | | }'
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "scene": "ç¨é¤",
|
| | | "confidence": 0.9,
|
| | | "greeting_strategy": "contextual",
|
| | | "suggested_intent": "What are you having for dinner?"
|
| | | }
|
| | | ```
|
| | |
|
| | | | åæ®µ | ç±»å | 说æ |
|
| | | |------|------|------|
|
| | | | scene | string | åºæ¯åç§° |
|
| | | | confidence | float | 置信度 0~1 |
|
| | | | greeting_strategy | string | `"contextual"` åºæ¯é®å / `"generic"` éç¨é®åï¼ç½®ä¿¡åº¦ < 0.7ï¼ |
|
| | | | suggested_intent | string | 建议çå¼åºç½ææé® |
|
| | |
|
| | | **ä½ç½®ä¿¡åº¦ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "scene": "éå¤",
|
| | | "confidence": 0.62,
|
| | | "greeting_strategy": "generic",
|
| | | "suggested_intent": "Hello! How can I help you practice English today?"
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ### 2.4 å¯¹è¯æµ
|
| | |
|
| | | #### GET /api/v1/dialogue/stream
|
| | |
|
| | | SSEï¼Server-Sent Eventsï¼æµå¼è¿åæå¦å¯¹è¯å
容ã
|
| | |
|
| | | **Query åæ°ï¼**
|
| | |
|
| | | | åæ° | ç±»å | å¿
å¡« | é»è®¤å¼ | 说æ |
|
| | | |------|------|------|--------|------|
|
| | | | dialogue_id | string | å¦ | èªå¨çæ UUID | å¯¹è¯ ID |
|
| | | | user_text | string | å¦ | â | ç¨æ·å夿æ¬ï¼ASR ç»æï¼ |
|
| | | | scene_prompt | string | å¦ | â | åºæ¯å¼åºç½ï¼æ¥èª scene/analyzeï¼ |
|
| | | | difficulty | string | å¦ | A2 | CEFR é¾åº¦ç级 |
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | # ä»
å¼åºé®å
|
| | | curl -N "http://localhost:9500/api/v1/dialogue/stream?scene_prompt=What%20are%20you%20having%20for%20dinner?&difficulty=A2"
|
| | |
|
| | | # å«ç¨æ·åå¤çåé¦
|
| | | curl -N "http://localhost:9500/api/v1/dialogue/stream?user_text=Good%20morning.&difficulty=A2"
|
| | | ```
|
| | |
|
| | | **ååºæ ¼å¼ï¼SSEï¼ï¼**
|
| | |
|
| | | ```
|
| | | event: message
|
| | | data: {"dialogue_id":"a1b2c3d4-...","text":"What are you having for dinner?","audio_url":null,"intent_type":"greeting","difficulty_level":"A2"}
|
| | |
|
| | | event: message
|
| | | data: {"dialogue_id":"a1b2c3d4-...","text":"Great job! Your sentence looks correct.","audio_url":null,"intent_type":"praise","difficulty_level":"A2"}
|
| | |
|
| | | event: message
|
| | | data: {"dialogue_id":"a1b2c3d4-...","text":"What would you do differently next time?","audio_url":null,"intent_type":"question","difficulty_level":"A2"}
|
| | |
|
| | | event: done
|
| | | data: {"dialogue_id":"a1b2c3d4-..."}
|
| | | ```
|
| | |
|
| | | **message äºä»¶ data åæ®µï¼**
|
| | |
|
| | | | åæ®µ | ç±»å | 说æ |
|
| | | |------|------|------|
|
| | | | dialogue_id | string | å¯¹è¯ ID |
|
| | | | text | string | å¯¹è¯ææ¬ |
|
| | | | audio_url | string \| null | é³é¢ URLï¼å½å为 nullï¼ |
|
| | | | intent_type | string | `greeting` / `praise` / `correction` / `question` |
|
| | | | difficulty_level | string | é¾åº¦ç级 |
|
| | |
|
| | | ---
|
| | |
|
| | | ### 2.5 课ç¨ä¸ä»»å¡
|
| | |
|
| | | #### GET /api/v1/courses
|
| | |
|
| | | è·åå·²åå¸è¯¾ç¨ç´¢å¼ï¼å¯¹åº `data/courses/index.json`ï¼ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/courses
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "courses": [
|
| | | {
|
| | | "course_id": "course01",
|
| | | "config": "courses/course01/course.json"
|
| | | },
|
| | | {
|
| | | "course_id": "course02",
|
| | | "config": "courses/course02/course.json"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/courses/{course_id}
|
| | |
|
| | | è·ååé¨è¯¾ç¨è¯¦æ
ï¼ç« èä¸è§é¢å表ï¼ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/courses/course01
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "description": "课ç¨01",
|
| | | "chapters": [
|
| | | {
|
| | | "chapter_id": "chapter01",
|
| | | "chapter_name": "第ä¸ç« ",
|
| | | "videos": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "课ç¨001",
|
| | | "path": "courses/course01/chapter01/resources/001.mp3"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/courses/base-manifest
|
| | |
|
| | | é¢è§å
¨é¨åºç¡è¯¾ç¨åæ¥å
ï¼MQTT ä¸ååç宿´ payloadï¼`url` 已补å
¨ï¼ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/courses/base-manifest
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "scope": "base",
|
| | | "course_count": 2,
|
| | | "courses": [
|
| | | {
|
| | | "version": "1.0",
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å",
|
| | | "assets": [
|
| | | {
|
| | | "url": "http://localhost:9500/static/courses/course01/chapter01/resources/001.mp3",
|
| | | "md5": "...",
|
| | | "title": "第ä¸ç« 课ç¨001",
|
| | | "target_path": "courses/course01/chapter01/resources/001.mp3",
|
| | | "cover_url": "http://localhost:9500/static/courses/course01/chapter01/resources/001.png",
|
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/courses/{course_id}/manifest
|
| | |
|
| | | è·ååé¨è¯¾ç¨ç忥å
ï¼`url` / `cover_url` 已补å
¨ä¸ºå®æ´ HTTP å°åï¼ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/courses/course01/manifest
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "version": "1.0",
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å",
|
| | | "assets": [
|
| | | {
|
| | | "url": "http://localhost:9500/static/courses/course01/chapter01/resources/001.mp3",
|
| | | "md5": "...",
|
| | | "title": "第ä¸ç« 课ç¨001",
|
| | | "target_path": "courses/course01/chapter01/resources/001.mp3",
|
| | | "cover_url": "http://localhost:9500/static/courses/course01/chapter01/resources/001.png",
|
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/courses/{course_id}/tasks
|
| | |
|
| | | è·ååå§ `courseTask.json`ï¼æ¬å°æä»¶å
容ï¼`url` 为ç¸å¯¹è·¯å¾ï¼ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/courses/course01/tasks
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "version": "1.0",
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å",
|
| | | "assets": [
|
| | | {
|
| | | "url": "courses/course01/chapter01/resources/001.mp3",
|
| | | "md5": "...",
|
| | | "title": "第ä¸ç« 课ç¨001",
|
| | | "target_path": "courses/course01/chapter01/resources/001.mp3",
|
| | | "cover_url": "courses/course01/chapter01/resources/001.png",
|
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/tasklist
|
| | |
|
| | | è·å设å¤å¼æºåå§å任塿¸
åï¼`data/tasklist.json`ï¼ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/tasklist
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "version": "1.0",
|
| | | "description": "ç»ç«¯è®¾å¤åå§å任塿¸
åï¼å¼æºåç±æå¡ç«¯ä¸åæ§è¡",
|
| | | "tasks": [
|
| | | {
|
| | | "task_id": "init_001",
|
| | | "name": "åæ¥ç³»ç»æ¶é",
|
| | | "action": "SET_CLOCK",
|
| | | "channel": "ctl",
|
| | | "ttl": 60,
|
| | | "payload": { "ts": 0 }
|
| | | },
|
| | | {
|
| | | "task_id": "init_002",
|
| | | "name": "设置é»è®¤é³é",
|
| | | "action": "SET_VOLUME",
|
| | | "channel": "ctl",
|
| | | "ttl": 60,
|
| | | "payload": { "level": 50 }
|
| | | },
|
| | | {
|
| | | "task_id": "init_003",
|
| | | "name": "å¤é设å¤",
|
| | | "action": "SET_POWER",
|
| | | "channel": "ctl",
|
| | | "ttl": 60,
|
| | | "payload": { "state": "wake" }
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ### 2.6 设å¤åå§å
|
| | |
|
| | | #### POST /api/v1/devices/init
|
| | |
|
| | | æå¨è§¦å设å¤å®æ´åå§åï¼åºç¡ ctl æä»¤ + å
¨é¨åºç¡è¯¾ç¨ SYNC_COURSE_TASKï¼ã
|
| | |
|
| | | **请æ±ä½ï¼**
|
| | |
|
| | | | åæ®µ | ç±»å | å¿
å¡« | é»è®¤å¼ | 说æ |
|
| | | |------|------|------|--------|------|
|
| | | | device_id | string | æ¯ | â | è®¾å¤ ID |
|
| | | | force | boolean | å¦ | false | 强å¶éæ°åå§åï¼å¿½ç¥å·²åå§åè®°å½ï¼ |
|
| | | | course_id | string | å¦ | â | æ¬æ¥å£ä¸ä½¿ç¨ |
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -X POST http://localhost:9500/api/v1/devices/init \
|
| | | -H "Content-Type: application/json" \
|
| | | -d '{
|
| | | "device_id": "SN-88293021",
|
| | | "force": true
|
| | | }'
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼æåä¸åï¼ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "commands": [
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "8f932149-719c-4464-99be-66dfcfa14f29",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 60
|
| | | },
|
| | | "action": "SET_CLOCK",
|
| | | "payload": { "ts": 1718365200 }
|
| | | },
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "e1c1de2a-3067-417d-b533-67c9a9827a8e",
|
| | | "timestamp": 1718365201,
|
| | | "ttl": 7200
|
| | | },
|
| | | "action": "SYNC_COURSE_TASK",
|
| | | "payload": {
|
| | | "version": "1.0",
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "assets": []
|
| | | }
|
| | | }
|
| | | ],
|
| | | "status": "dispatched"
|
| | | }
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼å·²åå§å䏿ª forceï¼ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "commands": [],
|
| | | "status": "already_initialized"
|
| | | }
|
| | | ```
|
| | |
|
| | | | status å¼ | 说æ |
|
| | | |-----------|------|
|
| | | | dispatched | æä»¤å·²ä¸å |
|
| | | | already_initialized | 设å¤å·²å¨åå§åè®°å½ä¸ï¼è·³è¿ |
|
| | |
|
| | | ---
|
| | |
|
| | | #### POST /api/v1/devices/course-init
|
| | |
|
| | | æå¨è§¦ååé¨è¯¾ç¨èµæºåæ¥ï¼ä¸å 1 æ¡ `SYNC_COURSE_TASK`ï¼ã
|
| | |
|
| | | **请æ±ä½ï¼**
|
| | |
|
| | | | åæ®µ | ç±»å | å¿
å¡« | 说æ |
|
| | | |------|------|------|------|
|
| | | | device_id | string | æ¯ | è®¾å¤ ID |
|
| | | | course_id | string | æ¯ | è¯¾ç¨ IDï¼å¦ `course01` |
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -X POST http://localhost:9500/api/v1/devices/course-init \
|
| | | -H "Content-Type: application/json" \
|
| | | -d '{
|
| | | "device_id": "SN-88293021",
|
| | | "course_id": "course01"
|
| | | }'
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "course_id": "course01",
|
| | | "commands": [
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
| | | "timestamp": 1718365300,
|
| | | "ttl": 7200
|
| | | },
|
| | | "action": "SYNC_COURSE_TASK",
|
| | | "payload": {
|
| | | "version": "1.0",
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å",
|
| | | "assets": [
|
| | | {
|
| | | "url": "http://localhost:9500/static/courses/course01/chapter01/resources/001.mp3",
|
| | | "md5": "...",
|
| | | "title": "第ä¸ç« 课ç¨001",
|
| | | "target_path": "courses/course01/chapter01/resources/001.mp3",
|
| | | "cover_url": "http://localhost:9500/static/courses/course01/chapter01/resources/001.png",
|
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png"
|
| | | }
|
| | | ]
|
| | | }
|
| | | }
|
| | | ],
|
| | | "status": "dispatched"
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ### 2.7 设å¤ç®¡ç
|
| | |
|
| | | #### GET /api/v1/devices
|
| | |
|
| | | è·åå½åå¨çº¿è®¾å¤åè¡¨ï¼æ¥èª MQTT `rpt/state` 䏿¥ï¼ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/devices
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "devices": [
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "cpu": 15.0,
|
| | | "mem": 45.0,
|
| | | "battery": 80.0,
|
| | | "net": "wifi",
|
| | | "mode": "idle",
|
| | | "last_seen": 1718365205
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/devices/initialized
|
| | |
|
| | | è·å已宿åå§åç设å¤è®°å½ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/devices/initialized
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "devices": {
|
| | | "SN-88293021": {
|
| | | "initialized_at": 1718365200,
|
| | | "source": "auto",
|
| | | "mode": "idle",
|
| | | "courses_synced": true,
|
| | | "courses_sync_at": 1718365201,
|
| | | "courses_sync_count": 2
|
| | | }
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/devices/{device_id}
|
| | |
|
| | | è·ååä¸ªè®¾å¤ææ°ç¶æã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/devices/SN-88293021
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "cpu": 15.0,
|
| | | "mem": 45.0,
|
| | | "battery": 80.0,
|
| | | "net": "wifi",
|
| | | "mode": "idle",
|
| | | "last_seen": 1718365205
|
| | | }
|
| | | ```
|
| | |
|
| | | **404 ååºï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "detail": "Device not found"
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/devices/{device_id}/init-status
|
| | |
|
| | | æ¥è¯¢è®¾å¤åå§åç¶æã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/devices/SN-88293021/init-status
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼å·²åå§åï¼ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "initialized": true,
|
| | | "record": {
|
| | | "initialized_at": 1718365200,
|
| | | "source": "auto",
|
| | | "mode": "idle",
|
| | | "courses_synced": true,
|
| | | "courses_sync_at": 1718365201,
|
| | | "courses_sync_count": 2
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼æªåå§åï¼ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "initialized": false,
|
| | | "record": null
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### DELETE /api/v1/devices/initialized/{device_id}
|
| | |
|
| | | æ¸
é¤è®¾å¤åå§åè®°å½ï¼ä¸æ¬¡ä¸æ¥ `rpt/state` æ¶å°éæ°è§¦åèªå¨åå§åã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -X DELETE http://localhost:9500/api/v1/devices/initialized/SN-88293021
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "status": "removed"
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/devices/{device_id}/acks
|
| | |
|
| | | è·åè®¾å¤æä»¤åæ§åå²ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/devices/SN-88293021/acks
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "acks": [
|
| | | {
|
| | | "req_id": "8f932149-719c-4464-99be-66dfcfa14f29",
|
| | | "status": "success",
|
| | | "error_code": 0
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### GET /api/v1/devices/{device_id}/events
|
| | |
|
| | | è·å设å¤äºä»¶ä¸è¿åº¦ä¸æ¥åå²ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/devices/SN-88293021/events
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "events": [
|
| | | {
|
| | | "event": "asr_result",
|
| | | "text": "Good morning",
|
| | | "confidence": 0.95,
|
| | | "_type": "event"
|
| | | },
|
| | | {
|
| | | "task_id": "uuid",
|
| | | "status": "downloading",
|
| | | "progress": 45,
|
| | | "info": "Downloading assets...",
|
| | | "_type": "progress"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### POST /api/v1/devices/{device_id}/cmd
|
| | |
|
| | | æå¨å设å¤ä¸å宿¶æ§å¶æä»¤ï¼MQTT `cmd/ctl`ï¼ã
|
| | |
|
| | | **请æ±ä½ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 60
|
| | | },
|
| | | "action": "SET_VOLUME",
|
| | | "payload": { "level": 80 }
|
| | | }
|
| | | ```
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -X POST http://localhost:9500/api/v1/devices/SN-88293021/cmd \
|
| | | -H "Content-Type: application/json" \
|
| | | -d '{
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 60
|
| | | },
|
| | | "action": "SET_VOLUME",
|
| | | "payload": { "level": 80 }
|
| | | }'
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼è¿åå®é
ä¸åç宿´æä»¤ï¼ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 60
|
| | | },
|
| | | "action": "SET_VOLUME",
|
| | | "payload": { "level": 80 }
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### POST /api/v1/devices/{device_id}/task
|
| | |
|
| | | æå¨å设å¤ä¸å弿¥ä»»å¡æä»¤ï¼MQTT `cmd/task`ï¼ã
|
| | |
|
| | | **请æ±ç¤ºä¾ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -X POST http://localhost:9500/api/v1/devices/SN-88293021/task \
|
| | | -H "Content-Type: application/json" \
|
| | | -d '{
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 7200
|
| | | },
|
| | | "action": "SYNC_COURSE_TASK",
|
| | | "payload": {
|
| | | "version": "1.0",
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "assets": []
|
| | | }
|
| | | }'
|
| | | ```
|
| | |
|
| | | **ååºç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "550e8400-e29b-41d4-a716-446655440000",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 7200
|
| | | },
|
| | | "action": "SYNC_COURSE_TASK",
|
| | | "payload": {
|
| | | "version": "1.0",
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "assets": []
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ## ä¸ãMQTT æ¥å£
|
| | |
|
| | | ### 3.1 Topic è§å
|
| | |
|
| | | ```
|
| | | snlp/v1/{device_id}/{direct}/{msg_type}
|
| | | ```
|
| | |
|
| | | | æ¹å | Topic | QoS | 说æ |
|
| | | |------|-------|-----|------|
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/state` | 0 | å¿è·³/ç¶æä¸æ¥ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/event` | 0 | äºä»¶ä¸æ¥ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/ack` | 1 | æä»¤æ§è¡åæ§ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/progress` | 0 | 弿¥ä»»å¡è¿åº¦ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/log` | 0 | ç³»ç»æ¥å¿ |
|
| | | | äº â ç»ç«¯ | `snlp/v1/{device_id}/cmd/ctl` | 1 | 宿¶æ§å¶æä»¤ |
|
| | | | äº â ç»ç«¯ | `snlp/v1/{device_id}/cmd/task` | 1 | 弿¥ä»»å¡æä»¤ |
|
| | |
|
| | | **åæ°è¯´æï¼**
|
| | |
|
| | | | åæ° | 说æ |
|
| | | |------|------|
|
| | | | v1 | åè®®çæ¬å· |
|
| | | | device_id | 设å¤å¯ä¸ IDï¼ä¸å¯ä¿®æ¹ |
|
| | | | direct | `rpt`ï¼ç»ç«¯ä¸æ¥ï¼/ `cmd`ï¼äºç«¯ä¸åï¼ |
|
| | | | msg_type | `state` / `event` / `ack` / `progress` / `log` / `ctl` / `task` |
|
| | |
|
| | | ---
|
| | |
|
| | | ### 3.2 æ¶æ¯éç¨æ ¼å¼
|
| | |
|
| | | #### äºç«¯ä¸åï¼cmd/ctlãcmd/taskï¼
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "550e8400-e29b-41d4-a716-446655440000",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 3600
|
| | | },
|
| | | "action": "ACTION_TYPE",
|
| | | "payload": {}
|
| | | }
|
| | | ```
|
| | |
|
| | | | header åæ®µ | ç±»å | 说æ |
|
| | | |-------------|------|------|
|
| | | | version | string | åè®®è¯ä¹çæ¬ï¼åºå® `"1.0"` |
|
| | | | device_id | string | ç®æ è®¾å¤ ID |
|
| | | | req_id | string | æä»¤å¯ä¸ IDï¼UUIDï¼ï¼ç»ç«¯ ack æ¶é¡»åä¼ |
|
| | | | timestamp | int | Unix æ¶é´æ³ |
|
| | | | ttl | int | æä»¤æææï¼ç§ï¼ |
|
| | |
|
| | | #### ç»ç«¯ä¸æ¥ï¼rpt/*ï¼
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365205
|
| | | },
|
| | | "msg_type": "state",
|
| | | "payload": {}
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ### 3.3 ç»ç«¯ä¸æ¥ï¼rptï¼
|
| | |
|
| | | #### rpt/state â å¿è·³/ç¶æ
|
| | |
|
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/state`
|
| | |
|
| | | **åå¸ç¤ºä¾ï¼ç»ç«¯ â äºï¼ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365205
|
| | | },
|
| | | "msg_type": "state",
|
| | | "payload": {
|
| | | "cpu": 15,
|
| | | "mem": 45,
|
| | | "battery": 80,
|
| | | "net": "wifi",
|
| | | "mode": "idle"
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | **æå¡ç«¯è¡ä¸ºï¼**
|
| | |
|
| | | - æ´æ°è®¾å¤å¨çº¿ç¶æ
|
| | | - è¥è®¾å¤æªåå§åä¸ MQTT å·²è¿æ¥ â èªå¨è§¦å宿´åå§åæµç¨
|
| | |
|
| | | **mode 常è§å¼ï¼** `idle` / `dialogue` / `translate` / `study` / `sleep`
|
| | |
|
| | | ---
|
| | |
|
| | | #### rpt/event â äºä»¶ä¸æ¥
|
| | |
|
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/event`
|
| | |
|
| | | **åå¸ç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365210
|
| | | },
|
| | | "msg_type": "event",
|
| | | "payload": {
|
| | | "event": "asr_result",
|
| | | "text": "Good morning",
|
| | | "confidence": 0.95
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | **å¸¸è§ event ç±»åï¼**
|
| | |
|
| | | | event | 说æ |
|
| | | |-------|------|
|
| | | | wake | å¤é触å |
|
| | | | sleep | ä¼ç 触å |
|
| | | | asr_result | ASR è¯å«ç»æ |
|
| | | | alarm | å¼å¸¸æ¥è¦ |
|
| | |
|
| | | ---
|
| | |
|
| | | #### rpt/ack â æä»¤åæ§
|
| | |
|
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/ack`
|
| | |
|
| | | **åå¸ç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365215
|
| | | },
|
| | | "msg_type": "ack",
|
| | | "payload": {
|
| | | "req_id": "550e8400-e29b-41d4-a716-446655440000",
|
| | | "status": "success",
|
| | | "error_code": 0
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | | payload åæ®µ | 说æ |
|
| | | |--------------|------|
|
| | | | req_id | 对åºä¸åæä»¤ç req_id |
|
| | | | status | `success` / `failed` |
|
| | | | error_code | 0 表示æåï¼é 0 为é误ç |
|
| | |
|
| | | ---
|
| | |
|
| | | #### rpt/progress â ä»»å¡è¿åº¦
|
| | |
|
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/progress`
|
| | |
|
| | | **åå¸ç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365220
|
| | | },
|
| | | "msg_type": "progress",
|
| | | "payload": {
|
| | | "task_id": "550e8400-e29b-41d4-a716-446655440000",
|
| | | "status": "downloading",
|
| | | "progress": 45,
|
| | | "info": "Downloading courses/course01/chapter01/resources/001.mp3"
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### rpt/log â ç³»ç»æ¥å¿
|
| | |
|
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/log`
|
| | |
|
| | | **åå¸ç¤ºä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365230
|
| | | },
|
| | | "msg_type": "log",
|
| | | "payload": {
|
| | | "level": "error",
|
| | | "module": "audio",
|
| | | "message": "Microphone init failed"
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ### 3.4 äºç«¯ä¸åï¼cmdï¼
|
| | |
|
| | | #### cmd/ctl â 宿¶æ§å¶æä»¤
|
| | |
|
| | | **Topicï¼** `snlp/v1/{device_id}/cmd/ctl`
|
| | |
|
| | | **Action å表ï¼**
|
| | |
|
| | | | Action | payload ç¤ºä¾ | 说æ |
|
| | | |--------|-------------|------|
|
| | | | SET_VOLUME | `{"level": 50}` | 设置é³é 0â100 |
|
| | | | SET_POWER | `{"state": "wake"}` | å¤é / `sleep` ä¼ç |
|
| | | | SET_CLOCK | `{"ts": 1718365200}` | 忥 Unix æ¶é´æ³ |
|
| | | | PLAY_MEDIA | `{"path": "courses/course01/chapter01/resources/001.mp3", "volume": 50, "loop": false}` | ææ¾æ¬å°åªä½ |
|
| | | | STOP_MEDIA | `{}` | åæ¢ææ¾ |
|
| | | | SET_ALARM | `{"id": "alarm_01", "time": "07:00", "repeat": [1,2,3,4,5], "action": "add"}` | 设置é¹é |
|
| | | | UNSET_ALARM | `{"id": "alarm_01"}` | å é¤é¹é |
|
| | | | REBOOT | `{"delay": 5}` | å»¶æ¶éå¯ |
|
| | |
|
| | | **SET_CLOCK ä¸å示ä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "8f932149-719c-4464-99be-66dfcfa14f29",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 60
|
| | | },
|
| | | "action": "SET_CLOCK",
|
| | | "payload": {
|
| | | "ts": 1718365200
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | **SET_VOLUME ä¸å示ä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "34dc4157-9e52-4a99-bdad-b3daf665df11",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 60
|
| | | },
|
| | | "action": "SET_VOLUME",
|
| | | "payload": {
|
| | | "level": 50
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | **SET_POWER ä¸å示ä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 60
|
| | | },
|
| | | "action": "SET_POWER",
|
| | | "payload": {
|
| | | "state": "wake"
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | #### cmd/task â 弿¥ä»»å¡æä»¤
|
| | |
|
| | | **Topicï¼** `snlp/v1/{device_id}/cmd/task`
|
| | |
|
| | | **å½åå®ç°ä½¿ç¨ç Actionï¼**
|
| | |
|
| | | | Action | 说æ |
|
| | | |--------|------|
|
| | | | SYNC_COURSE_TASK | ä¸å宿´è¯¾ç¨èµæºå
ï¼courseTask.json æ ¼å¼ï¼ï¼ç»ç«¯æ assets æ¹éä¸è½½ |
|
| | |
|
| | | **SYNC_COURSE_TASK ä¸å示ä¾ï¼**
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "e1c1de2a-3067-417d-b533-67c9a9827a8e",
|
| | | "timestamp": 1718365201,
|
| | | "ttl": 7200
|
| | | },
|
| | | "action": "SYNC_COURSE_TASK",
|
| | | "payload": {
|
| | | "version": "1.0",
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å",
|
| | | "assets": [
|
| | | {
|
| | | "url": "http://your-server:9500/static/courses/course01/chapter01/resources/001.mp3",
|
| | | "md5": "...",
|
| | | "title": "第ä¸ç« 课ç¨001",
|
| | | "target_path": "courses/course01/chapter01/resources/001.mp3",
|
| | | "cover_url": "http://your-server:9500/static/courses/course01/chapter01/resources/001.png",
|
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png"
|
| | | },
|
| | | {
|
| | | "url": "http://your-server:9500/static/courses/course01/chapter01/resources/002.mp3",
|
| | | "md5": "...",
|
| | | "title": "第ä¸ç« 课ç¨002",
|
| | | "target_path": "courses/course01/chapter01/resources/002.mp3",
|
| | | "cover_url": "http://your-server:9500/static/courses/course01/chapter01/resources/002.png",
|
| | | "cover_target_path": "courses/course01/chapter01/resources/002.png"
|
| | | }
|
| | | ]
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | **assets åæ®µè¯´æï¼**
|
| | |
|
| | | | åæ®µ | 说æ |
|
| | | |------|------|
|
| | | | url | èµæºå®æ´ HTTP ä¸è½½å°åï¼æå¡ç«¯èªå¨è¡¥å
¨ï¼ |
|
| | | | md5 | æä»¶æ ¡éªå¼ |
|
| | | | title | èµæºæ é¢ |
|
| | | | target_path | ç»ç«¯åå¨ç¸å¯¹è·¯å¾ï¼ä¸æå¡ç«¯ç®å½ç»æä¸è´ï¼ |
|
| | | | cover_url | å°é¢å®æ´ HTTP ä¸è½½å°å |
|
| | | | cover_target_path | å°é¢ç»ç«¯åå¨ç¸å¯¹è·¯å¾ |
|
| | |
|
| | | **ç»ç«¯å¤çé»è¾ï¼**
|
| | |
|
| | | 1. æ¶å° `SYNC_COURSE_TASK` åè§£æ `assets` å表
|
| | | 2. ææ¯é¡¹ç `url` ä¸è½½æä»¶ï¼ä¿åå°åå¨å¡ `target_path` è·¯å¾
|
| | | 3. æ `cover_url` ä¸è½½å°é¢ï¼ä¿åå° `cover_target_path`
|
| | | 4. ä¸è½½è¿ç¨ä¸éè¿ `rpt/progress` 䏿¥è¿åº¦
|
| | | 5. 宿åéè¿ `rpt/ack` åä¼ `req_id` 䏿§è¡ç»æ
|
| | |
|
| | | **PRD é¢ç Actionï¼å¯éè¿ HTTP `/devices/{id}/task` æå¨ä¸åï¼ï¼**
|
| | |
|
| | | | Action | payload ç¤ºä¾ | 说æ |
|
| | | |--------|-------------|------|
|
| | | | DOWNLOAD | `{"task_id":"uuid","url":"...","md5":"...","target_path":"..."}` | åæä»¶ä¸è½½ |
|
| | | | UPDATE_MANIFEST | `{"course_id":"...","assets":[...]}` | æ´æ°èµæºæ¸
å |
|
| | | | OTA_UPDATE | `{"url":"...","version":"1.0.2"}` | åºä»¶å级 |
|
| | | | DELETE_FILE | `{"path":"..."}` | å 餿件 |
|
| | |
|
| | | ---
|
| | |
|
| | | ### 3.5 æå¡ç«¯èªå¨ä¸åæµç¨
|
| | |
|
| | | 设å¤é¦æ¬¡ä¸æ¥ `rpt/state` æ¶ï¼æå¡ç«¯èªå¨æ§è¡ï¼
|
| | |
|
| | | ```
|
| | | 1. cmd/ctl SET_CLOCK {"ts": <å½åæ¶é´æ³>}
|
| | | 2. cmd/ctl SET_VOLUME {"level": 50}
|
| | | 3. cmd/ctl SET_POWER {"state": "wake"}
|
| | | 4. cmd/task SYNC_COURSE_TASK {course01 宿´ payload}
|
| | | 5. cmd/task SYNC_COURSE_TASK {course02 宿´ payload}
|
| | | ... index.json 䏿¯é¨åºç¡è¯¾ç¨å 1 æ¡
|
| | | ```
|
| | |
|
| | | **æå¨ MQTT åå¸ç¤ºä¾ï¼mosquitto_pubï¼ï¼**
|
| | |
|
| | | ```bash
|
| | | mosquitto_pub \
|
| | | -h your-mqtt-broker -p 2100 \
|
| | | -u your-username -P your-password \
|
| | | -t "snlp/v1/SN-88293021/cmd/task" \
|
| | | -q 1 \
|
| | | -m '{"header":{"version":"1.0","device_id":"SN-88293021","req_id":"manual-uuid","timestamp":1718365200,"ttl":7200},"action":"SYNC_COURSE_TASK","payload":{"version":"1.0","course_id":"course01","course_name":"å°æºå¦ä¹ 课ç¨01","assets":[]}}'
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ## åãæ¥å£éæ¥è¡¨
|
| | |
|
| | | ### HTTP æ¥å£
|
| | |
|
| | | | æ¹æ³ | è·¯å¾ | 说æ |
|
| | | |------|------|------|
|
| | | | GET | `/health` | å¥åº·æ£æ¥ |
|
| | | | GET | `/static/{path}` | éæèµæºä¸è½½ |
|
| | | | POST | `/api/v1/scene/analyze` | åºæ¯è¯å« |
|
| | | | GET | `/api/v1/dialogue/stream` | SSE å¯¹è¯æµ |
|
| | | | GET | `/api/v1/courses` | 课ç¨ç´¢å¼ |
|
| | | | GET | `/api/v1/courses/base-manifest` | å
¨é¨åºç¡è¯¾ç¨åæ¥å
é¢è§ |
|
| | | | GET | `/api/v1/courses/{course_id}` | 课ç¨è¯¦æ
|
|
| | | | GET | `/api/v1/courses/{course_id}/manifest` | å课ç¨åæ¥å
é¢è§ |
|
| | | | GET | `/api/v1/courses/{course_id}/tasks` | åå§ courseTask.json |
|
| | | | GET | `/api/v1/tasklist` | 设å¤åå§å任塿¸
å |
|
| | | | POST | `/api/v1/devices/init` | æå¨è®¾å¤åå§å |
|
| | | | POST | `/api/v1/devices/course-init` | æå¨å课ç¨åæ¥ |
|
| | | | GET | `/api/v1/devices` | å¨çº¿è®¾å¤å表 |
|
| | | | GET | `/api/v1/devices/initialized` | å·²åå§åè®¾å¤ |
|
| | | | DELETE | `/api/v1/devices/initialized/{device_id}` | æ¸
é¤åå§åè®°å½ |
|
| | | | GET | `/api/v1/devices/{device_id}` | 设å¤ç¶æ |
|
| | | | GET | `/api/v1/devices/{device_id}/init-status` | åå§åç¶æ |
|
| | | | GET | `/api/v1/devices/{device_id}/acks` | æä»¤åæ§åå² |
|
| | | | GET | `/api/v1/devices/{device_id}/events` | äºä»¶/è¿åº¦åå² |
|
| | | | POST | `/api/v1/devices/{device_id}/cmd` | æå¨ä¸å ctl æä»¤ |
|
| | | | POST | `/api/v1/devices/{device_id}/task` | æå¨ä¸å task æä»¤ |
|
| | |
|
| | | ### MQTT æ¥å£
|
| | |
|
| | | | æ¹å | Topic | å
³é® Action / ç¨é |
|
| | | |------|-------|-------------------|
|
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/state` | å¿è·³ï¼è§¦åèªå¨åå§å |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/event` | ASR ç»æãå¤é/ä¼ç äºä»¶ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/ack` | æä»¤æ§è¡åæ§ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/progress` | ä¸è½½/OTA è¿åº¦ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/log` | è¿ç¨æéæ¥å¿ |
|
| | | | äº â ç»ç«¯ | `snlp/v1/{id}/cmd/ctl` | SET_CLOCK / SET_VOLUME / SET_POWER / PLAY_MEDIA ç |
|
| | | | äº â ç»ç«¯ | `snlp/v1/{id}/cmd/task` | SYNC_COURSE_TASKï¼è¯¾ç¨èµæºå
åæ¥ï¼ |
|
| | |
|
| | | ---
|
| | |
|
| | | *ææ¡£çæ¬ï¼1.0 | å¯¹åºæå¡ç«¯çæ¬ï¼1.0.0*
|
| New file |
| | |
| | | # SmartLearning æ
å¢æç¥æºè½è±è¯å¦ä¹ ç³»ç»
|
| | |
|
| | | åºäº PRDï¼`docs/smartlearing_prd_v1.01.txt`ï¼å®ç°çäºç«¯æå¡ç«¯ï¼è´è´£åºæ¯è¯å«ãæºè½å¯¹è¯ã课ç¨èµæºç®¡çä¸ç»ç«¯è®¾å¤ MQTT æä»¤ä¸åï¼æ¯æãåºæ¯æç¥ â æå¾åæ â æºè½å¯¹è¯ â å¦ä¹ åé¦ãéç¯ã
|
| | |
|
| | | ---
|
| | |
|
| | | ## ç®å½
|
| | |
|
| | | - [ç³»ç»æ¶æ](#ç³»ç»æ¶æ)
|
| | | - [项ç®ç»æ](#项ç®ç»æ)
|
| | | - [æ ¸å¿æµç¨](#æ ¸å¿æµç¨)
|
| | | - [MQTT åè®®](#mqtt-åè®®)
|
| | | - [课ç¨ä¸ä»»å¡æ¸
å](#课ç¨ä¸ä»»å¡æ¸
å)
|
| | | - [HTTP API](#http-api)
|
| | | - [é
置说æ](#é
置说æ)
|
| | | - [æ¬å°å¼å](#æ¬å°å¼å)
|
| | | - [æå
ä¸é¨ç½²](#æå
ä¸é¨ç½²)
|
| | | - [常è§é®é¢](#常è§é®é¢)
|
| | |
|
| | | ---
|
| | |
|
| | | ## ç³»ç»æ¶æ
|
| | |
|
| | | ```
|
| | | âââââââââââââââ MQTT (snlp/v1/...) ââââââââââââââââââââ
|
| | | â å¦ä¹ ç»ç«¯ â âââââââââââââââââââââââââââ⺠â SmartLearning â
|
| | | â (设å¤) â HTTP API / SSE â Server (æ¬ä»åº) â
|
| | | âââââââââââââââ âââââââââââââââââââââââââââ⺠ââââââââââ¬ââââââââââ
|
| | | â
|
| | | ââââââââââ¼ââââââââââ
|
| | | â data/ 课ç¨èµæº â
|
| | | â runtime/ è¿è¡ç¶æ â
|
| | | ââââââââââââââââââââ
|
| | | ```
|
| | |
|
| | | | ç»ä»¶ | ææ¯æ | èè´£ |
|
| | | |------|--------|------|
|
| | | | HTTP æå¡ | FastAPI + Uvicorn | REST APIãSSE å¯¹è¯æµãéæèµæºæç®¡ |
|
| | | | MQTT 客æ·ç«¯ | paho-mqtt | 订é
ç»ç«¯ä¸æ¥ãä¸åæ§å¶/任塿令 |
|
| | | | è¯¾ç¨æ°æ® | JSON æä»¶ | 课ç¨ç´¢å¼ãç« èè§é¢æ¸
åãåå§åä»»å¡ |
|
| | | | è¿è¡ç¶æ | JSON æä¹
å | è®°å½å·²å®æåå§åçè®¾å¤ ID |
|
| | |
|
| | | ---
|
| | |
|
| | | ## 项ç®ç»æ
|
| | |
|
| | | ```
|
| | | smartlearning/
|
| | | âââ docs/
|
| | | â âââ smartlearing_prd_v1.01.txt # 产åéæ±ä¸ MQTT åè®®è§è
|
| | | âââ data/ # 课ç¨ä¸ä»»å¡æ°æ®ï¼åæ¶ä½ä¸ºéæèµæºæ ¹ç®å½ï¼
|
| | | â âââ tasklist.json # 设å¤å¼æºåå§å任塿¸
å
|
| | | â âââ courses/
|
| | | â âââ index.json # å·²åå¸è¯¾ç¨ç´¢å¼ï¼åºç¡è¯¾ç¨å
ï¼
|
| | | â âââ course01/
|
| | | â â âââ course.json # 课ç¨è§é¢æ¸
å
|
| | | â â âââ courseTask.json # å课ç¨åå§åä»»å¡ï¼å¯éæ©å±ï¼
|
| | | â â âââ chapter01/resources/ # è§é¢æä»¶ç®å½
|
| | | â âââ course02/
|
| | | â âââ ...
|
| | | âââ server/
|
| | | â âââ app/
|
| | | â â âââ main.py # åºç¨å
¥å£
|
| | | â â âââ config.py # é
ç½®ï¼æ¯æ .envï¼
|
| | | â â âââ routes/ # API è·¯ç±
|
| | | â â âââ services/ # ä¸å¡é»è¾
|
| | | â â âââ mqtt/ # MQTT 䏿令ä¸å
|
| | | â âââ runtime/
|
| | | â â âââ initialized_devices.json # å·²åå§å设å¤è®°å½ï¼è¿è¡æ¶çæï¼
|
| | | â âââ requirements.txt
|
| | | â âââ run.py # å¯å¨èæ¬
|
| | | âââ README.md
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ## æ ¸å¿æµç¨
|
| | |
|
| | | ### 1. 设å¤é¦æ¬¡ä¸çº¿ï¼èªå¨åå§åï¼
|
| | |
|
| | | ç»ç«¯å¼æºåéè¿ MQTT 䏿¥å¿è·³ `rpt/state`ï¼æå¡ç«¯æ£æµå°è¯¥ `device_id` å°æªåå§åæ¶ï¼**èªå¨**æ§è¡ä¸¤é¶æ®µæµç¨ï¼
|
| | |
|
| | | ```
|
| | | ç»ç«¯ä¸æ¥ rpt/stateï¼é¦æ¬¡ï¼
|
| | | â
|
| | | â¼
|
| | | âââââââââââââââââââââââââââââââââââââââââ
|
| | | â é¶æ®µ 1ï¼è®¾å¤åºç¡åå§åï¼cmd/ctlï¼ â
|
| | | â · SET_CLOCK åæ¥ç³»ç»æ¶é â
|
| | | â · SET_VOLUME 设置é»è®¤é³éï¼50ï¼ â
|
| | | â · SET_POWER å¤éè®¾å¤ â
|
| | | âââââââââââââââââââââââââââââââââââââââââ
|
| | | â
|
| | | â¼
|
| | | âââââââââââââââââââââââââââââââââââââââââ
|
| | | â é¶æ®µ 2ï¼åºç¡è¯¾ç¨èµæºåæ¥ï¼cmd/taskï¼ â
|
| | | â · UPDATE_MANIFEST ä¸å宿´èµæºæ¸
å â
|
| | | â · DOWNLOAD à N éæä»¶ä¸è½½èµæºå
â
|
| | | â ï¼index.json 䏿æè¯¾ç¨ï¼ â
|
| | | âââââââââââââââââââââââââââââââââââââââââ
|
| | | â
|
| | | â¼
|
| | | åå
¥ server/runtime/initialized_devices.json
|
| | | ï¼åä¸è®¾å¤ä¸ä¼éå¤è§¦åï¼é¤éæå¨å¼ºå¶ï¼
|
| | | ```
|
| | |
|
| | | **èªå¨è§¦åæ¡ä»¶ï¼é¡»åæ¶æ»¡è¶³ï¼ï¼**
|
| | |
|
| | | 1. æ¶å° `snlp/v1/{device_id}/rpt/state`
|
| | | 2. `device_id` ä¸å¨æ¬å°å·²åå§åè®°å½ä¸
|
| | | 3. MQTT å·²è¿æ¥
|
| | | 4. `AUTO_INIT_ENABLED=true`ï¼é»è®¤å¼å¯ï¼
|
| | |
|
| | | **ä¸ä¼èªå¨è§¦åçæ
åµï¼**
|
| | |
|
| | | - å·²åå§å设å¤åæ¬¡ä¸æ¥å¿è·³ â è·³è¿
|
| | | - MQTT æªè¿æ¥ â è·³è¿ï¼ä¸æ¬¡å¿è·³å¯éè¯ï¼
|
| | | - å¯éè¿ `DELETE /api/v1/devices/initialized/{device_id}` æ¸
é¤è®°å½åéæ°è§¦å
|
| | |
|
| | | ### 2. åºç¡è¯¾ç¨èµæºåæ¥è¯´æ
|
| | |
|
| | | `data/courses/index.json` ä¸ååºç课ç¨åè§ä¸º**åºç¡è¯¾ç¨**ï¼å¨è®¾å¤åå§å宿åèªå¨åæ¥å°ç»ç«¯åå¨å¡ã
|
| | |
|
| | | **ç»ç«¯åå¨è·¯å¾è§åï¼**
|
| | |
|
| | | ```
|
| | | 设å¤è·¯å¾ = {DEVICE_DATA_ROOT} + æå¡ç«¯ data/ ä¸çç¸å¯¹è·¯å¾
|
| | | ```
|
| | |
|
| | | é»è®¤ `DEVICE_DATA_ROOT=/data`ï¼ç¤ºä¾ï¼
|
| | |
|
| | | | æå¡ç«¯æä»¶ | ç»ç«¯ç®æ è·¯å¾ |
|
| | | |-----------|-------------|
|
| | | | `data/courses/index.json` | `/data/courses/index.json` |
|
| | | | `data/courses/course01/course.json` | `/data/courses/course01/course.json` |
|
| | | | `data/courses/course01/chapter01/resources/001.mp3` | `/data/courses/course01/chapter01/resources/001.mp3` |
|
| | |
|
| | | å½å `course01` + `course02` å
± **23 个æä»¶**ï¼ç´¢å¼ + é
ç½® + 任塿¸
å + 18 个è§é¢å ä½è·¯å¾ï¼ã
|
| | |
|
| | | ### 3. å课ç¨åå§åï¼æå¨ï¼å¯éï¼
|
| | |
|
| | | è¥éåç¬å¯¹æä¸é¨è¯¾ç¨æ§è¡å®æ´ä»»å¡ï¼å«ææ¾å¼å¯¼ï¼ï¼å¯è°ç¨ï¼
|
| | |
|
| | | ```http
|
| | | POST /api/v1/devices/course-init
|
| | | Content-Type: application/json
|
| | |
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "course_id": "course01"
|
| | | }
|
| | | ```
|
| | |
|
| | | 该æ¥å£è¯»å `courseTask.json`ï¼ä¸å `UPDATE_MANIFEST`ã`DOWNLOAD`ã`PLAY_MEDIA` çæä»¤ï¼éç¨äºé课åå¢é忥æéæ°æåã
|
| | |
|
| | | ### 4. åºæ¯æç¥ä¸å¯¹è¯
|
| | |
|
| | | ```
|
| | | ç¯å¢é³/è¯é³ â POST /api/v1/scene/analyze â åºæ¯è¯å«ï¼ç½®ä¿¡åº¦ < 0.7 èµ°éç¨é®åï¼
|
| | | â
|
| | | ç¨æ·åå¤ â GET /api/v1/dialogue/stream (SSE) â æµå¼è¿åæå¦å¯¹è¯ä¸åé¦
|
| | | ```
|
| | |
|
| | | ### 5. æå¨å¼ºå¶éæ°åå§å
|
| | |
|
| | | ```http
|
| | | POST /api/v1/devices/init
|
| | | Content-Type: application/json
|
| | |
|
| | | {
|
| | | "device_id": "SN-88293021",
|
| | | "force": true
|
| | | }
|
| | | ```
|
| | |
|
| | | `force: false` ä¸è®¾å¤å·²åå§åæ¶ï¼è¿å `status: "already_initialized"`ï¼ä¸ä¼éå¤ä¸åã
|
| | |
|
| | | ---
|
| | |
|
| | | ## MQTT åè®®
|
| | |
|
| | | ### Topic è§å
|
| | |
|
| | | ```
|
| | | snlp/v1/{device_id}/{direct}/{msg_type}
|
| | | ```
|
| | |
|
| | | | æ¹å | Topic | 说æ |
|
| | | |------|-------|------|
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/state` | å¿è·³/ç¶æï¼cpuãçµéãç½ç»ã模å¼ï¼ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/event` | äºä»¶ï¼å¤éãASR ç»æçï¼ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/ack` | æä»¤æ§è¡åæ§ |
|
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/progress` | 弿¥ä»»å¡è¿åº¦ï¼ä¸è½½/OTAï¼ |
|
| | | | äº â ç»ç«¯ | `snlp/v1/{device_id}/cmd/ctl` | 宿¶æ§å¶æä»¤ |
|
| | | | äº â ç»ç«¯ | `snlp/v1/{device_id}/cmd/task` | 弿¥ä»»å¡æä»¤ï¼ä¸è½½èµæºå
çï¼ |
|
| | |
|
| | | ### æä»¤ Action ä¸è§
|
| | |
|
| | | | Action | éé | 说æ |
|
| | | |--------|------|------|
|
| | | | `SET_CLOCK` | ctl | 忥æ¶é´æ³ |
|
| | | | `SET_VOLUME` | ctl | 设置é³é 0â100 |
|
| | | | `SET_POWER` | ctl | ä¼ç /å¤é |
|
| | | | `PLAY_MEDIA` | ctl | ææ¾æ¬å°åªä½ |
|
| | | | `STOP_MEDIA` | ctl | åæ¢ææ¾ |
|
| | | | `DOWNLOAD` | task | 弿¥ä¸è½½æä»¶å° `target_path` |
|
| | | | `UPDATE_MANIFEST` | task | æ´æ°è¯¾ç¨èµæºæ¸
å |
|
| | | | `OTA_UPDATE` | task | åºä»¶å级 |
|
| | | | `REBOOT` | ctl | å»¶æ¶éå¯ |
|
| | |
|
| | | ### ä¸åæä»¤ Payload æ ¼å¼
|
| | |
|
| | | ```json
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": "SN-88293021",
|
| | | "req_id": "uuid",
|
| | | "timestamp": 1718365200,
|
| | | "ttl": 3600
|
| | | },
|
| | | "action": "DOWNLOAD",
|
| | | "payload": {
|
| | | "task_id": "uuid",
|
| | | "url": "http://your-server:9500/static/courses/course01/chapter01/resources/001.mp3",
|
| | | "md5": "",
|
| | | "target_path": "/data/courses/course01/chapter01/resources/001.mp3"
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | ç»ç«¯æ§è¡åé¡»å `rpt/ack`ï¼ä¸è½½è¿ç¨ä¸éè¿ `rpt/progress` 䏿¥è¿åº¦ã
|
| | |
|
| | | ---
|
| | |
|
| | | ## 课ç¨ä¸ä»»å¡æ¸
å
|
| | |
|
| | | ### index.json â åºç¡è¯¾ç¨ç´¢å¼
|
| | |
|
| | | ```json
|
| | | {
|
| | | "courses": [
|
| | | { "course_id": "course01", "config": "courses/course01/course.json" },
|
| | | { "course_id": "course02", "config": "courses/course02/course.json" }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | æ°å¢è¯¾ç¨ï¼å¨ `data/courses/` ä¸å建ç®å½å `course.json`ï¼å¹¶ç»è®°å° `index.json`ï¼è®¾å¤éæ°åå§ååèªå¨åæ¥ã
|
| | |
|
| | | ### course.json â 课ç¨è§é¢æ¸
å
|
| | |
|
| | | ```json
|
| | | {
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "chapters": [
|
| | | {
|
| | | "chapter_id": "chapter01",
|
| | | "chapter_name": "第ä¸ç« ",
|
| | | "audios": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "课ç¨001",
|
| | | "path": "courses/course01/chapter01/resources/001.mp3"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ]
|
| | | }
|
| | | ```
|
| | |
|
| | | `path` 为ç¸å¯¹äº `data/` çè·¯å¾ï¼é¡»ä¸å®é
æä»¶ä½ç½®ä¸è´ã
|
| | |
|
| | | ### tasklist.json â 设å¤å¼æºä»»å¡
|
| | |
|
| | | å®ä¹è®¾å¤åºç¡åå§åæ¥éª¤ï¼æ¶éãé³éãå¤éï¼ãåºç¡è¯¾ç¨èµæºå
ç±æå¡ç«¯å¨åå§å宿å**èªå¨**éè¿ `cmd/task` ä¸åï¼æ é卿¤æä»¶ä¸é
ç½®ã
|
| | |
|
| | | ### courseTask.json â å课ç¨ä»»å¡ï¼å¯éï¼
|
| | |
|
| | | å®ä¹æä¸é¨è¯¾ç¨çæ©å±åå§åæµç¨ï¼å¦ä¸è½½ + ææ¾å¼å¯¼ï¼ï¼éè¿ `course-init` API æå¨è§¦åã
|
| | |
|
| | | ---
|
| | |
|
| | | ## HTTP API
|
| | |
|
| | | æå¡é»è®¤å°åï¼`http://localhost:9500` |
| | | 交äºå¼ææ¡£ï¼`http://localhost:9500/docs`
|
| | |
|
| | | ### å¥åº·æ£æ¥
|
| | |
|
| | | | æ¹æ³ | è·¯å¾ | 说æ |
|
| | | |------|------|------|
|
| | | | GET | `/health` | æå¡ç¶æä¸ MQTT è¿æ¥ç¶æ |
|
| | |
|
| | | ### åºæ¯ä¸å¯¹è¯
|
| | |
|
| | | | æ¹æ³ | è·¯å¾ | 说æ |
|
| | | |------|------|------|
|
| | | | POST | `/api/v1/scene/analyze` | åºæ¯è¯å« |
|
| | | | GET | `/api/v1/dialogue/stream` | SSE å¯¹è¯æµï¼åæ°ï¼`dialogue_id`ã`user_text`ã`scene_prompt`ã`difficulty`ï¼ |
|
| | |
|
| | | ### 课ç¨ä¸åå§å
|
| | |
|
| | | | æ¹æ³ | è·¯å¾ | 说æ |
|
| | | |------|------|------|
|
| | | | GET | `/api/v1/courses` | 课ç¨ç´¢å¼ |
|
| | | | GET | `/api/v1/courses/base-manifest` | é¢è§åºç¡è¯¾ç¨åæ¥èµæºå表 |
|
| | | | GET | `/api/v1/courses/{course_id}` | 课ç¨è¯¦æ
|
|
| | | | GET | `/api/v1/courses/{course_id}/manifest` | å课ç¨èµæºæ¸
å |
|
| | | | GET | `/api/v1/courses/{course_id}/tasks` | å课ç¨ä»»å¡æ¸
å |
|
| | | | GET | `/api/v1/tasklist` | 设å¤åå§å任塿¸
å |
|
| | | | POST | `/api/v1/devices/init` | æå¨è§¦å设å¤åå§åï¼æ¯æ `force`ï¼ |
|
| | | | POST | `/api/v1/devices/course-init` | å课ç¨åå§å |
|
| | |
|
| | | ### 设å¤ç®¡ç
|
| | |
|
| | | | æ¹æ³ | è·¯å¾ | 说æ |
|
| | | |------|------|------|
|
| | | | GET | `/api/v1/devices` | å¨çº¿è®¾å¤åè¡¨ï¼æ¥èª MQTT 䏿¥ï¼ |
|
| | | | GET | `/api/v1/devices/initialized` | å·²åå§å设å¤è®°å½ |
|
| | | | GET | `/api/v1/devices/{device_id}` | 设å¤ç¶æ |
|
| | | | GET | `/api/v1/devices/{device_id}/init-status` | åå§åç¶æ |
|
| | | | DELETE | `/api/v1/devices/initialized/{device_id}` | æ¸
é¤åå§åè®°å½ |
|
| | | | GET | `/api/v1/devices/{device_id}/acks` | æä»¤åæ§åå² |
|
| | | | GET | `/api/v1/devices/{device_id}/events` | äºä»¶åå² |
|
| | | | POST | `/api/v1/devices/{device_id}/cmd` | æå¨ä¸å ctl æä»¤ |
|
| | | | POST | `/api/v1/devices/{device_id}/task` | æå¨ä¸å task æä»¤ |
|
| | |
|
| | | ### éæèµæº
|
| | |
|
| | | ```
|
| | | GET /static/{path}
|
| | | ```
|
| | |
|
| | | 示ä¾ï¼`/static/courses/course01/chapter01/resources/001.mp3`
|
| | |
|
| | | ç»ç«¯ `DOWNLOAD` æä»¤ä¸ç `url` æåæ¤å°åï¼è¯·ç¡®ä¿ `BASE_URL` é
置为ç»ç«¯å¯è®¿é®çå
¬ç½/å
ç½å°åã
|
| | |
|
| | | ### 请æ±ç¤ºä¾
|
| | |
|
| | | **åºæ¯è¯å«ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -X POST http://localhost:9500/api/v1/scene/analyze \
|
| | | -H "Content-Type: application/json" \
|
| | | -d '{
|
| | | "timestamp": 1718365200,
|
| | | "audio_blob": "",
|
| | | "device_id": "SN-88293021",
|
| | | "context_hints": { "scene": "dining", "confidence": 0.9 }
|
| | | }'
|
| | | ```
|
| | |
|
| | | **å¯¹è¯æµï¼SSEï¼ï¼**
|
| | |
|
| | | ```bash
|
| | | curl -N "http://localhost:9500/api/v1/dialogue/stream?user_text=Good%20morning&difficulty=A2"
|
| | | ```
|
| | |
|
| | | **é¢è§åºç¡è¯¾ç¨åæ¥æ¸
åï¼**
|
| | |
|
| | | ```bash
|
| | | curl http://localhost:9500/api/v1/courses/base-manifest
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ## é
置说æ
|
| | |
|
| | | å¨ `server/` ç®å½ä¸å建 `.env` æä»¶ï¼æ¨èï¼é¿å
å°å¯é¥åå
¥ä»£ç ï¼ï¼
|
| | |
|
| | | ```env
|
| | | # HTTP æå¡
|
| | | HOST=0.0.0.0
|
| | | PORT=9500
|
| | | BASE_URL=http://your-public-ip:9500
|
| | |
|
| | | # è¯¾ç¨æ°æ®ç®å½ï¼é»è®¤æåé¡¹ç®æ ¹ data/ï¼
|
| | | # DATA_DIR=E:/smartlearning/data
|
| | |
|
| | | # MQTT
|
| | | MQTT_BROKER=your-mqtt-broker
|
| | | MQTT_PORT=1883
|
| | | MQTT_USERNAME=your-username
|
| | | MQTT_PASSWORD=your-password
|
| | | MQTT_CLIENT_ID=snlp-server
|
| | | MQTT_ENABLED=true
|
| | |
|
| | | # åå§å
|
| | | AUTO_INIT_ENABLED=true
|
| | | DEVICE_DATA_ROOT=/data
|
| | |
|
| | | # åºæ¯è¯å«
|
| | | SCENE_CONFIDENCE_THRESHOLD=0.7
|
| | | ```
|
| | |
|
| | | | åé | é»è®¤å¼ | 说æ |
|
| | | |------|--------|------|
|
| | | | `HOST` | `0.0.0.0` | çå¬å°å |
|
| | | | `PORT` | `9500` | çå¬ç«¯å£ |
|
| | | | `BASE_URL` | `http://localhost:9500` | èµæºä¸è½½ URL åç¼ï¼**é¨ç½²æ¶å¿
é¡»æ¹ä¸ºç»ç«¯å¯è®¿é®çå°å** |
|
| | | | `DATA_DIR` | `../data` | è¯¾ç¨æ°æ®ç®å½ |
|
| | | | `MQTT_BROKER` | â | MQTT Broker å°å |
|
| | | | `MQTT_PORT` | â | MQTT ç«¯å£ |
|
| | | | `MQTT_ENABLED` | `true` | 设为 `false` å¯ä»
è¿è¡ HTTP API |
|
| | | | `AUTO_INIT_ENABLED` | `true` | æ¯å¦èªå¨è§¦å设å¤åå§å |
|
| | | | `DEVICE_DATA_ROOT` | `/data` | ç»ç«¯åå¨å¡æ ¹è·¯å¾ |
|
| | |
|
| | | ### å·²åå§å设å¤è®°å½
|
| | |
|
| | | è·¯å¾ï¼`server/runtime/initialized_devices.json`
|
| | |
|
| | | ```json
|
| | | {
|
| | | "devices": {
|
| | | "SN-88293021": {
|
| | | "initialized_at": 1781701381,
|
| | | "source": "auto",
|
| | | "mode": "idle",
|
| | | "courses_synced": true,
|
| | | "courses_sync_at": 1781701381,
|
| | | "courses_download_count": 23
|
| | | }
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ## æ¬å°å¼å
|
| | |
|
| | | ### ç¯å¢è¦æ±
|
| | |
|
| | | - Python 3.10+
|
| | | - å¯è®¿é®ç MQTT Brokerï¼èªå¨åå§åä¾èµ MQTTï¼
|
| | |
|
| | | ### å®è£
ä¸å¯å¨
|
| | |
|
| | | ```bash
|
| | | cd server
|
| | | pip install -r requirements.txt
|
| | |
|
| | | # å建 .env å¹¶å¡«åé
ç½®ï¼è§ä¸æï¼
|
| | | # copy .env.example .env # å¦æç¤ºä¾æä»¶
|
| | |
|
| | | python run.py
|
| | | ```
|
| | |
|
| | | æä½¿ç¨ uvicorn ç´æ¥å¯å¨ï¼æ¯æçéè½½ï¼ï¼
|
| | |
|
| | | ```bash
|
| | | cd server
|
| | | uvicorn app.main:app --host 0.0.0.0 --port 9500 --reload
|
| | | ```
|
| | |
|
| | | ### éªè¯
|
| | |
|
| | | ```bash
|
| | | # å¥åº·æ£æ¥
|
| | | curl http://localhost:9500/health
|
| | |
|
| | | # 课ç¨ç´¢å¼
|
| | | curl http://localhost:9500/api/v1/courses
|
| | |
|
| | | # åºç¡è¯¾ç¨åæ¥æ¸
å
|
| | | curl http://localhost:9500/api/v1/courses/base-manifest
|
| | | ```
|
| | |
|
| | | è®¿é® `http://localhost:9500/docs` æ¥ç宿´ API ææ¡£ã
|
| | |
|
| | | ---
|
| | |
|
| | | ## æå
ä¸é¨ç½²
|
| | |
|
| | | ### æ¹å¼ä¸ï¼ç´æ¥é¨ç½²ï¼æ¨èå°è§æ¨¡/å
ç½ï¼
|
| | |
|
| | | **1. æå
项ç®**
|
| | |
|
| | | å¨å¼åæºä¸å°é¡¹ç®ç®å½æå
ï¼æé¤ç¼åä¸èæç¯å¢ï¼ï¼
|
| | |
|
| | | ```bash
|
| | | # Linux / macOS
|
| | | cd /path/to
|
| | | tar czvf smartlearning-server.tar.gz \
|
| | | --exclude='server/__pycache__' \
|
| | | --exclude='server/.venv' \
|
| | | --exclude='server/runtime/*.json' \
|
| | | smartlearning/
|
| | |
|
| | | # Windows PowerShell
|
| | | Compress-Archive -Path E:\smartlearning -DestinationPath smartlearning-server.zip
|
| | | ```
|
| | |
|
| | | ä¸ä¼ å°æå¡å¨åè§£åï¼
|
| | |
|
| | | ```bash
|
| | | tar xzvf smartlearning-server.tar.gz
|
| | | cd smartlearning/server
|
| | | ```
|
| | |
|
| | | **2. å建èæç¯å¢å¹¶å®è£
ä¾èµ**
|
| | |
|
| | | ```bash
|
| | | python3 -m venv .venv
|
| | | source .venv/bin/activate # Windows: .venv\Scripts\activate
|
| | | pip install -r requirements.txt
|
| | | ```
|
| | |
|
| | | **3. é
ç½®ç¯å¢åé**
|
| | |
|
| | | ```bash
|
| | | cp .env.example .env # ææå¨å建
|
| | | vim .env # å¡«å BASE_URLãMQTT ç
|
| | | ```
|
| | |
|
| | | > **éè¦ï¼** `BASE_URL` å¿
须设置为ç»ç«¯è½è®¿é®å°çå°åï¼å
¬ç½ IP æååï¼ï¼å¦å `DOWNLOAD` æä»¤ä¸çèµæº URL æ æã
|
| | |
|
| | | **4. åå¤è¯¾ç¨èµæº**
|
| | |
|
| | | å°è§é¢æä»¶æ¾å
¥ `data/courses/` 对åºç®å½ï¼ç¡®ä¿ä¸ `course.json` ä¸ `path` ä¸è´ã
|
| | |
|
| | | **5. ä½¿ç¨ systemd 宿¤è¿ç¨ï¼Linuxï¼**
|
| | |
|
| | | å建 `/etc/systemd/system/smartlearning.service`ï¼
|
| | |
|
| | | ```ini
|
| | | [Unit]
|
| | | Description=SmartLearning Server
|
| | | After=network.target
|
| | |
|
| | | [Service]
|
| | | Type=simple
|
| | | User=www-data
|
| | | WorkingDirectory=/opt/smartlearning/server
|
| | | EnvironmentFile=/opt/smartlearning/server/.env
|
| | | ExecStart=/opt/smartlearning/server/.venv/bin/python run.py
|
| | | Restart=always
|
| | | RestartSec=5
|
| | |
|
| | | [Install]
|
| | | WantedBy=multi-user.target
|
| | | ```
|
| | |
|
| | | ```bash
|
| | | sudo systemctl daemon-reload
|
| | | sudo systemctl enable smartlearning
|
| | | sudo systemctl start smartlearning
|
| | | sudo systemctl status smartlearning
|
| | | ```
|
| | |
|
| | | **6. Nginx åå代çï¼å¯éï¼**
|
| | |
|
| | | ```nginx
|
| | | server {
|
| | | listen 80;
|
| | | server_name your-domain.com;
|
| | |
|
| | | location / {
|
| | | proxy_pass http://127.0.0.1:9500;
|
| | | proxy_http_version 1.1;
|
| | | proxy_set_header Host $host;
|
| | | proxy_set_header X-Real-IP $remote_addr;
|
| | | proxy_buffering off; # SSE å¯¹è¯æµéè¦å
³éç¼å²
|
| | | }
|
| | | }
|
| | | ```
|
| | |
|
| | | è¥ä½¿ç¨ HTTPSï¼é
ç½® TLS è¯ä¹¦å¹¶å° `BASE_URL` æ¹ä¸º `https://your-domain.com`ã
|
| | |
|
| | | ---
|
| | |
|
| | | ### æ¹å¼äºï¼Docker é¨ç½²
|
| | |
|
| | | å¨é¡¹ç®æ ¹ç®å½å建 `Dockerfile`ï¼å¦å°æªåå¨ï¼å¯åè以ä¸å
容ï¼ï¼
|
| | |
|
| | | ```dockerfile
|
| | | FROM python:3.12-slim
|
| | |
|
| | | WORKDIR /app
|
| | |
|
| | | COPY server/requirements.txt .
|
| | | RUN pip install --no-cache-dir -r requirements.txt
|
| | |
|
| | | COPY server/ ./server/
|
| | | COPY data/ ./data/
|
| | |
|
| | | WORKDIR /app/server
|
| | |
|
| | | ENV HOST=0.0.0.0
|
| | | ENV PORT=9500
|
| | | ENV DATA_DIR=/app/data
|
| | |
|
| | | EXPOSE 9500
|
| | |
|
| | | CMD ["python", "run.py"]
|
| | | ```
|
| | |
|
| | | `docker-compose.yml` 示ä¾ï¼
|
| | |
|
| | | ```yaml
|
| | | services:
|
| | | smartlearning:
|
| | | build: .
|
| | | ports:
|
| | | - "9500:9500"
|
| | | env_file:
|
| | | - server/.env
|
| | | volumes:
|
| | | - ./data:/app/data:ro
|
| | | - ./server/runtime:/app/server/runtime
|
| | | restart: unless-stopped
|
| | | ```
|
| | |
|
| | | æå»ºä¸å¯å¨ï¼
|
| | |
|
| | | ```bash
|
| | | docker compose build
|
| | | docker compose up -d
|
| | | docker compose logs -f
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ### æ¹å¼ä¸ï¼Windows æå¡é¨ç½²
|
| | |
|
| | | **ä½¿ç¨ NSSM 注å为 Windows æå¡ï¼**
|
| | |
|
| | | ```powershell
|
| | | # å®è£
NSSM åæ§è¡
|
| | | nssm install SmartLearning "E:\smartlearning\server\.venv\Scripts\python.exe" "E:\smartlearning\server\run.py"
|
| | | nssm set SmartLearning AppDirectory "E:\smartlearning\server"
|
| | | nssm set SmartLearning AppEnvironmentExtra "BASE_URL=http://your-ip:9500"
|
| | | nssm start SmartLearning
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ### ç产ç¯å¢æ£æ¥æ¸
å
|
| | |
|
| | | | æ£æ¥é¡¹ | 说æ |
|
| | | |--------|------|
|
| | | | `BASE_URL` | ç»ç«¯å¯éè¿æ¤å°åä¸è½½ `/static/` èµæº |
|
| | | | MQTT è¿é | `/health` è¿å `"mqtt_connected": true` |
|
| | | | é²ç«å¢ | æ¾è¡ HTTP 端å£ï¼9500 æ Nginx 80/443ï¼ |
|
| | | | è¯¾ç¨æä»¶ | `data/courses/` ä¸è§é¢æä»¶å·²å°±ä½ |
|
| | | | æä¹
å | `server/runtime/` ç®å½éå¯åï¼åå§åè®°å½ï¼ |
|
| | | | å¯é¥å®å
¨ | MQTT è´¦å·å¯ç æ¾å¨ `.env`ï¼å¿æäº¤å°çæ¬åº |
|
| | | | è¿ç¨å®æ¤ | ä½¿ç¨ systemd / Docker `restart` çç¥ |
|
| | |
|
| | | ---
|
| | |
|
| | | ### å级ä¸åæ»
|
| | |
|
| | | **å级æ¥éª¤ï¼**
|
| | |
|
| | | ```bash
|
| | | # 1. å¤ä»½è¿è¡ç¶æ
|
| | | cp server/runtime/initialized_devices.json /backup/
|
| | |
|
| | | # 2. æ´æ°ä»£ç
|
| | | git pull # ææ¿æ¢è§£åå
|
| | |
|
| | | # 3. æ´æ°ä¾èµ
|
| | | cd server && pip install -r requirements.txt
|
| | |
|
| | | # 4. é坿å¡
|
| | | sudo systemctl restart smartlearning
|
| | | # æ docker compose restart
|
| | | ```
|
| | |
|
| | | **æ°å¢è¯¾ç¨åï¼** æ éé坿å¡ï¼å·²åå§å设å¤éæ¸
é¤è®°å½æ `force: true` éæ°åå§åæ¹å¯åæ¥æ°è¯¾ç¨ã
|
| | |
|
| | | ```bash
|
| | | curl -X DELETE http://localhost:9500/api/v1/devices/initialized/SN-88293021
|
| | | # 设å¤ä¸æ¬¡ä¸æ¥ state æ¶å°èªå¨éæ°åå§å并忥å
¨é¨åºç¡è¯¾ç¨
|
| | | ```
|
| | |
|
| | | ---
|
| | |
|
| | | ## 常è§é®é¢
|
| | |
|
| | | **Q: 设å¤ä¸æ¥äº stateï¼ä½æ²¡ææ¶å°åå§åæä»¤ï¼**
|
| | |
|
| | | 1. æ£æ¥ `/health` ä¸ `mqtt_connected` æ¯å¦ä¸º `true`
|
| | | 2. æ£æ¥è¯¥è®¾å¤æ¯å¦å·²å¨ `initialized_devices.json` ä¸
|
| | | 3. 确认 `AUTO_INIT_ENABLED=true`
|
| | | 4. ç¨ `GET /api/v1/devices/initialized` æ¥çè®°å½
|
| | |
|
| | | **Q: ç»ç«¯ä¸è½½èµæºå¤±è´¥ï¼**
|
| | |
|
| | | 1. 确认 `BASE_URL` æ¯ç»ç«¯ç½ç»å¯è¾¾çå°åï¼ä¸æ¯ `localhost`
|
| | | 2. 确认è§é¢æä»¶å·²ä¸ä¼ å° `data/courses/` 对åºè·¯å¾
|
| | | 3. ç¨æµè§å¨æ curl è®¿é® `BASE_URL/static/courses/...` éªè¯
|
| | |
|
| | | **Q: å¦ä½åªåæ¥åºç¡è¯¾ç¨ï¼ä¸éå¤åæ¶é/é³éåå§åï¼**
|
| | |
|
| | | å½å设计为ä¸ä½åæµç¨ãè¥ä»
éè¡¥åèµæºï¼å¯æå¨è°ç¨ï¼
|
| | |
|
| | | ```bash
|
| | | # éè¿å
é¨ API ä¸åï¼éèªè¡æ©å±ç«¯ç¹ï¼æç´æ¥å MQTT åå¸ DOWNLOAD æä»¤
|
| | | curl -X POST http://localhost:9500/api/v1/devices/SN-88293021/task \
|
| | | -H "Content-Type: application/json" \
|
| | | -d '{"header":{"device_id":"SN-88293021","timestamp":0},"action":"DOWNLOAD","payload":{"url":"...","target_path":"..."}}'
|
| | | ```
|
| | |
|
| | | ææ¸
é¤åå§åè®°å½å让设å¤éæ°èµ°å®æ´æµç¨ã
|
| | |
|
| | | **Q: MQTT Broker ææ¶ä¸å¯ç¨ï¼HTTP API è¿è½ç¨åï¼**
|
| | |
|
| | | å¯ä»¥ã设置 `MQTT_ENABLED=false` æ Broker è¿æ¥å¤±è´¥æ¶ï¼HTTP API 仿£å¸¸è¿è¡ï¼ä½èªå¨åå§å䏿令ä¸åä¸å¯ç¨ã
|
| | |
|
| | | ---
|
| | |
|
| | | ## ç¸å
³ææ¡£
|
| | |
|
| | | - 产åéæ±ä¸ MQTT 详ç»è§èï¼`docs/smartlearing_prd_v1.01.txt`
|
| | | - API 交äºå¼ææ¡£ï¼å¯å¨æå¡åè®¿é® `/docs`
|
| | |
|
| | | ---
|
| | |
|
| | | ## çæ¬
|
| | |
|
| | | - æå¡ç«¯çæ¬ï¼1.0.0
|
| | | - åè®®çæ¬ï¼snlp/v1
|
| | |
|
| | | http://localhost:9500/api/v1/courses/course01/manifest
|
| | | http://localhost:9500/api/v1/courses/base-manifest
|
| | |
|
| | | # ç¬¬ä¸æ¥ï¼æ¸
é¤è¯¥è®¾å¤çåå§åè®°å½
|
| | | curl.exe -X DELETE http://124.222.109.197:2101/api/v1/devices/initialized/498e93f3-cdc9-4362-b32a-d2fc10e69c12
|
| | |
|
| | | Invoke-WebRequest -Uri "http://124.222.109.197:2101/api/v1/devices/initialized/498e93f3-cdc9-4362-b32a-d2fc10e69c12" -Method Delete
|
| | |
|
| | | # ç¬¬äºæ¥ï¼å¼ºå¶éæ°åå§åï¼å«å
¨é¨åºç¡è¯¾ç¨åæ¥ï¼
|
| | | Invoke-RestMethod -Uri "http://124.222.109.197:2101/api/v1/devices/init" -Method Post -ContentType "application/json" -Body '{"device_id": "498e93f3-cdc9-4362-b32a-d2fc10e69c12", "force": true}'
|
| New file |
| | |
| | | {
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "description": "åºç¡è±è¯å¬å课ç¨ï¼å
嫿¥å¸¸å¯¹è¯ä¸åºæ¯åç»ä¹ ",
|
| | | "chapters": [
|
| | | {
|
| | | "chapter_id": "chapter01",
|
| | | "chapter_name": "æ¥å¸¸åºæ¯å¯¹è¯",
|
| | | "audios": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "é¤å
ç¹é¤å¯¹è¯",
|
| | | "path": "courses/course01/chapter01/resources/001.mp3"
|
| | | },
|
| | | {
|
| | | "id": "002",
|
| | | "title": "é
åºå
¥ä½å¯¹è¯",
|
| | | "path": "courses/course01/chapter01/resources/002.mp3"
|
| | | },
|
| | | {
|
| | | "id": "003",
|
| | | "title": "æºåºé®è·¯å¯¹è¯",
|
| | | "path": "courses/course01/chapter01/resources/003.mp3"
|
| | | }
|
| | | ]
|
| | | },
|
| | | {
|
| | | "chapter_id": "chapter02",
|
| | | "chapter_name": "åå¡è±è¯åºæ¯",
|
| | | "audios": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "ä¼è®®åè¨",
|
| | | "path": "courses/course01/chapter02/resources/001.mp3"
|
| | | },
|
| | | {
|
| | | "id": "002",
|
| | | "title": "çµè¯æ²é",
|
| | | "path": "courses/course01/chapter02/resources/002.mp3"
|
| | | },
|
| | | {
|
| | | "id": "003",
|
| | | "title": "é®ä»¶åä½",
|
| | | "path": "courses/course01/chapter02/resources/003.mp3"
|
| | | }
|
| | | ]
|
| | | },
|
| | | {
|
| | | "chapter_id": "chapter03",
|
| | | "chapter_name": "çæ´»å®ç¨åºæ¯",
|
| | | "audios": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "è´ç©å¯¹è¯",
|
| | | "path": "courses/course01/chapter03/resources/001.mp3"
|
| | | },
|
| | | {
|
| | | "id": "002",
|
| | | "title": "å»çé®è¯",
|
| | | "path": "courses/course01/chapter03/resources/002.mp3"
|
| | | },
|
| | | {
|
| | | "id": "003",
|
| | | "title": "å
Œ
±äº¤é",
|
| | | "path": "courses/course01/chapter03/resources/003.mp3"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ]
|
| | | }
|
| New file |
| | |
| | | {
|
| | | "course_id": "course01",
|
| | | "tasks": [
|
| | | {
|
| | | "task_id": "course01_chapter01_download",
|
| | | "action": "DOWNLOAD",
|
| | | "payload": {
|
| | | "url": "http://example.com/course01/chapter01/001.mp3",
|
| | | "md5": "abc123",
|
| | | "target_path": "/data/courses/course01/chapter01/resources/001.mp3"
|
| | | }
|
| | | },
|
| | | {
|
| | | "task_id": "course01_chapter01_download_002",
|
| | | "action": "DOWNLOAD",
|
| | | "payload": {
|
| | | "url": "http://example.com/course01/chapter01/002.mp3",
|
| | | "md5": "def456",
|
| | | "target_path": "/data/courses/course01/chapter01/resources/002.mp3"
|
| | | }
|
| | | },
|
| | | {
|
| | | "task_id": "course01_chapter01_download_003",
|
| | | "action": "DOWNLOAD",
|
| | | "payload": {
|
| | | "url": "http://example.com/course01/chapter01/003.mp3",
|
| | | "md5": "ghi789",
|
| | | "target_path": "/data/courses/course01/chapter01/resources/003.mp3"
|
| | | }
|
| | | }
|
| | | ]
|
| | | }
|
| New file |
| | |
| | | {
|
| | | "course_id": "course02",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨02",
|
| | | "description": "åå¡è±è¯è¿é¶è¯¾ç¨ï¼æ¶µçä¼è®®ãçµè¯æ²éä¸é®ä»¶åä½é«çº§æå·§",
|
| | | "chapters": [
|
| | | {
|
| | | "chapter_id": "chapter01",
|
| | | "chapter_name": "åå¡ä¼è®®åºæ¯",
|
| | | "audios": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "ä¼è®®ä¸»ææå·§",
|
| | | "path": "courses/course02/chapter01/resources/001.mp3"
|
| | | },
|
| | | {
|
| | | "id": "002",
|
| | | "title": "å¢éåä½è®¨è®º",
|
| | | "path": "courses/course02/chapter01/resources/002.mp3"
|
| | | },
|
| | | {
|
| | | "id": "003",
|
| | | "title": "é¡¹ç®æ±æ¥æ¼è®²",
|
| | | "path": "courses/course02/chapter01/resources/003.mp3"
|
| | | }
|
| | | ]
|
| | | },
|
| | | {
|
| | | "chapter_id": "chapter02",
|
| | | "chapter_name": "çµè¯æ²éè¿é¶",
|
| | | "audios": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "å½é
å®¢æ·æ¥å¾
",
|
| | | "path": "courses/course02/chapter02/resources/001.mp3"
|
| | | },
|
| | | {
|
| | | "id": "002",
|
| | | "title": "åå¡é约æå·§",
|
| | | "path": "courses/course02/chapter02/resources/002.mp3"
|
| | | },
|
| | | {
|
| | | "id": "003",
|
| | | "title": "è·¨æåæ²é",
|
| | | "path": "courses/course02/chapter02/resources/003.mp3"
|
| | | }
|
| | | ]
|
| | | },
|
| | | {
|
| | | "chapter_id": "chapter03",
|
| | | "chapter_name": "é®ä»¶åä½é«çº§",
|
| | | "audios": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "åå¡é®ä»¶ç»æ",
|
| | | "path": "courses/course02/chapter03/resources/001.mp3"
|
| | | },
|
| | | {
|
| | | "id": "002",
|
| | | "title": "æ£å¼é请å½åä½",
|
| | | "path": "courses/course02/chapter03/resources/002.mp3"
|
| | | },
|
| | | {
|
| | | "id": "003",
|
| | | "title": "项ç®è¿åº¦æ±æ¥",
|
| | | "path": "courses/course02/chapter03/resources/003.mp3"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ]
|
| | | }
|
| New file |
| | |
| | | {
|
| | | "version": "1.0",
|
| | | "course_id": "course02",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨02",
|
| | | "description": "课ç¨02ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å",
|
| | | "assets": [
|
| | | {
|
| | | "url": "courses/course02/chapter01/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001",
|
| | | "target_path": "courses/course02/chapter01/resources/001.mp3",
|
| | | "cover_url": "courses/course02/chapter01/resources/001.jpg",
|
| | | "cover_target_path": "courses/course02/chapter01/resources/001.jpg"
|
| | | },
|
| | | {
|
| | | "url": "courses/course02/chapter01/resources/002.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨002",
|
| | | "target_path": "courses/course02/chapter01/resources/002.mp3",
|
| | | "cover_url": "courses/course02/chapter01/resources/002.jpg",
|
| | | "cover_target_path": "courses/course02/chapter01/resources/002.jpg"
|
| | | },
|
| | | {
|
| | | "url": "courses/course02/chapter01/resources/003.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨003",
|
| | | "target_path": "courses/course02/chapter01/resources/003.mp3",
|
| | | "cover_url": "courses/course02/chapter01/resources/003.jpg",
|
| | | "cover_target_path": "courses/course02/chapter01/resources/003.jpg"
|
| | | },
|
| | | {
|
| | | "url": "courses/course02/chapter02/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第äºç« 课ç¨001",
|
| | | "target_path": "courses/course02/chapter02/resources/001.mp3",
|
| | | "cover_url": "courses/course02/chapter02/resources/001.jpg",
|
| | | "cover_target_path": "courses/course02/chapter02/resources/001.jpg"
|
| | | },
|
| | | {
|
| | | "url": "courses/course02/chapter02/resources/002.mp3", |
| | | "md5": "...", |
| | | "title": "第äºç« 课ç¨002",
|
| | | "target_path": "courses/course02/chapter02/resources/002.mp3",
|
| | | "cover_url": "courses/course02/chapter02/resources/002.jpg",
|
| | | "cover_target_path": "courses/course02/chapter02/resources/002.jpg"
|
| | | },
|
| | | {
|
| | | "url": "courses/course02/chapter02/resources/003.mp3", |
| | | "md5": "...", |
| | | "title": "第äºç« 课ç¨003",
|
| | | "target_path": "courses/course02/chapter02/resources/003.mp3",
|
| | | "cover_url": "courses/course02/chapter02/resources/003.jpg",
|
| | | "cover_target_path": "courses/course02/chapter02/resources/003.jpg"
|
| | | }, |
| | | {
|
| | | "url": "courses/course02/chapter03/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001",
|
| | | "target_path": "courses/course02/chapter03/resources/001.mp3",
|
| | | "cover_url": "courses/course02/chapter03/resources/001.jpg",
|
| | | "cover_target_path": "courses/course02/chapter03/resources/001.jpg"
|
| | | },
|
| | | {
|
| | | "url": "courses/course02/chapter03/resources/002.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨002",
|
| | | "target_path": "courses/course02/chapter03/resources/002.mp3",
|
| | | "cover_url": "courses/course02/chapter03/resources/002.jpg",
|
| | | "cover_target_path": "courses/course02/chapter03/resources/002.jpg"
|
| | | },
|
| | | {
|
| | | "url": "courses/course02/chapter03/resources/003.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨003",
|
| | | "target_path": "courses/course02/chapter03/resources/003.mp3",
|
| | | "cover_url": "courses/course02/chapter03/resources/003.jpg",
|
| | | "cover_target_path": "courses/course02/chapter03/resources/003.jpg"
|
| | | }
|
| | | ]
|
| | | |
| | | } |
| New file |
| | |
| | | {
|
| | | "courses": [
|
| | | {
|
| | | "course_id": "course01",
|
| | | "config": "courses/course01/course.json"
|
| | | },
|
| | | {
|
| | | "course_id": "course02",
|
| | | "config": "courses/course02/course.json"
|
| | | }
|
| | | ]
|
| | | }
|
| New file |
| | |
| | | {
|
| | | "tasks": [
|
| | | {
|
| | | "task_id": "init_course01",
|
| | | "action": "UPDATE_MANIFEST",
|
| | | "payload": {
|
| | | "course_id": "course01",
|
| | | "assets": [
|
| | | {
|
| | | "url": "http://example.com/course01/chapter01/001.mp3",
|
| | | "md5": "abc123",
|
| | | "target_path": "/data/courses/course01/chapter01/resources/001.mp3"
|
| | | },
|
| | | {
|
| | | "url": "http://example.com/course01/chapter01/002.mp3",
|
| | | "md5": "def456",
|
| | | "target_path": "/data/courses/course01/chapter01/resources/002.mp3"
|
| | | }
|
| | | ]
|
| | | }
|
| | | },
|
| | | {
|
| | | "task_id": "init_course02",
|
| | | "action": "UPDATE_MANIFEST",
|
| | | "payload": {
|
| | | "course_id": "course02",
|
| | | "assets": [
|
| | | {
|
| | | "url": "http://example.com/course02/chapter01/001.mp3",
|
| | | "md5": "ghi789",
|
| | | "target_path": "/data/courses/course02/chapter01/resources/001.mp3"
|
| | | }
|
| | | ]
|
| | | }
|
| | | }
|
| | | ]
|
| | | }
|
| New file |
| | |
| | | # SmartLearning æ¥å£ææ¡£ |
| | | |
| | | > æå¡é»è®¤å°åï¼`http://{host}:{port}`ï¼é»è®¤ `http://0.0.0.0:2101`ï¼ |
| | | > 交äºå¼ API ææ¡£ï¼å¯å¨æå¡åè®¿é® `/docs` |
| | | > åè®®çæ¬ï¼HTTP API v1 / MQTT snlp/v1 |
| | | |
| | | --- |
| | | |
| | | ## ç®å½ |
| | | |
| | | - [ä¸ãéç¨è¯´æ](#ä¸éç¨è¯´æ) |
| | | - [äºãHTTP æ¥å£](#äºhttp-æ¥å£) |
| | | - [2.1 ç³»ç»](#21-ç³»ç») |
| | | - [2.2 éæèµæº](#22-éæèµæº) |
| | | - [2.3 åºæ¯è¯å«](#23-åºæ¯è¯å«) |
| | | - [2.4 å¯¹è¯æµ](#24-å¯¹è¯æµ) |
| | | - [2.5 课ç¨ä¸ä»»å¡](#25-课ç¨ä¸ä»»å¡) |
| | | - [2.6 设å¤åå§å](#26-设å¤åå§å) |
| | | - [2.7 设å¤ç®¡ç](#27-设å¤ç®¡ç) |
| | | - [ä¸ãMQTT æ¥å£](#ä¸mqtt-æ¥å£) |
| | | - [3.1 Topic è§å](#31-topic-è§å) |
| | | - [3.2 æ¶æ¯éç¨æ ¼å¼](#32-æ¶æ¯éç¨æ ¼å¼) |
| | | - [3.3 ç»ç«¯ä¸æ¥ï¼rptï¼](#33-ç»ç«¯ä¸æ¥rpt) |
| | | - [3.4 äºç«¯ä¸åï¼cmdï¼](#34-äºç«¯ä¸åcmd) |
| | | - [3.5 æå¡ç«¯èªå¨ä¸åæµç¨](#35-æå¡ç«¯èªå¨ä¸åæµç¨) |
| | | - [åãæ¥å£éæ¥è¡¨](#忥å£éæ¥è¡¨) |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãéç¨è¯´æ |
| | | |
| | | ### 1.1 Base URL |
| | | |
| | | | ç¯å¢ | å°å | |
| | | |------|------| |
| | | | æ¬å°å¼å | `http://124.222.109.197:2101` | |
| | | | ç产é¨ç½² | ç± `.env` ä¸ `BASE_URL` é
ç½®ï¼ç»ç«¯ä¸è½½èµæºé¡»ä½¿ç¨è¯¥å°å | |
| | | |
| | | ### 1.2 请æ±å¤´ |
| | | |
| | | JSON æ¥å£ç»ä¸ä½¿ç¨ï¼ |
| | | |
| | | ``` |
| | | Content-Type: application/json |
| | | ``` |
| | | |
| | | SSE å¯¹è¯æµæ¥å£è¿åï¼ |
| | | |
| | | ``` |
| | | Content-Type: text/event-stream |
| | | ``` |
| | | |
| | | ### 1.3 é误ååº |
| | | |
| | | FastAPI æ åéè¯¯æ ¼å¼ï¼ |
| | | |
| | | ```json |
| | | { |
| | | "detail": "Course not found: course99" |
| | | } |
| | | ``` |
| | | |
| | | å¸¸è§ HTTP ç¶æç ï¼ |
| | | |
| | | | ç¶æç | 说æ | |
| | | |--------|------| |
| | | | 200 | æå | |
| | | | 400 | 请æ±åæ°é误 | |
| | | | 404 | èµæºä¸åå¨ | |
| | | | 422 | 请æ±ä½æ ¡éªå¤±è´¥ | |
| | | |
| | | --- |
| | | |
| | | ## äºãHTTP æ¥å£ |
| | | |
| | | ### 2.1 ç³»ç» |
| | | |
| | | #### GET /health |
| | | |
| | | å¥åº·æ£æ¥ï¼æ¥çæå¡ä¸ MQTT è¿æ¥ç¶æã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/health |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "status": "ok", |
| | | "mqtt_connected": true |
| | | } |
| | | ``` |
| | | |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | status | string | æå¡ç¶æï¼åºå® `"ok"` | |
| | | | mqtt_connected | boolean | MQTT Broker æ¯å¦å·²è¿æ¥ | |
| | | |
| | | --- |
| | | |
| | | ### 2.2 éæèµæº |
| | | |
| | | #### GET /static/{path} |
| | | |
| | | æç®¡ `data/` ç®å½ä¸çè¯¾ç¨æä»¶ï¼ä¾ç»ç«¯ HTTP ä¸è½½ã |
| | | |
| | | **URL è§åï¼** |
| | | |
| | | ``` |
| | | {BASE_URL}/static/{ç¸å¯¹è·¯å¾} |
| | | ``` |
| | | |
| | | **示ä¾ï¼** |
| | | |
| | | | èµæº | URL | |
| | | |------|-----| |
| | | | 课ç¨ç´¢å¼ | `http://124.222.109.197:2101/static/courses/index.json` | |
| | | | 课ç¨é
ç½® | `http://124.222.109.197:2101/static/courses/course01/course.json` | |
| | | | 课ç¨ä»»å¡å
| `http://124.222.109.197:2101/static/courses/course01/courseTask.json` | |
| | | | é³é¢ | `http://124.222.109.197:2101/static/courses/course01/chapter01/resources/001.mp3` | |
| | | | å°é¢ | `http://124.222.109.197:2101/static/courses/course01/chapter01/resources/001.png` | |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl -O http://124.222.109.197:2101/static/courses/course01/courseTask.json |
| | | ``` |
| | | |
| | | **ååºï¼** å¯¹åºæä»¶çäºè¿å¶/JSON å
容ã |
| | | |
| | | --- |
| | | |
| | | ### 2.3 åºæ¯è¯å« |
| | | |
| | | #### POST /api/v1/scene/analyze |
| | | |
| | | æ ¹æ®è®¾å¤ä¸ä¸æè¿è¡åºæ¯è¯å«ï¼è¿åé®åçç¥ä¸å»ºè®®æå¾ã |
| | | |
| | | **请æ±ä½ï¼** |
| | | |
| | | | åæ®µ | ç±»å | å¿
å¡« | 说æ | |
| | | |------|------|------|------| |
| | | | timestamp | int | æ¯ | Unix æ¶é´æ³ | |
| | | | audio_blob | string | æ¯ | Base64 é³é¢æå ä½ç¬¦ | |
| | | | device_id | string | æ¯ | è®¾å¤ ID | |
| | | | context_hints | object | å¦ | åºæ¯æç¤ºï¼å¦ `{"scene":"dining","confidence":0.9}` | |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl -X POST http://124.222.109.197:2101/api/v1/scene/analyze \ |
| | | -H "Content-Type: application/json" \ |
| | | -d '{ |
| | | "timestamp": 1718365200, |
| | | "audio_blob": "", |
| | | "device_id": "SN-88293021", |
| | | "context_hints": { |
| | | "scene": "dining", |
| | | "confidence": 0.9 |
| | | } |
| | | }' |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "scene": "ç¨é¤", |
| | | "confidence": 0.9, |
| | | "greeting_strategy": "contextual", |
| | | "suggested_intent": "What are you having for dinner?" |
| | | } |
| | | ``` |
| | | |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | scene | string | åºæ¯åç§° | |
| | | | confidence | float | 置信度 0~1 | |
| | | | greeting_strategy | string | `"contextual"` åºæ¯é®å / `"generic"` éç¨é®åï¼ç½®ä¿¡åº¦ < 0.7ï¼ | |
| | | | suggested_intent | string | 建议çå¼åºç½ææé® | |
| | | |
| | | **ä½ç½®ä¿¡åº¦ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "scene": "éå¤", |
| | | "confidence": 0.62, |
| | | "greeting_strategy": "generic", |
| | | "suggested_intent": "Hello! How can I help you practice English today?" |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 2.4 å¯¹è¯æµ |
| | | |
| | | #### GET /api/v1/dialogue/stream |
| | | |
| | | SSEï¼Server-Sent Eventsï¼æµå¼è¿åæå¦å¯¹è¯å
容ã |
| | | |
| | | **Query åæ°ï¼** |
| | | |
| | | | åæ° | ç±»å | å¿
å¡« | é»è®¤å¼ | 说æ | |
| | | |------|------|------|--------|------| |
| | | | dialogue_id | string | å¦ | èªå¨çæ UUID | å¯¹è¯ ID | |
| | | | user_text | string | å¦ | â | ç¨æ·å夿æ¬ï¼ASR ç»æï¼ | |
| | | | scene_prompt | string | å¦ | â | åºæ¯å¼åºç½ï¼æ¥èª scene/analyzeï¼ | |
| | | | difficulty | string | å¦ | A2 | CEFR é¾åº¦ç级 | |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | # ä»
å¼åºé®å |
| | | curl -N "http://124.222.109.197:2101/api/v1/dialogue/stream?scene_prompt=What%20are%20you%20having%20for%20dinner?&difficulty=A2" |
| | | |
| | | # å«ç¨æ·åå¤çåé¦ |
| | | curl -N "http://124.222.109.197:2101/api/v1/dialogue/stream?user_text=Good%20morning.&difficulty=A2" |
| | | ``` |
| | | |
| | | **ååºæ ¼å¼ï¼SSEï¼ï¼** |
| | | |
| | | ``` |
| | | event: message |
| | | data: {"dialogue_id":"a1b2c3d4-...","text":"What are you having for dinner?","audio_url":null,"intent_type":"greeting","difficulty_level":"A2"} |
| | | |
| | | event: message |
| | | data: {"dialogue_id":"a1b2c3d4-...","text":"Great job! Your sentence looks correct.","audio_url":null,"intent_type":"praise","difficulty_level":"A2"} |
| | | |
| | | event: message |
| | | data: {"dialogue_id":"a1b2c3d4-...","text":"What would you do differently next time?","audio_url":null,"intent_type":"question","difficulty_level":"A2"} |
| | | |
| | | event: done |
| | | data: {"dialogue_id":"a1b2c3d4-..."} |
| | | ``` |
| | | |
| | | **message äºä»¶ data åæ®µï¼** |
| | | |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | dialogue_id | string | å¯¹è¯ ID | |
| | | | text | string | å¯¹è¯ææ¬ | |
| | | | audio_url | string \| null | é³é¢ URLï¼å½å为 nullï¼ | |
| | | | intent_type | string | `greeting` / `praise` / `correction` / `question` | |
| | | | difficulty_level | string | é¾åº¦ç级 | |
| | | |
| | | --- |
| | | |
| | | ### 2.5 课ç¨ä¸ä»»å¡ |
| | | |
| | | #### GET /api/v1/courses |
| | | |
| | | è·åå·²åå¸è¯¾ç¨ç´¢å¼ï¼å¯¹åº `data/courses/index.json`ï¼ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/courses |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "courses": [ |
| | | { |
| | | "course_id": "course01", |
| | | "config": "courses/course01/course.json" |
| | | }, |
| | | { |
| | | "course_id": "course02", |
| | | "config": "courses/course02/course.json" |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/courses/{course_id} |
| | | |
| | | è·ååé¨è¯¾ç¨è¯¦æ
ï¼ç« èä¸è§é¢å表ï¼ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/courses/course01 |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01", |
| | | "chapters": [ |
| | | { |
| | | "chapter_id": "chapter01", |
| | | "chapter_name": "第ä¸ç« ", |
| | | "videos": [ |
| | | { |
| | | "id": "001", |
| | | "title": "课ç¨001", |
| | | "path": "courses/course01/chapter01/resources/001.mp3" |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/courses/base-manifest |
| | | |
| | | é¢è§å
¨é¨åºç¡è¯¾ç¨åæ¥å
ï¼MQTT ä¸ååç宿´ payloadï¼`url` 已补å
¨ï¼ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/courses/base-manifest |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "scope": "base", |
| | | "course_count": 2, |
| | | "courses": [ |
| | | { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å", |
| | | "assets": [ |
| | | { |
| | | "url": "http://124.222.109.197:2101/static/courses/course01/chapter01/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001", |
| | | "target_path": "courses/course01/chapter01/resources/001.mp3", |
| | | "cover_url": "http://124.222.109.197:2101/static/courses/course01/chapter01/resources/001.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png" |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/courses/{course_id}/manifest |
| | | |
| | | è·ååé¨è¯¾ç¨ç忥å
ï¼`url` / `cover_url` 已补å
¨ä¸ºå®æ´ HTTP å°åï¼ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/courses/course01/manifest |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å", |
| | | "assets": [ |
| | | { |
| | | "url": "http://124.222.109.197:2101/static/courses/course01/chapter01/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001", |
| | | "target_path": "courses/course01/chapter01/resources/001.mp3", |
| | | "cover_url": "http://124.222.109.197:2101/static/courses/course01/chapter01/resources/001.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png" |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/courses/{course_id}/tasks |
| | | |
| | | è·ååå§ `courseTask.json`ï¼æ¬å°æä»¶å
容ï¼`url` 为ç¸å¯¹è·¯å¾ï¼ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/courses/course01/tasks |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å", |
| | | "assets": [ |
| | | { |
| | | "url": "courses/course01/chapter01/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001", |
| | | "target_path": "courses/course01/chapter01/resources/001.mp3", |
| | | "cover_url": "courses/course01/chapter01/resources/001.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png" |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/tasklist |
| | | |
| | | è·å设å¤å¼æºåå§å任塿¸
åï¼`data/tasklist.json`ï¼ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/tasklist |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "version": "1.0", |
| | | "description": "ç»ç«¯è®¾å¤åå§å任塿¸
åï¼å¼æºåç±æå¡ç«¯ä¸åæ§è¡", |
| | | "tasks": [ |
| | | { |
| | | "task_id": "init_001", |
| | | "name": "åæ¥ç³»ç»æ¶é", |
| | | "action": "SET_CLOCK", |
| | | "channel": "ctl", |
| | | "ttl": 60, |
| | | "payload": { "ts": 0 } |
| | | }, |
| | | { |
| | | "task_id": "init_002", |
| | | "name": "设置é»è®¤é³é", |
| | | "action": "SET_VOLUME", |
| | | "channel": "ctl", |
| | | "ttl": 60, |
| | | "payload": { "level": 50 } |
| | | }, |
| | | { |
| | | "task_id": "init_003", |
| | | "name": "å¤é设å¤", |
| | | "action": "SET_POWER", |
| | | "channel": "ctl", |
| | | "ttl": 60, |
| | | "payload": { "state": "wake" } |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 2.6 设å¤åå§å |
| | | |
| | | #### POST /api/v1/devices/init |
| | | |
| | | æå¨è§¦å设å¤å®æ´åå§åï¼åºç¡ ctl æä»¤ + å
¨é¨åºç¡è¯¾ç¨ SYNC_COURSE_TASKï¼ã |
| | | |
| | | **请æ±ä½ï¼** |
| | | |
| | | | åæ®µ | ç±»å | å¿
å¡« | é»è®¤å¼ | 说æ | |
| | | |------|------|------|--------|------| |
| | | | device_id | string | æ¯ | â | è®¾å¤ ID | |
| | | | force | boolean | å¦ | false | 强å¶éæ°åå§åï¼å¿½ç¥å·²åå§åè®°å½ï¼ | |
| | | | course_id | string | å¦ | â | æ¬æ¥å£ä¸ä½¿ç¨ | |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl -X POST http://124.222.109.197:2101/api/v1/devices/init \ |
| | | -H "Content-Type: application/json" \ |
| | | -d '{ |
| | | "device_id": "SN-88293021", |
| | | "force": true |
| | | }' |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼æåä¸åï¼ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "commands": [ |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "8f932149-719c-4464-99be-66dfcfa14f29", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_CLOCK", |
| | | "payload": { "ts": 1718365200 } |
| | | }, |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "e1c1de2a-3067-417d-b533-67c9a9827a8e", |
| | | "timestamp": 1718365201, |
| | | "ttl": 7200 |
| | | }, |
| | | "action": "SYNC_COURSE_TASK", |
| | | "payload": { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "assets": [] |
| | | } |
| | | } |
| | | ], |
| | | "status": "dispatched" |
| | | } |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼å·²åå§å䏿ª forceï¼ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "commands": [], |
| | | "status": "already_initialized" |
| | | } |
| | | ``` |
| | | |
| | | | status å¼ | 说æ | |
| | | |-----------|------| |
| | | | dispatched | æä»¤å·²ä¸å | |
| | | | already_initialized | 设å¤å·²å¨åå§åè®°å½ä¸ï¼è·³è¿ | |
| | | |
| | | --- |
| | | |
| | | #### POST /api/v1/devices/course-init |
| | | |
| | | æå¨è§¦ååé¨è¯¾ç¨èµæºåæ¥ï¼ä¸å 1 æ¡ `SYNC_COURSE_TASK`ï¼ã |
| | | |
| | | **请æ±ä½ï¼** |
| | | |
| | | | åæ®µ | ç±»å | å¿
å¡« | 说æ | |
| | | |------|------|------|------| |
| | | | device_id | string | æ¯ | è®¾å¤ ID | |
| | | | course_id | string | æ¯ | è¯¾ç¨ IDï¼å¦ `course01` | |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl -X POST http://124.222.109.197:2101/api/v1/devices/course-init \ |
| | | -H "Content-Type: application/json" \ |
| | | -d '{ |
| | | "device_id": "SN-88293021", |
| | | "course_id": "course01" |
| | | }' |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "course_id": "course01", |
| | | "commands": [ |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", |
| | | "timestamp": 1718365300, |
| | | "ttl": 7200 |
| | | }, |
| | | "action": "SYNC_COURSE_TASK", |
| | | "payload": { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å", |
| | | "assets": [ |
| | | { |
| | | "url": "http://124.222.109.197:2101/static/courses/course01/chapter01/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001", |
| | | "target_path": "courses/course01/chapter01/resources/001.mp3", |
| | | "cover_url": "http://124.222.109.197:2101/static/courses/course01/chapter01/resources/001.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png" |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | ], |
| | | "status": "dispatched" |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 2.7 设å¤ç®¡ç |
| | | |
| | | #### GET /api/v1/devices |
| | | |
| | | è·åå½åå¨çº¿è®¾å¤åè¡¨ï¼æ¥èª MQTT `rpt/state` 䏿¥ï¼ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/devices |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "devices": [ |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "cpu": 15.0, |
| | | "mem": 45.0, |
| | | "battery": 80.0, |
| | | "net": "wifi", |
| | | "mode": "idle", |
| | | "last_seen": 1718365205 |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/devices/initialized |
| | | |
| | | è·å已宿åå§åç设å¤è®°å½ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/devices/initialized |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "devices": { |
| | | "SN-88293021": { |
| | | "initialized_at": 1718365200, |
| | | "source": "auto", |
| | | "mode": "idle", |
| | | "courses_synced": true, |
| | | "courses_sync_at": 1718365201, |
| | | "courses_sync_count": 2 |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/devices/{device_id} |
| | | |
| | | è·ååä¸ªè®¾å¤ææ°ç¶æã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/devices/SN-88293021 |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "cpu": 15.0, |
| | | "mem": 45.0, |
| | | "battery": 80.0, |
| | | "net": "wifi", |
| | | "mode": "idle", |
| | | "last_seen": 1718365205 |
| | | } |
| | | ``` |
| | | |
| | | **404 ååºï¼** |
| | | |
| | | ```json |
| | | { |
| | | "detail": "Device not found" |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/devices/{device_id}/init-status |
| | | |
| | | æ¥è¯¢è®¾å¤åå§åç¶æã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/devices/SN-88293021/init-status |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼å·²åå§åï¼ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "initialized": true, |
| | | "record": { |
| | | "initialized_at": 1718365200, |
| | | "source": "auto", |
| | | "mode": "idle", |
| | | "courses_synced": true, |
| | | "courses_sync_at": 1718365201, |
| | | "courses_sync_count": 2 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼æªåå§åï¼ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "initialized": false, |
| | | "record": null |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### DELETE /api/v1/devices/initialized/{device_id} |
| | | |
| | | æ¸
é¤è®¾å¤åå§åè®°å½ï¼ä¸æ¬¡ä¸æ¥ `rpt/state` æ¶å°éæ°è§¦åèªå¨åå§åã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl -X DELETE http://124.222.109.197:2101/api/v1/devices/initialized/SN-88293021 |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "status": "removed" |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/devices/{device_id}/acks |
| | | |
| | | è·åè®¾å¤æä»¤åæ§åå²ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/devices/SN-88293021/acks |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "acks": [ |
| | | { |
| | | "req_id": "8f932149-719c-4464-99be-66dfcfa14f29", |
| | | "status": "success", |
| | | "error_code": 0 |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### GET /api/v1/devices/{device_id}/events |
| | | |
| | | è·å设å¤äºä»¶ä¸è¿åº¦ä¸æ¥åå²ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl http://124.222.109.197:2101/api/v1/devices/SN-88293021/events |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "device_id": "SN-88293021", |
| | | "events": [ |
| | | { |
| | | "event": "asr_result", |
| | | "text": "Good morning", |
| | | "confidence": 0.95, |
| | | "_type": "event" |
| | | }, |
| | | { |
| | | "task_id": "uuid", |
| | | "status": "downloading", |
| | | "progress": 45, |
| | | "info": "Downloading assets...", |
| | | "_type": "progress" |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### POST /api/v1/devices/{device_id}/cmd |
| | | |
| | | æå¨å设å¤ä¸å宿¶æ§å¶æä»¤ï¼MQTT `cmd/ctl`ï¼ã |
| | | |
| | | **请æ±ä½ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_VOLUME", |
| | | "payload": { "level": 80 } |
| | | } |
| | | ``` |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl -X POST http://124.222.109.197:2101/api/v1/devices/SN-88293021/cmd \ |
| | | -H "Content-Type: application/json" \ |
| | | -d '{ |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_VOLUME", |
| | | "payload": { "level": 80 } |
| | | }' |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼è¿åå®é
ä¸åç宿´æä»¤ï¼ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_VOLUME", |
| | | "payload": { "level": 80 } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### POST /api/v1/devices/{device_id}/task |
| | | |
| | | æå¨å设å¤ä¸å弿¥ä»»å¡æä»¤ï¼MQTT `cmd/task`ï¼ã |
| | | |
| | | **请æ±ç¤ºä¾ï¼** |
| | | |
| | | ```bash |
| | | curl -X POST http://124.222.109.197:2101/api/v1/devices/SN-88293021/task \ |
| | | -H "Content-Type: application/json" \ |
| | | -d '{ |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365200, |
| | | "ttl": 7200 |
| | | }, |
| | | "action": "SYNC_COURSE_TASK", |
| | | "payload": { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "assets": [] |
| | | } |
| | | }' |
| | | ``` |
| | | |
| | | **ååºç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "550e8400-e29b-41d4-a716-446655440000", |
| | | "timestamp": 1718365200, |
| | | "ttl": 7200 |
| | | }, |
| | | "action": "SYNC_COURSE_TASK", |
| | | "payload": { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "assets": [] |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãMQTT æ¥å£ |
| | | |
| | | ### 3.1 Topic è§å |
| | | |
| | | ``` |
| | | snlp/v1/{device_id}/{direct}/{msg_type} |
| | | ``` |
| | | |
| | | | æ¹å | Topic | QoS | 说æ | |
| | | |------|-------|-----|------| |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/state` | 0 | å¿è·³/ç¶æä¸æ¥ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/event` | 0 | äºä»¶ä¸æ¥ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/ack` | 1 | æä»¤æ§è¡åæ§ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/progress` | 0 | 弿¥ä»»å¡è¿åº¦ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/log` | 0 | ç³»ç»æ¥å¿ | |
| | | | äº â ç»ç«¯ | `snlp/v1/{device_id}/cmd/ctl` | 1 | 宿¶æ§å¶æä»¤ | |
| | | | äº â ç»ç«¯ | `snlp/v1/{device_id}/cmd/task` | 1 | 弿¥ä»»å¡æä»¤ | |
| | | |
| | | **åæ°è¯´æï¼** |
| | | |
| | | | åæ° | 说æ | |
| | | |------|------| |
| | | | v1 | åè®®çæ¬å· | |
| | | | device_id | 设å¤å¯ä¸ IDï¼ä¸å¯ä¿®æ¹ | |
| | | | direct | `rpt`ï¼ç»ç«¯ä¸æ¥ï¼/ `cmd`ï¼äºç«¯ä¸åï¼ | |
| | | | msg_type | `state` / `event` / `ack` / `progress` / `log` / `ctl` / `task` | |
| | | |
| | | --- |
| | | |
| | | ### 3.2 æ¶æ¯éç¨æ ¼å¼ |
| | | |
| | | #### äºç«¯ä¸åï¼cmd/ctlãcmd/taskï¼ |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "550e8400-e29b-41d4-a716-446655440000", |
| | | "timestamp": 1718365200, |
| | | "ttl": 3600 |
| | | }, |
| | | "action": "ACTION_TYPE", |
| | | "payload": {} |
| | | } |
| | | ``` |
| | | |
| | | | header åæ®µ | ç±»å | 说æ | |
| | | |-------------|------|------| |
| | | | version | string | åè®®è¯ä¹çæ¬ï¼åºå® `"1.0"` | |
| | | | device_id | string | ç®æ è®¾å¤ ID | |
| | | | req_id | string | æä»¤å¯ä¸ IDï¼UUIDï¼ï¼ç»ç«¯ ack æ¶é¡»åä¼ | |
| | | | timestamp | int | Unix æ¶é´æ³ | |
| | | | ttl | int | æä»¤æææï¼ç§ï¼ | |
| | | |
| | | #### ç»ç«¯ä¸æ¥ï¼rpt/*ï¼ |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365205 |
| | | }, |
| | | "msg_type": "state", |
| | | "payload": {} |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 3.3 ç»ç«¯ä¸æ¥ï¼rptï¼ |
| | | |
| | | #### rpt/state â å¿è·³/ç¶æ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/state` |
| | | |
| | | **åå¸ç¤ºä¾ï¼ç»ç«¯ â äºï¼ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365205 |
| | | }, |
| | | "msg_type": "state", |
| | | "payload": { |
| | | "cpu": 15, |
| | | "mem": 45, |
| | | "battery": 80, |
| | | "net": "wifi", |
| | | "mode": "idle" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **æå¡ç«¯è¡ä¸ºï¼** |
| | | |
| | | - æ´æ°è®¾å¤å¨çº¿ç¶æ |
| | | - è¥è®¾å¤æªåå§åä¸ MQTT å·²è¿æ¥ â èªå¨è§¦å宿´åå§åæµç¨ |
| | | |
| | | **mode 常è§å¼ï¼** `idle` / `dialogue` / `translate` / `study` / `sleep` |
| | | |
| | | --- |
| | | |
| | | #### rpt/event â äºä»¶ä¸æ¥ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/event` |
| | | |
| | | **åå¸ç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365210 |
| | | }, |
| | | "msg_type": "event", |
| | | "payload": { |
| | | "event": "asr_result", |
| | | "text": "Good morning", |
| | | "confidence": 0.95 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **å¸¸è§ event ç±»åï¼** |
| | | |
| | | | event | 说æ | |
| | | |-------|------| |
| | | | wake | å¤é触å | |
| | | | sleep | ä¼ç 触å | |
| | | | asr_result | ASR è¯å«ç»æ | |
| | | | alarm | å¼å¸¸æ¥è¦ | |
| | | |
| | | --- |
| | | |
| | | #### rpt/ack â æä»¤åæ§ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/ack` |
| | | |
| | | **åå¸ç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365215 |
| | | }, |
| | | "msg_type": "ack", |
| | | "payload": { |
| | | "req_id": "550e8400-e29b-41d4-a716-446655440000", |
| | | "status": "success", |
| | | "error_code": 0 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | | payload åæ®µ | 说æ | |
| | | |--------------|------| |
| | | | req_id | 对åºä¸åæä»¤ç req_id | |
| | | | status | `success` / `failed` | |
| | | | error_code | 0 表示æåï¼é 0 为é误ç | |
| | | |
| | | --- |
| | | |
| | | #### rpt/progress â ä»»å¡è¿åº¦ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/progress` |
| | | |
| | | **åå¸ç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365220 |
| | | }, |
| | | "msg_type": "progress", |
| | | "payload": { |
| | | "task_id": "550e8400-e29b-41d4-a716-446655440000", |
| | | "status": "downloading", |
| | | "progress": 45, |
| | | "info": "Downloading courses/course01/chapter01/resources/001.mp3" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### rpt/log â ç³»ç»æ¥å¿ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/log` |
| | | |
| | | **åå¸ç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365230 |
| | | }, |
| | | "msg_type": "log", |
| | | "payload": { |
| | | "level": "error", |
| | | "module": "audio", |
| | | "message": "Microphone init failed" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 3.4 äºç«¯ä¸åï¼cmdï¼ |
| | | |
| | | #### cmd/ctl â 宿¶æ§å¶æä»¤ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/cmd/ctl` |
| | | |
| | | **Action å表ï¼** |
| | | |
| | | | Action | payload ç¤ºä¾ | 说æ | |
| | | |--------|-------------|------| |
| | | | SET_VOLUME | `{"level": 50}` | 设置é³é 0â100 | |
| | | | SET_POWER | `{"state": "wake"}` | å¤é / `sleep` ä¼ç | |
| | | | SET_CLOCK | `{"ts": 1718365200}` | 忥 Unix æ¶é´æ³ | |
| | | | PLAY_MEDIA | `{"path": "courses/course01/chapter01/resources/001.mp3", "volume": 50, "loop": false}` | ææ¾æ¬å°åªä½ | |
| | | | STOP_MEDIA | `{}` | åæ¢ææ¾ | |
| | | | SET_ALARM | `{"id": "alarm_01", "time": "07:00", "repeat": [1,2,3,4,5], "action": "add"}` | 设置é¹é | |
| | | | UNSET_ALARM | `{"id": "alarm_01"}` | å é¤é¹é | |
| | | | REBOOT | `{"delay": 5}` | å»¶æ¶éå¯ | |
| | | |
| | | **SET_CLOCK ä¸å示ä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "8f932149-719c-4464-99be-66dfcfa14f29", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_CLOCK", |
| | | "payload": { |
| | | "ts": 1718365200 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **SET_VOLUME ä¸å示ä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "34dc4157-9e52-4a99-bdad-b3daf665df11", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_VOLUME", |
| | | "payload": { |
| | | "level": 50 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **SET_POWER ä¸å示ä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_POWER", |
| | | "payload": { |
| | | "state": "wake" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### cmd/task â 弿¥ä»»å¡æä»¤ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/cmd/task` |
| | | |
| | | **å½åå®ç°ä½¿ç¨ç Actionï¼** |
| | | |
| | | | Action | 说æ | |
| | | |--------|------| |
| | | | SYNC_COURSE_TASK | ä¸å宿´è¯¾ç¨èµæºå
ï¼courseTask.json æ ¼å¼ï¼ï¼ç»ç«¯æ assets æ¹éä¸è½½ | |
| | | |
| | | **SYNC_COURSE_TASK ä¸å示ä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "e1c1de2a-3067-417d-b533-67c9a9827a8e", |
| | | "timestamp": 1718365201, |
| | | "ttl": 7200 |
| | | }, |
| | | "action": "SYNC_COURSE_TASK", |
| | | "payload": { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å", |
| | | "assets": [ |
| | | { |
| | | "url": "http://your-server:2101/static/courses/course01/chapter01/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001", |
| | | "target_path": "courses/course01/chapter01/resources/001.mp3", |
| | | "cover_url": "http://your-server:2101/static/courses/course01/chapter01/resources/001.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png" |
| | | }, |
| | | { |
| | | "url": "http://your-server:2101/static/courses/course01/chapter01/resources/002.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨002", |
| | | "target_path": "courses/course01/chapter01/resources/002.mp3", |
| | | "cover_url": "http://your-server:2101/static/courses/course01/chapter01/resources/002.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/002.png" |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **assets åæ®µè¯´æï¼** |
| | | |
| | | | åæ®µ | 说æ | |
| | | |------|------| |
| | | | url | èµæºå®æ´ HTTP ä¸è½½å°åï¼æå¡ç«¯èªå¨è¡¥å
¨ï¼ | |
| | | | md5 | æä»¶æ ¡éªå¼ | |
| | | | title | èµæºæ é¢ | |
| | | | target_path | ç»ç«¯åå¨ç¸å¯¹è·¯å¾ï¼ä¸æå¡ç«¯ç®å½ç»æä¸è´ï¼ | |
| | | | cover_url | å°é¢å®æ´ HTTP ä¸è½½å°å | |
| | | | cover_target_path | å°é¢ç»ç«¯åå¨ç¸å¯¹è·¯å¾ | |
| | | |
| | | **ç»ç«¯å¤çé»è¾ï¼** |
| | | |
| | | 1. æ¶å° `SYNC_COURSE_TASK` åè§£æ `assets` å表 |
| | | 2. ææ¯é¡¹ç `url` ä¸è½½æä»¶ï¼ä¿åå°åå¨å¡ `target_path` è·¯å¾ |
| | | 3. æ `cover_url` ä¸è½½å°é¢ï¼ä¿åå° `cover_target_path` |
| | | 4. ä¸è½½è¿ç¨ä¸éè¿ `rpt/progress` 䏿¥è¿åº¦ |
| | | 5. 宿åéè¿ `rpt/ack` åä¼ `req_id` 䏿§è¡ç»æ |
| | | |
| | | **PRD é¢ç Actionï¼å¯éè¿ HTTP `/devices/{id}/task` æå¨ä¸åï¼ï¼** |
| | | |
| | | | Action | payload ç¤ºä¾ | 说æ | |
| | | |--------|-------------|------| |
| | | | DOWNLOAD | `{"task_id":"uuid","url":"...","md5":"...","target_path":"..."}` | åæä»¶ä¸è½½ | |
| | | | UPDATE_MANIFEST | `{"course_id":"...","assets":[...]}` | æ´æ°èµæºæ¸
å | |
| | | | OTA_UPDATE | `{"url":"...","version":"1.0.2"}` | åºä»¶å级 | |
| | | | DELETE_FILE | `{"path":"..."}` | å 餿件 | |
| | | |
| | | --- |
| | | |
| | | ### 3.5 æå¡ç«¯èªå¨ä¸åæµç¨ |
| | | |
| | | 设å¤é¦æ¬¡ä¸æ¥ `rpt/state` æ¶ï¼æå¡ç«¯èªå¨æ§è¡ï¼ |
| | | |
| | | ``` |
| | | 1. cmd/ctl SET_CLOCK {"ts": <å½åæ¶é´æ³>} |
| | | 2. cmd/ctl SET_VOLUME {"level": 50} |
| | | 3. cmd/ctl SET_POWER {"state": "wake"} |
| | | 4. cmd/task SYNC_COURSE_TASK {course01 宿´ payload} |
| | | 5. cmd/task SYNC_COURSE_TASK {course02 宿´ payload} |
| | | ... index.json 䏿¯é¨åºç¡è¯¾ç¨å 1 æ¡ |
| | | ``` |
| | | |
| | | **æå¨ MQTT åå¸ç¤ºä¾ï¼mosquitto_pubï¼ï¼** |
| | | |
| | | ```bash |
| | | mosquitto_pub \ |
| | | -h your-mqtt-broker -p 2100 \ |
| | | -u your-username -P your-password \ |
| | | -t "snlp/v1/SN-88293021/cmd/task" \ |
| | | -q 1 \ |
| | | -m '{"header":{"version":"1.0","device_id":"SN-88293021","req_id":"manual-uuid","timestamp":1718365200,"ttl":7200},"action":"SYNC_COURSE_TASK","payload":{"version":"1.0","course_id":"course01","course_name":"å°æºå¦ä¹ 课ç¨01","assets":[]}}' |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åãæ¥å£éæ¥è¡¨ |
| | | |
| | | ### HTTP æ¥å£ |
| | | |
| | | | æ¹æ³ | è·¯å¾ | 说æ | |
| | | |------|------|------| |
| | | | GET | `/health` | å¥åº·æ£æ¥ | |
| | | | GET | `/static/{path}` | éæèµæºä¸è½½ | |
| | | | POST | `/api/v1/scene/analyze` | åºæ¯è¯å« | |
| | | | GET | `/api/v1/dialogue/stream` | SSE å¯¹è¯æµ | |
| | | | GET | `/api/v1/courses` | 课ç¨ç´¢å¼ | |
| | | | GET | `/api/v1/courses/base-manifest` | å
¨é¨åºç¡è¯¾ç¨åæ¥å
é¢è§ | |
| | | | GET | `/api/v1/courses/{course_id}` | 课ç¨è¯¦æ
| |
| | | | GET | `/api/v1/courses/{course_id}/manifest` | å课ç¨åæ¥å
é¢è§ | |
| | | | GET | `/api/v1/courses/{course_id}/tasks` | åå§ courseTask.json | |
| | | | GET | `/api/v1/tasklist` | 设å¤åå§å任塿¸
å | |
| | | | POST | `/api/v1/devices/init` | æå¨è®¾å¤åå§å | |
| | | | POST | `/api/v1/devices/course-init` | æå¨å课ç¨åæ¥ | |
| | | | GET | `/api/v1/devices` | å¨çº¿è®¾å¤å表 | |
| | | | GET | `/api/v1/devices/initialized` | å·²åå§åè®¾å¤ | |
| | | | DELETE | `/api/v1/devices/initialized/{device_id}` | æ¸
é¤åå§åè®°å½ | |
| | | | GET | `/api/v1/devices/{device_id}` | 设å¤ç¶æ | |
| | | | GET | `/api/v1/devices/{device_id}/init-status` | åå§åç¶æ | |
| | | | GET | `/api/v1/devices/{device_id}/acks` | æä»¤åæ§åå² | |
| | | | GET | `/api/v1/devices/{device_id}/events` | äºä»¶/è¿åº¦åå² | |
| | | | POST | `/api/v1/devices/{device_id}/cmd` | æå¨ä¸å ctl æä»¤ | |
| | | | POST | `/api/v1/devices/{device_id}/task` | æå¨ä¸å task æä»¤ | |
| | | |
| | | ### MQTT æ¥å£ |
| | | |
| | | | æ¹å | Topic | å
³é® Action / ç¨é | |
| | | |------|-------|-------------------| |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/state` | å¿è·³ï¼è§¦åèªå¨åå§å | |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/event` | ASR ç»æãå¤é/ä¼ç äºä»¶ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/ack` | æä»¤æ§è¡åæ§ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/progress` | ä¸è½½/OTA è¿åº¦ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/log` | è¿ç¨æéæ¥å¿ | |
| | | | äº â ç»ç«¯ | `snlp/v1/{id}/cmd/ctl` | SET_CLOCK / SET_VOLUME / SET_POWER / PLAY_MEDIA ç | |
| | | | äº â ç»ç«¯ | `snlp/v1/{id}/cmd/task` | SYNC_COURSE_TASKï¼è¯¾ç¨èµæºå
åæ¥ï¼ | |
| | | |
| | | --- |
| | | |
| | | *ææ¡£çæ¬ï¼1.0 | å¯¹åºæå¡ç«¯çæ¬ï¼1.0.0* |
| | | |
| New file |
| | |
| | | # æ¥å£ææ¡£ |
| | | |
| | | > 交äºå¼ API ææ¡£ï¼å¯å¨æå¡åè®¿é® `/docs` |
| | | > åè®®çæ¬ï¼/ MQTT snlp/v1 |
| | | |
| | | --- |
| | | |
| | | ## ç®å½ |
| | | - [ä¸ãMQTT æ¥å£](#ä¸mqtt-æ¥å£) |
| | | - [1.1 Topic è§å](#11-topic-è§å) |
| | | - [1.2 æ¶æ¯éç¨æ ¼å¼](#12-æ¶æ¯éç¨æ ¼å¼) |
| | | - [1.3 ç»ç«¯ä¸æ¥ï¼rptï¼](#13-ç»ç«¯ä¸æ¥rpt) |
| | | - [1.4 äºç«¯ä¸åï¼cmdï¼](#14-äºç«¯ä¸åcmd) |
| | | - [1.5 æå¡ç«¯èªå¨ä¸åæµç¨](#15-æå¡ç«¯èªå¨ä¸åæµç¨) |
| | | - [äºãæ¥å£éæ¥è¡¨](#äºæ¥å£éæ¥è¡¨) |
| | | |
| | | --- |
| | | |
| | | |
| | | ## ä¸ãMQTT æ¥å£ |
| | | |
| | | ### 1.1 Topic è§å |
| | | |
| | | ``` |
| | | snlp/v1/{device_id}/{direct}/{msg_type} |
| | | ``` |
| | | |
| | | | æ¹å | Topic | QoS | 说æ | |
| | | |------|-------|-----|------| |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/state` | 0 | å¿è·³/ç¶æä¸æ¥ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/event` | 0 | äºä»¶ä¸æ¥ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/ack` | 1 | æä»¤æ§è¡åæ§ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/progress` | 0 | 弿¥ä»»å¡è¿åº¦ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{device_id}/rpt/log` | 0 | ç³»ç»æ¥å¿ | |
| | | | äº â ç»ç«¯ | `snlp/v1/{device_id}/cmd/ctl` | 1 | 宿¶æ§å¶æä»¤ | |
| | | | äº â ç»ç«¯ | `snlp/v1/{device_id}/cmd/task` | 1 | 弿¥ä»»å¡æä»¤ | |
| | | |
| | | **åæ°è¯´æï¼** |
| | | |
| | | | åæ° | 说æ | |
| | | |------|------| |
| | | | v1 | åè®®çæ¬å· | |
| | | | device_id | 设å¤å¯ä¸ IDï¼ä¸å¯ä¿®æ¹ | |
| | | | direct | `rpt`ï¼ç»ç«¯ä¸æ¥ï¼/ `cmd`ï¼äºç«¯ä¸åï¼ | |
| | | | msg_type | `state` / `event` / `ack` / `progress` / `log` / `ctl` / `task` | |
| | | |
| | | --- |
| | | |
| | | ### 1.2 æ¶æ¯éç¨æ ¼å¼ |
| | | |
| | | #### äºç«¯ä¸åï¼cmd/ctlãcmd/taskï¼ |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "550e8400-e29b-41d4-a716-446655440000", |
| | | "timestamp": 1718365200, |
| | | "ttl": 3600 |
| | | }, |
| | | "action": "ACTION_TYPE", |
| | | "payload": {} |
| | | } |
| | | ``` |
| | | |
| | | | header åæ®µ | ç±»å | 说æ | |
| | | |-------------|------|------| |
| | | | version | string | åè®®è¯ä¹çæ¬ï¼åºå® `"1.0"` | |
| | | | device_id | string | ç®æ è®¾å¤ ID | |
| | | | req_id | string | æä»¤å¯ä¸ IDï¼UUIDï¼ï¼ç»ç«¯ ack æ¶é¡»åä¼ | |
| | | | timestamp | int | Unix æ¶é´æ³ | |
| | | | ttl | int | æä»¤æææï¼ç§ï¼ | |
| | | |
| | | #### ç»ç«¯ä¸æ¥ï¼rpt/*ï¼ |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365205 |
| | | }, |
| | | "msg_type": "state", |
| | | "payload": {} |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 1.3 ç»ç«¯ä¸æ¥ï¼rptï¼ |
| | | |
| | | #### rpt/state â å¿è·³/ç¶æ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/state` |
| | | |
| | | **åå¸ç¤ºä¾ï¼ç»ç«¯ â äºï¼ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365205 |
| | | }, |
| | | "msg_type": "state", |
| | | "payload": { |
| | | "cpu": 15, |
| | | "mem": 45, |
| | | "battery": 80, |
| | | "net": "wifi", |
| | | "mode": "idle" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **æå¡ç«¯è¡ä¸ºï¼** |
| | | |
| | | - æ´æ°è®¾å¤å¨çº¿ç¶æ |
| | | - è¥è®¾å¤æªåå§åä¸ MQTT å·²è¿æ¥ â èªå¨è§¦å宿´åå§åæµç¨ |
| | | |
| | | **mode 常è§å¼ï¼** `idle` / `dialogue` / `translate` / `study` / `sleep` |
| | | |
| | | --- |
| | | |
| | | #### rpt/event â äºä»¶ä¸æ¥ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/event` |
| | | |
| | | **åå¸ç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365210 |
| | | }, |
| | | "msg_type": "event", |
| | | "payload": { |
| | | "event": "asr_result", |
| | | "text": "Good morning", |
| | | "confidence": 0.95 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **å¸¸è§ event ç±»åï¼** |
| | | |
| | | | event | 说æ | |
| | | |-------|------| |
| | | | wake | å¤é触å | |
| | | | sleep | ä¼ç 触å | |
| | | | asr_result | ASR è¯å«ç»æ | |
| | | | alarm | å¼å¸¸æ¥è¦ | |
| | | |
| | | --- |
| | | |
| | | #### rpt/ack â æä»¤åæ§ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/ack` |
| | | |
| | | **åå¸ç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365215 |
| | | }, |
| | | "msg_type": "ack", |
| | | "payload": { |
| | | "req_id": "550e8400-e29b-41d4-a716-446655440000", |
| | | "status": "success", |
| | | "error_code": 0 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | | payload åæ®µ | 说æ | |
| | | |--------------|------| |
| | | | req_id | 对åºä¸åæä»¤ç req_id | |
| | | | status | `success` / `failed` | |
| | | | error_code | 0 表示æåï¼é 0 为é误ç | |
| | | |
| | | --- |
| | | |
| | | #### rpt/progress â ä»»å¡è¿åº¦ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/progress` |
| | | |
| | | **åå¸ç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365220 |
| | | }, |
| | | "msg_type": "progress", |
| | | "payload": { |
| | | "task_id": "550e8400-e29b-41d4-a716-446655440000", |
| | | "status": "downloading", |
| | | "progress": 45, |
| | | "info": "Downloading courses/course01/chapter01/resources/001.mp3" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### rpt/log â ç³»ç»æ¥å¿ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/rpt/log` |
| | | |
| | | **åå¸ç¤ºä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365230 |
| | | }, |
| | | "msg_type": "log", |
| | | "payload": { |
| | | "level": "error", |
| | | "module": "audio", |
| | | "message": "Microphone init failed" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 1.4 äºç«¯ä¸åï¼cmdï¼ |
| | | |
| | | #### cmd/ctl â 宿¶æ§å¶æä»¤ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/cmd/ctl` |
| | | |
| | | **Action å表ï¼** |
| | | |
| | | | Action | payload ç¤ºä¾ | 说æ | |
| | | |--------|-------------|------| |
| | | | SET_VOLUME | `{"level": 50}` | 设置é³é 0â100 | |
| | | | SET_POWER | `{"state": "wake"}` | å¤é / `sleep` ä¼ç | |
| | | | SET_CLOCK | `{"ts": 1718365200}` | 忥 Unix æ¶é´æ³ | |
| | | | PLAY_MEDIA | `{"path": "courses/course01/chapter01/resources/001.mp3", "volume": 50, "loop": false}` | ææ¾æ¬å°åªä½ | |
| | | | STOP_MEDIA | `{}` | åæ¢ææ¾ | |
| | | | SET_ALARM | `{"id": "alarm_01", "time": "07:00", "repeat": [1,2,3,4,5], "action": "add"}` | 设置é¹é | |
| | | | UNSET_ALARM | `{"id": "alarm_01"}` | å é¤é¹é | |
| | | | REBOOT | `{"delay": 5}` | å»¶æ¶éå¯ | |
| | | |
| | | **SET_CLOCK ä¸å示ä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "8f932149-719c-4464-99be-66dfcfa14f29", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_CLOCK", |
| | | "payload": { |
| | | "ts": 1718365200 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **SET_VOLUME ä¸å示ä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "34dc4157-9e52-4a99-bdad-b3daf665df11", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_VOLUME", |
| | | "payload": { |
| | | "level": 50 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **SET_POWER ä¸å示ä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", |
| | | "timestamp": 1718365200, |
| | | "ttl": 60 |
| | | }, |
| | | "action": "SET_POWER", |
| | | "payload": { |
| | | "state": "wake" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | #### cmd/task â 弿¥ä»»å¡æä»¤ |
| | | |
| | | **Topicï¼** `snlp/v1/{device_id}/cmd/task` |
| | | |
| | | **å½åå®ç°ä½¿ç¨ç Actionï¼** |
| | | |
| | | | Action | 说æ | |
| | | |--------|------| |
| | | | SYNC_COURSE_TASK | ä¸å宿´è¯¾ç¨èµæºå
ï¼courseTask.json æ ¼å¼ï¼ï¼ç»ç«¯æ assets æ¹éä¸è½½ | |
| | | |
| | | **SYNC_COURSE_TASK ä¸å示ä¾ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "e1c1de2a-3067-417d-b533-67c9a9827a8e", |
| | | "timestamp": 1718365201, |
| | | "ttl": 7200 |
| | | }, |
| | | "action": "SYNC_COURSE_TASK", |
| | | "payload": { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å", |
| | | "assets": [ |
| | | { |
| | | "url": "http://your-server:9500/static/courses/course01/chapter01/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001", |
| | | "target_path": "courses/course01/chapter01/resources/001.mp3", |
| | | "cover_url": "http://your-server:9500/static/courses/course01/chapter01/resources/001.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png" |
| | | }, |
| | | { |
| | | "url": "http://your-server:9500/static/courses/course01/chapter01/resources/002.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨002", |
| | | "target_path": "courses/course01/chapter01/resources/002.mp3", |
| | | "cover_url": "http://your-server:9500/static/courses/course01/chapter01/resources/002.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/002.png" |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **assets åæ®µè¯´æï¼** |
| | | |
| | | | åæ®µ | 说æ | |
| | | |------|------| |
| | | | url | èµæºå®æ´ HTTP ä¸è½½å°åï¼æå¡ç«¯èªå¨è¡¥å
¨ï¼ | |
| | | | md5 | æä»¶æ ¡éªå¼ | |
| | | | title | èµæºæ é¢ | |
| | | | target_path | ç»ç«¯åå¨ç¸å¯¹è·¯å¾ï¼ä¸æå¡ç«¯ç®å½ç»æä¸è´ï¼ | |
| | | | cover_url | å°é¢å®æ´ HTTP ä¸è½½å°å | |
| | | | cover_target_path | å°é¢ç»ç«¯åå¨ç¸å¯¹è·¯å¾ | |
| | | |
| | | **ç»ç«¯å¤çé»è¾ï¼** |
| | | |
| | | 1. æ¶å° `SYNC_COURSE_TASK` åè§£æ `assets` å表 |
| | | 2. ææ¯é¡¹ç `url` ä¸è½½æä»¶ï¼ä¿åå°åå¨å¡ `target_path` è·¯å¾ |
| | | 1. æ `cover_url` ä¸è½½å°é¢ï¼ä¿åå° `cover_target_path` |
| | | 4. ä¸è½½è¿ç¨ä¸éè¿ `rpt/progress` 䏿¥è¿åº¦ |
| | | 5. 宿åéè¿ `rpt/ack` åä¼ `req_id` 䏿§è¡ç»æ |
| | | |
| | | **PRD é¢ç Actionï¼å¯éè¿ HTTP `/devices/{id}/task` æå¨ä¸åï¼ï¼** |
| | | |
| | | | Action | payload ç¤ºä¾ | 说æ | |
| | | |--------|-------------|------| |
| | | | DOWNLOAD | `{"task_id":"uuid","url":"...","md5":"...","target_path":"..."}` | åæä»¶ä¸è½½ | |
| | | | UPDATE_MANIFEST | `{"course_id":"...","assets":[...]}` | æ´æ°èµæºæ¸
å | |
| | | | OTA_UPDATE | `{"url":"...","version":"1.0.2"}` | åºä»¶å级 | |
| | | | DELETE_FILE | `{"path":"..."}` | å 餿件 | |
| | | |
| | | --- |
| | | |
| | | ### 1.5 æå¡ç«¯èªå¨ä¸åæµç¨ |
| | | |
| | | 设å¤é¦æ¬¡ä¸æ¥ `rpt/state` æ¶ï¼æå¡ç«¯èªå¨æ§è¡ï¼ |
| | | |
| | | ``` |
| | | 1. cmd/ctl SET_CLOCK {"ts": <å½åæ¶é´æ³>} |
| | | 2. cmd/ctl SET_VOLUME {"level": 50} |
| | | 1. cmd/ctl SET_POWER {"state": "wake"} |
| | | 4. cmd/task SYNC_COURSE_TASK {course01 宿´ payload} |
| | | 5. cmd/task SYNC_COURSE_TASK {course02 宿´ payload} |
| | | ... index.json 䏿¯é¨åºç¡è¯¾ç¨å 1 æ¡ |
| | | ``` |
| | | |
| | | **æå¨ MQTT åå¸ç¤ºä¾ï¼mosquitto_pubï¼ï¼** |
| | | |
| | | ```bash |
| | | mosquitto_pub \ |
| | | -h your-mqtt-broker -p 2100 \ |
| | | -u your-username -P your-password \ |
| | | -t "snlp/v1/SN-88293021/cmd/task" \ |
| | | -q 1 \ |
| | | -m '{"header":{"version":"1.0","device_id":"SN-88293021","req_id":"manual-uuid","timestamp":1718365200,"ttl":7200},"action":"SYNC_COURSE_TASK","payload":{"version":"1.0","course_id":"course01","course_name":"å°æºå¦ä¹ 课ç¨01","assets":[]}}' |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## äºãæ¥å£éæ¥è¡¨ |
| | | |
| | | ### MQTT æ¥å£ |
| | | |
| | | | æ¹å | Topic | å
³é® Action / ç¨é | |
| | | |------|-------|-------------------| |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/state` | å¿è·³ï¼è§¦åèªå¨åå§å | |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/event` | ASR ç»æãå¤é/ä¼ç äºä»¶ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/ack` | æä»¤æ§è¡åæ§ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/progress` | ä¸è½½/OTA è¿åº¦ | |
| | | | ç»ç«¯ â äº | `snlp/v1/{id}/rpt/log` | è¿ç¨æéæ¥å¿ | |
| | | | äº â ç»ç«¯ | `snlp/v1/{id}/cmd/ctl` | SET_CLOCK / SET_VOLUME / SET_POWER / PLAY_MEDIA ç | |
| | | | äº â ç»ç«¯ | `snlp/v1/{id}/cmd/task` | SYNC_COURSE_TASKï¼è¯¾ç¨èµæºå
åæ¥ï¼ | |
| | | |
| | | --- |
| | | |
| | | *ææ¡£çæ¬ï¼1.0 | å¯¹åºæå¡ç«¯çæ¬ï¼1.0.0* |
| | | |
| New file |
| | |
| | | 第ä¸é¨åï¼äº§åéæ±è§æ ¼è¯´æä¹¦ï¼PRDï¼ï¼æ
å¢æç¥æºè½è±è¯å¦ä¹ ç³»ç» |
| | | |
| | | 1. é¡¹ç®æ¦è¿° |
| | | 1.1 项ç®èæ¯ |
| | | ç°æè±è¯å¦ä¹ 产åå¤ä¸ºç¦»æ£çâé¢åºå¼âæâ课æ¬å¼âæå¦ï¼ç¼ºä¹è¯å¢ä¸åºç¨ç»åãæ¬äº§åéè¿å¼å
¥è¾¹ç¼è®¡ç®ä¸å¤§æ¨¡åææ¯ï¼å®ç°åºäºç©çç¯å¢æç¥çæ²æµ¸å¼è±è¯æå¦ï¼è§£å³âå¦èä¸ç¨âçé®é¢ã |
| | | |
| | | 1.2 产åç®æ |
| | | - å®ç°âåºæ¯æç¥ -> æå¾åæ -> æºè½å¯¹è¯ -> å¦ä¹ åé¦âçéç¯ç³»ç»ã |
| | | - æé å
·å¤ IoT åºç¡æ§å¶ãå¤åè½éæä¸é«éç§ä¿æ¤çå¦ä¹ ç»ç«¯ã |
| | | - æå»ºäºç«¯ AI æå¦ç©éµï¼æ¯æèªéåºé¾åº¦è°æ´ä¸ä¸ªæ§åå¦ä¹ 轨迹ã |
| | | |
| | | 2. ç»ç«¯è®¾å¤è¯¦ç»éæ± |
| | | 2.1 交äºç¶ææºï¼FSMï¼ |
| | | - å¾
æºï¼Idleï¼ï¼å®æ¶è¿è¡ VAD æ£æµï¼ä½åèè¿è¡ã |
| | | - å¯¹è¯æ¨¡å¼ï¼Dialogueï¼ï¼å¤çå
¨åå·¥è¯é³æµï¼æ¯æææï¼Barge-inï¼ã |
| | | - ç¿»è¯æ¨¡å¼ï¼Translateï¼ï¼çæ¶å½é³ï¼äºç«¯ææ¬å°æ¨¡åè¿è¡ç¿»è¯å¹¶ææ¾ã |
| | | - æå¦æ¨¡å¼ï¼Studyï¼ï¼å è½½é¢è®¾è¯¾ç¨å
å®¹ï¼æ¯æç¹è¯»ãè·è¯»ãè¯åã |
| | | - ä¼ç ï¼Sleepï¼ï¼ä»
ä¿çå¿è·³æ£æµä¸é¢è®¾å¤éè¯çå¬ï¼ç¦ç¨éº¦å
é£éµåã |
| | | |
| | | 2.2 æ ¸å¿åè½ææ¯ææ |
| | | - è¯é³ééï¼æ¯æ 3 ç±³è¿åºæ¾é³ï¼ä¿¡åªæ¯ (SNR) > 15dBï¼æ¯æå£°æºå®ä½ (DoA)ã |
| | | - éåª/å声æ¶é¤ (AEC)ï¼æ¯æ 50dB 以ä¸å声æ¶é¤ï¼ç¡®ä¿ç»ç«¯ææ¾é³é¢æ¶ä»å¯æ¾åç¨æ·è¯é³ã |
| | | - æ¬å°ç¿»è¯ï¼æ¯æç¦»çº¿ä¸è±äºè¯ï¼é¦åå»¶è¿ < 300msï¼ã |
| | | - ååºé度ï¼ä»ééå°æå¡ç«¯ä¸åååºç RTT (Round Trip Time) éæ§å¶å¨ 1.5s 以å
ã |
| | | |
| | | 2.3 IoT ä¸ç³»ç»éæ |
| | | - çµæºç®¡çï¼æ¯æ RTC 宿¶ï¼æ¯æå¼å¸¸æçµèªå¯å¨ï¼æ¯æçµé使¶èªå¨è¿å
¥ççµæ¨¡å¼ã |
| | | - ç½ç»è¿æ¥ï¼èªå¨åæ¢ Wi-Fi/4G/5Gï¼æ¯ææç½æ¬å°ç¼åï¼æå¤§æ¯æ 100MB è¯é³ç段ç¼åï¼ã |
| | | - å®å
¨æ§ï¼ç©ç麦å
é£éé³å¼å
³ï¼ç¡¬ä»¶çº§æå¼ï¼ï¼ç«¯ä¾§æ°æ®å å¯åå¨ (AES-128)ã |
| | | |
| | | 3. æå¡ç«¯å¹³å°è¯¦ç»éæ± |
| | | 3.1 åºæ¯è¯å«å¼æ (AI Context Engine) |
| | | - åºæ¯è§£æï¼éè¿ LLM 对ç¯å¢é³è¯ä¹çè§£ï¼åç±»å¨å®ä¹ä¸ºï¼å±
å®¶(å§å®¤/卿¿/书æ¿)ãæ ¡å
ãéå¤ãè¿å¨çã |
| | | - 对è¯çæï¼éæ LLM Agentï¼åºäºç¨æ·ç»ååå²ä¸å½ååºæ¯ï¼å¨æçæä¸ä¸è½®æé®ï¼æ¯æå¤è½®ä¸ä¸æç®¡çï¼ã |
| | | - 置信度æºå¶ï¼è¥åºæ¯è¯å«ç½®ä¿¡åº¦ < 0.7ï¼è§¦åéç¨é®åçç¥ï¼é¿å
å¹²æ°ç¨æ·ã |
| | | |
| | | 3.2 æå¦ç®¡æ§ç³»ç» |
| | | - ç¨æ·æ¨¡åï¼è®°å½ç¨æ·çCEFRç级ãè¯æ±åºãé误çï¼éè¿åé¦å¾ªç¯å¨æè°èé¾åº¦ã |
| | | - æ°æ®åå¨ï¼ç¬¦å GDPR/PIPL éç§åè§ï¼ææè¯é³æ°æ®å®æè±æå¤çï¼ä¸çåæªç»è®¸å¯çåå§é³é¢ã |
| | | |
| | | 4. éåè½æ§éæ±ï¼SLAï¼ |
| | | - å¯ç¨æ§ï¼ç³»ç»å¯ç¨æ§ 99.9%ã |
| | | - æ§è½ç¶é¢ï¼å¤§æ¨¡åæ¨çæ¥å£æ¯æè³å° 1000 QPS å¹¶åã |
| | | - å®å
¨åè§ï¼éè¿å å¯ééï¼TLS 1.3ï¼éä¿¡ï¼è®¾å¤ç»å®å
·å¤å¯ä¸çå®å
¨è¯çæ ¡éªï¼Secure Elementï¼ã |
| | | - 坿©å±æ§ï¼æ¯æéè¿æä»¶æºå¶å¢å æ°è¯ç§æåºæ¯æ¨¡æ¿ã |
| | | |
| | | 5. 交äºåè®®ä¸æ°æ®æµï¼APIè§èï¼ |
| | | - è¯·æ±æ¥å£ï¼/api/v1/scene/analyze (POST) |
| | | - Payload: {timestamp, audio_blob, device_id, context_hints} |
| | | - ååºæ¥å£ï¼/api/v1/dialogue/stream (SSE) |
| | | - Payload: {dialogue_id, text, audio_url, intent_type, difficulty_level} |
| | | - æä»¤åæ¥ï¼éè¿ MQTT Broker ä¸åæ§å¶æä»¤ï¼å¦ï¼SetVolume, Sleep, UpdateFirmwareï¼ã |
| | | |
| | | 6. å
¸ååºæ¯é»è¾æµç¨å®ä¹ |
| | | - åºæ¯è¯å«ï¼æ£æµå°ç¹å®ç¯å¢é³çº¹ -> å¤å®ä¸ºâç¨é¤åºæ¯â -> åéæä»¤è³ç»ç«¯ææ¾å¼å¯¼æç¤ºé³ã |
| | | - åå¤å¤çï¼æ£æµç¨æ·åå¤ -> ASRè¯å« -> åæè¯æ³ä¸æ£ç¡®æ§ -> è¿ååé¦ï¼èµèµæçº éï¼-> è¿å
¥ä¸ä¸è½®æé®ã |
| | | |
| | | |
| | | |
| | | 第äºé¨åï¼åç
§ä»¥ä¸éæ±ï¼åæ¥ä¸å¥mqttæ¶æ¯çtopicè§åå¦ä¸: |
| | | ä¸ãæ¶æ¯topicå®ä¹ï¼ snlp/v1/{device_id}/{direct}/{msg_type} |
| | | è¡¨æ ¼å¦ä¸ï¼ |
| | | æ¹å Topic è·¯å¾ msg_type 说æ |
| | | rpt (ç»ç«¯->äº) snlp/v1/{device_id}/rpt/state ç¶æä¸æ¥ å
å«å¿è·³ãçµéãç½ç»ãå½å工使¨¡å¼ |
| | | snlp/v1/{device_id}/rpt/event äºä»¶ä¸æ¥ å¤é/ä¼ç 触åãASRè¯å«ç»æãå¼å¸¸æ¥è¦ |
| | | snlp/v1/{device_id}/rpt/log ç³»ç»æ¥å¿ ç¨äºè¿ç¨æéçç³»ç»è¿è¡æ¥å¿ |
| | | snlp/v1/{device_id}/rpt/ack æä»¤ååº å
³é®ï¼ å¯¹åº CMD çæ§è¡ç»æ (æå/失败/è¿åº¦) |
| | | snlp/v1/{device_id}/rpt/progress è¿ç¨è¿åº¦ é对â弿¥æä»¶ä¸è½½âåâOTAå级â |
| | | cmd (äº->ç»ç«¯) snlp/v1/{device_id}/cmd/ctl 宿¶æä»¤ 宿¶æä»¤ï¼é³éãä¼ç ãNTPï¼ |
| | | snlp/v1/{device_id}/cmd/task 弿¥ä»»å¡æä»¤ 弿¥ä»»å¡æä»¤ï¼OTAãä¸è½½èµæºå
ï¼ |
| | | |
| | | v1 : è¡¨ç¤ºçæ¬å·1ï¼ ç¨äºçæ¬è¿æ¸¡æ¶çå
¼å®¹æ§å¤ç |
| | | device_idï¼ æ¯è®¾å¤çå¯ä¸idï¼åªè¯»ï¼æ¯ä¸è½è¢«ä¿®æ¹ç ï¼ä»¥ååèèå¢å æ³¨åæºå¶,æ¢åæ£å¼ç Access Tokenï¼ |
| | | directï¼ rpt (ç»ç«¯->äº) ï¼ cmd (äº->ç»ç«¯) |
| | | msg_type: state, event,log,ack,progress, task, ctl |
| | | |
| | | |
| | | äºãPayload 设计è§èï¼ä¸ºäºä¿æä¸è´æ§ï¼ææ Payload åéç¨ JSON æ ¼å¼ï¼å¹¶ç»ä¸å
å« Header åºåã |
| | | 1. æå¡ç«¯ä¸åæä»¤ï¼ snlp/v1/{device_id}/cmd/ctl |
| | | { |
| | | "header": { |
| | | "version": "1.0", // åè®®è¯ä¹çæ¬ |
| | | "device_id": "...", // å¿
é¡»å
å« |
| | | "req_id": "UUID-...", // ä»
CMD ç±»æä»¤å¿
é¡» |
| | | "timestamp": 1718365200, // ç»ä¸ Unix æ¶é´æ³ |
| | | "ttl": 1001 // å½ä»¤æææï¼åä½ä¸ºç§ |
| | | }, |
| | | "action": "ACTION_TYPE", // å¦: PLAY_MEDIA, SYNC_CLOCK, SET_ALARM |
| | | "payload": { ... } // å
·ä½æä½åæ° |
| | | } |
| | | å
·ä½çpayload示ä¾ï¼ |
| | | ææ¾é³é¢ (play_media): {"url": "...", "md5": "...", "volume": 50, "loop": false} |
| | | 宿¶é¹é (set_alarm): {"id": "alarm_01", "time": "07:00", "repeat": [1,2,3,4,5], "action": "add"} |
| | | ä¸è½½ä»»å¡ (download_task): {"url": "...", "md5": "...", "target_path": "/data/unit1/lesson1.mp3"} |
| | | ä»»å¡è¿åº¦åé¦ (task_progress): { "task_id": "UUID-...", "status": "downloading", "progress": 45, "info": "Downloading assets..." } |
| | | |
| | | |
| | | 2. ç»ç«¯ä¸æ¥ï¼ ç»ç«¯ä¸æ¥æ°æ® (Topic: rpt/state, rpt/event, rpt/ack) |
| | | { |
| | | "header": { |
| | | "device_id": "SN-88293021", |
| | | "timestamp": 1718365205 |
| | | }, |
| | | "msg_type": "state", // æ event, ack |
| | | "payload": { ... } |
| | | } |
| | | å
·ä½çpayload示ä¾ï¼ |
| | | å¿è·³/å¥åº· (state): {"cpu": 15, "mem": 45, "battery": 80, "net": "wifi", "mode": "idle"} |
| | | æä»¤ååº (ack - å¿
å): { "req_id": "UUID-xxxx-xxxx", "status": "success", "error_code": 0 } |
| | | äºä»¶ä¸æ¥ (event): {"event": "asr_result", "text": "Good morning", "confidence": 0.95} |
| | | |
| | | |
| | | ä¸ã action åæ®µä¸å
·ä½æ§è¡å¨ä½ç对åºå
³ç³»è¡¨ |
| | | 1. æä»¤æ å°åå
¸ |
| | | æä»¤åç±» åè½æè¿° Action åæ®µ åæ°å®ä¹ (Payload 示ä¾) |
| | | åºç¡æ§å¶ 设置设å¤é³é (0-100) SET_VOLUME {"level": 50} |
| | | åæ¢çµæºç¶æ SET_POWER {"state": "sleep/wake"} |
| | | 忥 NTP æ¶é´æ³ SET_CLOCK {"ts": 1718365200} |
| | | åªä½ç®¡ç 触åé³é¢ææ¾ PLAY_MEDIA {"path": "/data/unit1/lesson1.mp3", "volume": 50, "loop": false} |
| | | 忢å½åææ¾ STOP_MEDIA {} |
| | | æä»¶ç®¡ç 触å弿¥æä»¶ä¸è½½ DOWNLOAD {"url": "...", "md5": "...", "target_path": "/data/unit1/lesson1.mp3"} |
| | | å 餿å®è·¯å¾æä»¶ DELETE_FILE {"path": "/data/..."} |
| | | å¦ä¹ é
ç½® ä¸å/æ´æ°è¯¾ç¨èµæºç´¢å¼ UPDATE_MANIFEST {"course_id": "...", "assets": [{"url": "...", "md5": "...", "target_path": "/data/unit1/lesson1.mp3"}]} |
| | | 管ç弿ºé¹é SET_ALARM {"id": "alarm_01", "time": "07:00", "repeat": [1,2,3,4,5], "action": "add"} |
| | | å é¤å¼æºé¹é UNSET_ALARM {"id": "alarm_01"} |
| | | ç³»ç»ç»´æ¤ 触ååºä»¶å¨çº¿å级 OTA_UPDATE {"url": "...", "version": "1.0.2"} |
| | | å»¶æ¶éå¯è®¾å¤ REBOOT {"delay": 5} |
| | | |
| | | 2. è¯¦ç»æ¶æ¯æµé»è¾è¡¨ |
| | | 设å¤å¼æº/宿¶ å¿è·³/ç¶æ rpt/state ä¸éè¦ |
| | | æ§è¡å½ä»¤ æä»¤ååº rpt/ack å¿
é¡» (峿¶åé¦) |
| | | åºæ¯åæ¢/ASR äºä»¶ä¸æ¥ rpt/event ä¸éè¦ |
| | | æå¡ç«¯æä½ æ§å¶æä»¤ cmd/operate 设å¤å¤çåå¿
é¡»å rpt/ack |
| | | |
| | | |
| | | åãå
³äºä»»å¡åè¡¨ä¸æä»¶ç®¡çï¼ |
| | | ä¸è½½ä»»å¡ï¼ ä¸å任塿¶ï¼Payload åºå
嫿件 URL å MD5ãç»ç«¯ä¸è½½è¿ç¨ä¸éè¿ rpt/ack 宿¶ä¸æ¥ progress (0-100)ã |
| | | ä»»å¡åè¡¨ï¼ æå¡ç«¯å¯ä»¥ä¸åä¸ä¸ªâ课ç¨èµæºæ¸
åâç JSON æ°ç»ï¼ç»ç«¯æ¶å°ååå
¥æ¬å°æä»¶ï¼å®ç°èµæºç管çä¸å¯¹æ¯ã |
| | | |
| | | äºã åè®®å®æ½å»ºè®®ï¼Checklistï¼ |
| | | 1. QoS 设置çç¥ï¼ |
| | | cmd/ctl (宿¶æ§å¶): QoS 1 (è³å°éè¾¾ä¸æ¬¡)ã |
| | | rpt/state (å¿è·³): QoS 0 (ä¸ä¿è¯éè¾¾ï¼å轻并ååå)ã |
| | | rpt/ack (æä»¤ååº): QoS 1 (å¿
须确ä¿äºç«¯ç¥æä»»å¡å®ææ
åµ)ã |
| | | 2. å®å
¨æ§è¡¥å
ï¼ |
| | | å¨ MQTT è¿æ¥é¶æ®µéç¨ TLS ååè®¤è¯ (MQTTS)ã |
| | | Payload å
é¨è½ç¶æ timestampï¼ä½å¨ Broker å±å»ºè®®å¼å¯ MQTT Client ID å¯ä¸æ§æ ¡éªï¼é²æ¢å¤è®¾å¤æ¢å IDã |
| | | 3. æ©å±æ§é¢çï¼ |
| | | å¨ cmd/operate ç Action åæ®µè®¾è®¡ä¸ï¼é¢ç USER_DEFINED_xxx æ¥å£ï¼ä»¥ä¾¿åç»å¿«éå¢å å¦âè¿ç¨è°è¯âãâæ¥å¿æåâçåè½ï¼æ éä¿®æ¹åæè§£ææ¶æã |
| | | |
| | | 第ä¸é¨åï¼è§é¢æä»¶ç®å½ç»æåç»æ |
| | | 1. ææè¯¾ç¨çæ ¹ç®å½å¨ï¼./data/coursesï¼ ./data/coursesindex.jsonæä»¶å
容æ¯å·²ç¡®å®åå¸ç课ç¨ï¼ç»æå¦ï¼ |
| | | { |
| | | "courses": [ |
| | | { |
| | | "course_id": "course01", |
| | | "config": "courses/course01/course.json" |
| | | }, |
| | | { |
| | | "course_id": "course02", |
| | | "config": "courses/course02/course.json" |
| | | } |
| | | ] |
| | | } |
| | | 2. æ¯ä¸ªè¯¾ç¨ç®å½ä¸æä¸ä»½course.jsonï¼è¯¥æä»¶å
容æ¯å½åç®å½æå¯¹åºç课ç¨çè§é¢æä»¶æ¸
åï¼ç»æå¦ï¼ |
| | | { |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01", |
| | | "chapters": [ |
| | | { |
| | | "chapter_id": "chapter01", |
| | | "chapter_name": "第ä¸ç« ", |
| | | "videos": [ |
| | | { |
| | | "id": "001", |
| | | "title": "课ç¨001", |
| | | "path": "courses/course01/chapter01/resources/001.mp4" |
| | | }, |
| | | { |
| | | "id": "002", |
| | | "title": "课ç¨002", |
| | | "path": "courses/course01/chapter01/resources/002.mp4" |
| | | }, |
| | | { |
| | | "id": "003", |
| | | "title": "课ç¨003", |
| | | "path": "courses/course01/chapter01/resources/003.mp4" |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | |
| | | #TODOï¼ |
| | | æ ¹æ®ä»¥ä¸éæ±æè¿°åmqttè§èå®ä¹ï¼è¯·å®æä»¥ä¸å·¥ä½ï¼ |
| | | 1. 宿server端çåºç¡åè½å¼åï¼ |
| | | 2. å®ä¹ä¸å¥åºç¡è¯¾ç¨ï¼åæ¾å¨æ¬å°ç®å½ï¼åæå¯ä»¥äººå·¥æ ¹æ®è¿ä¸ªåºç¡ççæ¬è¿è¡è¯¾ç¨å¢è®¾ï¼ï¼ |
| | | 3. å®ä¹tasklist.json(å¶å®ä¸å¥åºç¡ç任塿¸
åï¼ç¨äºä¸åå°ç»ç«¯è®¾å¤è¿è¡åå§å)ï¼ä¿è¯å®¢æ·ç«¯å¯ä»¥èªå¨åå§åçåè½ï¼ |
| | | 4. å®ä¹courseTask.json(å¶å®æä¸é¨è¯¾ç¨ä»»å¡æ¸
åï¼ç¨äºä¸åå°ç»ç«¯è®¾å¤è¿è¡è¯¾ç¨åå§å)ï¼ä½¿å®¢æ·ç«¯åæå¡ç«¯å¯ä»¥æä¸å¥èªå¨æºå¶ï¼ |
| | | 客æ·ç«¯åæå¡ç«¯å¯ä»¥èªå¨åçé
å宿叏è§çåè½åæ£å¸¸ç工使µç¨ï¼ |
| | | |
| | | |
| | | |
| | | |
| | | 以䏿¯ä¸æ¹ä»£ç æ
åµä¸ç**æå¨æ´æ°è¯¾ç¨**æä½è¯´æï¼åä¸ç§æ¹å¼ã |
| | | |
| | | --- |
| | | |
| | | ## æ¹å¼ä¸ï¼HTTP API 触åï¼æ¨èï¼æç®åï¼ |
| | | |
| | | æå¡ç«¯ä¼éè¿ MQTT èªå¨åç»ç«¯ä¸å `SYNC_COURSE_TASK`ã |
| | | |
| | | ### 1. æ´æ°åé¨è¯¾ç¨ |
| | | |
| | | ```bash |
| | | curl -X POST http://ä½ çæå¡å¨:9500/api/v1/devices/course-init \ |
| | | -H "Content-Type: application/json" \ |
| | | -d '{ |
| | | "device_id": "SN-88293021", |
| | | "course_id": "course01" |
| | | }' |
| | | ``` |
| | | |
| | | ç»ç«¯ä¼æ¶å° **1 æ¡** `cmd/task` æ¶æ¯ï¼å
容为 `course01` ç宿´ `courseTask.json`ï¼å« `assets` å表ï¼ã |
| | | |
| | | ### 2. 鿰忥å
¨é¨åºç¡è¯¾ç¨ï¼index.json 䏿æè¯¾ï¼ |
| | | |
| | | ```bash |
| | | # ç¬¬ä¸æ¥ï¼æ¸
é¤è¯¥è®¾å¤çåå§åè®°å½ |
| | | curl -X DELETE http://ä½ çæå¡å¨:9500/api/v1/devices/initialized/SN-88293021 |
| | | |
| | | # ç¬¬äºæ¥ï¼å¼ºå¶éæ°åå§åï¼å«å
¨é¨åºç¡è¯¾ç¨åæ¥ï¼ |
| | | curl -X POST http://ä½ çæå¡å¨:9500/api/v1/devices/init \ |
| | | -H "Content-Type: application/json" \ |
| | | -d '{ |
| | | "device_id": "SN-88293021", |
| | | "force": true |
| | | }' |
| | | ``` |
| | | |
| | | ä¼å¯¹ `index.json` 鿝é¨è¯¾åä¸å **1 æ¡** `SYNC_COURSE_TASK`ï¼å½å为 course01ãcourse02 å
± 2 æ¡ï¼ã |
| | | |
| | | --- |
| | | |
| | | ## æ¹å¼äºï¼ç´æ¥å MQTT æ¶æ¯ |
| | | |
| | | ç¨ MQTT 客æ·ç«¯ï¼MQTTXãmosquitto_pub çï¼è¿æ¥ Brokerï¼åç»ç«¯å叿¶æ¯ã |
| | | |
| | | **Topicï¼** |
| | | ``` |
| | | snlp/v1/{device_id}/cmd/task |
| | | ``` |
| | | |
| | | **示ä¾ï¼course01ï¼ï¼** |
| | | |
| | | ```json |
| | | { |
| | | "header": { |
| | | "version": "1.0", |
| | | "device_id": "SN-88293021", |
| | | "req_id": "æå¨å¡«å-uuid", |
| | | "timestamp": 1718365200, |
| | | "ttl": 7200 |
| | | }, |
| | | "action": "SYNC_COURSE_TASK", |
| | | "payload": { |
| | | "version": "1.0", |
| | | "course_id": "course01", |
| | | "course_name": "å°æºå¦ä¹ 课ç¨01", |
| | | "description": "课ç¨01ç»ç«¯åå§åä»»å¡ï¼ä¸è½½ç« èè§é¢å¹¶æ´æ°æ¬å°èµæºæ¸
å", |
| | | "assets": [ |
| | | { |
| | | "url": "http://ä½ çæå¡å¨:9500/static/courses/course01/chapter01/resources/001.mp3", |
| | | "md5": "...", |
| | | "title": "第ä¸ç« 课ç¨001", |
| | | "target_path": "courses/course01/chapter01/resources/001.mp3", |
| | | "cover_url": "http://ä½ çæå¡å¨:9500/static/courses/course01/chapter01/resources/001.png", |
| | | "cover_target_path": "courses/course01/chapter01/resources/001.png" |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **注æï¼** |
| | | - `url` / `cover_url` å¿
é¡»æ¯ç»ç«¯è½è®¿é®ç**宿´ HTTP å°å**ï¼ä¸è½ç¨ `localhost`ï¼ |
| | | - `target_path` / `cover_target_path` ä¿æç¸å¯¹è·¯å¾ï¼ç»ç«¯ææ¤è·¯å¾åå°åå¨å¡ |
| | | - å¯å
è° API æ¿å°å®æ´ payloadï¼ååæ ·åå° MQTTï¼ |
| | | |
| | | ```bash |
| | | # è·å已补å
¨ URL ç courseTask å
容 |
| | | curl http://ä½ çæå¡å¨:9500/api/v1/courses/course01/manifest |
| | | |
| | | # è·åå
¨é¨åºç¡è¯¾ç¨ |
| | | curl http://ä½ çæå¡å¨:9500/api/v1/courses/base-manifest |
| | | ``` |
| | | |
| | | æè¿åç JSON ä½ä¸º MQTT æ¶æ¯ç `payload` åæ®µå³å¯ã |
| | | |
| | | **mosquitto_pub 示ä¾ï¼** |
| | | |
| | | ```bash |
| | | mosquitto_pub -h ä½ çMQTTå°å -p 2100 \ |
| | | -u ç¨æ·å -P å¯ç \ |
| | | -t "snlp/v1/SN-88293021/cmd/task" \ |
| | | -m '{"header":{...},"action":"SYNC_COURSE_TASK","payload":{...}}' |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## æ¹å¼ä¸ï¼ç»ç«¯ç´æ¥éè¿ URL æåï¼ä¸ç»è¿ MQTT 触åï¼ |
| | | |
| | | è¥ç»ç«¯æ¯æèªè¡æåï¼å¯ç´æ¥è®¿é®æå¡ç«¯éæèµæºï¼ |
| | | |
| | | | èµæº | URL | |
| | | |------|-----| |
| | | | 课ç¨ä»»å¡å
| `http://æå¡å¨:9500/static/courses/course01/courseTask.json` | |
| | | | 课ç¨é
ç½® | `http://æå¡å¨:9500/static/courses/course01/course.json` | |
| | | | 课ç¨ç´¢å¼ | `http://æå¡å¨:9500/static/courses/index.json` | |
| | | | é³é¢æä»¶ | `http://æå¡å¨:9500/static/courses/course01/chapter01/resources/001.mp3` | |
| | | | å°é¢å¾ | `http://æå¡å¨:9500/static/courses/course01/chapter01/resources/001.png` | |
| | | |
| | | **æå¨æ´æ°æµç¨ï¼** |
| | | |
| | | 1. 卿å¡ç«¯æ´æ° `data/courses/` ä¸ç `courseTask.json` åèµæºæä»¶ |
| | | 2. ç»ç«¯ä¸è½½ææ°ç `courseTask.json` |
| | | 3. æ `assets` 鿝ä¸é¡¹ç `url` ä¸è½½æä»¶ï¼ä¿åå° `target_path` 对åºè·¯å¾ |
| | | |
| | | æ¬å° `courseTask.json` 鿝ç¸å¯¹è·¯å¾ï¼ç»ç«¯éèªè¡æ¼æï¼ |
| | | |
| | | ``` |
| | | ä¸è½½å°å = http://æå¡å¨:9500/static/ + assets[].url |
| | | ä¿åè·¯å¾ = åå¨å¡æ ¹ç®å½ + assets[].target_path |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## æ¹å¼å¯¹æ¯ |
| | | |
| | | | æ¹å¼ | éç¨åºæ¯ | æ¯å¦éè¦ MQTT | |
| | | |------|----------|---------------| |
| | | | `POST /devices/course-init` | æ´æ°åé¨è¯¾ | æ¯ï¼æå¡ç«¯ä»£åï¼ | |
| | | | `POST /devices/init?force=true` | å
¨éé忥 | æ¯ | |
| | | | ç´æ¥å MQTT `SYNC_COURSE_TASK` | è°è¯ãç¬¬ä¸æ¹ç³»ç»å¯¹æ¥ | æ¯ | |
| | | | ç»ç«¯ç´æ¥ HTTP ä¸è½½ | ç»ç«¯èªå¸¦æåé»è¾ | å¦ | |
| | | |
| | | --- |
| | | |
| | | ## æ´æ°æå¡ç«¯è¯¾ç¨èµæºåç建议æ¥éª¤ |
| | | |
| | | 1. ä¿®æ¹ `data/courses/course01/courseTask.json`ï¼åé³é¢ãå°é¢æä»¶ï¼ |
| | | 2. 确认æµè§å¨è½è®¿é®ï¼ |
| | | `http://æå¡å¨:9500/static/courses/course01/courseTask.json` |
| | | 3. éæå¨è§¦åæ¹å¼ï¼ |
| | | - åªæ´æ°ä¸é¨è¯¾ â `course-init` |
| | | - æ´æ°å
¨é¨åºç¡è¯¾ â `force: true` éæ° initï¼æéé¨è¯¾è° `course-init` |
| | | 4. ç»ç«¯æ§è¡å®åéè¿ `rpt/ack` / `rpt/progress` 䏿¥ç»æï¼è¥ç»ç«¯å·²å®ç°ï¼ |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| New file |
| | |
| | | 第ä¸é¨åï¼äº§åéæ±è§æ ¼è¯´æä¹¦ï¼PRDï¼ï¼æ
å¢æç¥æºè½è±è¯å¦ä¹ ç³»ç»
|
| | |
|
| | | 1. é¡¹ç®æ¦è¿°
|
| | | 1.1 项ç®èæ¯
|
| | | ç°æè±è¯å¦ä¹ 产åå¤ä¸ºç¦»æ£çâé¢åºå¼âæâ课æ¬å¼âæå¦ï¼ç¼ºä¹è¯å¢ä¸åºç¨ç»åãæ¬äº§åéè¿å¼å
¥è¾¹ç¼è®¡ç®ä¸å¤§æ¨¡åææ¯ï¼å®ç°åºäºç©çç¯å¢æç¥çæ²æµ¸å¼è±è¯æå¦ï¼è§£å³âå¦èä¸ç¨âçé®é¢ã
|
| | |
|
| | | 1.2 产åç®æ
|
| | | - å®ç°âåºæ¯æç¥ -> æå¾åæ -> æºè½å¯¹è¯ -> å¦ä¹ åé¦âçéç¯ç³»ç»ã
|
| | | - æé å
·å¤ IoT åºç¡æ§å¶ãå¤åè½éæä¸é«éç§ä¿æ¤çå¦ä¹ ç»ç«¯ã
|
| | | - æå»ºäºç«¯ AI æå¦ç©éµï¼æ¯æèªéåºé¾åº¦è°æ´ä¸ä¸ªæ§åå¦ä¹ 轨迹ã
|
| | |
|
| | | 2. ç»ç«¯è®¾å¤è¯¦ç»éæ±
|
| | | 2.1 交äºç¶ææºï¼FSMï¼
|
| | | - å¾
æºï¼Idleï¼ï¼å®æ¶è¿è¡ VAD æ£æµï¼ä½åèè¿è¡ã
|
| | | - å¯¹è¯æ¨¡å¼ï¼Dialogueï¼ï¼å¤çå
¨åå·¥è¯é³æµï¼æ¯æææï¼Barge-inï¼ã
|
| | | - ç¿»è¯æ¨¡å¼ï¼Translateï¼ï¼çæ¶å½é³ï¼äºç«¯ææ¬å°æ¨¡åè¿è¡ç¿»è¯å¹¶ææ¾ã
|
| | | - æå¦æ¨¡å¼ï¼Studyï¼ï¼å è½½é¢è®¾è¯¾ç¨å
å®¹ï¼æ¯æç¹è¯»ãè·è¯»ãè¯åã
|
| | | - ä¼ç ï¼Sleepï¼ï¼ä»
ä¿çå¿è·³æ£æµä¸é¢è®¾å¤éè¯çå¬ï¼ç¦ç¨éº¦å
é£éµåã
|
| | |
|
| | | 2.2 æ ¸å¿åè½ææ¯ææ
|
| | | - è¯é³ééï¼æ¯æ 3 ç±³è¿åºæ¾é³ï¼ä¿¡åªæ¯ (SNR) > 15dBï¼æ¯æå£°æºå®ä½ (DoA)ã
|
| | | - éåª/å声æ¶é¤ (AEC)ï¼æ¯æ 50dB 以ä¸å声æ¶é¤ï¼ç¡®ä¿ç»ç«¯ææ¾é³é¢æ¶ä»å¯æ¾åç¨æ·è¯é³ã
|
| | | - æ¬å°ç¿»è¯ï¼æ¯æç¦»çº¿ä¸è±äºè¯ï¼é¦åå»¶è¿ < 300msï¼ã
|
| | | - ååºé度ï¼ä»ééå°æå¡ç«¯ä¸åååºç RTT (Round Trip Time) éæ§å¶å¨ 1.5s 以å
ã
|
| | |
|
| | | 2.3 IoT ä¸ç³»ç»éæ
|
| | | - çµæºç®¡çï¼æ¯æ RTC 宿¶ï¼æ¯æå¼å¸¸æçµèªå¯å¨ï¼æ¯æçµé使¶èªå¨è¿å
¥ççµæ¨¡å¼ã
|
| | | - ç½ç»è¿æ¥ï¼èªå¨åæ¢ Wi-Fi/4G/5Gï¼æ¯ææç½æ¬å°ç¼åï¼æå¤§æ¯æ 100MB è¯é³ç段ç¼åï¼ã
|
| | | - å®å
¨æ§ï¼ç©ç麦å
é£éé³å¼å
³ï¼ç¡¬ä»¶çº§æå¼ï¼ï¼ç«¯ä¾§æ°æ®å å¯åå¨ (AES-128)ã
|
| | |
|
| | | 3. æå¡ç«¯å¹³å°è¯¦ç»éæ±
|
| | | 3.1 åºæ¯è¯å«å¼æ (AI Context Engine)
|
| | | - åºæ¯è§£æï¼éè¿ LLM 对ç¯å¢é³è¯ä¹çè§£ï¼åç±»å¨å®ä¹ä¸ºï¼å±
å®¶(å§å®¤/卿¿/书æ¿)ãæ ¡å
ãéå¤ãè¿å¨çã
|
| | | - 对è¯çæï¼éæ LLM Agentï¼åºäºç¨æ·ç»ååå²ä¸å½ååºæ¯ï¼å¨æçæä¸ä¸è½®æé®ï¼æ¯æå¤è½®ä¸ä¸æç®¡çï¼ã
|
| | | - 置信度æºå¶ï¼è¥åºæ¯è¯å«ç½®ä¿¡åº¦ < 0.7ï¼è§¦åéç¨é®åçç¥ï¼é¿å
å¹²æ°ç¨æ·ã
|
| | |
|
| | | 3.2 æå¦ç®¡æ§ç³»ç»
|
| | | - ç¨æ·æ¨¡åï¼è®°å½ç¨æ·çCEFRç级ãè¯æ±åºãé误çï¼éè¿åé¦å¾ªç¯å¨æè°èé¾åº¦ã
|
| | | - æ°æ®åå¨ï¼ç¬¦å GDPR/PIPL éç§åè§ï¼ææè¯é³æ°æ®å®æè±æå¤çï¼ä¸çåæªç»è®¸å¯çåå§é³é¢ã
|
| | |
|
| | | 4. éåè½æ§éæ±ï¼SLAï¼
|
| | | - å¯ç¨æ§ï¼ç³»ç»å¯ç¨æ§ 99.9%ã
|
| | | - æ§è½ç¶é¢ï¼å¤§æ¨¡åæ¨çæ¥å£æ¯æè³å° 1000 QPS å¹¶åã
|
| | | - å®å
¨åè§ï¼éè¿å å¯ééï¼TLS 1.3ï¼éä¿¡ï¼è®¾å¤ç»å®å
·å¤å¯ä¸çå®å
¨è¯çæ ¡éªï¼Secure Elementï¼ã
|
| | | - 坿©å±æ§ï¼æ¯æéè¿æä»¶æºå¶å¢å æ°è¯ç§æåºæ¯æ¨¡æ¿ã
|
| | |
|
| | | 5. 交äºåè®®ä¸æ°æ®æµï¼APIè§èï¼
|
| | | - è¯·æ±æ¥å£ï¼/api/v1/scene/analyze (POST)
|
| | | - Payload: {timestamp, audio_blob, device_id, context_hints}
|
| | | - ååºæ¥å£ï¼/api/v1/dialogue/stream (SSE)
|
| | | - Payload: {dialogue_id, text, audio_url, intent_type, difficulty_level}
|
| | | - æä»¤åæ¥ï¼éè¿ MQTT Broker ä¸åæ§å¶æä»¤ï¼å¦ï¼SetVolume, Sleep, UpdateFirmwareï¼ã
|
| | |
|
| | | 6. å
¸ååºæ¯é»è¾æµç¨å®ä¹
|
| | | - åºæ¯è¯å«ï¼æ£æµå°ç¹å®ç¯å¢é³çº¹ -> å¤å®ä¸ºâç¨é¤åºæ¯â -> åéæä»¤è³ç»ç«¯ææ¾å¼å¯¼æç¤ºé³ã
|
| | | - åå¤å¤çï¼æ£æµç¨æ·åå¤ -> ASRè¯å« -> åæè¯æ³ä¸æ£ç¡®æ§ -> è¿ååé¦ï¼èµèµæçº éï¼-> è¿å
¥ä¸ä¸è½®æé®ã
|
| | |
|
| | |
|
| | |
|
| | | 第äºé¨åï¼åç
§ä»¥ä¸éæ±ï¼åæ¥ä¸å¥mqttæ¶æ¯çtopicè§åå¦ä¸:
|
| | | ä¸ãæ¶æ¯topicå®ä¹ï¼ snlp/v1/{device_id}/{direct}/{msg_type} |
| | | è¡¨æ ¼å¦ä¸ï¼ |
| | | æ¹å Topic è·¯å¾ msg_type 说æ
|
| | | rpt (ç»ç«¯->äº) snlp/v1/{device_id}/rpt/state ç¶æä¸æ¥ å
å«å¿è·³ãçµéãç½ç»ãå½å工使¨¡å¼
|
| | | snlp/v1/{device_id}/rpt/event äºä»¶ä¸æ¥ å¤é/ä¼ç 触åãASRè¯å«ç»æãå¼å¸¸æ¥è¦
|
| | | snlp/v1/{device_id}/rpt/log ç³»ç»æ¥å¿ ç¨äºè¿ç¨æéçç³»ç»è¿è¡æ¥å¿
|
| | | snlp/v1/{device_id}/rpt/ack æä»¤ååº å
³é®ï¼ å¯¹åº CMD çæ§è¡ç»æ (æå/失败/è¿åº¦)
|
| | | snlp/v1/{device_id}/rpt/progress è¿ç¨è¿åº¦ é对â弿¥æä»¶ä¸è½½âåâOTAå级â
|
| | | cmd (äº->ç»ç«¯) snlp/v1/{device_id}/cmd/ctl 宿¶æä»¤ 宿¶æä»¤ï¼é³éãä¼ç ãNTPï¼
|
| | | snlp/v1/{device_id}/cmd/task 弿¥ä»»å¡æä»¤ 弿¥ä»»å¡æä»¤ï¼OTAãä¸è½½èµæºå
ï¼
|
| | |
|
| | | v1 : è¡¨ç¤ºçæ¬å·1ï¼ ç¨äºçæ¬è¿æ¸¡æ¶çå
¼å®¹æ§å¤ç
|
| | | device_idï¼ æ¯è®¾å¤çå¯ä¸idï¼åªè¯»ï¼æ¯ä¸è½è¢«ä¿®æ¹ç ï¼ä»¥ååèèå¢å æ³¨åæºå¶,æ¢åæ£å¼ç Access Tokenï¼ |
| | | directï¼ rpt (ç»ç«¯->äº) ï¼ cmd (äº->ç»ç«¯) |
| | | msg_type: state, event,log,ack,progress, task, ctl |
| | |
|
| | |
|
| | | äºãPayload 设计è§èï¼ä¸ºäºä¿æä¸è´æ§ï¼ææ Payload åéç¨ JSON æ ¼å¼ï¼å¹¶ç»ä¸å
å« Header åºåã
|
| | | 1. æå¡ç«¯ä¸åæä»¤ï¼ snlp/v1/{device_id}/cmd/ctl
|
| | | {
|
| | | "header": {
|
| | | "version": "1.0", // åè®®è¯ä¹çæ¬
|
| | | "device_id": "...", // å¿
é¡»å
å«
|
| | | "req_id": "UUID-...", // ä»
CMD ç±»æä»¤å¿
é¡»
|
| | | "timestamp": 1718365200, // ç»ä¸ Unix æ¶é´æ³
|
| | | "ttl": 1001 // å½ä»¤æææï¼åä½ä¸ºç§
|
| | | },
|
| | | "action": "ACTION_TYPE", // å¦: PLAY_MEDIA, SYNC_CLOCK, SET_ALARM
|
| | | "payload": { ... } // å
·ä½æä½åæ°
|
| | | }
|
| | | å
·ä½çpayload示ä¾ï¼
|
| | | ææ¾é³é¢ (play_media): {"url": "...", "md5": "...", "volume": 50, "loop": false}
|
| | | 宿¶é¹é (set_alarm): {"id": "alarm_01", "time": "07:00", "repeat": [1,2,3,4,5], "action": "add"}
|
| | | ä¸è½½ä»»å¡ (download_task): {"url": "...", "md5": "...", "target_path": "/data/unit1/lesson1.mp3"}
|
| | | ä»»å¡è¿åº¦åé¦ (task_progress): { "task_id": "UUID-...", "status": "downloading", "progress": 45, "info": "Downloading assets..." }
|
| | |
|
| | |
|
| | | 2. ç»ç«¯ä¸æ¥ï¼ ç»ç«¯ä¸æ¥æ°æ® (Topic: rpt/state, rpt/event, rpt/ack)
|
| | | {
|
| | | "header": {
|
| | | "device_id": "SN-88293021",
|
| | | "timestamp": 1718365205
|
| | | },
|
| | | "msg_type": "state", // æ event, ack
|
| | | "payload": { ... }
|
| | | }
|
| | | å
·ä½çpayload示ä¾ï¼
|
| | | å¿è·³/å¥åº· (state): {"cpu": 15, "mem": 45, "battery": 80, "net": "wifi", "mode": "idle"}
|
| | | æä»¤ååº (ack - å¿
å): { "req_id": "UUID-xxxx-xxxx", "status": "success", "error_code": 0 }
|
| | | äºä»¶ä¸æ¥ (event): {"event": "asr_result", "text": "Good morning", "confidence": 0.95}
|
| | |
|
| | |
|
| | | ä¸ã action åæ®µä¸å
·ä½æ§è¡å¨ä½ç对åºå
³ç³»è¡¨
|
| | | 1. æä»¤æ å°åå
¸
|
| | | æä»¤åç±» åè½æè¿° Action åæ®µ åæ°å®ä¹ (Payload 示ä¾)
|
| | | åºç¡æ§å¶ 设置设å¤é³é (0-100) SET_VOLUME {"level": 50}
|
| | | åæ¢çµæºç¶æ SET_POWER {"state": "sleep/wake"}
|
| | | 忥 NTP æ¶é´æ³ SET_CLOCK {"ts": 1718365200}
|
| | | åªä½ç®¡ç 触åé³é¢ææ¾ PLAY_MEDIA {"path": "/data/unit1/lesson1.mp3", "volume": 50, "loop": false}
|
| | | 忢å½åææ¾ STOP_MEDIA {}
|
| | | æä»¶ç®¡ç 触å弿¥æä»¶ä¸è½½ DOWNLOAD {"url": "...", "md5": "...", "target_path": "/data/unit1/lesson1.mp3"}
|
| | | å 餿å®è·¯å¾æä»¶ DELETE_FILE {"path": "/data/..."}
|
| | | å¦ä¹ é
ç½® ä¸å/æ´æ°è¯¾ç¨èµæºç´¢å¼ UPDATE_MANIFEST {"course_id": "...", "assets": [{"url": "...", "md5": "...", "target_path": "/data/unit1/lesson1.mp3"}]}
|
| | | 管ç弿ºé¹é SET_ALARM {"id": "alarm_01", "time": "07:00", "repeat": [1,2,3,4,5], "action": "add"}
|
| | | å é¤å¼æºé¹é UNSET_ALARM {"id": "alarm_01"}
|
| | | ç³»ç»ç»´æ¤ 触ååºä»¶å¨çº¿å级 OTA_UPDATE {"url": "...", "version": "1.0.2"}
|
| | | å»¶æ¶éå¯è®¾å¤ REBOOT {"delay": 5}
|
| | |
|
| | | 2. è¯¦ç»æ¶æ¯æµé»è¾è¡¨
|
| | | 设å¤å¼æº/宿¶ å¿è·³/ç¶æ rpt/state ä¸éè¦
|
| | | æ§è¡å½ä»¤ æä»¤ååº rpt/ack å¿
é¡» (峿¶åé¦)
|
| | | åºæ¯åæ¢/ASR äºä»¶ä¸æ¥ rpt/event ä¸éè¦
|
| | | æå¡ç«¯æä½ æ§å¶æä»¤ cmd/operate 设å¤å¤çåå¿
é¡»å rpt/ack
|
| | |
|
| | |
|
| | | åãå
³äºä»»å¡åè¡¨ä¸æä»¶ç®¡çï¼
|
| | | ä¸è½½ä»»å¡ï¼ ä¸å任塿¶ï¼Payload åºå
嫿件 URL å MD5ãç»ç«¯ä¸è½½è¿ç¨ä¸éè¿ rpt/ack 宿¶ä¸æ¥ progress (0-100)ã
|
| | | ä»»å¡åè¡¨ï¼ æå¡ç«¯å¯ä»¥ä¸åä¸ä¸ªâ课ç¨èµæºæ¸
åâç JSON æ°ç»ï¼ç»ç«¯æ¶å°ååå
¥æ¬å°æä»¶ï¼å®ç°èµæºç管çä¸å¯¹æ¯ã
|
| | |
|
| | | äºã åè®®å®æ½å»ºè®®ï¼Checklistï¼
|
| | | 1. QoS 设置çç¥ï¼
|
| | | cmd/ctl (宿¶æ§å¶): QoS 1 (è³å°éè¾¾ä¸æ¬¡)ã
|
| | | rpt/state (å¿è·³): QoS 0 (ä¸ä¿è¯éè¾¾ï¼å轻并ååå)ã
|
| | | rpt/ack (æä»¤ååº): QoS 1 (å¿
须确ä¿äºç«¯ç¥æä»»å¡å®ææ
åµ)ã
|
| | | 2. å®å
¨æ§è¡¥å
ï¼
|
| | | å¨ MQTT è¿æ¥é¶æ®µéç¨ TLS ååè®¤è¯ (MQTTS)ã
|
| | | Payload å
é¨è½ç¶æ timestampï¼ä½å¨ Broker å±å»ºè®®å¼å¯ MQTT Client ID å¯ä¸æ§æ ¡éªï¼é²æ¢å¤è®¾å¤æ¢å IDã
|
| | | 3. æ©å±æ§é¢çï¼
|
| | | å¨ cmd/operate ç Action åæ®µè®¾è®¡ä¸ï¼é¢ç USER_DEFINED_xxx æ¥å£ï¼ä»¥ä¾¿åç»å¿«éå¢å å¦âè¿ç¨è°è¯âãâæ¥å¿æåâçåè½ï¼æ éä¿®æ¹åæè§£ææ¶æã
|
| | |
|
| | | 第ä¸é¨åï¼è§é¢æä»¶ç®å½ç»æåç»æ
|
| | | 1. ææè¯¾ç¨çæ ¹ç®å½å¨ï¼./data/coursesï¼ ./data/courses/index.jsonæä»¶å
容æ¯å·²ç¡®å®åå¸ç课ç¨ï¼ç»æå¦ï¼
|
| | | {
|
| | | "courses": [
|
| | | {
|
| | | "course_id": "course01",
|
| | | "config": "courses/course01/course.json"
|
| | | },
|
| | | {
|
| | | "course_id": "course02",
|
| | | "config": "courses/course02/course.json"
|
| | | }
|
| | | ]
|
| | | }
|
| | | 2. æ¯ä¸ªè¯¾ç¨ç®å½ä¸æä¸ä»½course.jsonï¼å¦:courses/course02/course.jsonæä»¶å
容æ¯å½åç®å½æå¯¹åºç课ç¨çè§é¢æä»¶æ¸
åï¼ç»æå¦ï¼
|
| | | {
|
| | | "course_id": "course01",
|
| | | "course_name": "å°æºå¦ä¹ 课ç¨01",
|
| | | "description": "课ç¨01",
|
| | | "chapters": [
|
| | | {
|
| | | "chapter_id": "chapter01",
|
| | | "chapter_name": "第ä¸ç« ",
|
| | | "audios": [
|
| | | {
|
| | | "id": "001",
|
| | | "title": "课ç¨001",
|
| | | "path": "courses/course01/chapter01/resources/001.mp3"
|
| | | },
|
| | | {
|
| | | "id": "002",
|
| | | "title": "课ç¨002",
|
| | | "path": "courses/course01/chapter01/resources/002.mp3"
|
| | | },
|
| | | {
|
| | | "id": "003",
|
| | | "title": "课ç¨003",
|
| | | "path": "courses/course01/chapter01/resources/003.mp3"
|
| | | }
|
| | | ]
|
| | | }
|
| | | ]
|
| | | }
|
| | |
|
| | | #TODOï¼
|
| | | æ ¹æ®ä»¥ä¸éæ±æè¿°åmqttè§èå®ä¹ï¼è¯·å®æä»¥ä¸å·¥ä½ï¼
|
| | | 1. 宿server端çåºç¡åè½å¼åï¼
|
| | | 2. å®ä¹ä¸å¥åºç¡è¯¾ç¨ï¼åæ¾å¨æ¬å°ç®å½ï¼åæå¯ä»¥äººå·¥æ ¹æ®è¿ä¸ªåºç¡ççæ¬è¿è¡è¯¾ç¨å¢è®¾ï¼ï¼
|
| | | 3. å®ä¹tasklist.json(å¶å®ä¸å¥åºç¡ç任塿¸
åï¼ç¨äºä¸åå°ç»ç«¯è®¾å¤è¿è¡åå§å)ï¼ä¿è¯å®¢æ·ç«¯å¯ä»¥èªå¨åå§åçåè½ï¼
|
| | | 4. å®ä¹courseTask.json(å¶å®æä¸é¨è¯¾ç¨ä»»å¡æ¸
åï¼ç¨äºä¸åå°ç»ç«¯è®¾å¤è¿è¡è¯¾ç¨åå§å)ï¼ä½¿å®¢æ·ç«¯åæå¡ç«¯å¯ä»¥æä¸å¥èªå¨æºå¶ï¼
|
| | | 客æ·ç«¯åæå¡ç«¯å¯ä»¥èªå¨åçé
å宿叏è§çåè½åæ£å¸¸ç工使µç¨ï¼
|
| New file |
| | |
| | | # HTTP æå¡
|
| | | HOST=0.0.0.0
|
| | | PORT=2101
|
| | | BASE_URL=http://124.222.109.197:2101
|
| | |
|
| | | # è¯¾ç¨æ°æ®ç®å½ï¼é»è®¤æåé¡¹ç®æ ¹ data/ï¼ä¸è¬æ éä¿®æ¹ï¼
|
| | | # DATA_DIR=
|
| | |
|
| | | # MQTT
|
| | | MQTT_BROKER=127.0.0.1
|
| | | MQTT_PORT=2100
|
| | | MQTT_USERNAME="AI_XIAOZHI"
|
| | | MQTT_PASSWORD="V9qL7!mX2@rT8$kN5^wP1&c"
|
| | | MQTT_CLIENT_ID=snlp-server
|
| | | MQTT_ENABLED=true
|
| | |
|
| | | # åå§å
|
| | | AUTO_INIT_ENABLED=true
|
| | | DEVICE_DATA_ROOT=/data
|
| | |
|
| | | # åºæ¯è¯å«ç½®ä¿¡åº¦éå¼
|
| | | SCENE_CONFIDENCE_THRESHOLD=0.7
|
| New file |
| | |
| | | from pathlib import Path
|
| | |
|
| | | from pydantic_settings import BaseSettings, SettingsConfigDict
|
| | |
|
| | |
|
| | | class Settings(BaseSettings):
|
| | | model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
| | |
|
| | | host: str = "0.0.0.0"
|
| | | port: int = 9500
|
| | | data_dir: Path = Path(__file__).resolve().parents[2] / "data"
|
| | |
|
| | | mqtt_broker: str = "124.222.109.197"
|
| | | mqtt_port: int = 2100
|
| | | mqtt_username: str = "AI_XIAOZHI"
|
| | | mqtt_password: str = "V9qL7!mX2@rT8$kN5^wP1&c"
|
| | | mqtt_client_id: str = "snlp-server"
|
| | | mqtt_enabled: bool = True
|
| | |
|
| | | scene_confidence_threshold: float = 0.7
|
| | | base_url: str = "http://localhost:9500"
|
| | |
|
| | | runtime_dir: Path = Path(__file__).resolve().parents[1] / "runtime"
|
| | | initialized_devices_file: Path | None = None
|
| | | auto_init_enabled: bool = True
|
| | | device_data_root: str = "/data"
|
| | |
|
| | | @property
|
| | | def resolved_initialized_devices_file(self) -> Path:
|
| | | if self.initialized_devices_file:
|
| | | return self.initialized_devices_file
|
| | | return self.runtime_dir / "initialized_devices.json"
|
| | |
|
| | | settings = Settings()
|
| New file |
| | |
| | | import logging
|
| | | from contextlib import asynccontextmanager
|
| | | from pathlib import Path
|
| | |
|
| | | from fastapi import FastAPI
|
| | | from fastapi.middleware.cors import CORSMiddleware
|
| | | from fastapi.staticfiles import StaticFiles
|
| | |
|
| | | from app.config import settings
|
| | | from app.mqtt.client import MqttService
|
| | | from app.mqtt.init_dispatcher import InitTaskDispatcher
|
| | | from app.routes import courses, devices, dialogue, scene
|
| | | from app.services.auto_init import AutoInitService
|
| | | from app.services.course_service import CourseService
|
| | | from app.services.device_store import DeviceStore
|
| | | from app.services.init_registry import InitRegistry
|
| | |
|
| | | logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
| | | logger = logging.getLogger(__name__)
|
| | |
|
| | |
|
| | | @asynccontextmanager
|
| | | async def lifespan(app: FastAPI):
|
| | | device_store = DeviceStore()
|
| | | init_registry = InitRegistry()
|
| | | course_service = CourseService()
|
| | | mqtt_service = MqttService(device_store)
|
| | | init_dispatcher = InitTaskDispatcher(mqtt_service, course_service)
|
| | | auto_init = AutoInitService(init_registry, init_dispatcher, mqtt_service)
|
| | | mqtt_service.auto_init = auto_init
|
| | |
|
| | | app.state.device_store = device_store
|
| | | app.state.init_registry = init_registry
|
| | | app.state.mqtt = mqtt_service
|
| | | app.state.init_dispatcher = init_dispatcher
|
| | | app.state.auto_init = auto_init
|
| | |
|
| | | mqtt_service.start()
|
| | | logger.info("SmartLearning server started")
|
| | | yield
|
| | | mqtt_service.stop()
|
| | | logger.info("SmartLearning server stopped")
|
| | |
|
| | |
|
| | | app = FastAPI(
|
| | | title="SmartLearning Server",
|
| | | description="æ
å¢æç¥æºè½è±è¯å¦ä¹ ç³»ç» - æå¡ç«¯",
|
| | | version="1.0.0",
|
| | | lifespan=lifespan,
|
| | | )
|
| | |
|
| | | app.add_middleware(
|
| | | CORSMiddleware,
|
| | | allow_origins=["*"],
|
| | | allow_credentials=True,
|
| | | allow_methods=["*"],
|
| | | allow_headers=["*"],
|
| | | )
|
| | |
|
| | | app.include_router(scene.router)
|
| | | app.include_router(dialogue.router)
|
| | | app.include_router(courses.router)
|
| | | app.include_router(devices.router)
|
| | |
|
| | | data_path = Path(settings.data_dir)
|
| | | if data_path.exists():
|
| | | app.mount("/static", StaticFiles(directory=str(data_path)), name="static")
|
| | |
|
| | |
|
| | | @app.get("/health")
|
| | | async def health():
|
| | | mqtt = app.state.mqtt if hasattr(app.state, "mqtt") else None
|
| | | return {
|
| | | "status": "ok",
|
| | | "mqtt_connected": mqtt.connected if mqtt else False,
|
| | | }
|
| New file |
| | |
| | | from typing import Any, Literal
|
| | |
|
| | | from pydantic import BaseModel, Field
|
| | |
|
| | |
|
| | | class SceneAnalyzeRequest(BaseModel):
|
| | | timestamp: int
|
| | | audio_blob: str = Field(description="Base64 encoded audio or placeholder")
|
| | | device_id: str
|
| | | context_hints: dict[str, Any] = Field(default_factory=dict)
|
| | |
|
| | |
|
| | | class SceneAnalyzeResponse(BaseModel):
|
| | | scene: str
|
| | | confidence: float
|
| | | greeting_strategy: Literal["contextual", "generic"]
|
| | | suggested_intent: str
|
| | |
|
| | |
|
| | | class DialogueStreamPayload(BaseModel):
|
| | | dialogue_id: str
|
| | | text: str
|
| | | audio_url: str | None = None
|
| | | intent_type: str
|
| | | difficulty_level: str
|
| | |
|
| | |
|
| | | class MqttHeader(BaseModel):
|
| | | version: str | None = None
|
| | | device_id: str
|
| | | req_id: str | None = None
|
| | | timestamp: int
|
| | | ttl: int | None = None
|
| | |
|
| | |
|
| | | class MqttCommand(BaseModel):
|
| | | header: MqttHeader
|
| | | action: str
|
| | | payload: dict[str, Any] = Field(default_factory=dict)
|
| | |
|
| | |
|
| | | class MqttReport(BaseModel):
|
| | | header: MqttHeader
|
| | | msg_type: Literal["state", "event", "log", "ack", "progress"]
|
| | | payload: dict[str, Any] = Field(default_factory=dict)
|
| | |
|
| | |
|
| | | class DeviceState(BaseModel):
|
| | | device_id: str
|
| | | cpu: float | None = None
|
| | | mem: float | None = None
|
| | | battery: float | None = None
|
| | | net: str | None = None
|
| | | mode: str | None = None
|
| | | last_seen: int
|
| | |
|
| | |
|
| | | class InitTaskRequest(BaseModel):
|
| | | device_id: str
|
| | | course_id: str | None = None
|
| | | force: bool = False
|
| New file |
| | |
| | | import json
|
| | | import logging
|
| | | import time
|
| | | import uuid
|
| | | from typing import Any
|
| | |
|
| | | import paho.mqtt.client as mqtt
|
| | |
|
| | | from app.config import settings
|
| | | from app.services.device_store import DeviceStore
|
| | |
|
| | | logger = logging.getLogger(__name__)
|
| | |
|
| | | TOPIC_RPT_STATE = "snlp/v1/+/rpt/state"
|
| | | TOPIC_RPT_EVENT = "snlp/v1/+/rpt/event"
|
| | | TOPIC_RPT_ACK = "snlp/v1/+/rpt/ack"
|
| | | TOPIC_RPT_PROGRESS = "snlp/v1/+/rpt/progress"
|
| | | TOPIC_RPT_LOG = "snlp/v1/+/rpt/log"
|
| | |
|
| | |
|
| | | class MqttService:
|
| | | def __init__(self, device_store: DeviceStore, auto_init: object | None = None) -> None:
|
| | | self.device_store = device_store
|
| | | self.auto_init = auto_init
|
| | | self._client: mqtt.Client | None = None
|
| | | self._connected = False
|
| | |
|
| | | def start(self) -> None:
|
| | | if not settings.mqtt_enabled:
|
| | | logger.info("MQTT disabled by configuration")
|
| | | return
|
| | |
|
| | | self._client = mqtt.Client(
|
| | | callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
|
| | | client_id=settings.mqtt_client_id,
|
| | | )
|
| | | if settings.mqtt_username:
|
| | | self._client.username_pw_set(settings.mqtt_username, settings.mqtt_password)
|
| | |
|
| | | self._client.on_connect = self._on_connect
|
| | | self._client.on_message = self._on_message
|
| | | self._client.on_disconnect = self._on_disconnect
|
| | |
|
| | | try:
|
| | | self._client.connect(settings.mqtt_broker, settings.mqtt_port, keepalive=60)
|
| | | self._client.loop_start()
|
| | | logger.info("MQTT client connecting to %s:%s", settings.mqtt_broker, settings.mqtt_port)
|
| | | except Exception:
|
| | | logger.exception("Failed to connect MQTT broker; running in API-only mode")
|
| | |
|
| | | def stop(self) -> None:
|
| | | if self._client:
|
| | | self._client.loop_stop()
|
| | | self._client.disconnect()
|
| | |
|
| | | def _on_connect(self, client: mqtt.Client, userdata: Any, flags: Any, reason_code: Any, properties: Any = None) -> None:
|
| | | if reason_code == 0 or str(reason_code) == "Success":
|
| | | self._connected = True
|
| | | for topic in (TOPIC_RPT_STATE, TOPIC_RPT_EVENT, TOPIC_RPT_ACK, TOPIC_RPT_PROGRESS, TOPIC_RPT_LOG):
|
| | | client.subscribe(topic, qos=1 if "ack" in topic else 0)
|
| | | logger.info("MQTT connected and subscribed")
|
| | | else:
|
| | | logger.error("MQTT connect failed: %s", reason_code)
|
| | |
|
| | | def _on_disconnect(self, client: mqtt.Client, userdata: Any, flags: Any, reason_code: Any, properties: Any = None) -> None:
|
| | | self._connected = False
|
| | | logger.warning("MQTT disconnected: %s", reason_code)
|
| | |
|
| | | def _on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage) -> None:
|
| | | try:
|
| | | payload = json.loads(msg.payload.decode("utf-8"))
|
| | | parts = msg.topic.split("/")
|
| | | device_id = parts[2] if len(parts) > 2 else payload.get("header", {}).get("device_id", "unknown")
|
| | | msg_type = parts[-1] if len(parts) > 4 else payload.get("msg_type", "unknown")
|
| | | header = payload.get("header", {})
|
| | | timestamp = int(header.get("timestamp", time.time()))
|
| | | body = payload.get("payload", {})
|
| | |
|
| | | if msg_type == "state":
|
| | | self.device_store.update_state(device_id, body, timestamp)
|
| | | if self.auto_init:
|
| | | self.auto_init.on_device_state(device_id, body, timestamp)
|
| | | elif msg_type == "ack":
|
| | | self.device_store.record_ack(device_id, body)
|
| | | elif msg_type in ("event", "progress"):
|
| | | self.device_store.record_event(device_id, {**body, "_type": msg_type})
|
| | | elif msg_type == "log":
|
| | | logger.info("Device %s log: %s", device_id, body)
|
| | | except Exception:
|
| | | logger.exception("Failed to handle MQTT message on %s", msg.topic)
|
| | |
|
| | | def publish_ctl(self, device_id: str, action: str, payload: dict[str, Any], ttl: int = 60) -> dict[str, Any]:
|
| | | command = self._build_command(device_id, action, payload, ttl)
|
| | | topic = f"snlp/v1/{device_id}/cmd/ctl"
|
| | | self._publish(topic, command, qos=1)
|
| | | return command
|
| | |
|
| | | def publish_task(self, device_id: str, action: str, payload: dict[str, Any], ttl: int = 3600) -> dict[str, Any]:
|
| | | command = self._build_command(device_id, action, payload, ttl)
|
| | | topic = f"snlp/v1/{device_id}/cmd/task"
|
| | | self._publish(topic, command, qos=1)
|
| | | return command
|
| | |
|
| | | def _build_command(self, device_id: str, action: str, payload: dict[str, Any], ttl: int) -> dict[str, Any]:
|
| | | return {
|
| | | "header": {
|
| | | "version": "1.0",
|
| | | "device_id": device_id,
|
| | | "req_id": str(uuid.uuid4()),
|
| | | "timestamp": int(time.time()),
|
| | | "ttl": ttl,
|
| | | },
|
| | | "action": action,
|
| | | "payload": payload,
|
| | | }
|
| | |
|
| | | def _publish(self, topic: str, payload: dict[str, Any], qos: int = 1) -> None:
|
| | | if not self._client or not self._connected:
|
| | | logger.warning("MQTT not connected; command queued locally only: %s", topic)
|
| | | return
|
| | | self._client.publish(topic, json.dumps(payload, ensure_ascii=False), qos=qos)
|
| | |
|
| | | @property
|
| | | def connected(self) -> bool:
|
| | | return self._connected
|
| New file |
| | |
| | | from typing import Any
|
| | | import time
|
| | |
|
| | | from app.config import settings
|
| | | from app.mqtt.client import MqttService
|
| | | from app.services.course_service import CourseService
|
| | |
|
| | | SYNC_COURSE_TASK_ACTION = "SYNC_COURSE_TASK"
|
| | |
|
| | |
|
| | | class InitTaskDispatcher:
|
| | | def __init__(self, mqtt: MqttService, course_service: CourseService) -> None:
|
| | | self.mqtt = mqtt
|
| | | self.course_service = course_service
|
| | |
|
| | | def dispatch_device_init(self, device_id: str) -> list[dict[str, Any]]:
|
| | | tasklist = self.course_service.get_tasklist()
|
| | | commands: list[dict[str, Any]] = []
|
| | |
|
| | | for task in tasklist.get("tasks", []):
|
| | | action = task["action"]
|
| | | payload = dict(task.get("payload", {}))
|
| | | channel = task.get("channel", "ctl")
|
| | |
|
| | | if action == "SET_CLOCK" and payload.get("ts") == 0:
|
| | | payload["ts"] = int(time.time())
|
| | |
|
| | | cmd = self._send(device_id, action, payload, channel, task.get("ttl", 60))
|
| | | commands.append(cmd)
|
| | |
|
| | | return commands
|
| | |
|
| | | def dispatch_base_courses_sync(self, device_id: str) -> list[dict[str, Any]]:
|
| | | commands: list[dict[str, Any]] = []
|
| | | for payload in self.course_service.build_base_course_sync_payloads(settings.base_url):
|
| | | cmd = self.mqtt.publish_task(device_id, SYNC_COURSE_TASK_ACTION, payload, ttl=7200)
|
| | | commands.append(cmd)
|
| | | return commands
|
| | |
|
| | | def dispatch_course_init(self, device_id: str, course_id: str) -> list[dict[str, Any]]:
|
| | | payload = self.course_service.build_course_task_sync_payload(course_id, settings.base_url)
|
| | | cmd = self.mqtt.publish_task(device_id, SYNC_COURSE_TASK_ACTION, payload, ttl=7200)
|
| | | return [cmd]
|
| | |
|
| | | def _send(self, device_id: str, action: str, payload: dict[str, Any], channel: str, ttl: int) -> dict[str, Any]:
|
| | | if channel == "task":
|
| | | return self.mqtt.publish_task(device_id, action, payload, ttl)
|
| | | return self.mqtt.publish_ctl(device_id, action, payload, ttl)
|
| New file |
| | |
| | | from fastapi import APIRouter, HTTPException, Request
|
| | |
|
| | | from app.config import settings
|
| | | from app.models.schemas import InitTaskRequest
|
| | | from app.mqtt.init_dispatcher import InitTaskDispatcher
|
| | | from app.services.course_service import CourseService
|
| | |
|
| | | router = APIRouter(prefix="/api/v1", tags=["courses"])
|
| | | course_service = CourseService()
|
| | |
|
| | |
|
| | | def _get_dispatcher(request: Request) -> InitTaskDispatcher:
|
| | | return request.app.state.init_dispatcher
|
| | |
|
| | |
|
| | | @router.get("/courses")
|
| | | async def list_courses():
|
| | | return course_service.get_course_index()
|
| | |
|
| | |
|
| | | @router.get("/courses/base-manifest")
|
| | | async def get_base_courses_manifest():
|
| | | payloads = course_service.build_base_course_sync_payloads(settings.base_url)
|
| | | return {
|
| | | "scope": "base",
|
| | | "course_count": len(payloads),
|
| | | "courses": payloads,
|
| | | }
|
| | |
|
| | |
|
| | | @router.get("/courses/{course_id}")
|
| | | async def get_course(course_id: str):
|
| | | try:
|
| | | return course_service.get_course(course_id)
|
| | | except FileNotFoundError as exc:
|
| | | raise HTTPException(status_code=404, detail=str(exc)) from exc
|
| | |
|
| | |
|
| | | @router.get("/tasklist")
|
| | | async def get_tasklist():
|
| | | return course_service.get_tasklist()
|
| | |
|
| | |
|
| | | @router.get("/courses/{course_id}/tasks")
|
| | | async def get_course_tasks(course_id: str):
|
| | | try:
|
| | | return course_service.get_course_task(course_id)
|
| | | except FileNotFoundError as exc:
|
| | | raise HTTPException(status_code=404, detail=str(exc)) from exc
|
| | |
|
| | |
|
| | | @router.get("/courses/{course_id}/manifest")
|
| | | async def get_course_manifest(course_id: str):
|
| | | try:
|
| | | payload = course_service.build_course_task_sync_payload(course_id, settings.base_url)
|
| | | return payload
|
| | | except FileNotFoundError as exc:
|
| | | raise HTTPException(status_code=404, detail=str(exc)) from exc
|
| | |
|
| | |
|
| | | @router.post("/devices/init")
|
| | | async def init_device(request: Request, body: InitTaskRequest):
|
| | | auto_init = request.app.state.auto_init
|
| | | commands, status = auto_init.dispatch_manual_init(body.device_id, force=body.force)
|
| | | return {"device_id": body.device_id, "commands": commands, "status": status}
|
| | |
|
| | |
|
| | | @router.post("/devices/course-init")
|
| | | async def init_course_on_device(request: Request, body: InitTaskRequest):
|
| | | if not body.course_id:
|
| | | raise HTTPException(status_code=400, detail="course_id is required")
|
| | | dispatcher = _get_dispatcher(request)
|
| | | commands = dispatcher.dispatch_course_init(body.device_id, body.course_id)
|
| | | return {
|
| | | "device_id": body.device_id,
|
| | | "course_id": body.course_id,
|
| | | "commands": commands,
|
| | | "status": "dispatched",
|
| | | }
|
| New file |
| | |
| | | from fastapi import APIRouter, HTTPException, Request
|
| | |
|
| | | from app.models.schemas import MqttCommand
|
| | |
|
| | | router = APIRouter(prefix="/api/v1/devices", tags=["devices"])
|
| | |
|
| | |
|
| | | def _get_mqtt(request: Request):
|
| | | return request.app.state.mqtt
|
| | |
|
| | |
|
| | | @router.get("/initialized")
|
| | | async def list_initialized_devices(request: Request):
|
| | | registry = request.app.state.init_registry
|
| | | return {"devices": registry.list_initialized()}
|
| | |
|
| | |
|
| | | @router.delete("/initialized/{device_id}")
|
| | | async def reset_initialized_device(request: Request, device_id: str):
|
| | | registry = request.app.state.init_registry
|
| | | if not registry.remove(device_id):
|
| | | raise HTTPException(status_code=404, detail="Device not in initialized registry")
|
| | | return {"device_id": device_id, "status": "removed"}
|
| | |
|
| | |
|
| | | @router.get("/{device_id}/init-status")
|
| | | async def get_device_init_status(request: Request, device_id: str):
|
| | | registry = request.app.state.init_registry
|
| | | record = registry.get_record(device_id)
|
| | | return {
|
| | | "device_id": device_id,
|
| | | "initialized": record is not None,
|
| | | "record": record,
|
| | | }
|
| | |
|
| | |
|
| | | @router.get("")
|
| | | async def list_devices(request: Request):
|
| | | store = request.app.state.device_store
|
| | | return {"devices": [d.model_dump() for d in store.list_devices()]}
|
| | |
|
| | |
|
| | | @router.get("/{device_id}")
|
| | | async def get_device(request: Request, device_id: str):
|
| | | store = request.app.state.device_store
|
| | | device = store.get_device(device_id)
|
| | | if not device:
|
| | | raise HTTPException(status_code=404, detail="Device not found")
|
| | | return device
|
| | |
|
| | |
|
| | | @router.get("/{device_id}/acks")
|
| | | async def get_device_acks(request: Request, device_id: str):
|
| | | store = request.app.state.device_store
|
| | | return {"device_id": device_id, "acks": store.get_acks(device_id)}
|
| | |
|
| | |
|
| | | @router.get("/{device_id}/events")
|
| | | async def get_device_events(request: Request, device_id: str):
|
| | | store = request.app.state.device_store
|
| | | return {"device_id": device_id, "events": store.get_events(device_id)}
|
| | |
|
| | |
|
| | | @router.post("/{device_id}/cmd")
|
| | | async def send_command(request: Request, device_id: str, command: MqttCommand):
|
| | | mqtt = _get_mqtt(request)
|
| | | result = mqtt.publish_ctl(device_id, command.action, command.payload, command.header.ttl or 60)
|
| | | return result
|
| | |
|
| | |
|
| | | @router.post("/{device_id}/task")
|
| | | async def send_task(request: Request, device_id: str, command: MqttCommand):
|
| | | mqtt = _get_mqtt(request)
|
| | | result = mqtt.publish_task(device_id, command.action, command.payload, command.header.ttl or 3600)
|
| | | return result
|
| New file |
| | |
| | | import json
|
| | |
|
| | | from fastapi import APIRouter, Query
|
| | | from sse_starlette.sse import EventSourceResponse
|
| | |
|
| | | from app.services.dialogue_service import DialogueService
|
| | |
|
| | | router = APIRouter(prefix="/api/v1/dialogue", tags=["dialogue"])
|
| | | dialogue_service = DialogueService()
|
| | |
|
| | |
|
| | | @router.get("/stream")
|
| | | async def stream_dialogue(
|
| | | dialogue_id: str | None = Query(default=None),
|
| | | user_text: str | None = Query(default=None),
|
| | | scene_prompt: str | None = Query(default=None),
|
| | | difficulty: str = Query(default="A2"),
|
| | | ) -> EventSourceResponse:
|
| | | resolved_id = dialogue_id or dialogue_service.create_dialogue_id()
|
| | |
|
| | | async def event_generator():
|
| | | async for chunk in dialogue_service.stream_dialogue(
|
| | | dialogue_id=resolved_id,
|
| | | user_text=user_text,
|
| | | scene_prompt=scene_prompt,
|
| | | difficulty=difficulty,
|
| | | ):
|
| | | yield {"event": "message", "data": json.dumps(chunk, ensure_ascii=False)}
|
| | | yield {"event": "done", "data": json.dumps({"dialogue_id": resolved_id})}
|
| | |
|
| | | return EventSourceResponse(event_generator())
|
| New file |
| | |
| | | from fastapi import APIRouter, HTTPException, Query
|
| | |
|
| | | from app.models.schemas import SceneAnalyzeRequest, SceneAnalyzeResponse
|
| | | from app.services.scene_engine import SceneEngine
|
| | |
|
| | | router = APIRouter(prefix="/api/v1/scene", tags=["scene"])
|
| | | scene_engine = SceneEngine()
|
| | |
|
| | |
|
| | | @router.post("/analyze", response_model=SceneAnalyzeResponse)
|
| | | async def analyze_scene(request: SceneAnalyzeRequest) -> SceneAnalyzeResponse:
|
| | | result = scene_engine.analyze(
|
| | | device_id=request.device_id,
|
| | | timestamp=request.timestamp,
|
| | | context_hints=request.context_hints,
|
| | | )
|
| | | return SceneAnalyzeResponse(
|
| | | scene=result["scene"],
|
| | | confidence=result["confidence"],
|
| | | greeting_strategy=result["greeting_strategy"],
|
| | | suggested_intent=result["suggested_intent"],
|
| | | )
|
| New file |
| | |
| | | import logging
|
| | | import time
|
| | | from typing import Any
|
| | |
|
| | | from app.config import settings
|
| | | from app.mqtt.init_dispatcher import InitTaskDispatcher
|
| | | from app.mqtt.client import MqttService
|
| | | from app.services.init_registry import InitRegistry
|
| | |
|
| | | logger = logging.getLogger(__name__)
|
| | |
|
| | |
|
| | | class AutoInitService:
|
| | | def __init__(
|
| | | self,
|
| | | registry: InitRegistry,
|
| | | dispatcher: InitTaskDispatcher,
|
| | | mqtt: MqttService,
|
| | | ) -> None:
|
| | | self.registry = registry
|
| | | self.dispatcher = dispatcher
|
| | | self.mqtt = mqtt
|
| | |
|
| | | def _run_full_init(self, device_id: str) -> list[dict[str, Any]]:
|
| | | commands = self.dispatcher.dispatch_device_init(device_id)
|
| | | logger.info("Device init dispatched for %s (%d commands)", device_id, len(commands))
|
| | |
|
| | | course_commands = self.dispatcher.dispatch_base_courses_sync(device_id)
|
| | | commands.extend(course_commands)
|
| | | logger.info(
|
| | | "Base courses sync dispatched for %s (%d courseTask messages)",
|
| | | device_id,
|
| | | len(course_commands),
|
| | | )
|
| | |
|
| | | self.registry.update_record(
|
| | | device_id,
|
| | | courses_synced=True,
|
| | | courses_sync_at=int(time.time()),
|
| | | courses_sync_count=len(course_commands),
|
| | | )
|
| | | return commands
|
| | |
|
| | | def on_device_state(self, device_id: str, payload: dict[str, Any], timestamp: int) -> list[dict[str, Any]] | None:
|
| | | if not settings.auto_init_enabled:
|
| | | return None
|
| | |
|
| | | if self.registry.is_initialized(device_id):
|
| | | return None
|
| | |
|
| | | if not self.mqtt.connected:
|
| | | logger.warning("Skip auto-init for %s: MQTT not connected", device_id)
|
| | | return None
|
| | |
|
| | | mode = payload.get("mode")
|
| | | if not self.registry.try_claim(device_id, source="auto", mode=mode, timestamp=timestamp):
|
| | | return None
|
| | |
|
| | | try:
|
| | | commands = self._run_full_init(device_id)
|
| | | logger.info("Auto-init completed for device %s (total %d commands)", device_id, len(commands))
|
| | | return commands
|
| | | except Exception:
|
| | | self.registry.remove(device_id)
|
| | | logger.exception("Auto-init failed for device %s, registry entry rolled back", device_id)
|
| | | raise
|
| | |
|
| | | def dispatch_manual_init(self, device_id: str, force: bool = False) -> tuple[list[dict[str, Any]], str]:
|
| | | if self.registry.is_initialized(device_id) and not force:
|
| | | return [], "already_initialized"
|
| | |
|
| | | if force:
|
| | | self.registry.remove(device_id)
|
| | |
|
| | | if not self.registry.try_claim(device_id, source="manual"):
|
| | | return [], "already_initialized"
|
| | |
|
| | | try:
|
| | | commands = self._run_full_init(device_id)
|
| | | return commands, "dispatched"
|
| | | except Exception:
|
| | | self.registry.remove(device_id)
|
| | | raise
|
| New file |
| | |
| | | import json
|
| | | from copy import deepcopy
|
| | | from pathlib import Path
|
| | | from typing import Any
|
| | |
|
| | | from app.config import settings
|
| | |
|
| | |
|
| | | class CourseService:
|
| | | def __init__(self, data_dir: Path | None = None) -> None:
|
| | | self.data_dir = data_dir or settings.data_dir
|
| | |
|
| | | def _read_json(self, path: Path) -> dict[str, Any]:
|
| | | with path.open(encoding="utf-8") as f:
|
| | | return json.load(f)
|
| | |
|
| | | def static_url(self, server_rel_path: str, base_url: str) -> str:
|
| | | rel = server_rel_path.replace("\\", "/").lstrip("/")
|
| | | return f"{base_url.rstrip('/')}/static/{rel}"
|
| | |
|
| | | def _resolve_media_url(self, path: str, base_url: str) -> str:
|
| | | if path.startswith("http://") or path.startswith("https://"):
|
| | | return path
|
| | | return self.static_url(path, base_url)
|
| | |
|
| | | def _resolve_asset_urls(self, asset: dict[str, Any], base_url: str) -> dict[str, Any]:
|
| | | resolved = dict(asset)
|
| | | if resolved.get("url"):
|
| | | resolved["url"] = self._resolve_media_url(resolved["url"], base_url)
|
| | | if resolved.get("cover_url"):
|
| | | resolved["cover_url"] = self._resolve_media_url(resolved["cover_url"], base_url)
|
| | | return resolved
|
| | |
|
| | | def get_course_index(self) -> dict[str, Any]:
|
| | | return self._read_json(self.data_dir / "courses" / "index.json")
|
| | |
|
| | | def get_course(self, course_id: str) -> dict[str, Any]:
|
| | | index = self.get_course_index()
|
| | | for item in index.get("courses", []):
|
| | | if item["course_id"] == course_id:
|
| | | config_path = self.data_dir / item["config"]
|
| | | return self._read_json(config_path)
|
| | | raise FileNotFoundError(f"Course not found: {course_id}")
|
| | |
|
| | | def get_tasklist(self) -> dict[str, Any]:
|
| | | return self._read_json(self.data_dir / "tasklist.json")
|
| | |
|
| | | def get_course_task(self, course_id: str) -> dict[str, Any]:
|
| | | path = self.data_dir / "courses" / course_id / "courseTask.json"
|
| | | if not path.exists():
|
| | | raise FileNotFoundError(f"Course task not found: {course_id}")
|
| | | return self._read_json(path)
|
| | |
|
| | | def build_course_task_sync_payload(self, course_id: str, base_url: str) -> dict[str, Any]:
|
| | | task = deepcopy(self.get_course_task(course_id))
|
| | | assets = task.get("assets", [])
|
| | | task["assets"] = [self._resolve_asset_urls(asset, base_url) for asset in assets]
|
| | | return task
|
| | |
|
| | | def build_base_course_sync_payloads(self, base_url: str) -> list[dict[str, Any]]:
|
| | | index = self.get_course_index()
|
| | | payloads: list[dict[str, Any]] = []
|
| | | for item in index.get("courses", []):
|
| | | course_id = item["course_id"]
|
| | | task_path = self.data_dir / "courses" / course_id / "courseTask.json"
|
| | | if task_path.exists():
|
| | | payloads.append(self.build_course_task_sync_payload(course_id, base_url))
|
| | | return payloads
|
| | |
|
| | | def build_manifest_assets(self, course_id: str, base_url: str) -> list[dict[str, Any]]:
|
| | | task = self.get_course_task(course_id)
|
| | | return [self._resolve_asset_urls(asset, base_url) for asset in task.get("assets", [])]
|
| New file |
| | |
| | | from threading import Lock
|
| | |
|
| | | from app.models.schemas import DeviceState
|
| | |
|
| | |
|
| | | class DeviceStore:
|
| | | def __init__(self) -> None:
|
| | | self._devices: dict[str, DeviceState] = {}
|
| | | self._acks: dict[str, list[dict]] = {}
|
| | | self._events: dict[str, list[dict]] = {}
|
| | | self._lock = Lock()
|
| | |
|
| | | def update_state(self, device_id: str, payload: dict, timestamp: int) -> DeviceState:
|
| | | with self._lock:
|
| | | state = DeviceState(
|
| | | device_id=device_id,
|
| | | cpu=payload.get("cpu"),
|
| | | mem=payload.get("mem"),
|
| | | battery=payload.get("battery"),
|
| | | net=payload.get("net"),
|
| | | mode=payload.get("mode"),
|
| | | last_seen=timestamp,
|
| | | )
|
| | | self._devices[device_id] = state
|
| | | return state
|
| | |
|
| | | def record_ack(self, device_id: str, payload: dict) -> None:
|
| | | with self._lock:
|
| | | self._acks.setdefault(device_id, []).append(payload)
|
| | |
|
| | | def record_event(self, device_id: str, payload: dict) -> None:
|
| | | with self._lock:
|
| | | self._events.setdefault(device_id, []).append(payload)
|
| | |
|
| | | def get_device(self, device_id: str) -> DeviceState | None:
|
| | | return self._devices.get(device_id)
|
| | |
|
| | | def list_devices(self) -> list[DeviceState]:
|
| | | return list(self._devices.values())
|
| | |
|
| | | def get_acks(self, device_id: str) -> list[dict]:
|
| | | return list(self._acks.get(device_id, []))
|
| | |
|
| | | def get_events(self, device_id: str) -> list[dict]:
|
| | | return list(self._events.get(device_id, []))
|
| New file |
| | |
| | | import uuid
|
| | | from typing import AsyncIterator
|
| | |
|
| | | from app.services.scene_engine import GENERIC_GREETING
|
| | |
|
| | |
|
| | | class DialogueService:
|
| | | def create_dialogue_id(self) -> str:
|
| | | return str(uuid.uuid4())
|
| | |
|
| | | async def stream_dialogue(
|
| | | self,
|
| | | dialogue_id: str,
|
| | | user_text: str | None = None,
|
| | | scene_prompt: str | None = None,
|
| | | difficulty: str = "A2",
|
| | | ) -> AsyncIterator[dict]:
|
| | | opening = scene_prompt or GENERIC_GREETING
|
| | | chunks = [
|
| | | {
|
| | | "dialogue_id": dialogue_id,
|
| | | "text": opening,
|
| | | "audio_url": None,
|
| | | "intent_type": "greeting",
|
| | | "difficulty_level": difficulty,
|
| | | }
|
| | | ]
|
| | |
|
| | | if user_text:
|
| | | feedback = self._evaluate_response(user_text)
|
| | | chunks.append(
|
| | | {
|
| | | "dialogue_id": dialogue_id,
|
| | | "text": feedback["text"],
|
| | | "audio_url": None,
|
| | | "intent_type": feedback["intent_type"],
|
| | | "difficulty_level": difficulty,
|
| | | }
|
| | | )
|
| | | if feedback["follow_up"]:
|
| | | chunks.append(
|
| | | {
|
| | | "dialogue_id": dialogue_id,
|
| | | "text": feedback["follow_up"],
|
| | | "audio_url": None,
|
| | | "intent_type": "question",
|
| | | "difficulty_level": difficulty,
|
| | | }
|
| | | )
|
| | |
|
| | | for chunk in chunks:
|
| | | yield chunk
|
| | |
|
| | | def _evaluate_response(self, text: str) -> dict:
|
| | | normalized = text.strip()
|
| | | if not normalized:
|
| | | return {
|
| | | "text": "I didn't catch that. Could you try again?",
|
| | | "intent_type": "correction",
|
| | | "follow_up": "Please repeat your answer slowly.",
|
| | | }
|
| | |
|
| | | if normalized[0].islower():
|
| | | return {
|
| | | "text": f'Good try! Remember to capitalize: "{normalized[0].upper()}{normalized[1:]}"',
|
| | | "intent_type": "correction",
|
| | | "follow_up": "Can you say the full sentence again?",
|
| | | }
|
| | |
|
| | | if not normalized.endswith((".", "!", "?")):
|
| | | return {
|
| | | "text": "Nice answer! Don't forget punctuation at the end.",
|
| | | "intent_type": "praise",
|
| | | "follow_up": "Tell me one more detail about it.",
|
| | | }
|
| | |
|
| | | return {
|
| | | "text": "Great job! Your sentence looks correct.",
|
| | | "intent_type": "praise",
|
| | | "follow_up": "What would you do differently next time?",
|
| | | }
|
| New file |
| | |
| | | import json
|
| | | import logging
|
| | | import time
|
| | | from pathlib import Path
|
| | | from threading import Lock
|
| | | from typing import Any, Literal
|
| | |
|
| | | from app.config import settings
|
| | |
|
| | | logger = logging.getLogger(__name__)
|
| | |
|
| | | InitSource = Literal["auto", "manual"]
|
| | |
|
| | |
|
| | | class InitRegistry:
|
| | | def __init__(self, storage_path: Path | None = None) -> None:
|
| | | self.storage_path = storage_path or settings.resolved_initialized_devices_file
|
| | | self._lock = Lock()
|
| | | self._data = self._load()
|
| | |
|
| | | def _load(self) -> dict[str, Any]:
|
| | | if self.storage_path.exists():
|
| | | with self.storage_path.open(encoding="utf-8") as f:
|
| | | return json.load(f)
|
| | | return {"devices": {}}
|
| | |
|
| | | def _save(self) -> None:
|
| | | self.storage_path.parent.mkdir(parents=True, exist_ok=True)
|
| | | with self.storage_path.open("w", encoding="utf-8") as f:
|
| | | json.dump(self._data, f, ensure_ascii=False, indent=2)
|
| | |
|
| | | def is_initialized(self, device_id: str) -> bool:
|
| | | with self._lock:
|
| | | return device_id in self._data["devices"]
|
| | |
|
| | | def try_claim(
|
| | | self,
|
| | | device_id: str,
|
| | | source: InitSource,
|
| | | mode: str | None = None,
|
| | | timestamp: int | None = None,
|
| | | ) -> bool:
|
| | | with self._lock:
|
| | | if device_id in self._data["devices"]:
|
| | | return False
|
| | | self._data["devices"][device_id] = {
|
| | | "initialized_at": timestamp or int(time.time()),
|
| | | "source": source,
|
| | | "mode": mode,
|
| | | }
|
| | | self._save()
|
| | | return True
|
| | |
|
| | | def mark_initialized(
|
| | | self,
|
| | | device_id: str,
|
| | | source: InitSource,
|
| | | mode: str | None = None,
|
| | | timestamp: int | None = None,
|
| | | ) -> None:
|
| | | with self._lock:
|
| | | self._data["devices"][device_id] = {
|
| | | "initialized_at": timestamp or int(time.time()),
|
| | | "source": source,
|
| | | "mode": mode,
|
| | | }
|
| | | self._save()
|
| | |
|
| | | def remove(self, device_id: str) -> bool:
|
| | | with self._lock:
|
| | | if device_id not in self._data["devices"]:
|
| | | return False
|
| | | del self._data["devices"][device_id]
|
| | | self._save()
|
| | | return True
|
| | |
|
| | | def list_initialized(self) -> dict[str, Any]:
|
| | | with self._lock:
|
| | | return dict(self._data["devices"])
|
| | |
|
| | | def get_record(self, device_id: str) -> dict[str, Any] | None:
|
| | | with self._lock:
|
| | | record = self._data["devices"].get(device_id)
|
| | | return dict(record) if record else None
|
| | |
|
| | | def update_record(self, device_id: str, **fields: Any) -> bool:
|
| | | with self._lock:
|
| | | if device_id not in self._data["devices"]:
|
| | | return False
|
| | | self._data["devices"][device_id].update(fields)
|
| | | self._save()
|
| | | return True
|
| New file |
| | |
| | | import hashlib
|
| | | import random
|
| | | from typing import Any
|
| | |
|
| | | from app.config import settings
|
| | |
|
| | | SCENE_CATALOG = [
|
| | | ("home_bedroom", "å±
å®¶-å§å®¤", "Let's talk about your morning routine."),
|
| | | ("home_kitchen", "å±
å®¶-卿¿", "What would you like to cook today?"),
|
| | | ("home_study", "å±
å®¶-书æ¿", "Shall we review your vocabulary?"),
|
| | | ("school", "æ ¡å
", "How was your class today?"),
|
| | | ("commute", "éå¤", "What do you see on your way?"),
|
| | | ("exercise", "è¿å¨", "How do you stay active?"),
|
| | | ("dining", "ç¨é¤", "What are you having for dinner?"),
|
| | | ]
|
| | |
|
| | | GENERIC_GREETING = "Hello! How can I help you practice English today?"
|
| | |
|
| | |
|
| | | class SceneEngine:
|
| | | def __init__(self, threshold: float | None = None) -> None:
|
| | | self.threshold = threshold or settings.scene_confidence_threshold
|
| | |
|
| | | def _derive_seed(self, device_id: str, timestamp: int, hints: dict[str, Any]) -> int:
|
| | | raw = f"{device_id}:{timestamp}:{hints.get('mode', '')}"
|
| | | return int(hashlib.md5(raw.encode()).hexdigest()[:8], 16)
|
| | |
|
| | | def analyze(
|
| | | self,
|
| | | device_id: str,
|
| | | timestamp: int,
|
| | | context_hints: dict[str, Any] | None = None,
|
| | | ) -> dict[str, Any]:
|
| | | hints = context_hints or {}
|
| | | rng = random.Random(self._derive_seed(device_id, timestamp, hints))
|
| | |
|
| | | if hints.get("scene"):
|
| | | scene_key = hints["scene"]
|
| | | match = next((s for s in SCENE_CATALOG if s[0] == scene_key), None)
|
| | | if match:
|
| | | confidence = float(hints.get("confidence", 0.85))
|
| | | return self._build_result(match, confidence)
|
| | |
|
| | | idx = rng.randint(0, len(SCENE_CATALOG) - 1)
|
| | | confidence = round(rng.uniform(0.55, 0.95), 2)
|
| | | return self._build_result(SCENE_CATALOG[idx], confidence)
|
| | |
|
| | | def _build_result(self, scene: tuple[str, str, str], confidence: float) -> dict[str, Any]:
|
| | | scene_key, scene_label, prompt = scene
|
| | | use_generic = confidence < self.threshold
|
| | | return {
|
| | | "scene": scene_label,
|
| | | "scene_key": scene_key,
|
| | | "confidence": confidence,
|
| | | "greeting_strategy": "generic" if use_generic else "contextual",
|
| | | "suggested_intent": GENERIC_GREETING if use_generic else prompt,
|
| | | }
|
| New file |
| | |
| | | fastapi>=0.115.0
|
| | | uvicorn[standard]>=0.32.0
|
| | | paho-mqtt>=2.1.0
|
| | | pydantic>=2.9.0
|
| | | pydantic-settings>=2.6.0
|
| | | python-multipart>=0.0.12
|
| | | sse-starlette>=2.1.0
|
| New file |
| | |
| | | import uvicorn
|
| | |
|
| | | from app.config import settings
|
| | |
|
| | |
|
| | | def main() -> None:
|
| | | uvicorn.run("app.main:app", host=settings.host, port=settings.port, reload=False)
|
| | |
|
| | |
|
| | | if __name__ == "__main__":
|
| | | main()
|
| New file |
| | |
| | | --- |
| | | Language: Cpp |
| | | BasedOnStyle: Google |
| | | AccessModifierOffset: -4 |
| | | AlignAfterOpenBracket: Align |
| | | AlignConsecutiveAssignments: false |
| | | AlignConsecutiveDeclarations: false |
| | | AlignEscapedNewlines: Left |
| | | AlignOperands: true |
| | | AlignTrailingComments: true |
| | | AllowAllParametersOfDeclarationOnNextLine: true |
| | | AllowShortBlocksOnASingleLine: false |
| | | AllowShortCaseLabelsOnASingleLine: false |
| | | AllowShortFunctionsOnASingleLine: All |
| | | AllowShortIfStatementsOnASingleLine: false |
| | | AllowShortLoopsOnASingleLine: false |
| | | AlwaysBreakAfterReturnType: ExceptShortType |
| | | AlwaysBreakBeforeMultilineStrings: true |
| | | AlwaysBreakTemplateDeclarations: Yes |
| | | BinPackArguments: true |
| | | BinPackParameters: true |
| | | BraceWrapping: |
| | | AfterClass: false |
| | | AfterControlStatement: false |
| | | AfterEnum: false |
| | | AfterFunction: false |
| | | AfterNamespace: false |
| | | AfterObjCDeclaration: false |
| | | AfterStruct: false |
| | | AfterUnion: false |
| | | AfterExternBlock: false |
| | | BeforeCatch: false |
| | | BeforeElse: false |
| | | IndentBraces: false |
| | | SplitEmptyFunction: false |
| | | SplitEmptyRecord: false |
| | | SplitEmptyNamespace: false |
| | | BreakBeforeBinaryOperators: None |
| | | BreakBeforeBraces: Attach |
| | | BreakBeforeInheritanceComma: false |
| | | BreakInheritanceList: BeforeColon |
| | | BreakBeforeTernaryOperators: true |
| | | BreakConstructorInitializersBeforeComma: false |
| | | BreakConstructorInitializers: BeforeColon |
| | | BreakAfterJavaFieldAnnotations: false |
| | | BreakStringLiterals: true |
| | | ColumnLimit: 100 |
| | | CommentPragmas: '^ IWYU pragma:' |
| | | CompactNamespaces: false |
| | | ConstructorInitializerAllOnOneLineOrOnePerLine: true |
| | | ConstructorInitializerIndentWidth: 4 |
| | | ContinuationIndentWidth: 4 |
| | | Cpp11BracedListStyle: true |
| | | DerivePointerAlignment: false |
| | | DisableFormat: false |
| | | ExperimentalAutoDetectBinPacking: false |
| | | FixNamespaceComments: true |
| | | ForEachMacros: |
| | | - cJSON_ArrayForEach |
| | | IncludeBlocks: Preserve |
| | | IncludeCategories: |
| | | - Regex: '^<esp_.*\.h>' |
| | | Priority: 1 |
| | | - Regex: '^<driver/.*\.h>' |
| | | Priority: 1 |
| | | - Regex: '^<.*\.h>' |
| | | Priority: 2 |
| | | - Regex: '^<.*' |
| | | Priority: 3 |
| | | - Regex: '.*' |
| | | Priority: 4 |
| | | IncludeIsMainRegex: '([-_](test|unittest))?$' |
| | | IndentCaseLabels: true |
| | | IndentPPDirectives: None |
| | | IndentWidth: 4 |
| | | IndentWrappedFunctionNames: false |
| | | KeepEmptyLinesAtTheStartOfBlocks: false |
| | | MacroBlockBegin: '' |
| | | MacroBlockEnd: '' |
| | | MaxEmptyLinesToKeep: 1 |
| | | NamespaceIndentation: None |
| | | PenaltyBreakAssignment: 2 |
| | | PenaltyBreakBeforeFirstCallParameter: 1 |
| | | PenaltyBreakComment: 300 |
| | | PenaltyBreakFirstLessLess: 120 |
| | | PenaltyBreakString: 1000 |
| | | PenaltyBreakTemplateDeclaration: 10 |
| | | PenaltyExcessCharacter: 1000000 |
| | | PenaltyReturnTypeOnItsOwnLine: 200 |
| | | PointerAlignment: Left |
| | | RawStringFormats: |
| | | - Language: Cpp |
| | | Delimiters: |
| | | - cc |
| | | - CC |
| | | - cpp |
| | | - Cpp |
| | | - CPP |
| | | - 'c++' |
| | | - 'C++' |
| | | CanonicalDelimiter: '' |
| | | BasedOnStyle: google |
| | | ReflowComments: true |
| | | SortIncludes: true |
| | | SortUsingDeclarations: true |
| | | SpaceAfterCStyleCast: false |
| | | SpaceAfterTemplateKeyword: true |
| | | SpaceBeforeAssignmentOperators: true |
| | | SpaceBeforeCpp11BracedList: false |
| | | SpaceBeforeCtorInitializerColon: true |
| | | SpaceBeforeInheritanceColon: true |
| | | SpaceBeforeParens: ControlStatements |
| | | SpaceBeforeRangeBasedForLoopColon: true |
| | | SpaceInEmptyParentheses: false |
| | | SpacesBeforeTrailingComments: 2 |
| | | SpacesInAngles: false |
| | | SpacesInContainerLiterals: true |
| | | SpacesInCStyleCastParentheses: false |
| | | SpacesInParentheses: false |
| | | SpacesInSquareBrackets: false |
| | | Standard: Latest |
| | | StatementMacros: |
| | | - Q_UNUSED |
| | | - QT_REQUIRE_VERSION |
| | | TabWidth: 4 |
| | | UseTab: Never |
| New file |
| | |
| | | tmp/ |
| | | components/ |
| | | managed_components/ |
| | | build/ |
| | | dist/ |
| | | .vscode/ |
| | | .devcontainer/ |
| | | sdkconfig.old |
| | | sdkconfig |
| | | dependencies.lock |
| | | .env |
| | | releases/ |
| | | main/assets/lang_config.h |
| | | main/mmap_generate_emoji.h |
| | | .DS_Store |
| | | .cache |
| | | *.pyc |
| | | *.bin |
| | | mmap_generate_*.h |
| | | .clangd |
| New file |
| | |
| | | # The following lines of boilerplate have to be in your project's |
| | | # CMakeLists in this exact order for cmake to work correctly |
| | | cmake_minimum_required(VERSION 3.16) |
| | | |
| | | # Add this line to disable the specific warning |
| | | add_compile_options(-Wno-missing-field-initializers) |
| | | |
| | | include($ENV{IDF_PATH}/tools/cmake/project.cmake) |
| | | # "Trim" the build. Include the minimal set of components, main, and anything it depends on. |
| | | idf_build_set_property(MINIMAL_BUILD ON) |
| | | |
| | | set(PROJECT_VER "2.2.6") |
| | | project(xiaozhi) |
| New file |
| | |
| | | MIT License |
| | | |
| | | Copyright (c) 2025 Shenzhen Xinzhi Future Technology Co., Ltd. |
| | | Copyright (c) 2025 Project Contributors |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| New file |
| | |
| | | # An MCP-based Chatbot |
| | | |
| | | (English | [䏿](README_zh.md) | [æ¥æ¬èª](README_ja.md)) |
| | | |
| | | ## Introduction |
| | | |
| | | ð [Human: Give AI a camera vs AI: Instantly finds out the owner hasn't washed hair for three daysãbilibiliã](https://www.bilibili.com/video/BV1bpjgzKEhd/) |
| | | |
| | | ð [Handcraft your AI girlfriend, beginner's guideãbilibiliã](https://www.bilibili.com/video/BV1XnmFYLEJN/) |
| | | |
| | | As a voice interaction entry, the XiaoZhi AI chatbot leverages the AI capabilities of large models like Qwen / DeepSeek, and achieves multi-terminal control via the MCP protocol. |
| | | |
| | | <img src="docs/mcp-based-graph.jpg" alt="Control everything via MCP" width="320"> |
| | | |
| | | ## Version Notes |
| | | |
| | | The current v2 version is incompatible with the v1 partition table, so it is not possible to upgrade from v1 to v2 via OTA. For partition table details, see [partitions/v2/README.md](partitions/v2/README.md). |
| | | |
| | | All hardware running v1 can be upgraded to v2 by manually flashing the firmware. |
| | | |
| | | The stable version of v1 is 1.9.2. You can switch to v1 by running `git checkout v1`. The v1 branch will be maintained until February 2026. |
| | | |
| | | ### Features Implemented |
| | | |
| | | - Wi-Fi / ML307 Cat.1 4G |
| | | - Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr) |
| | | - Supports two communication protocols ([Websocket](docs/websocket.md) or MQTT+UDP) |
| | | - Uses OPUS audio codec |
| | | - Voice interaction based on streaming ASR + LLM + TTS architecture |
| | | - Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker) |
| | | - OLED / LCD display, supports emoji display |
| | | - Battery display and power management |
| | | - Multi-language support (Chinese, English, Japanese) |
| | | - Supports ESP32-C3, ESP32-S3, ESP32-P4 chip platforms |
| | | - Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.) |
| | | - Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.) |
| | | - Customizable wake words, fonts, emojis, and chat backgrounds with online web-based editing ([Custom Assets Generator](https://github.com/78/xiaozhi-assets-generator)) |
| | | |
| | | ## Hardware |
| | | |
| | | ### Breadboard DIY Practice |
| | | |
| | | See the Feishu document tutorial: |
| | | |
| | | ð ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) |
| | | |
| | | Breadboard demo: |
| | | |
| | |  |
| | | |
| | | ### Supports 70+ Open Source Hardware (Partial List) |
| | | |
| | | - <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a> |
| | | - <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a> |
| | | - <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a> |
| | | - <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a> |
| | | - <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="Magic Button 2.4">Magic Button 2.4</a> |
| | | - <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a> |
| | | - <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a> |
| | | - <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a> |
| | | - <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AI Pendant</a> |
| | | - <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="WMnologo-Xingzhi-1.54">WMnologo-Xingzhi-1.54TFT</a> |
| | | - <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a> |
| | | - <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI Low Cost Robot Dog">ESP-HI Low Cost Robot Dog</a> |
| | | |
| | | <div style="display: flex; justify-content: space-between;"> |
| | | <a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board"> |
| | | <img src="docs/v1/lichuang-s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3"> |
| | | <img src="docs/v1/espbox3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3"> |
| | | <img src="docs/v1/m5cores3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base"> |
| | | <img src="docs/v1/atoms3r.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/magiclick.jpg" target="_blank" title="Magic Button 2.4"> |
| | | <img src="docs/v1/magiclick.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8"> |
| | | <img src="docs/v1/waveshare.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3"> |
| | | <img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/xmini-c3.jpg" target="_blank" title="XiaGe Mini C3"> |
| | | <img src="docs/v1/xmini-c3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan"> |
| | | <img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="WMnologo-Xingzhi-1.54"> |
| | | <img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher"> |
| | | <img src="docs/v1/sensecap_watcher.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI Low Cost Robot Dog"> |
| | | <img src="docs/v1/esp-hi.jpg" width="240" /> |
| | | </a> |
| | | </div> |
| | | |
| | | ## Software |
| | | |
| | | ### Firmware Flashing |
| | | |
| | | For beginners, it is recommended to use the firmware that can be flashed without setting up a development environment. |
| | | |
| | | The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by default. Personal users can register an account to use the Qwen real-time model for free. |
| | | |
| | | ð [Beginner's Firmware Flashing Guide](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) |
| | | |
| | | ### Development Environment |
| | | |
| | | - Cursor or VSCode |
| | | - Install ESP-IDF plugin, select SDK version 5.4 or above |
| | | - Linux is better than Windows for faster compilation and fewer driver issues |
| | | - This project uses Google C++ code style, please ensure compliance when submitting code |
| | | |
| | | ### Developer Documentation |
| | | |
| | | - [Custom Board Guide](docs/custom-board.md) - Learn how to create custom boards for XiaoZhi AI |
| | | - [MCP Protocol IoT Control Usage](docs/mcp-usage.md) - Learn how to control IoT devices via MCP protocol |
| | | - [MCP Protocol Interaction Flow](docs/mcp-protocol.md) - Device-side MCP protocol implementation |
| | | - [MQTT + UDP Hybrid Communication Protocol Document](docs/mqtt-udp.md) |
| | | - [A detailed WebSocket communication protocol document](docs/websocket.md) |
| | | |
| | | ## Large Model Configuration |
| | | |
| | | If you already have a XiaoZhi AI chatbot device and have connected to the official server, you can log in to the [xiaozhi.me](https://xiaozhi.me) console for configuration. |
| | | |
| | | ð [Backend Operation Video Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/) |
| | | |
| | | ## Related Open Source Projects |
| | | |
| | | For server deployment on personal computers, refer to the following open-source projects: |
| | | |
| | | - [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python server |
| | | - [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java server |
| | | - [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang server |
| | | - [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golang server |
| | | |
| | | Other client projects using the XiaoZhi communication protocol: |
| | | |
| | | - [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python client |
| | | - [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android client |
| | | - [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) Linux client by 100ask |
| | | - [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) Bluetooth chip firmware by Sichuan |
| | | - [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) QuecPython firmware by Quectel |
| | | |
| | | Custom Assets Tools: |
| | | |
| | | - [78/xiaozhi-assets-generator](https://github.com/78/xiaozhi-assets-generator) Custom Assets Generator (Wake words, fonts, emojis, backgrounds) |
| | | |
| | | ## About the Project |
| | | |
| | | This is an open-source ESP32 project, released under the MIT license, allowing anyone to use it for free, including for commercial purposes. |
| | | |
| | | We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices. |
| | | |
| | | If you have any ideas or suggestions, please feel free to raise Issues or join our [Discord](https://discord.gg/C759fGMBcZ) or QQ group: 994694848 |
| | | |
| | | ## Star History |
| | | |
| | | <a href="https://star-history.com/#78/xiaozhi-esp32&Date"> |
| | | <picture> |
| | | <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" /> |
| | | <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" /> |
| | | <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" /> |
| | | </picture> |
| | | </a> |
| New file |
| | |
| | | # MCP ãã¼ã¹ã®ãã£ããããã |
| | | |
| | | ï¼æ¥æ¬èª | [䏿](README_zh.md) | [English](README.md)ï¼ |
| | | |
| | | ## ã¯ããã« |
| | | |
| | | ð [人éï¼AIã«ã«ã¡ã©ãè£
ç vs AIï¼ãã®å ´ã§é£¼ã主ã3æ¥éé«ªãæ´ã£ã¦ããªããã¨ãçºè¦ãbilibiliã](https://www.bilibili.com/video/BV1bpjgzKEhd/) |
| | | |
| | | ð [æä½ãã§AIã¬ã¼ã«ãã¬ã³ããä½ããåå¿è
å
¥éãã¥ã¼ããªã¢ã«ãbilibiliã](https://www.bilibili.com/video/BV1XnmFYLEJN/) |
| | | |
| | | ã·ã£ãªã¸ã¼AIãã£ãããããã¯é³å£°ã¤ã³ã¿ã©ã¯ã·ã§ã³ã®å
¥å£ã¨ãã¦ãQwen / DeepSeekãªã©ã®å¤§è¦æ¨¡ã¢ãã«ã®AIè½åãæ´»ç¨ããMCPãããã³ã«ãéãã¦ãã«ãã¨ã³ãå¶å¾¡ãå®ç¾ãã¾ãã |
| | | |
| | | <img src="docs/mcp-based-graph.jpg" alt="MCPã§ãããããã®ãå¶å¾¡" width="320"> |
| | | |
| | | ## ãã¼ã¸ã§ã³ãã¼ã |
| | | |
| | | ç¾å¨ã®v2ãã¼ã¸ã§ã³ã¯v1ãã¼ãã£ã·ã§ã³ãã¼ãã«ã¨äºææ§ããªããããv1ããv2ã¸OTAã§ã¢ããã°ã¬ã¼ããããã¨ã¯ã§ãã¾ããããã¼ãã£ã·ã§ã³ãã¼ãã«ã®è©³ç´°ã«ã¤ãã¦ã¯ã[partitions/v2/README.md](partitions/v2/README.md)ããåç
§ãã ããã |
| | | |
| | | v1ãå®è¡ãã¦ãããã¹ã¦ã®ãã¼ãã¦ã§ã¢ã¯ããã¡ã¼ã ã¦ã§ã¢ãæåã§æ¸ãè¾¼ããã¨ã§v2ã«ã¢ããã°ã¬ã¼ãã§ãã¾ãã |
| | | |
| | | v1ã®å®å®çã¯1.9.2ã§ãã`git checkout v1`ã§v1ã«åãæ¿ãããã¨ãã§ãã¾ããv1ãã©ã³ãã¯2026å¹´2æã¾ã§ç¶ç¶çã«ã¡ã³ããã³ã¹ããã¾ãã |
| | | |
| | | ### å®è£
æ¸ã¿æ©è½ |
| | | |
| | | - Wi-Fi / ML307 Cat.1 4G |
| | | - ãªãã©ã¤ã³é³å£°ã¦ã§ã¤ã¯ã¢ãã [ESP-SR](https://github.com/espressif/esp-sr) |
| | | - 2種é¡ã®éä¿¡ãããã³ã«ã«å¯¾å¿ï¼[Websocket](docs/websocket.md) ã¾ã㯠MQTT+UDPï¼ |
| | | - OPUSãªã¼ãã£ãªã³ã¼ããã¯ãæ¡ç¨ |
| | | - ã¹ããªã¼ãã³ã°ASR + LLM + TTSã¢ã¼ããã¯ãã£ã«åºã¥ãé³å£°ã¤ã³ã¿ã©ã¯ã·ã§ã³ |
| | | - 話è
èªèãç¾å¨è©±ãã¦ãã人ãèå¥ [3D Speaker](https://github.com/modelscope/3D-Speaker) |
| | | - OLED / LCDãã£ã¹ãã¬ã¤ã表æ
è¡¨ç¤ºå¯¾å¿ |
| | | - ããããªã¼è¡¨ç¤ºã¨é»æºç®¡ç |
| | | - å¤è¨èªå¯¾å¿ï¼ä¸å½èªãè±èªãæ¥æ¬èªï¼ |
| | | - ESP32-C3ãESP32-S3ãESP32-P4ããããã©ãããã©ã¼ã å¯¾å¿ |
| | | - ããã¤ã¹å´MCPã«ããããã¤ã¹å¶å¾¡ï¼é³éã»æãã調æ´ãã¢ã¯ã·ã§ã³å¶å¾¡ãªã©ï¼ |
| | | - ã¯ã©ã¦ãå´MCPã§å¤§è¦æ¨¡ã¢ãã«è½åãæ¡å¼µï¼ã¹ãã¼ããã¼ã å¶å¾¡ãPCãã¹ã¯ãããæä½ãç¥èæ¤ç´¢ãã¡ã¼ã«éåä¿¡ãªã©ï¼ |
| | | - ã«ã¹ã¿ãã¤ãºå¯è½ãªã¦ã§ã¤ã¯ã¯ã¼ãããã©ã³ããçµµæåããã£ããèæ¯ããªã³ã©ã¤ã³Webç·¨éã«å¯¾å¿ ([ã«ã¹ã¿ã ã¢ã»ããã¸ã§ãã¬ã¼ã¿ã¼](https://github.com/78/xiaozhi-assets-generator)) |
| | | |
| | | ## ãã¼ãã¦ã§ã¢ |
| | | |
| | | ### ãã¬ãããã¼ãæä½ãå®è·µ |
| | | |
| | | Feishuããã¥ã¡ã³ããã¥ã¼ããªã¢ã«ãã覧ãã ããï¼ |
| | | |
| | | ð [ãã·ã£ãªã¸ã¼AIãã£ãããããç¾ç§äºå
¸ã](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) |
| | | |
| | | ãã¬ãããã¼ãã®ãã¢ï¼ |
| | | |
| | |  |
| | | |
| | | ### 70種é¡ä»¥ä¸ã®ãªã¼ãã³ã½ã¼ã¹ãã¼ãã¦ã§ã¢ã«å¯¾å¿ï¼ä¸é¨ã®ã¿è¡¨ç¤ºï¼ |
| | | |
| | | - <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="ç«åµã»å®æ¦æ´¾ ESP32-S3 éçºãã¼ã">ç«åµã»å®æ¦æ´¾ ESP32-S3 éçºãã¼ã</a> |
| | | - <a href="https://github.com/espressif/esp-box" target="_blank" title="æ¥½é« ESP32-S3-BOX3">æ¥½é« ESP32-S3-BOX3</a> |
| | | - <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a> |
| | | - <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a> |
| | | - <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="ãã¸ãã¯ãã¿ã³2.4">ãã¸ãã¯ãã¿ã³2.4</a> |
| | | - <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="å¾®éªé»å ESP32-S3-Touch-AMOLED-1.8">å¾®éªé»å ESP32-S3-Touch-AMOLED-1.8</a> |
| | | - <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a> |
| | | - <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="ã¨ãå
ãã Mini C3">ã¨ãå
ãã Mini C3</a> |
| | | - <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AIãã³ãã³ã</a> |
| | | - <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="ç¡åç§æNologo-ææº-1.54">ç¡åç§æNologo-ææº-1.54TFT</a> |
| | | - <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a> |
| | | - <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI è¶
ä½ã³ã¹ãããããç¬">ESP-HI è¶
ä½ã³ã¹ãããããç¬</a> |
| | | |
| | | <div style="display: flex; justify-content: space-between;"> |
| | | <a href="docs/v1/lichuang-s3.jpg" target="_blank" title="ç«åµã»å®æ¦æ´¾ ESP32-S3 éçºãã¼ã"> |
| | | <img src="docs/v1/lichuang-s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/espbox3.jpg" target="_blank" title="æ¥½é« ESP32-S3-BOX3"> |
| | | <img src="docs/v1/espbox3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3"> |
| | | <img src="docs/v1/m5cores3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base"> |
| | | <img src="docs/v1/atoms3r.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/magiclick.jpg" target="_blank" title="ãã¸ãã¯ãã¿ã³2.4"> |
| | | <img src="docs/v1/magiclick.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/waveshare.jpg" target="_blank" title="å¾®éªé»å ESP32-S3-Touch-AMOLED-1.8"> |
| | | <img src="docs/v1/waveshare.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3"> |
| | | <img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/xmini-c3.jpg" target="_blank" title="ã¨ãå
ãã Mini C3"> |
| | | <img src="docs/v1/xmini-c3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan"> |
| | | <img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="ç¡åç§æNologo-ææº-1.54"> |
| | | <img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher"> |
| | | <img src="docs/v1/sensecap_watcher.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI è¶
ä½ã³ã¹ãããããç¬"> |
| | | <img src="docs/v1/esp-hi.jpg" width="240" /> |
| | | </a> |
| | | </div> |
| | | |
| | | ## ã½ããã¦ã§ã¢ |
| | | |
| | | ### ãã¡ã¼ã ã¦ã§ã¢æ¸ã込㿠|
| | | |
| | | åå¿è
ã®æ¹ã¯ãã¾ãéçºç°å¢ãæ§ç¯ããã«æ¸ãè¾¼ã¿å¯è½ãªãã¡ã¼ã ã¦ã§ã¢ã使ç¨ãããã¨ããããããã¾ãã |
| | | |
| | | ãã¡ã¼ã ã¦ã§ã¢ã¯ããã©ã«ãã§å
¬å¼ [xiaozhi.me](https://xiaozhi.me) ãµã¼ãã¼ã«æ¥ç¶ãã¾ããå人ã¦ã¼ã¶ã¼ã¯ã¢ã«ã¦ã³ãç»é²ã§Qwenãªã¢ã«ã¿ã¤ã ã¢ãã«ãç¡æã§å©ç¨ã§ãã¾ãã |
| | | |
| | | ð [åå¿è
åããã¡ã¼ã ã¦ã§ã¢æ¸ãè¾¼ã¿ã¬ã¤ã](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) |
| | | |
| | | ### éçºç°å¢ |
| | | |
| | | - Cursor ã¾ã㯠VSCode |
| | | - ESP-IDFãã©ã°ã¤ã³ãã¤ã³ã¹ãã¼ã«ããSDKãã¼ã¸ã§ã³5.4以ä¸ã鏿 |
| | | - Linuxã¯Windowsãããåªãã¦ãããã³ã³ãã¤ã«ãéãããã©ã¤ãã®åé¡ãå°ãªã |
| | | - æ¬ããã¸ã§ã¯ãã¯Google C++ã³ã¼ãã¹ã¿ã¤ã«ãæ¡ç¨ãã³ã¼ãæåºæã¯æºæ ã確èªãã¦ãã ãã |
| | | |
| | | ### éçºè
ããã¥ã¡ã³ã |
| | | |
| | | - [ã«ã¹ã¿ã éçºãã¼ãã¬ã¤ã](docs/custom-board.md) - ã·ã£ãªã¸ã¼AIç¨ã®ã«ã¹ã¿ã éçºãã¼ãä½ææ¹æ³ |
| | | - [MCPãããã³ã«IoTå¶å¾¡ä½¿ç¨æ³](docs/mcp-usage.md) - MCPãããã³ã«ã§IoTããã¤ã¹ãå¶å¾¡ããæ¹æ³ |
| | | - [MCPãããã³ã«ã¤ã³ã¿ã©ã¯ã·ã§ã³ããã¼](docs/mcp-protocol.md) - ããã¤ã¹å´MCPãããã³ã«ã®å®è£
æ¹æ³ |
| | | - [MQTT + UDP ãã¤ããªããéä¿¡ãããã³ã«ããã¥ã¡ã³ã](docs/mqtt-udp.md) |
| | | - [詳細ãªWebSocketéä¿¡ãããã³ã«ããã¥ã¡ã³ã](docs/websocket.md) |
| | | |
| | | ## å¤§è¦æ¨¡ã¢ãã«è¨å® |
| | | |
| | | ãã§ã«ã·ã£ãªã¸ã¼AIãã£ãããããããã¤ã¹ããæã¡ã§ãå
¬å¼ãµã¼ãã¼ã«æ¥ç¶æ¸ã¿ã®å ´åã¯ã[xiaozhi.me](https://xiaozhi.me) ã³ã³ã½ã¼ã«ã§è¨å®ã§ãã¾ãã |
| | | |
| | | ð [ããã¯ã¨ã³ãæä½ãããªãã¥ã¼ããªã¢ã«ï¼æ§ã¤ã³ã¿ã¼ãã§ã¼ã¹ï¼](https://www.bilibili.com/video/BV1jUCUY2EKM/) |
| | | |
| | | ## é¢é£ãªã¼ãã³ã½ã¼ã¹ããã¸ã§ã¯ã |
| | | |
| | | å人PCã§ãµã¼ãã¼ããããã¤ããå ´åã¯ã以ä¸ã®ãªã¼ãã³ã½ã¼ã¹ããã¸ã§ã¯ããåç
§ãã¦ãã ããï¼ |
| | | |
| | | - [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Pythonãµã¼ãã¼ |
| | | - [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Javaãµã¼ãã¼ |
| | | - [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golangãµã¼ãã¼ |
| | | - [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golangãµã¼ãã¼ |
| | | |
| | | ã·ã£ãªã¸ã¼éä¿¡ãããã³ã«ãå©ç¨ããä»ã®ã¯ã©ã¤ã¢ã³ãããã¸ã§ã¯ãï¼ |
| | | |
| | | - [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Pythonã¯ã©ã¤ã¢ã³ã |
| | | - [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Androidã¯ã©ã¤ã¢ã³ã |
| | | - [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) ç¾åç§ææä¾ã®Linuxã¯ã©ã¤ã¢ã³ã |
| | | - [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) ææ¾ç§æã®Bluetoothããããã¡ã¼ã ã¦ã§ã¢ |
| | | - [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) ç§»é æä¾ã®QuecPythonãã¡ã¼ã ã¦ã§ã¢ |
| | | |
| | | ## ããã¸ã§ã¯ãã«ã¤ã㦠|
| | | |
| | | ããã¯ã¨ãå
ããããªã¼ãã³ã½ã¼ã¹ã§å
¬éãã¦ããESP32ããã¸ã§ã¯ãã§ãMITã©ã¤ã»ã³ã¹ã®ãã¨ã誰ã§ãç¡æã§ãåç¨å©ç¨ãå¯è½ã§ãã |
| | | |
| | | ãã®ããã¸ã§ã¯ããéãã¦ãAIãã¼ãã¦ã§ã¢éçºãçè§£ããæ¥éã«é²åããå¤§è¦æ¨¡è¨èªã¢ãã«ãå®éã®ãã¼ãã¦ã§ã¢ããã¤ã¹ã«å¿ç¨ã§ããããã«ãªããã¨ãç®æãã¦ãã¾ãã |
| | | |
| | | ãæè¦ããææ¡ãããã°ããã¤ã§ãIssueãæåºãããã[Discord](https://discord.gg/C759fGMBcZ) ã¾ã㯠QQã°ã«ã¼ãï¼1011329060 ã«ãåå ãã ããã |
| | | |
| | | ## ã¹ã¿ã¼å±¥æ´ |
| | | |
| | | <a href="https://star-history.com/#78/xiaozhi-esp32&Date"> |
| | | <picture> |
| | | <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" /> |
| | | <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" /> |
| | | <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" /> |
| | | </picture> |
| | | </a> |
| New file |
| | |
| | | # An MCP-based Chatbot |
| | | |
| | | ï¼ä¸æ | [English](README.md) | [æ¥æ¬èª](README_ja.md)ï¼ |
| | | |
| | | ## ä»ç» |
| | | |
| | | ð [人类ï¼ç» AI è£
æå头 vs AIï¼å½åºåç°ä¸»äººä¸å¤©æ²¡æ´å¤´ãbilibiliã](https://www.bilibili.com/video/BV1bpjgzKEhd/) |
| | | |
| | | ð [æå·¥æé ä½ ç AI 女åï¼æ°æå
¥é¨æç¨ãbilibiliã](https://www.bilibili.com/video/BV1XnmFYLEJN/) |
| | | |
| | | å°æº AI è天æºå¨äººä½ä¸ºä¸ä¸ªè¯é³äº¤äºå
¥å£ï¼å©ç¨ Qwen / DeepSeek ç大模åç AI è½åï¼éè¿ MCP åè®®å®ç°å¤ç«¯æ§å¶ã |
| | | |
| | | <img src="docs/mcp-based-graph.jpg" alt="éè¿MCPæ§å¶ä¸ç©" width="320"> |
| | | |
| | | ### çæ¬è¯´æ |
| | | |
| | | å½å v2 çæ¬ä¸ v1 çæ¬ååºè¡¨ä¸å
¼å®¹ï¼æä»¥æ æ³ä» v1 çæ¬éè¿ OTA åçº§å° v2 çæ¬ãååºè¡¨è¯´æåè§ [partitions/v2/README.md](partitions/v2/README.md)ã |
| | | |
| | | ä½¿ç¨ v1 çæ¬çææç¡¬ä»¶ï¼å¯ä»¥éè¿æå¨ç§å½åºä»¶æ¥åçº§å° v2 çæ¬ã |
| | | |
| | | v1 ç稳å®çæ¬ä¸º 1.9.2ï¼å¯ä»¥éè¿ `git checkout v1` æ¥åæ¢å° v1 çæ¬ï¼è¯¥åæ¯ä¼æç»ç»´æ¤å° 2026 å¹´ 2 æã |
| | | |
| | | ### å·²å®ç°åè½ |
| | | |
| | | - Wi-Fi / ML307 Cat.1 4G |
| | | - 离线è¯é³å¤é [ESP-SR](https://github.com/espressif/esp-sr) |
| | | - æ¯æä¸¤ç§éä¿¡åè®®ï¼[Websocket](docs/websocket_zh.md) æ MQTT+UDPï¼ |
| | | - éç¨ OPUS é³é¢ç¼è§£ç |
| | | - åºäºæµå¼ ASR + LLM + TTS æ¶æçè¯é³äº¤äº |
| | | - 声纹è¯å«ï¼è¯å«å½å说è¯äººç身份 [3D Speaker](https://github.com/modelscope/3D-Speaker) |
| | | - OLED / LCD æ¾ç¤ºå±ï¼æ¯æè¡¨æ
æ¾ç¤º |
| | | - çµéæ¾ç¤ºä¸çµæºç®¡ç |
| | | - æ¯æå¤è¯è¨ï¼ä¸æãè±æãæ¥æï¼ |
| | | - æ¯æ ESP32-C3ãESP32-S3ãESP32-P4 è¯çå¹³å° |
| | | - éè¿è®¾å¤ç«¯ MCP å®ç°è®¾å¤æ§å¶ï¼é³éãç¯å
ãçµæºãGPIO çï¼ |
| | | - éè¿äºç«¯ MCP æ©å±å¤§æ¨¡åè½åï¼æºè½å®¶å±
æ§å¶ãPCæ¡é¢æä½ãç¥è¯æç´¢ãé®ä»¶æ¶åçï¼ |
| | | - èªå®ä¹å¤éè¯ãåä½ã表æ
ä¸èå¤©èæ¯ï¼æ¯æç½é¡µç«¯å¨çº¿ä¿®æ¹ ([èªå®ä¹Assetsçæå¨](https://github.com/78/xiaozhi-assets-generator)) |
| | | |
| | | ## 硬件 |
| | | |
| | | ### é¢å
æ¿æå·¥å¶ä½å®è·µ |
| | | |
| | | 详è§é£ä¹¦ææ¡£æç¨ï¼ |
| | | |
| | | ð [ãå°æº AI è天æºå¨äººç¾ç§å
¨ä¹¦ã](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) |
| | | |
| | | é¢å
æ¿ææå¾å¦ä¸ï¼ |
| | | |
| | |  |
| | | |
| | | ### æ¯æ 70 å¤ä¸ªå¼æºç¡¬ä»¶ï¼ä»
å±ç¤ºé¨åï¼ |
| | | |
| | | - <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="ç«åÂ·å®ææ´¾ ESP32-S3 å¼åæ¿">ç«åÂ·å®ææ´¾ ESP32-S3 å¼åæ¿</a> |
| | | - <a href="https://github.com/espressif/esp-box" target="_blank" title="ä¹é« ESP32-S3-BOX3">ä¹é« ESP32-S3-BOX3</a> |
| | | - <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a> |
| | | - <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a> |
| | | - <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="ç¥å¥æé® 2.4">ç¥å¥æé® 2.4</a> |
| | | - <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="å¾®éªçµå ESP32-S3-Touch-AMOLED-1.8">å¾®éªçµå ESP32-S3-Touch-AMOLED-1.8</a> |
| | | - <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a> |
| | | - <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="è¾å¥ Mini C3">è¾å¥ Mini C3</a> |
| | | - <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">çç¨Â·AI åå </a> |
| | | - <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="æ åç§æNologo-ææº-1.54">æ åç§æ Nologo-ææº-1.54TFT</a> |
| | | - <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a> |
| | | - <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI è¶
ä½ææ¬æºå¨ç">ESP-HI è¶
ä½ææ¬æºå¨ç</a> |
| | | |
| | | <div style="display: flex; justify-content: space-between;"> |
| | | <a href="docs/v1/lichuang-s3.jpg" target="_blank" title="ç«åÂ·å®ææ´¾ ESP32-S3 å¼åæ¿"> |
| | | <img src="docs/v1/lichuang-s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/espbox3.jpg" target="_blank" title="ä¹é« ESP32-S3-BOX3"> |
| | | <img src="docs/v1/espbox3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3"> |
| | | <img src="docs/v1/m5cores3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base"> |
| | | <img src="docs/v1/atoms3r.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/magiclick.jpg" target="_blank" title="ç¥å¥æé® 2.4"> |
| | | <img src="docs/v1/magiclick.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/waveshare.jpg" target="_blank" title="å¾®éªçµå ESP32-S3-Touch-AMOLED-1.8"> |
| | | <img src="docs/v1/waveshare.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3"> |
| | | <img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/xmini-c3.jpg" target="_blank" title="è¾å¥ Mini C3"> |
| | | <img src="docs/v1/xmini-c3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan"> |
| | | <img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="æ åç§æNologo-ææº-1.54"> |
| | | <img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher"> |
| | | <img src="docs/v1/sensecap_watcher.jpg" width="240" /> |
| | | </a> |
| | | <a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI è¶
ä½ææ¬æºå¨ç"> |
| | | <img src="docs/v1/esp-hi.jpg" width="240" /> |
| | | </a> |
| | | </div> |
| | | |
| | | ## 软件 |
| | | |
| | | ### åºä»¶ç§å½ |
| | | |
| | | æ°æç¬¬ä¸æ¬¡æä½å»ºè®®å
ä¸è¦æå»ºå¼åç¯å¢ï¼ç´æ¥ä½¿ç¨å
å¼åç¯å¢ç§å½çåºä»¶ã |
| | | |
| | | åºä»¶é»è®¤æ¥å
¥ [xiaozhi.me](https://xiaozhi.me) 宿¹æå¡å¨ï¼ä¸ªäººç¨æ·æ³¨åè´¦å·å¯ä»¥å
è´¹ä½¿ç¨ Qwen 宿¶æ¨¡åã |
| | | |
| | | ð [æ°æç§å½åºä»¶æç¨](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) |
| | | |
| | | ### å¼åç¯å¢ |
| | | |
| | | - Cursor æ VSCode |
| | | - å®è£
ESP-IDF æä»¶ï¼éæ© SDK çæ¬ 5.4 æä»¥ä¸ |
| | | - Linux æ¯ Windows æ´å¥½ï¼ç¼è¯é度快ï¼ä¹å
å»é©±å¨é®é¢çå°æ° |
| | | - æ¬é¡¹ç®ä½¿ç¨ Google C++ 代ç 飿 ¼ï¼æäº¤ä»£ç æ¶è¯·ç¡®ä¿ç¬¦åè§è |
| | | |
| | | ### å¼åè
ææ¡£ |
| | | |
| | | - [èªå®ä¹å¼åæ¿æå](docs/custom-board_zh.md) - å¦ä¹ å¦ä½ä¸ºå°æº AI å建èªå®ä¹å¼åæ¿ |
| | | - [MCP åè®®ç©èç½æ§å¶ç¨æ³è¯´æ](docs/mcp-usage_zh.md) - äºè§£å¦ä½éè¿ MCP åè®®æ§å¶ç©èç½è®¾å¤ |
| | | - [MCP åè®®äº¤äºæµç¨](docs/mcp-protocol_zh.md) - 设å¤ç«¯ MCP åè®®çå®ç°æ¹å¼ |
| | | - [MQTT + UDP æ··åéä¿¡åè®®ææ¡£](docs/mqtt-udp_zh.md) |
| | | - [ä¸ä»½è¯¦ç»ç WebSocket éä¿¡åè®®ææ¡£](docs/websocket_zh.md) |
| | | |
| | | ## 大模åé
ç½® |
| | | |
| | | å¦æä½ å·²ç»æ¥æä¸ä¸ªå°æº AI è天æºå¨äººè®¾å¤ï¼å¹¶ä¸å·²æ¥å
¥å®æ¹æå¡å¨ï¼å¯ä»¥ç»å½ [xiaozhi.me](https://xiaozhi.me) æ§å¶å°è¿è¡é
ç½®ã |
| | | |
| | | ð [åå°æä½è§é¢æç¨ï¼æ§ççé¢ï¼](https://www.bilibili.com/video/BV1jUCUY2EKM/) |
| | | |
| | | ## ç¸å
³å¼æºé¡¹ç® |
| | | |
| | | å¨ä¸ªäººçµèä¸é¨ç½²æå¡å¨ï¼å¯ä»¥åè以ä¸ç¬¬ä¸æ¹å¼æºç项ç®ï¼ |
| | | |
| | | - [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python æå¡å¨ |
| | | - [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java æå¡å¨ |
| | | - [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang æå¡å¨ |
| | | - [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golang æå¡å¨ |
| | | |
| | | 使ç¨å°æºéä¿¡åè®®çç¬¬ä¸æ¹å®¢æ·ç«¯é¡¹ç®ï¼ |
| | | |
| | | - [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python 客æ·ç«¯ |
| | | - [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客æ·ç«¯ |
| | | - [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) ç¾é®ç§ææä¾ç Linux 客æ·ç«¯ |
| | | - [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) ææ¾ç§æçèçè¯çåºä»¶ |
| | | - [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) ç§»è¿æä¾ç QuecPython åºä»¶ |
| | | |
| | | ## å
³äºé¡¹ç® |
| | | |
| | | è¿æ¯ä¸ä¸ªç±è¾å¥å¼æºç ESP32 项ç®ï¼ä»¥ MIT 许å¯è¯åå¸ï¼å
许任ä½äººå
费使ç¨ï¼ä¿®æ¹æç¨äºåä¸ç¨éã |
| | | |
| | | æä»¬å¸æéè¿è¿ä¸ªé¡¹ç®ï¼è½å¤å¸®å©å¤§å®¶äºè§£ AI 硬件å¼åï¼å°å½ä¸é£éåå±ç大è¯è¨æ¨¡ååºç¨å°å®é
ç硬件设å¤ä¸ã |
| | | |
| | | å¦æä½ æä»»ä½æ³æ³æå»ºè®®ï¼è¯·éæ¶æåº Issues æå å
¥ [Discord](https://discord.gg/C759fGMBcZ) æ QQ 群ï¼1011329060 |
| | | |
| | | ## Star History |
| | | |
| | | <a href="https://star-history.com/#78/xiaozhi-esp32&Date"> |
| | | <picture> |
| | | <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" /> |
| | | <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" /> |
| | | <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" /> |
| | | </picture> |
| | | </a> |
| New file |
| | |
| | | # BluFi Provisioning (with `esp-wifi-connect`) |
| | | |
| | | This document explains how to enable and use BluFi (BLE-based WiFi provisioning) in the XiaoZhi firmware, together with the in-tree `esp-wifi-connect` component that handles WiFi connection and credential storage. See the official [Espressif BluFi documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/ble/blufi.html) for the protocol details. |
| | | |
| | | ## Prerequisites |
| | | |
| | | - A chip and firmware configuration that support BLE. |
| | | - In `idf.py menuconfig`, enable `WiFi Configuration Method -> Esp Blufi` (`CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING=y`). If you want to use BluFi, disable the Hotspot option in the same menu; otherwise hotspot provisioning wins by default. |
| | | - Keep the default NVS and event-loop initialization provided by the project's `app_main`. |
| | | - Exactly one of `CONFIG_BT_BLUEDROID_ENABLED` / `CONFIG_BT_NIMBLE_ENABLED` must be selected; they are mutually exclusive. |
| | | |
| | | ## Workflow |
| | | |
| | | 1. A phone (using the official EspBlufi app or another BluFi client) connects to the device over BLE and sends the target WiFi SSID / password. The phone can also request the list of WiFi networks scanned by the device through the BluFi protocol. |
| | | 2. In `ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP`, the device stores the credentials into `SsidManager` (persisted in NVS by the `esp-wifi-connect` component). |
| | | 3. The device then launches `WifiStation` to scan and connect; progress is reported back over BluFi. |
| | | 4. If provisioning succeeds, the device connects to the new WiFi automatically. If it fails, an error status is sent back. |
| | | |
| | | ## Steps |
| | | |
| | | 1. **Configure**: turn on `Esp Blufi` in menuconfig, then build and flash the firmware. |
| | | 2. **Trigger provisioning**: at first boot with no stored WiFi credentials the device enters provisioning automatically. |
| | | 3. **Phone side**: open the EspBlufi app (or another BluFi client), scan and connect to the device, optionally enable encryption, then enter the WiFi SSID / password and send them. |
| | | 4. **Observe the result**: |
| | | - Success: BluFi reports success and the device connects to WiFi. |
| | | - Failure: BluFi reports failure; retry or check the router. |
| | | |
| | | ## Notes |
| | | |
| | | - BluFi cannot be used at the same time as hotspot provisioning. If hotspot provisioning is already enabled, the device will use it. Keep only one provisioning method in menuconfig. |
| | | - When running repeated tests, clear or overwrite the stored SSID (`wifi` NVS namespace) to avoid stale credentials interfering with the next run. |
| | | - If you write your own BluFi client, follow the official protocol frame format linked above. |
| | | - The EspBlufi app download links are listed in the official documentation. |
| | | - Because the BluFi API changed in IDF 5.5.2, firmware built with 5.5.2 advertises the Bluetooth name as `"Xiaozhi-Blufi"`, while 5.5.1 uses `"BLUFI_DEVICE"`. |
| New file |
| | |
| | | # BluFi é
ç½ï¼éæ esp-wifi-connectï¼ |
| | | |
| | | æ¬ææ¡£è¯´æå¦ä½å¨å°æºåºä»¶ä¸å¯ç¨åä½¿ç¨ BluFiï¼BLE WiâFi é
ç½ï¼ï¼å¹¶ç»å项ç®å
ç½®ç `esp-wifi-connect` ç»ä»¶å®æ WiâFi è¿æ¥ä¸åå¨ã宿¹ |
| | | BluFi |
| | | å议说æè¯·åè [Espressif ææ¡£](https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-guides/ble/blufi.html)ã |
| | | |
| | | ## åç½®æ¡ä»¶ |
| | | |
| | | - éè¦æ¯æ BLE çè¯çä¸åºä»¶é
ç½®ã |
| | | - å¨ `idf.py menuconfig` ä¸å¯ç¨ `WiFi Configuration Method -> Esp Blufi`ï¼`CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING=y` |
| | | ï¼ã妿æ³ä½¿ç¨ BluFiï¼å¿
é¡»å
³éåä¸èåä¸ç Hotspot é项ï¼å¦åé»è®¤ä½¿ç¨ Hotspot é
ç½æ¨¡å¼ã |
| | | |
| | | - ä¿æé»è®¤ç NVS ä¸äºä»¶å¾ªç¯åå§åï¼é¡¹ç®ç `app_main` å·²å¤çï¼ã |
| | | - CONFIG_BT_BLUEDROID_ENABLEDãCONFIG_BT_NIMBLE_ENABLEDè¿ä¸¤ä¸ªå®åºäºéä¸ï¼ä¸è½åæ¶å¯ç¨ã |
| | | ## 工使µç¨ |
| | | |
| | | 1) ææºç«¯éè¿ BluFiï¼å¦å®æ¹ EspBlufi App æèªç 客æ·ç«¯ï¼è¿æ¥è®¾å¤ï¼åé WiâFi SSID/å¯ç ï¼ææºç«¯å¯ä»¥éè¿blufiåè®®è·å设å¤ç«¯æ«æå°çWiFiå表ã |
| | | 2) 设å¤ä¾§å¨ `ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP` ä¸å°åæ®åå
¥ `SsidManager`ï¼åå¨å° NVSï¼å±äº `esp-wifi-connect` ç»ä»¶ï¼ã |
| | | 3) éåå¯å¨ `WifiStation` æ«æå¹¶è¿æ¥ï¼ç¶æéè¿ BluFi è¿åã |
| | | 4) é
ç½æåå设å¤ä¼èªå¨è¿æ¥æ° WiâFiï¼å¤±è´¥åè¿åå¤±è´¥ç¶æã |
| | | |
| | | ## ä½¿ç¨æ¥éª¤ |
| | | |
| | | 1. é
ç½®ï¼å¨ menuconfig å¼å¯ `Esp Blufi`ãç¼è¯å¹¶ç§å½åºä»¶ã |
| | | 2. 触åé
ç½ï¼è®¾å¤é¦æ¬¡å¯å¨ä¸æ²¡æå·²ä¿åç WiâFi æ¶ä¼èªå¨è¿å
¥é
ç½ã |
| | | 3. ææºç«¯æä½ï¼æå¼ EspBlufi Appï¼æå
¶ä» BluFi 客æ·ç«¯ï¼ï¼æç´¢å¹¶è¿æ¥è®¾å¤ï¼å¯ä»¥éæ©æ¯å¦å å¯ï¼ææç¤ºè¾å
¥ WiâFi SSID/å¯ç å¹¶åéã |
| | | 4. è§å¯ç»æï¼ |
| | | - æåï¼BluFi æ¥åè¿æ¥æåï¼è®¾å¤èªå¨è¿æ¥ WiâFiã |
| | | - 失败ï¼BluFi è¿åå¤±è´¥ç¶æï¼å¯éæ°åéææ£æ¥è·¯ç±å¨ã |
| | | |
| | | ## 注æäºé¡¹ |
| | | |
| | | - BluFi é
ç½ä¸æ¯æä¸çç¹é
ç½åæ¶å¼å¯ã妿çç¹é
ç½å·²ç»å¯å¨ï¼åé»è®¤ä½¿ç¨çç¹é
ç½ãè¯·å¨ menuconfig ä¸åªä¿çä¸ç§é
ç½æ¹å¼ã |
| | | - è¥å¤æ¬¡æµè¯ï¼å»ºè®®æ¸
餿è¦çåå¨ç SSIDï¼`wifi` å½å空é´ï¼ï¼é¿å
æ§é
置干æ°ã |
| | | - å¦æä½¿ç¨èªå®ä¹ BluFi 客æ·ç«¯ï¼ééµå¾ªå®æ¹åè®®å¸§æ ¼å¼ï¼åè䏿宿¹ææ¡£é¾æ¥ã |
| | | - 宿¹ææ¡£ä¸å·²æä¾EspBlufi APPä¸è½½å°å |
| | | - ç±äºIDF5.5.2çblufiæ¥å£åçåå,5.5.2çæ¬ç¼è¯åèçå称为"Xiaozhi-Blufi",5.5.1çæ¬ä¸èçå称为"BLUFI_DEVICE" |
| New file |
| | |
| | | # Code Style Guide |
| | | |
| | | ## Formatting Tool |
| | | |
| | | This project uses `clang-format` to keep the code style consistent. The `.clang-format` file in the project root is based on the Google C++ style guide with a few project-specific tweaks. |
| | | |
| | | ### Installing clang-format |
| | | |
| | | Make sure `clang-format` is available before you use it: |
| | | |
| | | - **Windows**: |
| | | ```powershell |
| | | winget install LLVM |
| | | # or with Chocolatey |
| | | choco install llvm |
| | | ``` |
| | | |
| | | - **Linux**: |
| | | ```bash |
| | | sudo apt install clang-format # Ubuntu/Debian |
| | | sudo dnf install clang-tools-extra # Fedora |
| | | ``` |
| | | |
| | | - **macOS**: |
| | | ```bash |
| | | brew install clang-format |
| | | ``` |
| | | |
| | | ### Usage |
| | | |
| | | 1. **Format a single file**: |
| | | ```bash |
| | | clang-format -i path/to/your/file.cpp |
| | | ``` |
| | | |
| | | 2. **Format the entire project**: |
| | | ```bash |
| | | # Run from the project root |
| | | find main -iname '*.h' -o -iname '*.cc' | xargs clang-format -i |
| | | ``` |
| | | |
| | | 3. **Check formatting without modifying files (useful in CI / pre-commit)**: |
| | | ```bash |
| | | clang-format --dry-run -Werror path/to/your/file.cpp |
| | | ``` |
| | | |
| | | ### IDE Integration |
| | | |
| | | - **Visual Studio Code**: |
| | | 1. Install the C/C++ extension. |
| | | 2. Set `C_Cpp.formatting` to `clangFormat` in settings. |
| | | 3. Optionally enable `editor.formatOnSave`. |
| | | |
| | | - **CLion**: |
| | | 1. Open `Editor > Code Style > C/C++` in the settings. |
| | | 2. Set `Formatter` to `clang-format`. |
| | | 3. Choose "use the .clang-format file in the project". |
| | | |
| | | ### Main Rules |
| | | |
| | | - Indent with 4 spaces. |
| | | - Line width capped at 100 characters. |
| | | - Attach-style braces (`{` on the same line as the control statement). |
| | | - Pointers and references bind to the type (left alignment). |
| | | - Includes are sorted automatically. |
| | | - Access specifiers are indented by -4 spaces. |
| | | |
| | | ### Notes |
| | | |
| | | 1. Make sure the code has been formatted before committing. |
| | | 2. Do not fix up alignment by hand after running clang-format. |
| | | 3. To exclude a block from formatting, wrap it with: |
| | | ```cpp |
| | | // clang-format off |
| | | your code |
| | | // clang-format on |
| | | ``` |
| | | |
| | | ### FAQ |
| | | |
| | | 1. **Formatting fails**: |
| | | - Check whether `clang-format` is too old. |
| | | - Make sure the file is UTF-8 encoded. |
| | | - Validate the syntax of your `.clang-format` file. |
| | | |
| | | 2. **Output differs from what you expected**: |
| | | - Verify that the `.clang-format` in the project root is actually picked up. |
| | | - Make sure no other `.clang-format` higher in the tree is winning. |
| | | |
| | | Questions and suggestions are welcome - please open an issue or a pull request. |
| New file |
| | |
| | | # 代ç 飿 ¼æå |
| | | |
| | | ## ä»£ç æ ¼å¼åå·¥å
· |
| | | |
| | | æ¬é¡¹ç®ä½¿ç¨ clang-format å·¥å
·æ¥ç»ä¸ä»£ç 飿 ¼ãæä»¬å·²ç»å¨é¡¹ç®æ ¹ç®å½ä¸æä¾äº `.clang-format` é
ç½®æä»¶ï¼è¯¥é
ç½®åºäº Google C++ 飿 ¼æåï¼å¹¶åäºä¸äºèªå®ä¹è°æ´ã |
| | | |
| | | ### å®è£
clang-format |
| | | |
| | | å¨ä½¿ç¨ä¹åï¼è¯·ç¡®ä¿ä½ å·²ç»å®è£
äº clang-format å·¥å
·ï¼ |
| | | |
| | | - **Windows**ï¼ |
| | | ```powershell |
| | | winget install LLVM |
| | | # æè
ä½¿ç¨ Chocolatey |
| | | choco install llvm |
| | | ``` |
| | | |
| | | - **Linux**ï¼ |
| | | ```bash |
| | | sudo apt install clang-format # Ubuntu/Debian |
| | | sudo dnf install clang-tools-extra # Fedora |
| | | ``` |
| | | |
| | | - **macOS**ï¼ |
| | | ```bash |
| | | brew install clang-format |
| | | ``` |
| | | |
| | | ### ä½¿ç¨æ¹æ³ |
| | | |
| | | 1. **æ ¼å¼åå个æä»¶**ï¼ |
| | | ```bash |
| | | clang-format -i path/to/your/file.cpp |
| | | ``` |
| | | |
| | | 2. **æ ¼å¼åæ´ä¸ªé¡¹ç®**ï¼ |
| | | ```bash |
| | | # å¨é¡¹ç®æ ¹ç®å½ä¸æ§è¡ |
| | | find main -iname *.h -o -iname *.cc | xargs clang-format -i |
| | | ``` |
| | | |
| | | 3. **å¨æäº¤ä»£ç åæ£æ¥æ ¼å¼**ï¼ |
| | | ```bash |
| | | # æ£æ¥æä»¶æ ¼å¼æ¯å¦ç¬¦åè§èï¼ä¸ä¿®æ¹æä»¶ï¼ |
| | | clang-format --dry-run -Werror path/to/your/file.cpp |
| | | ``` |
| | | |
| | | ### IDE éæ |
| | | |
| | | - **Visual Studio Code**ï¼ |
| | | 1. å®è£
C/C++ æ©å± |
| | | 2. å¨è®¾ç½®ä¸å¯ç¨ `C_Cpp.formatting` 为 `clang-format` |
| | | 3. å¯ä»¥è®¾ç½®ä¿åæ¶èªå¨æ ¼å¼åï¼`editor.formatOnSave: true` |
| | | |
| | | - **CLion**ï¼ |
| | | 1. å¨è®¾ç½®ä¸éæ© `Editor > Code Style > C/C++` |
| | | 2. å° `Formatter` 设置为 `clang-format` |
| | | 3. éæ©ä½¿ç¨é¡¹ç®ä¸ç `.clang-format` é
ç½®æä»¶ |
| | | |
| | | ### ä¸»è¦æ ¼å¼è§å |
| | | |
| | | - 缩è¿ä½¿ç¨ 4 ä¸ªç©ºæ ¼ |
| | | - è¡å®½éå¶ä¸º 100 å符 |
| | | - 大æ¬å·éç¨ Attach 飿 ¼ï¼ä¸æ§å¶è¯å¥å¨åä¸è¡ï¼ |
| | | - æéåå¼ç¨ç¬¦å·é å·¦å¯¹é½ |
| | | - èªå¨æåºå¤´æä»¶å
å« |
| | | - 类访é®ä¿®é¥°ç¬¦ç¼©è¿ä¸º -4 ç©ºæ ¼ |
| | | |
| | | ### 注æäºé¡¹ |
| | | |
| | | 1. æäº¤ä»£ç å请确ä¿ä»£ç å·²ç»è¿æ ¼å¼å |
| | | 2. ä¸è¦æå¨è°æ´å·²æ ¼å¼åç代ç å¯¹é½ |
| | | 3. å¦æææ®µä»£ç ä¸å¸æè¢«æ ¼å¼åï¼å¯ä»¥ä½¿ç¨ä»¥ä¸æ³¨éå
å´ï¼ |
| | | ```cpp |
| | | // clang-format off |
| | | // ä½ ç代ç |
| | | // clang-format on |
| | | ``` |
| | | |
| | | ### 常è§é®é¢ |
| | | |
| | | 1. **æ ¼å¼å失败**ï¼ |
| | | - æ£æ¥ clang-format çæ¬æ¯å¦è¿ä½ |
| | | - 确认æä»¶ç¼ç 为 UTF-8 |
| | | - éªè¯ .clang-format æä»¶è¯æ³æ¯å¦æ£ç¡® |
| | | |
| | | 2. **ä¸æææ ¼å¼ä¸ç¬¦**ï¼ |
| | | - æ£æ¥æ¯å¦ä½¿ç¨äºé¡¹ç®æ ¹ç®å½ä¸ç .clang-format é
ç½® |
| | | - 确认没æå
¶ä»ä½ç½®ç .clang-format æä»¶è¢«ä¼å
ä½¿ç¨ |
| | | |
| | | 妿任ä½é®é¢æå»ºè®®ï¼æ¬¢è¿æåº issue æ pull requestã |
| New file |
| | |
| | | # Custom Board Guide |
| | | |
| | | This guide describes how to add a new board to the XiaoZhi AI voice assistant project. XiaoZhi AI supports 70+ ESP32-series boards; each one lives in its own directory under `main/boards/`. |
| | | |
| | | ## Important |
| | | |
| | | > **Warning**: for a custom board whose IO configuration differs from an existing board, never overwrite the original board's configuration. Always create a new board type - or use the `builds` array in `config.json` to produce a distinct firmware name with different `sdkconfig` macros. Use `python scripts/release.py [board-directory]` to build and package the firmware. |
| | | > |
| | | > Overwriting an existing board's configuration is dangerous because OTA updates may replace your custom firmware with the stock firmware for the original board. Every board must have a unique identity and its own firmware update channel. |
| | | |
| | | ## Directory Layout |
| | | |
| | | A board directory typically contains: |
| | | |
| | | - `xxx_board.cc` - board-level initialization and glue code. |
| | | - `config.h` - pin assignments and board-level settings. |
| | | - `config.json` - build configuration consumed by `scripts/release.py`. |
| | | - `README.md` - board-specific notes. |
| | | |
| | | Boards can live directly under `main/boards/` or be grouped by manufacturer under `main/boards/<manufacturer>/<board>/` (see [Manufacturer Sub-directories](#manufacturer-sub-directories) below). |
| | | |
| | | ## Steps |
| | | |
| | | ### 1. Create the Board Directory |
| | | |
| | | Create a new directory under `main/boards/` using the `[vendor]-[model]` naming style (e.g. `m5stack-tab5`): |
| | | |
| | | ```bash |
| | | mkdir main/boards/my-custom-board |
| | | ``` |
| | | |
| | | ### 2. Create the Configuration Files |
| | | |
| | | #### config.h |
| | | |
| | | Define all hardware settings in `config.h`: |
| | | |
| | | - Audio sample rates and I2S pin mapping. |
| | | - Audio codec I2C address and pins. |
| | | - Button and LED pins. |
| | | - Display parameters and pins. |
| | | |
| | | Example (from `lichuang-c3-dev`): |
| | | |
| | | ```c |
| | | #ifndef _BOARD_CONFIG_H_ |
| | | #define _BOARD_CONFIG_H_ |
| | | |
| | | #include <driver/gpio.h> |
| | | |
| | | // Audio |
| | | #define AUDIO_INPUT_SAMPLE_RATE 24000 |
| | | #define AUDIO_OUTPUT_SAMPLE_RATE 24000 |
| | | |
| | | #define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 |
| | | #define AUDIO_I2S_GPIO_WS GPIO_NUM_12 |
| | | #define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 |
| | | #define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 |
| | | #define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 |
| | | |
| | | #define AUDIO_CODEC_PA_PIN GPIO_NUM_13 |
| | | #define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0 |
| | | #define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 |
| | | #define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR |
| | | |
| | | // Buttons |
| | | #define BOOT_BUTTON_GPIO GPIO_NUM_9 |
| | | |
| | | // Display |
| | | #define DISPLAY_SPI_SCK_PIN GPIO_NUM_3 |
| | | #define DISPLAY_SPI_MOSI_PIN GPIO_NUM_5 |
| | | #define DISPLAY_DC_PIN GPIO_NUM_6 |
| | | #define DISPLAY_SPI_CS_PIN GPIO_NUM_4 |
| | | |
| | | #define DISPLAY_WIDTH 320 |
| | | #define DISPLAY_HEIGHT 240 |
| | | #define DISPLAY_MIRROR_X true |
| | | #define DISPLAY_MIRROR_Y false |
| | | #define DISPLAY_SWAP_XY true |
| | | |
| | | #define DISPLAY_OFFSET_X 0 |
| | | #define DISPLAY_OFFSET_Y 0 |
| | | |
| | | #define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2 |
| | | #define DISPLAY_BACKLIGHT_OUTPUT_INVERT true |
| | | |
| | | #endif // _BOARD_CONFIG_H_ |
| | | ``` |
| | | |
| | | #### config.json |
| | | |
| | | `config.json` drives `scripts/release.py`: |
| | | |
| | | ```json |
| | | { |
| | | "target": "esp32s3", |
| | | "builds": [ |
| | | { |
| | | "name": "my-custom-board", |
| | | "sdkconfig_append": [ |
| | | "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", |
| | | "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"" |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | **Fields**: |
| | | - `target`: target chip, must match the real hardware (`esp32`, `esp32s3`, `esp32c3`, `esp32c6`, `esp32p4`, ...). |
| | | - `name`: firmware package name; typically matches the directory name. |
| | | - `sdkconfig_append`: extra sdkconfig lines merged into the defaults. |
| | | |
| | | **Common `sdkconfig_append` entries**: |
| | | |
| | | ```json |
| | | // Flash size |
| | | "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y" |
| | | "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y" |
| | | "CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y" |
| | | |
| | | // Partition table |
| | | "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\"" |
| | | "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"" |
| | | "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"" |
| | | |
| | | // Language |
| | | "CONFIG_LANGUAGE_EN_US=y" |
| | | "CONFIG_LANGUAGE_ZH_CN=y" |
| | | |
| | | // Wake word configuration |
| | | "CONFIG_USE_DEVICE_AEC=y" // enable on-device AEC |
| | | "CONFIG_WAKE_WORD_DISABLED=y" // disable wake word detection |
| | | ``` |
| | | |
| | | ### 3. Implement the Board Class |
| | | |
| | | Create `my_custom_board.cc` containing the board-level implementation. |
| | | |
| | | A basic board class has: |
| | | |
| | | 1. **Class declaration**: derive from `WifiBoard` or `Ml307Board`. |
| | | 2. **Initialization helpers**: I2C, display, buttons, IoT/MCP tools, etc. |
| | | 3. **Virtual overrides**: `GetAudioCodec()`, `GetDisplay()`, `GetBacklight()`, ... |
| | | 4. **Board registration**: `DECLARE_BOARD(ClassName)`. |
| | | |
| | | ```cpp |
| | | #include "wifi_board.h" |
| | | #include "codecs/es8311_audio_codec.h" |
| | | #include "display/lcd_display.h" |
| | | #include "application.h" |
| | | #include "button.h" |
| | | #include "config.h" |
| | | #include "mcp_server.h" |
| | | |
| | | #include <esp_log.h> |
| | | #include <driver/i2c_master.h> |
| | | #include <driver/spi_common.h> |
| | | |
| | | #define TAG "MyCustomBoard" |
| | | |
| | | class MyCustomBoard : public WifiBoard { |
| | | private: |
| | | i2c_master_bus_handle_t codec_i2c_bus_; |
| | | Button boot_button_; |
| | | LcdDisplay* display_; |
| | | |
| | | void InitializeI2c() { |
| | | i2c_master_bus_config_t i2c_bus_cfg = { |
| | | .i2c_port = I2C_NUM_0, |
| | | .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, |
| | | .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, |
| | | .clk_source = I2C_CLK_SRC_DEFAULT, |
| | | .glitch_ignore_cnt = 7, |
| | | .intr_priority = 0, |
| | | .trans_queue_depth = 0, |
| | | .flags = { |
| | | .enable_internal_pullup = 1, |
| | | }, |
| | | }; |
| | | ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_)); |
| | | } |
| | | |
| | | void InitializeSpi() { |
| | | spi_bus_config_t buscfg = {}; |
| | | buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN; |
| | | buscfg.miso_io_num = GPIO_NUM_NC; |
| | | buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN; |
| | | buscfg.quadwp_io_num = GPIO_NUM_NC; |
| | | buscfg.quadhd_io_num = GPIO_NUM_NC; |
| | | buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t); |
| | | ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); |
| | | } |
| | | |
| | | void InitializeButtons() { |
| | | boot_button_.OnClick([this]() { |
| | | auto& app = Application::GetInstance(); |
| | | if (app.GetDeviceState() == kDeviceStateStarting) { |
| | | EnterWifiConfigMode(); |
| | | return; |
| | | } |
| | | app.ToggleChatState(); |
| | | }); |
| | | } |
| | | |
| | | void InitializeDisplay() { |
| | | esp_lcd_panel_io_handle_t panel_io = nullptr; |
| | | esp_lcd_panel_handle_t panel = nullptr; |
| | | |
| | | esp_lcd_panel_io_spi_config_t io_config = {}; |
| | | io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN; |
| | | io_config.dc_gpio_num = DISPLAY_DC_PIN; |
| | | io_config.spi_mode = 2; |
| | | io_config.pclk_hz = 80 * 1000 * 1000; |
| | | io_config.trans_queue_depth = 10; |
| | | io_config.lcd_cmd_bits = 8; |
| | | io_config.lcd_param_bits = 8; |
| | | ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io)); |
| | | |
| | | esp_lcd_panel_dev_config_t panel_config = {}; |
| | | panel_config.reset_gpio_num = GPIO_NUM_NC; |
| | | panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; |
| | | panel_config.bits_per_pixel = 16; |
| | | ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); |
| | | |
| | | esp_lcd_panel_reset(panel); |
| | | esp_lcd_panel_init(panel); |
| | | esp_lcd_panel_invert_color(panel, true); |
| | | esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); |
| | | esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); |
| | | |
| | | display_ = new SpiLcdDisplay(panel_io, panel, |
| | | DISPLAY_WIDTH, DISPLAY_HEIGHT, |
| | | DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, |
| | | DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); |
| | | } |
| | | |
| | | void InitializeTools() { |
| | | // Register MCP tools here; see docs/mcp-usage.md. |
| | | } |
| | | |
| | | public: |
| | | MyCustomBoard() : boot_button_(BOOT_BUTTON_GPIO) { |
| | | InitializeI2c(); |
| | | InitializeSpi(); |
| | | InitializeDisplay(); |
| | | InitializeButtons(); |
| | | InitializeTools(); |
| | | GetBacklight()->SetBrightness(100); |
| | | } |
| | | |
| | | virtual AudioCodec* GetAudioCodec() override { |
| | | static Es8311AudioCodec audio_codec( |
| | | codec_i2c_bus_, |
| | | I2C_NUM_0, |
| | | AUDIO_INPUT_SAMPLE_RATE, |
| | | AUDIO_OUTPUT_SAMPLE_RATE, |
| | | AUDIO_I2S_GPIO_MCLK, |
| | | AUDIO_I2S_GPIO_BCLK, |
| | | AUDIO_I2S_GPIO_WS, |
| | | AUDIO_I2S_GPIO_DOUT, |
| | | AUDIO_I2S_GPIO_DIN, |
| | | AUDIO_CODEC_PA_PIN, |
| | | AUDIO_CODEC_ES8311_ADDR); |
| | | return &audio_codec; |
| | | } |
| | | |
| | | virtual Display* GetDisplay() override { |
| | | return display_; |
| | | } |
| | | |
| | | virtual Backlight* GetBacklight() override { |
| | | static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); |
| | | return &backlight; |
| | | } |
| | | }; |
| | | |
| | | DECLARE_BOARD(MyCustomBoard); |
| | | ``` |
| | | |
| | | ### 4. Hook Up the Build System |
| | | |
| | | #### Add a Kconfig entry |
| | | |
| | | In `main/Kconfig.projbuild`, add an entry to the `choice BOARD_TYPE` block: |
| | | |
| | | ```kconfig |
| | | choice BOARD_TYPE |
| | | prompt "Board Type" |
| | | default BOARD_TYPE_BREAD_COMPACT_WIFI |
| | | help |
| | | Board type. |
| | | |
| | | # ... other entries ... |
| | | |
| | | config BOARD_TYPE_MY_CUSTOM_BOARD |
| | | bool "My Custom Board" |
| | | depends on IDF_TARGET_ESP32S3 # pick the matching target |
| | | endchoice |
| | | ``` |
| | | |
| | | Notes: |
| | | - The identifier must be uppercase and underscore-separated. |
| | | - `depends on` restricts the entry to the correct target (`IDF_TARGET_ESP32S3`, `IDF_TARGET_ESP32C3`, ...). |
| | | - The label can be localized. |
| | | |
| | | #### Add a branch in CMakeLists.txt |
| | | |
| | | Open `main/CMakeLists.txt` and extend the board-type chain: |
| | | |
| | | ```cmake |
| | | elseif(CONFIG_BOARD_TYPE_MY_CUSTOM_BOARD) |
| | | set(BOARD_TYPE "my-custom-board") # must match the directory name |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) # pick a font for the display |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) // optional, for emoji display |
| | | ``` |
| | | |
| | | **Font and emoji guidance**: |
| | | |
| | | Pick a font size that matches the display resolution: |
| | | - Small (128x64 OLED): `font_puhui_basic_14_1` / `font_awesome_14_1` |
| | | - Small-medium (240x240): `font_puhui_basic_16_4` / `font_awesome_16_4` |
| | | - Medium (240x320): `font_puhui_basic_20_4` / `font_awesome_20_4` |
| | | - Large (480x320+): `font_puhui_basic_30_4` / `font_awesome_30_4` |
| | | |
| | | Emoji collections: |
| | | - `twemoji_32` - 32x32 pixels (small screens). |
| | | - `twemoji_64` - 64x64 pixels (large screens). |
| | | |
| | | ### 5. Build and Flash |
| | | |
| | | #### Option A - use `idf.py` manually |
| | | |
| | | 1. Set the target chip (first time, or when switching targets): |
| | | ```bash |
| | | idf.py set-target esp32s3 # ESP32-S3 |
| | | idf.py set-target esp32c3 # ESP32-C3 |
| | | idf.py set-target esp32 # ESP32 |
| | | ``` |
| | | |
| | | 2. Clean stale configuration: |
| | | ```bash |
| | | idf.py fullclean |
| | | ``` |
| | | |
| | | 3. Select the board via menuconfig: |
| | | ```bash |
| | | idf.py menuconfig |
| | | ``` |
| | | Navigate to `Xiaozhi Assistant -> Board Type` and choose your board. |
| | | |
| | | 4. Build and flash: |
| | | ```bash |
| | | idf.py build |
| | | idf.py flash monitor |
| | | ``` |
| | | |
| | | #### Option B - use `release.py` (recommended) |
| | | |
| | | If the board directory contains a `config.json`, you can build and package automatically: |
| | | |
| | | ```bash |
| | | python scripts/release.py my-custom-board |
| | | ``` |
| | | |
| | | The script: |
| | | - Reads `target` from `config.json` and calls `idf.py set-target`. |
| | | - Appends the entries listed in `sdkconfig_append`. |
| | | - Builds and packages the firmware. |
| | | |
| | | ### 6. Write the README |
| | | |
| | | In `README.md`, describe the board, hardware requirements, build instructions, and any special notes. |
| | | |
| | | ## Manufacturer Sub-directories |
| | | |
| | | Boards can be grouped by manufacturer under `main/boards/<manufacturer>/<board>/`. This is the recommended layout when a single vendor ships several variants - for example `main/boards/waveshare/esp32-p4-nano/` or `main/boards/lceda-course-examples/eda-tv-pro/`. |
| | | |
| | | To enable the layout, set the `MANUFACTURER` variable in `main/CMakeLists.txt` for your board: |
| | | |
| | | ```cmake |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_NANO) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-nano") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | ``` |
| | | |
| | | When `MANUFACTURER` is set, the build system globs source files from `main/boards/${MANUFACTURER}/${BOARD_TYPE}/`. When it is empty, it falls back to the flat `main/boards/${BOARD_TYPE}/` layout. |
| | | |
| | | Rules of thumb: |
| | | - Use the manufacturer layout when you have two or more boards from the same vendor that share drivers, assets, or documentation. |
| | | - Use the flat layout for one-off boards and community examples. |
| | | - Directory names use lowercase with dashes (e.g. `waveshare`, `lceda-course-examples`). |
| | | |
| | | ## Common Board Components |
| | | |
| | | Several reusable components live in `main/boards/common/`. You can include them directly from your board class: |
| | | |
| | | ### Display drivers |
| | | |
| | | Supported LCD families include: |
| | | - ST7789 (SPI) |
| | | - ILI9341 (SPI) |
| | | - SH8601 (QSPI) |
| | | - and many more. |
| | | |
| | | ### Audio codecs |
| | | |
| | | - `Es8311AudioCodec` (most common) |
| | | - `Es8374AudioCodec` |
| | | - `Es8388AudioCodec` |
| | | - `Es8389AudioCodec` |
| | | - `BoxAudioCodec` (ES7210 mic array + codec combo used on ESP-Box boards) |
| | | - `NoAudioCodec` (direct I2S without external codec) |
| | | - `DummyAudioCodec` (placeholder for boards without audio) |
| | | |
| | | ### Power management |
| | | |
| | | - `Axp2101` power management IC helpers. |
| | | - `Sy6970` battery charger helpers. |
| | | - `AdcBatteryMonitor` - simple ADC-based battery voltage monitor. |
| | | - `PowerSaveTimer` / `SleepTimer` - helpers for light-sleep scheduling. |
| | | |
| | | ### Networking |
| | | |
| | | - `WifiBoard` - WiFi-only base class. |
| | | - `Ml307Board` / `Nt26Board` - 4G modem base classes. |
| | | - `DualNetworkBoard` - switchable WiFi / 4G base class. |
| | | - `RndisBoard` - RNDIS-over-USB networking (ESP32-S3 / ESP32-P4). |
| | | - `EspVideo` helpers for ESP-Video on ESP32-S3 / ESP32-P4. |
| | | |
| | | ### Input helpers |
| | | |
| | | - `Button` - standard push buttons (click, long-press, multi-click). |
| | | - `Knob` - rotary encoder wrapper. |
| | | - `PressToTalkMcpTool` - push-to-talk tool that registers itself through MCP. |
| | | - `AfskDemod` - AFSK demodulator used by some acoustic provisioning flows. |
| | | - `SystemReset` - helper that performs a safe factory reset when a button is held at boot. |
| | | |
| | | ### MCP integration |
| | | |
| | | Any board can register custom tools - speaker control, screen brightness, battery readout, light control, etc. See [MCP IoT control usage](./mcp-usage.md). |
| | | |
| | | ## Board Class Hierarchy |
| | | |
| | | - `Board` - base class |
| | | - `WifiBoard` - WiFi-connected board |
| | | - `Ml307Board` / `Nt26Board` - 4G modem boards |
| | | - `DualNetworkBoard` - WiFi + 4G switchable board |
| | | - `RndisBoard` - RNDIS-over-USB board |
| | | |
| | | ## Tips |
| | | |
| | | 1. **Start from a similar board** - copying and tweaking an existing board is usually faster than starting from scratch. |
| | | 2. **Bring up incrementally** - get the display up first, then audio, then the full stack. |
| | | 3. **Double check pin assignments** - every pin defined in `config.h` must match your schematic. |
| | | 4. **Check hardware compatibility** - especially codec / PMIC / touch controller combinations. |
| | | |
| | | ## Troubleshooting |
| | | |
| | | 1. **Display looks wrong** - verify SPI configuration, mirroring, and color inversion. |
| | | 2. **No audio** - check I2S wiring, PA enable pin, and codec I2C address. |
| | | 3. **Cannot connect to WiFi** - re-check WiFi credentials and provisioning method. |
| | | 4. **Cannot reach the server** - verify the WebSocket / MQTT endpoint configuration. |
| | | |
| | | ## References |
| | | |
| | | - ESP-IDF documentation: https://docs.espressif.com/projects/esp-idf/ |
| | | - LVGL documentation: https://docs.lvgl.io/ |
| | | - ESP-SR documentation: https://github.com/espressif/esp-sr |
| New file |
| | |
| | | # èªå®ä¹å¼åæ¿æå |
| | | |
| | | æ¬æåä»ç»å¦ä½ä¸ºå°æºAIè¯é³è天æºå¨äººé¡¹ç®å®å¶ä¸ä¸ªæ°çå¼åæ¿åå§åç¨åºãå°æºAIæ¯æ70å¤ç§ESP32ç³»åå¼åæ¿ï¼æ¯ä¸ªå¼åæ¿çåå§å代ç 齿¾å¨å¯¹åºçç®å½ä¸ã |
| | | |
| | | ## éè¦æç¤º |
| | | |
| | | > **è¦å**: 对äºèªå®ä¹å¼åæ¿ï¼å½IOé
ç½®ä¸åæå¼åæ¿ä¸åæ¶ï¼åå¿ç´æ¥è¦çåæå¼åæ¿çé
ç½®ç¼è¯åºä»¶ãå¿
é¡»å建æ°çå¼åæ¿ç±»åï¼æè
éè¿config.jsonæä»¶ä¸çbuildsé
ç½®ä¸åçnameåsdkconfigå®å®ä¹æ¥åºåãä½¿ç¨ `python scripts/release.py [å¼åæ¿ç®å½åå]` æ¥ç¼è¯æå
åºä»¶ã |
| | | > |
| | | > å¦æç´æ¥è¦çåæé
ç½®ï¼å°æ¥OTAå级æ¶ï¼æ¨çèªå®ä¹åºä»¶å¯è½ä¼è¢«åæå¼åæ¿çæ ååºä»¶è¦çï¼å¯¼è´æ¨çè®¾å¤æ æ³æ£å¸¸å·¥ä½ãæ¯ä¸ªå¼åæ¿æå¯ä¸çæ è¯å对åºçåºä»¶å级ééï¼ä¿æå¼åæ¿æ è¯çå¯ä¸æ§é常éè¦ã |
| | | |
| | | ## ç®å½ç»æ |
| | | |
| | | æ¯ä¸ªå¼åæ¿çç®å½ç»æé常å
å«ä»¥ä¸æä»¶ï¼ |
| | | |
| | | - `xxx_board.cc` - 主è¦çæ¿çº§åå§å代ç ï¼å®ç°äºæ¿åç¸å
³çåå§åååè½ |
| | | - `config.h` - æ¿çº§é
ç½®æä»¶ï¼å®ä¹äºç¡¬ä»¶ç®¡èæ å°åå
¶ä»é
置项 |
| | | - `config.json` - ç¼è¯é
ç½®ï¼æå®ç®æ è¯çåç¹æ®çç¼è¯é项 |
| | | - `README.md` - å¼åæ¿ç¸å
³çè¯´æææ¡£ |
| | | |
| | | ## å®å¶å¼åæ¿æ¥éª¤ |
| | | |
| | | ### 1. å建æ°çå¼åæ¿ç®å½ |
| | | |
| | | é¦å
å¨`boards/`ç®å½ä¸å建ä¸ä¸ªæ°çç®å½ï¼å½åæ¹å¼åºä½¿ç¨ `[åçå]-[å¼åæ¿ç±»å]` çå½¢å¼ï¼ä¾å¦ `m5stack-tab5`ï¼ |
| | | |
| | | ```bash |
| | | mkdir main/boards/my-custom-board |
| | | ``` |
| | | |
| | | ### 2. å建é
ç½®æä»¶ |
| | | |
| | | #### config.h |
| | | |
| | | å¨`config.h`ä¸å®ä¹ææç硬件é
ç½®ï¼å
æ¬: |
| | | |
| | | - é³é¢éæ ·çåI2Så¼èé
ç½® |
| | | - é³é¢ç¼è§£ç è¯çå°ååI2Cå¼èé
ç½® |
| | | - æé®åLEDå¼èé
ç½® |
| | | - æ¾ç¤ºå±åæ°åå¼èé
ç½® |
| | | |
| | | åè示ä¾ï¼æ¥èªlichuang-c3-devï¼ï¼ |
| | | |
| | | ```c |
| | | #ifndef _BOARD_CONFIG_H_ |
| | | #define _BOARD_CONFIG_H_ |
| | | |
| | | #include <driver/gpio.h> |
| | | |
| | | // é³é¢é
ç½® |
| | | #define AUDIO_INPUT_SAMPLE_RATE 24000 |
| | | #define AUDIO_OUTPUT_SAMPLE_RATE 24000 |
| | | |
| | | #define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 |
| | | #define AUDIO_I2S_GPIO_WS GPIO_NUM_12 |
| | | #define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 |
| | | #define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 |
| | | #define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 |
| | | |
| | | #define AUDIO_CODEC_PA_PIN GPIO_NUM_13 |
| | | #define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0 |
| | | #define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 |
| | | #define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR |
| | | |
| | | // æé®é
ç½® |
| | | #define BOOT_BUTTON_GPIO GPIO_NUM_9 |
| | | |
| | | // æ¾ç¤ºå±é
ç½® |
| | | #define DISPLAY_SPI_SCK_PIN GPIO_NUM_3 |
| | | #define DISPLAY_SPI_MOSI_PIN GPIO_NUM_5 |
| | | #define DISPLAY_DC_PIN GPIO_NUM_6 |
| | | #define DISPLAY_SPI_CS_PIN GPIO_NUM_4 |
| | | |
| | | #define DISPLAY_WIDTH 320 |
| | | #define DISPLAY_HEIGHT 240 |
| | | #define DISPLAY_MIRROR_X true |
| | | #define DISPLAY_MIRROR_Y false |
| | | #define DISPLAY_SWAP_XY true |
| | | |
| | | #define DISPLAY_OFFSET_X 0 |
| | | #define DISPLAY_OFFSET_Y 0 |
| | | |
| | | #define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2 |
| | | #define DISPLAY_BACKLIGHT_OUTPUT_INVERT true |
| | | |
| | | #endif // _BOARD_CONFIG_H_ |
| | | ``` |
| | | |
| | | #### config.json |
| | | |
| | | å¨`config.json`ä¸å®ä¹ç¼è¯é
ç½®ï¼è¿ä¸ªæä»¶ç¨äº `scripts/release.py` èæ¬èªå¨åç¼è¯ï¼ |
| | | |
| | | ```json |
| | | { |
| | | "target": "esp32s3", // ç®æ è¯çåå·: esp32, esp32s3, esp32c3, esp32c6, esp32p4ç |
| | | "builds": [ |
| | | { |
| | | "name": "my-custom-board", // å¼åæ¿åç§°ï¼ç¨äºçæåºä»¶å
|
| | | "sdkconfig_append": [ |
| | | // ç¹å« Flash 大å°é
ç½® |
| | | "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", |
| | | // ç¹å«ååºè¡¨é
ç½® |
| | | "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"" |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | **é
置项说æï¼** |
| | | - `target`: ç®æ è¯çåå·ï¼å¿
é¡»ä¸ç¡¬ä»¶å¹é
|
| | | - `name`: ç¼è¯è¾åºçåºä»¶å
åç§°ï¼å»ºè®®ä¸ç®å½åä¸è´ |
| | | - `sdkconfig_append`: é¢å¤ç sdkconfig é
置项æ°ç»ï¼ä¼è¿½å å°é»è®¤é
ç½®ä¸ |
| | | |
| | | **常ç¨ç sdkconfig_append é
ç½®ï¼** |
| | | ```json |
| | | // Flash å¤§å° |
| | | "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y" // 4MB Flash |
| | | "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y" // 8MB Flash |
| | | "CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y" // 16MB Flash |
| | | |
| | | // ååºè¡¨ |
| | | "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\"" // 4MB ååºè¡¨ |
| | | "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"" // 8MB ååºè¡¨ |
| | | "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"" // 16MB ååºè¡¨ |
| | | |
| | | // è¯è¨é
ç½® |
| | | "CONFIG_LANGUAGE_EN_US=y" // è±è¯ |
| | | "CONFIG_LANGUAGE_ZH_CN=y" // ç®ä½ä¸æ |
| | | |
| | | // å¤éè¯é
ç½® |
| | | "CONFIG_USE_DEVICE_AEC=y" // å¯ç¨è®¾å¤ç«¯ AEC |
| | | "CONFIG_WAKE_WORD_DISABLED=y" // ç¦ç¨å¤éè¯ |
| | | ``` |
| | | |
| | | ### 3. ç¼åæ¿çº§åå§å代ç |
| | | |
| | | å建ä¸ä¸ª`my_custom_board.cc`æä»¶ï¼å®ç°å¼åæ¿çææåå§åé»è¾ã |
| | | |
| | | ä¸ä¸ªåºæ¬çå¼åæ¿ç±»å®ä¹å
å«ä»¥ä¸å 个é¨åï¼ |
| | | |
| | | 1. **ç±»å®ä¹**ï¼ç»§æ¿èª`WifiBoard`æ`Ml307Board` |
| | | 2. **åå§å彿°**ï¼å
æ¬I2Cãæ¾ç¤ºå±ãæé®ãIoTçç»ä»¶çåå§å |
| | | 3. **è彿°éå**ï¼å¦`GetAudioCodec()`ã`GetDisplay()`ã`GetBacklight()`ç |
| | | 4. **注åå¼åæ¿**ï¼ä½¿ç¨`DECLARE_BOARD`宿³¨åå¼åæ¿ |
| | | |
| | | ```cpp |
| | | #include "wifi_board.h" |
| | | #include "codecs/es8311_audio_codec.h" |
| | | #include "display/lcd_display.h" |
| | | #include "application.h" |
| | | #include "button.h" |
| | | #include "config.h" |
| | | #include "mcp_server.h" |
| | | |
| | | #include <esp_log.h> |
| | | #include <driver/i2c_master.h> |
| | | #include <driver/spi_common.h> |
| | | |
| | | #define TAG "MyCustomBoard" |
| | | |
| | | class MyCustomBoard : public WifiBoard { |
| | | private: |
| | | i2c_master_bus_handle_t codec_i2c_bus_; |
| | | Button boot_button_; |
| | | LcdDisplay* display_; |
| | | |
| | | // I2Cåå§å |
| | | void InitializeI2c() { |
| | | i2c_master_bus_config_t i2c_bus_cfg = { |
| | | .i2c_port = I2C_NUM_0, |
| | | .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, |
| | | .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, |
| | | .clk_source = I2C_CLK_SRC_DEFAULT, |
| | | .glitch_ignore_cnt = 7, |
| | | .intr_priority = 0, |
| | | .trans_queue_depth = 0, |
| | | .flags = { |
| | | .enable_internal_pullup = 1, |
| | | }, |
| | | }; |
| | | ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_)); |
| | | } |
| | | |
| | | // SPIåå§åï¼ç¨äºæ¾ç¤ºå±ï¼ |
| | | void InitializeSpi() { |
| | | spi_bus_config_t buscfg = {}; |
| | | buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN; |
| | | buscfg.miso_io_num = GPIO_NUM_NC; |
| | | buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN; |
| | | buscfg.quadwp_io_num = GPIO_NUM_NC; |
| | | buscfg.quadhd_io_num = GPIO_NUM_NC; |
| | | buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t); |
| | | ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); |
| | | } |
| | | |
| | | // æé®åå§å |
| | | void InitializeButtons() { |
| | | boot_button_.OnClick([this]() { |
| | | auto& app = Application::GetInstance(); |
| | | if (app.GetDeviceState() == kDeviceStateStarting) { |
| | | EnterWifiConfigMode(); |
| | | return; |
| | | } |
| | | app.ToggleChatState(); |
| | | }); |
| | | } |
| | | |
| | | // æ¾ç¤ºå±åå§åï¼ä»¥ST7789为ä¾ï¼ |
| | | void InitializeDisplay() { |
| | | esp_lcd_panel_io_handle_t panel_io = nullptr; |
| | | esp_lcd_panel_handle_t panel = nullptr; |
| | | |
| | | esp_lcd_panel_io_spi_config_t io_config = {}; |
| | | io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN; |
| | | io_config.dc_gpio_num = DISPLAY_DC_PIN; |
| | | io_config.spi_mode = 2; |
| | | io_config.pclk_hz = 80 * 1000 * 1000; |
| | | io_config.trans_queue_depth = 10; |
| | | io_config.lcd_cmd_bits = 8; |
| | | io_config.lcd_param_bits = 8; |
| | | ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io)); |
| | | |
| | | esp_lcd_panel_dev_config_t panel_config = {}; |
| | | panel_config.reset_gpio_num = GPIO_NUM_NC; |
| | | panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; |
| | | panel_config.bits_per_pixel = 16; |
| | | ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); |
| | | |
| | | esp_lcd_panel_reset(panel); |
| | | esp_lcd_panel_init(panel); |
| | | esp_lcd_panel_invert_color(panel, true); |
| | | esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); |
| | | esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); |
| | | |
| | | // å建æ¾ç¤ºå±å¯¹è±¡ |
| | | display_ = new SpiLcdDisplay(panel_io, panel, |
| | | DISPLAY_WIDTH, DISPLAY_HEIGHT, |
| | | DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, |
| | | DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); |
| | | } |
| | | |
| | | // MCP Tools åå§å |
| | | void InitializeTools() { |
| | | // åè MCP ææ¡£ |
| | | } |
| | | |
| | | public: |
| | | // æé 彿° |
| | | MyCustomBoard() : boot_button_(BOOT_BUTTON_GPIO) { |
| | | InitializeI2c(); |
| | | InitializeSpi(); |
| | | InitializeDisplay(); |
| | | InitializeButtons(); |
| | | InitializeTools(); |
| | | GetBacklight()->SetBrightness(100); |
| | | } |
| | | |
| | | // è·åé³é¢ç¼è§£ç å¨ |
| | | virtual AudioCodec* GetAudioCodec() override { |
| | | static Es8311AudioCodec audio_codec( |
| | | codec_i2c_bus_, |
| | | I2C_NUM_0, |
| | | AUDIO_INPUT_SAMPLE_RATE, |
| | | AUDIO_OUTPUT_SAMPLE_RATE, |
| | | AUDIO_I2S_GPIO_MCLK, |
| | | AUDIO_I2S_GPIO_BCLK, |
| | | AUDIO_I2S_GPIO_WS, |
| | | AUDIO_I2S_GPIO_DOUT, |
| | | AUDIO_I2S_GPIO_DIN, |
| | | AUDIO_CODEC_PA_PIN, |
| | | AUDIO_CODEC_ES8311_ADDR); |
| | | return &audio_codec; |
| | | } |
| | | |
| | | // è·åæ¾ç¤ºå± |
| | | virtual Display* GetDisplay() override { |
| | | return display_; |
| | | } |
| | | |
| | | // è·åèå
æ§å¶ |
| | | virtual Backlight* GetBacklight() override { |
| | | static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); |
| | | return &backlight; |
| | | } |
| | | }; |
| | | |
| | | // 注åå¼åæ¿ |
| | | DECLARE_BOARD(MyCustomBoard); |
| | | ``` |
| | | |
| | | ### 4. æ·»å æå»ºç³»ç»é
ç½® |
| | | |
| | | #### å¨ Kconfig.projbuild 䏿·»å å¼åæ¿é项 |
| | | |
| | | æå¼ `main/Kconfig.projbuild` æä»¶ï¼å¨ `choice BOARD_TYPE` é¨åæ·»å æ°çå¼åæ¿é
ç½®é¡¹ï¼ |
| | | |
| | | ```kconfig |
| | | choice BOARD_TYPE |
| | | prompt "Board Type" |
| | | default BOARD_TYPE_BREAD_COMPACT_WIFI |
| | | help |
| | | Board type. å¼åæ¿ç±»å |
| | | |
| | | # ... å
¶ä»å¼åæ¿é项 ... |
| | | |
| | | config BOARD_TYPE_MY_CUSTOM_BOARD |
| | | bool "My Custom Board (æçèªå®ä¹å¼åæ¿)" |
| | | depends on IDF_TARGET_ESP32S3 # æ ¹æ®ä½ çç®æ è¯çä¿®æ¹ |
| | | endchoice |
| | | ``` |
| | | |
| | | **注æäºé¡¹ï¼** |
| | | - `BOARD_TYPE_MY_CUSTOM_BOARD` æ¯é
置项åç§°ï¼éè¦å
¨å¤§åï¼ä½¿ç¨ä¸å线åé |
| | | - `depends on` æå®äºç®æ è¯çç±»åï¼å¦ `IDF_TARGET_ESP32S3`ã`IDF_TARGET_ESP32C3` çï¼ |
| | | - æè¿°æåå¯ä»¥ä½¿ç¨ä¸è±æ |
| | | |
| | | #### å¨ CMakeLists.txt 䏿·»å å¼åæ¿é
ç½® |
| | | |
| | | æå¼ `main/CMakeLists.txt` æä»¶ï¼å¨å¼åæ¿ç±»å夿é¨åæ·»å æ°çé
ç½®ï¼ |
| | | |
| | | ```cmake |
| | | # å¨ elseif é¾ä¸æ·»å ä½ çå¼åæ¿é
ç½® |
| | | elseif(CONFIG_BOARD_TYPE_MY_CUSTOM_BOARD) |
| | | set(BOARD_TYPE "my-custom-board") # ä¸ç®å½åä¸è´ |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) # æ ¹æ®å±å¹å¤§å°éæ©åéçåä½ |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) # å¯éï¼å¦æéè¦è¡¨æ
æ¾ç¤º |
| | | endif() |
| | | ``` |
| | | |
| | | **åä½å表æ
é
置说æï¼** |
| | | |
| | | æ ¹æ®å±å¹å辨çéæ©åéçåä½å¤§å°ï¼ |
| | | - å°å±å¹ï¼128x64 OLEDï¼ï¼`font_puhui_basic_14_1` / `font_awesome_14_1` |
| | | - ä¸å°å±å¹ï¼240x240ï¼ï¼`font_puhui_basic_16_4` / `font_awesome_16_4` |
| | | - ä¸çå±å¹ï¼240x320ï¼ï¼`font_puhui_basic_20_4` / `font_awesome_20_4` |
| | | - 大å±å¹ï¼480x320+ï¼ï¼`font_puhui_basic_30_4` / `font_awesome_30_4` |
| | | |
| | | 表æ
éåéé¡¹ï¼ |
| | | - `twemoji_32` - 32x32 åç´ è¡¨æ
ï¼å°å±å¹ï¼ |
| | | - `twemoji_64` - 64x64 åç´ è¡¨æ
ï¼å¤§å±å¹ï¼ |
| | | |
| | | ### 5. é
ç½®åç¼è¯ |
| | | |
| | | #### æ¹æ³ä¸ï¼ä½¿ç¨ idf.py æå¨é
ç½® |
| | | |
| | | 1. **è®¾ç½®ç®æ è¯ç**ï¼é¦æ¬¡é
ç½®ææ´æ¢è¯çæ¶ï¼ï¼ |
| | | ```bash |
| | | # å¯¹äº ESP32-S3 |
| | | idf.py set-target esp32s3 |
| | | |
| | | # å¯¹äº ESP32-C3 |
| | | idf.py set-target esp32c3 |
| | | |
| | | # å¯¹äº ESP32 |
| | | idf.py set-target esp32 |
| | | ``` |
| | | |
| | | 2. **æ¸
çæ§é
ç½®**ï¼ |
| | | ```bash |
| | | idf.py fullclean |
| | | ``` |
| | | |
| | | 3. **è¿å
¥é
ç½®èå**ï¼ |
| | | ```bash |
| | | idf.py menuconfig |
| | | ``` |
| | | |
| | | å¨èåä¸å¯¼èªå°ï¼`Xiaozhi Assistant` -> `Board Type`ï¼éæ©ä½ çèªå®ä¹å¼åæ¿ã |
| | | |
| | | 4. **ç¼è¯åç§å½**ï¼ |
| | | ```bash |
| | | idf.py build |
| | | idf.py flash monitor |
| | | ``` |
| | | |
| | | #### æ¹æ³äºï¼ä½¿ç¨ release.py èæ¬ï¼æ¨èï¼ |
| | | |
| | | å¦æä½ çå¼åæ¿ç®å½ä¸æ `config.json` æä»¶ï¼å¯ä»¥ä½¿ç¨æ¤èæ¬èªå¨å®æé
ç½®åç¼è¯ï¼ |
| | | |
| | | ```bash |
| | | python scripts/release.py my-custom-board |
| | | ``` |
| | | |
| | | æ¤èæ¬ä¼èªå¨ï¼ |
| | | - 读å `config.json` ä¸ç `target` é
ç½®å¹¶è®¾ç½®ç®æ è¯ç |
| | | - åºç¨ `sdkconfig_append` ä¸çç¼è¯é项 |
| | | - 宿ç¼è¯å¹¶æå
åºä»¶ |
| | | |
| | | ### 6. å建README.md |
| | | |
| | | å¨README.mdä¸è¯´æå¼åæ¿çç¹æ§ãç¡¬ä»¶è¦æ±ãç¼è¯åç§å½æ¥éª¤ï¼ |
| | | |
| | | |
| | | ## 常è§å¼åæ¿ç»ä»¶ |
| | | |
| | | ### 1. æ¾ç¤ºå± |
| | | |
| | | é¡¹ç®æ¯æå¤ç§æ¾ç¤ºå±é©±å¨ï¼å
æ¬: |
| | | - ST7789 (SPI) |
| | | - ILI9341 (SPI) |
| | | - SH8601 (QSPI) |
| | | - ç... |
| | | |
| | | ### 2. é³é¢ç¼è§£ç å¨ |
| | | |
| | | æ¯æçç¼è§£ç å¨å
æ¬: |
| | | - ES8311 (常ç¨) |
| | | - ES7210 (麦å
é£éµå) |
| | | - AW88298 (åæ¾) |
| | | - ç... |
| | | |
| | | ### 3. çµæºç®¡ç |
| | | |
| | | ä¸äºå¼åæ¿ä½¿ç¨çµæºç®¡çè¯ç: |
| | | - AXP2101 |
| | | - å
¶ä»å¯ç¨çPMIC |
| | | |
| | | ### 4. MCPè®¾å¤æ§å¶ |
| | | |
| | | å¯ä»¥æ·»å åç§MCPå·¥å
·ï¼è®©AIè½å¤ä½¿ç¨: |
| | | - Speaker (æ¬å£°å¨æ§å¶) |
| | | - Screen (å±å¹äº®åº¦è°è) |
| | | - Battery (çµæ± çµé读å) |
| | | - Light (ç¯å
æ§å¶) |
| | | - ç... |
| | | |
| | | ## å¼åæ¿ç±»ç»§æ¿å
³ç³» |
| | | |
| | | - `Board` - åºç¡æ¿çº§ç±» |
| | | - `WifiBoard` - Wi-Fiè¿æ¥çå¼åæ¿ |
| | | - `Ml307Board` - 使ç¨4G模åçå¼åæ¿ |
| | | - `DualNetworkBoard` - æ¯æWi-Fiä¸4Gç½ç»åæ¢çå¼åæ¿ |
| | | |
| | | ## å¼åæå·§ |
| | | |
| | | 1. **åèç¸ä¼¼çå¼åæ¿**ï¼å¦ææ¨çæ°å¼åæ¿ä¸ç°æå¼åæ¿æç¸ä¼¼ä¹å¤ï¼å¯ä»¥åèç°æå®ç° |
| | | 2. **忥è°è¯**ï¼å
å®ç°åºç¡åè½ï¼å¦æ¾ç¤ºï¼ï¼åæ·»å æ´å¤æçåè½ï¼å¦é³é¢ï¼ |
| | | 3. **ç®¡èæ å°**ï¼ç¡®ä¿å¨config.h䏿£ç¡®é
ç½®ææç®¡èæ å° |
| | | 4. **æ£æ¥ç¡¬ä»¶å
¼å®¹æ§**ï¼ç¡®è®¤ææè¯çå驱å¨ç¨åºçå
¼å®¹æ§ |
| | | |
| | | ## å¯è½éå°çé®é¢ |
| | | |
| | | 1. **æ¾ç¤ºå±ä¸æ£å¸¸**ï¼æ£æ¥SPIé
ç½®ãéå设置åé¢è²å转设置 |
| | | 2. **é³é¢æ è¾åº**ï¼æ£æ¥I2Sé
ç½®ãPA使è½å¼èåç¼è§£ç å¨å°å |
| | | 3. **æ æ³è¿æ¥ç½ç»**ï¼æ£æ¥Wi-Fiåæ®åç½ç»é
ç½® |
| | | 4. **æ æ³ä¸æå¡å¨éä¿¡**ï¼æ£æ¥MQTTæWebSocketé
ç½® |
| | | |
| | | ## åèèµæ |
| | | |
| | | - ESP-IDF ææ¡£: https://docs.espressif.com/projects/esp-idf/ |
| | | - LVGL ææ¡£: https://docs.lvgl.io/ |
| | | - ESP-SR ææ¡£: https://github.com/espressif/esp-sr |
| New file |
| | |
| | | # MCP (Model Context Protocol) Interaction Flow |
| | | |
| | | NOTICE: This document was AI-assisted; when implementing a backend, always cross-check the details against the code. |
| | | |
| | | In this project, MCP is used between the backend API (MCP client) and the ESP32 device (MCP server) to let the backend discover and invoke the device's capabilities (tools). |
| | | |
| | | ## Message Format |
| | | |
| | | From `main/protocols/protocol.cc` and `main/mcp_server.cc`, MCP messages are wrapped inside the underlying transport (WebSocket or MQTT). The inner payload follows the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) specification. |
| | | |
| | | Overall message layout: |
| | | |
| | | ```json |
| | | { |
| | | "session_id": "...", // session id |
| | | "type": "mcp", // fixed value "mcp" |
| | | "payload": { // JSON-RPC 2.0 payload |
| | | "jsonrpc": "2.0", |
| | | "method": "...", // method name ("initialize", "tools/list", "tools/call", ...) |
| | | "params": { ... }, // arguments (for requests) |
| | | "id": ..., // request id (for requests and responses) |
| | | "result": { ... }, // success result (response) |
| | | "error": { ... } // error (response) |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | The `payload` follows standard JSON-RPC 2.0: |
| | | |
| | | - `jsonrpc`: always `"2.0"`. |
| | | - `method`: the method name (requests). |
| | | - `params`: structured parameters, usually an object (requests). |
| | | - `id`: request identifier; echoed back in responses. |
| | | - `result`: success value (responses). |
| | | - `error`: error information (responses). |
| | | |
| | | ## Interaction Flow |
| | | |
| | | MCP interactions are driven by the client (backend) discovering and invoking tools on the device. |
| | | |
| | | 1. **Connection and capability announcement** |
| | | |
| | | - **When**: after the device boots and connects to the backend. |
| | | - **Direction**: device -> backend. |
| | | - **Message**: the device sends the transport hello, advertising supported capabilities. MCP support is signaled via `"mcp": true` in the `features` map. |
| | | - **Example (transport hello, not an MCP payload):** |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 1, |
| | | "features": { |
| | | "mcp": true |
| | | }, |
| | | "transport": "websocket", |
| | | "audio_params": { ... }, |
| | | "session_id": "..." |
| | | } |
| | | ``` |
| | | |
| | | 2. **Initialize the MCP session** |
| | | |
| | | - **When**: after the backend sees that the device supports MCP. Usually the first MCP request. |
| | | - **Direction**: backend -> device. |
| | | - **Method**: `initialize` |
| | | - **Message (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "initialize", |
| | | "params": { |
| | | "capabilities": { |
| | | // optional client capabilities |
| | | "vision": { |
| | | "url": "...", // camera image upload endpoint (must be an http URL, not a websocket URL) |
| | | "token": "..." // token for the upload URL |
| | | } |
| | | // ... other client capabilities |
| | | } |
| | | }, |
| | | "id": 1 |
| | | } |
| | | ``` |
| | | |
| | | - **Device response:** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "id": 1, |
| | | "result": { |
| | | "protocolVersion": "2024-11-05", |
| | | "capabilities": { |
| | | "tools": {} |
| | | }, |
| | | "serverInfo": { |
| | | "name": "...", // device name (BOARD_NAME) |
| | | "version": "..." // firmware version |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 3. **Discover the tools** |
| | | |
| | | - **When**: whenever the backend needs the list of callable tools and their signatures. |
| | | - **Direction**: backend -> device. |
| | | - **Method**: `tools/list` |
| | | - **Request parameters**: |
| | | - `cursor` (string, optional): pagination cursor. Empty on the first request. |
| | | - `withUserTools` (boolean, optional, default `false`): if `true`, the device also includes "user-only" tools (see "User-only tools" below) in the listing. This is typically used by a companion app that lets the user trigger privileged actions directly. |
| | | - **Message (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/list", |
| | | "params": { |
| | | "cursor": "", |
| | | "withUserTools": false |
| | | }, |
| | | "id": 2 |
| | | } |
| | | ``` |
| | | - **Device response:** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "id": 2, |
| | | "result": { |
| | | "tools": [ |
| | | { |
| | | "name": "self.get_device_status", |
| | | "description": "...", |
| | | "inputSchema": { ... } |
| | | }, |
| | | { |
| | | "name": "self.audio_speaker.set_volume", |
| | | "description": "...", |
| | | "inputSchema": { ... } |
| | | } |
| | | // ... more tools |
| | | ], |
| | | "nextCursor": "..." |
| | | } |
| | | } |
| | | ``` |
| | | - **Pagination**: when `nextCursor` is non-empty, the backend must send another `tools/list` request with that cursor to fetch the next page. |
| | | |
| | | 4. **Call a tool** |
| | | |
| | | - **When**: the backend wants to execute a specific device function. |
| | | - **Direction**: backend -> device. |
| | | - **Method**: `tools/call` |
| | | - **Message (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.audio_speaker.set_volume", |
| | | "arguments": { |
| | | "volume": 50 |
| | | } |
| | | }, |
| | | "id": 3 |
| | | } |
| | | ``` |
| | | - **Successful response:** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "id": 3, |
| | | "result": { |
| | | "content": [ |
| | | { "type": "text", "text": "true" } |
| | | ], |
| | | "isError": false |
| | | } |
| | | } |
| | | ``` |
| | | - **Error response:** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "id": 3, |
| | | "error": { |
| | | "code": -32601, |
| | | "message": "Unknown tool: self.non_existent_tool" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 5. **Device-initiated notifications** |
| | | |
| | | - **When**: the device wants to inform the backend of internal events (e.g. state transitions). `Application::SendMcpMessage` is the outbound entry point. |
| | | - **Direction**: device -> backend. |
| | | - **Method**: conventionally `notifications/...` or any custom method. |
| | | - **Message (MCP payload)**: JSON-RPC notifications have no `id`. |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "notifications/state_changed", |
| | | "params": { |
| | | "newState": "idle", |
| | | "oldState": "connecting" |
| | | } |
| | | } |
| | | ``` |
| | | - **Backend handling**: process the notification without replying. |
| | | |
| | | ## User-only Tools |
| | | |
| | | The MCP server on the device maintains two kinds of tools: |
| | | |
| | | - **Regular tools** - registered via `McpServer::AddTool`. Exposed to the backend (and hence the AI model) by default. |
| | | - **User-only tools** - registered via `McpServer::AddUserOnlyTool`. These are hidden from standard `tools/list` results, because they are privileged or user-facing actions that should not be invoked autonomously by the AI. Examples include system reboot, firmware upgrade, and screen snapshot upload. |
| | | |
| | | The backend opts in to user-only tools by sending `tools/list` with `params.withUserTools = true`. Typical usage: a companion app screen that exposes these actions to the end user. |
| | | |
| | | See [MCP IoT control usage](./mcp-usage.md) for how to register either kind of tool on the device side. |
| | | |
| | | ## Sequence Diagram |
| | | |
| | | A simplified diagram of the main MCP message flow: |
| | | |
| | | ```mermaid |
| | | sequenceDiagram |
| | | participant Device as ESP32 Device |
| | | participant BackendAPI as Backend API (Client) |
| | | |
| | | Note over Device, BackendAPI: Establish WebSocket / MQTT |
| | | |
| | | Device->>BackendAPI: Hello (features.mcp = true) |
| | | |
| | | BackendAPI->>Device: MCP Initialize request |
| | | Note over BackendAPI: method: initialize |
| | | Note over BackendAPI: params: { capabilities: ... } |
| | | |
| | | Device->>BackendAPI: MCP Initialize response |
| | | Note over Device: result: { protocolVersion, serverInfo, ... } |
| | | |
| | | BackendAPI->>Device: MCP tools/list request |
| | | Note over BackendAPI: params: { cursor: "", withUserTools: false } |
| | | |
| | | Device->>BackendAPI: MCP tools/list response |
| | | Note over Device: result: { tools: [...], nextCursor: ... } |
| | | |
| | | loop Optional pagination |
| | | BackendAPI->>Device: MCP tools/list request |
| | | Note over BackendAPI: params: { cursor: "..." } |
| | | Device->>BackendAPI: MCP tools/list response |
| | | Note over Device: result: { tools: [...], nextCursor: "" } |
| | | end |
| | | |
| | | BackendAPI->>Device: MCP tools/call request |
| | | Note over BackendAPI: params: { name, arguments } |
| | | |
| | | alt Call succeeds |
| | | Device->>BackendAPI: MCP tools/call success response |
| | | Note over Device: result: { content, isError: false } |
| | | else Call fails |
| | | Device->>BackendAPI: MCP tools/call error response |
| | | Note over Device: error: { code, message } |
| | | end |
| | | |
| | | opt Device notification |
| | | Device->>BackendAPI: MCP notification |
| | | Note over Device: method: notifications/... |
| | | end |
| | | ``` |
| | | |
| | | This document summarizes the MCP interaction flow in this project. For exact parameter shapes, behavior, and available tools, refer to `McpServer::AddCommonTools` / `AddUserOnlyTools` in `main/mcp_server.cc` and the per-board `InitializeTools` implementations. |
| New file |
| | |
| | | # MCP (Model Context Protocol) äº¤äºæµç¨ |
| | | |
| | | NOTICE: AI è¾
å©çæ, å¨å®ç°å尿塿¶, 请åç
§ä»£ç 确认ç»è!! |
| | | |
| | | æ¬é¡¹ç®ä¸ç MCP åè®®ç¨äºåå° APIï¼MCP 客æ·ç«¯ï¼ä¸ ESP32 设å¤ï¼MCP æå¡å¨ï¼ä¹é´çéä¿¡ï¼ä»¥ä¾¿åå°è½å¤åç°åè°ç¨è®¾å¤æä¾çåè½ï¼å·¥å
·ï¼ã |
| | | |
| | | ## åè®®æ ¼å¼ |
| | | |
| | | æ ¹æ®ä»£ç (`main/protocols/protocol.cc`, `main/mcp_server.cc`)ï¼MCP æ¶æ¯æ¯å°è£
å¨åºç¡éä¿¡åè®®ï¼å¦ WebSocket æ MQTTï¼çæ¶æ¯ä½ä¸çãå
¶å
é¨ç»æéµå¾ª [JSON-RPC 2.0](https://www.jsonrpc.org/specification) è§èã |
| | | |
| | | æ´ä½æ¶æ¯ç»æç¤ºä¾ï¼ |
| | | |
| | | ```json |
| | | { |
| | | "session_id": "...", // ä¼è¯ ID |
| | | "type": "mcp", // æ¶æ¯ç±»åï¼åºå®ä¸º "mcp" |
| | | "payload": { // JSON-RPC 2.0 è´è½½ |
| | | "jsonrpc": "2.0", |
| | | "method": "...", // æ¹æ³å (å¦ "initialize", "tools/list", "tools/call") |
| | | "params": { ... }, // æ¹æ³åæ° (å¯¹äº request) |
| | | "id": ..., // è¯·æ± ID (å¯¹äº request å response) |
| | | "result": { ... }, // æ¹æ³æ§è¡ç»æ (å¯¹äº success response) |
| | | "error": { ... } // éè¯¯ä¿¡æ¯ (å¯¹äº error response) |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | å
¶ä¸ï¼`payload` é¨åæ¯æ åç JSON-RPC 2.0 æ¶æ¯ï¼ |
| | | |
| | | - `jsonrpc`: åºå®çå符串 "2.0"ã |
| | | - `method`: è¦è°ç¨çæ¹æ³åç§° (å¯¹äº Request)ã |
| | | - `params`: æ¹æ³çåæ°ï¼ä¸ä¸ªç»æåå¼ï¼é常为对象 (å¯¹äº Request)ã |
| | | - `id`: 请æ±çæ è¯ç¬¦ï¼å®¢æ·ç«¯åéè¯·æ±æ¶æä¾ï¼æå¡å¨ååºæ¶åæ ·è¿åãç¨äºå¹é
请æ±åååºã |
| | | - `result`: æ¹æ³æåæ§è¡æ¶çç»æ (å¯¹äº Success Response)ã |
| | | - `error`: æ¹æ³æ§è¡å¤±è´¥æ¶çéè¯¯ä¿¡æ¯ (å¯¹äº Error Response)ã |
| | | |
| | | ## äº¤äºæµç¨ååéæ¶æº |
| | | |
| | | MCP ç交äºä¸»è¦å´ç»å®¢æ·ç«¯ï¼åå° APIï¼åç°åè°ç¨è®¾å¤ä¸çâå·¥å
·âï¼Toolï¼è¿è¡ã |
| | | |
| | | 1. **è¿æ¥å»ºç«ä¸è½åéå** |
| | | |
| | | - **æ¶æºï¼** 设å¤å¯å¨å¹¶æåè¿æ¥å°åå° API åã |
| | | - **åéæ¹ï¼** 设å¤ã |
| | | - **æ¶æ¯ï¼** 设å¤åéåºç¡åè®®ç "hello" æ¶æ¯ç»åå° APIï¼æ¶æ¯ä¸å
å«è®¾å¤æ¯æçè½åå表ï¼ä¾å¦éè¿æ¯æ MCP åè®® (`"mcp": true`)ã |
| | | - **ç¤ºä¾ (é MCP è´è½½ï¼èæ¯åºç¡åè®®æ¶æ¯):** |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": ..., |
| | | "features": { |
| | | "mcp": true, |
| | | ... |
| | | }, |
| | | "transport": "websocket", // æ "mqtt" |
| | | "audio_params": { ... }, |
| | | "session_id": "..." // è®¾å¤æ¶å°æå¡å¨helloåå¯è½è®¾ç½® |
| | | } |
| | | ``` |
| | | |
| | | 2. **åå§å MCP ä¼è¯** |
| | | |
| | | - **æ¶æºï¼** åå° API æ¶å°è®¾å¤ "hello" æ¶æ¯ï¼ç¡®è®¤è®¾å¤æ¯æ MCP åï¼é常ä½ä¸º MCP ä¼è¯ç第ä¸ä¸ªè¯·æ±åéã |
| | | - **åéæ¹ï¼** åå° API (客æ·ç«¯)ã |
| | | - **æ¹æ³ï¼** `initialize` |
| | | - **æ¶æ¯ (MCP payload):** |
| | | |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "initialize", |
| | | "params": { |
| | | "capabilities": { |
| | | // 客æ·ç«¯è½åï¼å¯é |
| | | |
| | | // æå头è§è§ç¸å
³ |
| | | "vision": { |
| | | "url": "...", //æå头: å¾çå¤çå°å(å¿
é¡»æ¯httpå°å, 䏿¯websocketå°å) |
| | | "token": "..." // url token |
| | | } |
| | | |
| | | // ... å
¶ä»å®¢æ·ç«¯è½å |
| | | } |
| | | }, |
| | | "id": 1 // è¯·æ± ID |
| | | } |
| | | ``` |
| | | |
| | | - **设å¤ååºæ¶æºï¼** è®¾å¤æ¶å° `initialize` 请æ±å¹¶å¤çåã |
| | | - **设å¤ååºæ¶æ¯ (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "id": 1, // å¹é
è¯·æ± ID |
| | | "result": { |
| | | "protocolVersion": "2024-11-05", |
| | | "capabilities": { |
| | | "tools": {} // è¿éç tools ä¼¼ä¹ä¸ååºè¯¦ç»ä¿¡æ¯ï¼éè¦ tools/list |
| | | }, |
| | | "serverInfo": { |
| | | "name": "...", // 设å¤åç§° (BOARD_NAME) |
| | | "version": "..." // 设å¤åºä»¶çæ¬ |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 3. **åç°è®¾å¤å·¥å
·å表** |
| | | |
| | | - **æ¶æºï¼** åå° API éè¦è·å设å¤å½åæ¯æçå
·ä½åè½ï¼å·¥å
·ï¼å表åå
¶è°ç¨æ¹å¼æ¶ã |
| | | - **åéæ¹ï¼** åå° API (客æ·ç«¯)ã |
| | | - **æ¹æ³ï¼** `tools/list` |
| | | - **æ¶æ¯ (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/list", |
| | | "params": { |
| | | "cursor": "" // ç¨äºå页ï¼é¦æ¬¡è¯·æ±ä¸ºç©ºå符串 |
| | | }, |
| | | "id": 2 // è¯·æ± ID |
| | | } |
| | | ``` |
| | | - **设å¤ååºæ¶æºï¼** è®¾å¤æ¶å° `tools/list` 请æ±å¹¶çæå·¥å
·å表åã |
| | | - **设å¤ååºæ¶æ¯ (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "id": 2, // å¹é
è¯·æ± ID |
| | | "result": { |
| | | "tools": [ // å·¥å
·å¯¹è±¡å表 |
| | | { |
| | | "name": "self.get_device_status", |
| | | "description": "...", |
| | | "inputSchema": { ... } // åæ° schema |
| | | }, |
| | | { |
| | | "name": "self.audio_speaker.set_volume", |
| | | "description": "...", |
| | | "inputSchema": { ... } // åæ° schema |
| | | } |
| | | // ... æ´å¤å·¥å
· |
| | | ], |
| | | "nextCursor": "..." // 妿å表å¾å¤§éè¦å页ï¼è¿éä¼å
å«ä¸ä¸ä¸ªè¯·æ±ç cursor å¼ |
| | | } |
| | | } |
| | | ``` |
| | | - **å页å¤çï¼** 妿 `nextCursor` åæ®µé空ï¼å®¢æ·ç«¯éè¦å次åé `tools/list` 请æ±ï¼å¹¶å¨ `params` ä¸å¸¦ä¸è¿ä¸ª `cursor` å¼ä»¥è·åä¸ä¸é¡µå·¥å
·ã |
| | | |
| | | 4. **è°ç¨è®¾å¤å·¥å
·** |
| | | |
| | | - **æ¶æºï¼** åå° API éè¦æ§è¡è®¾å¤ä¸çæä¸ªå
·ä½åè½æ¶ã |
| | | - **åéæ¹ï¼** åå° API (客æ·ç«¯)ã |
| | | - **æ¹æ³ï¼** `tools/call` |
| | | - **æ¶æ¯ (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.audio_speaker.set_volume", // è¦è°ç¨çå·¥å
·åç§° |
| | | "arguments": { |
| | | // å·¥å
·åæ°ï¼å¯¹è±¡æ ¼å¼ |
| | | "volume": 50 // åæ°ååå
¶å¼ |
| | | } |
| | | }, |
| | | "id": 3 // è¯·æ± ID |
| | | } |
| | | ``` |
| | | - **设å¤ååºæ¶æºï¼** è®¾å¤æ¶å° `tools/call` 请æ±ï¼æ§è¡ç¸åºçå·¥å
·å½æ°åã |
| | | - **è®¾å¤æåååºæ¶æ¯ (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "id": 3, // å¹é
è¯·æ± ID |
| | | "result": { |
| | | "content": [ |
| | | // å·¥å
·æ§è¡ç»æå
容 |
| | | { "type": "text", "text": "true" } // 示ä¾ï¼set_volume è¿å bool |
| | | ], |
| | | "isError": false // 表示æå |
| | | } |
| | | } |
| | | ``` |
| | | - **设å¤å¤±è´¥ååºæ¶æ¯ (MCP payload):** |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "id": 3, // å¹é
è¯·æ± ID |
| | | "error": { |
| | | "code": -32601, // JSON-RPC é误ç ï¼ä¾å¦ Method not found (-32601) |
| | | "message": "Unknown tool: self.non_existent_tool" // é误æè¿° |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 5. **设å¤ä¸»å¨åéæ¶æ¯ (Notifications)** |
| | | - **æ¶æºï¼** 设å¤å
é¨åçéè¦éç¥åå° API çäºä»¶æ¶ï¼ä¾å¦ï¼ç¶æååï¼è½ç¶ä»£ç 示ä¾ä¸æ²¡ææç¡®çå·¥å
·åéæ¤ç±»æ¶æ¯ï¼ä½ `Application::SendMcpMessage` çåå¨æç¤ºäºè®¾å¤å¯è½ä¸»å¨åé MCP æ¶æ¯ï¼ã |
| | | - **åéæ¹ï¼** è®¾å¤ (æå¡å¨)ã |
| | | - **æ¹æ³ï¼** å¯è½æ¯ä»¥ `notifications/` å¼å¤´çæ¹æ³åï¼æè
å
¶ä»èªå®ä¹æ¹æ³ã |
| | | - **æ¶æ¯ (MCP payload):** éµå¾ª JSON-RPC Notification æ ¼å¼ï¼æ²¡æ `id` åæ®µã |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "notifications/state_changed", // ç¤ºä¾æ¹æ³å |
| | | "params": { |
| | | "newState": "idle", |
| | | "oldState": "connecting" |
| | | } |
| | | // 没æ id åæ®µ |
| | | } |
| | | ``` |
| | | - **åå° API å¤çï¼** æ¥æ¶å° Notification åï¼åå° API è¿è¡ç¸åºçå¤çï¼ä½ä¸åå¤ã |
| | | |
| | | ## 交äºå¾ |
| | | |
| | | ä¸é¢æ¯ä¸ä¸ªç®åç交äºåºåå¾ï¼å±ç¤ºäºä¸»è¦ç MCP æ¶æ¯æµç¨ï¼ |
| | | |
| | | ```mermaid |
| | | sequenceDiagram |
| | | participant Device as ESP32 Device |
| | | participant BackendAPI as åå° API (Client) |
| | | |
| | | Note over Device, BackendAPI: å»ºç« WebSocket / MQTT è¿æ¥ |
| | | |
| | | Device->>BackendAPI: Hello Message (å
å« "mcp": true) |
| | | |
| | | BackendAPI->>Device: MCP Initialize Request |
| | | Note over BackendAPI: method: initialize |
| | | Note over BackendAPI: params: { capabilities: ... } |
| | | |
| | | Device->>BackendAPI: MCP Initialize Response |
| | | Note over Device: result: { protocolVersion: ..., serverInfo: ... } |
| | | |
| | | BackendAPI->>Device: MCP Get Tools List Request |
| | | Note over BackendAPI: method: tools/list |
| | | Note over BackendAPI: params: { cursor: "" } |
| | | |
| | | Device->>BackendAPI: MCP Get Tools List Response |
| | | Note over Device: result: { tools: [...], nextCursor: ... } |
| | | |
| | | loop Optional Pagination |
| | | BackendAPI->>Device: MCP Get Tools List Request |
| | | Note over BackendAPI: method: tools/list |
| | | Note over BackendAPI: params: { cursor: "..." } |
| | | Device->>BackendAPI: MCP Get Tools List Response |
| | | Note over Device: result: { tools: [...], nextCursor: "" } |
| | | end |
| | | |
| | | BackendAPI->>Device: MCP Call Tool Request |
| | | Note over BackendAPI: method: tools/call |
| | | Note over BackendAPI: params: { name: "...", arguments: { ... } } |
| | | |
| | | alt Tool Call Successful |
| | | Device->>BackendAPI: MCP Tool Call Success Response |
| | | Note over Device: result: { content: [...], isError: false } |
| | | else Tool Call Failed |
| | | Device->>BackendAPI: MCP Tool Call Error Response |
| | | Note over Device: error: { code: ..., message: ... } |
| | | end |
| | | |
| | | opt Device Notification |
| | | Device->>BackendAPI: MCP Notification |
| | | Note over Device: method: notifications/... |
| | | Note over Device: params: { ... } |
| | | end |
| | | ``` |
| | | |
| | | è¿ä»½ææ¡£æ¦è¿°äºè¯¥é¡¹ç®ä¸ MCP åè®®ç主è¦äº¤äºæµç¨ãå
·ä½çåæ°ç»èåå·¥å
·åè½éè¦åè `main/mcp_server.cc` ä¸ `McpServer::AddCommonTools` 以åå个工å
·çå®ç°ã |
| New file |
| | |
| | | # MCP IoT Control Usage |
| | | |
| | | > This document describes how to implement IoT control for ESP32 devices using the MCP protocol. For the detailed wire protocol, see [`mcp-protocol.md`](./mcp-protocol.md). |
| | | |
| | | ## Introduction |
| | | |
| | | MCP (Model Context Protocol) is the recommended protocol for IoT control in this project. It uses JSON-RPC 2.0 to let the backend discover and invoke "tools" registered by the device, giving you a flexible way to expose device functionality. |
| | | |
| | | ## Typical Flow |
| | | |
| | | 1. The device boots and connects to the backend over WebSocket or MQTT. |
| | | 2. The backend sends an `initialize` call to start the MCP session. |
| | | 3. The backend issues `tools/list` to discover available tools and their input schemas. |
| | | 4. The backend calls individual tools with `tools/call` to control the device. |
| | | |
| | | See [`mcp-protocol.md`](./mcp-protocol.md) for the exact message format. |
| | | |
| | | ## Registering Tools on the Device |
| | | |
| | | Tools are registered through the `McpServer` singleton. There are two registration APIs: |
| | | |
| | | - `McpServer::AddTool` - regular tool, visible in the default `tools/list` response and callable by the AI model. |
| | | - `McpServer::AddUserOnlyTool` - hidden tool, only returned when the backend lists tools with `withUserTools=true`. Use this for privileged or user-initiated actions (reboot, firmware upgrade, snapshots, etc.) that must not be invoked autonomously by the model. |
| | | |
| | | Both APIs share the same signature: |
| | | |
| | | ```cpp |
| | | void AddTool( |
| | | const std::string& name, // unique tool name, e.g. self.dog.forward |
| | | const std::string& description, // short description for the model |
| | | const PropertyList& properties, // input parameters (may be empty); supported types: bool, int, string |
| | | std::function<ReturnValue(const PropertyList&)> callback // implementation |
| | | ); |
| | | |
| | | void AddUserOnlyTool( |
| | | const std::string& name, |
| | | const std::string& description, |
| | | const PropertyList& properties, |
| | | std::function<ReturnValue(const PropertyList&)> callback |
| | | ); |
| | | ``` |
| | | |
| | | - `name` - unique identifier. A `module.action` naming style works well. |
| | | - `description` - natural-language description; used by the AI to decide when to call the tool. |
| | | - `properties` - input parameters. Supported property types are boolean, integer, and string, with optional min/max and default values. |
| | | - `callback` - implementation. Return values may be `bool`, `int`, or `std::string`. |
| | | |
| | | ## Example (ESP-Hi) |
| | | |
| | | ```cpp |
| | | void InitializeTools() { |
| | | auto& mcp_server = McpServer::GetInstance(); |
| | | |
| | | // Example 1: no arguments - move the robot forward |
| | | mcp_server.AddTool("self.dog.forward", |
| | | "Move the robot forward", |
| | | PropertyList(), |
| | | [this](const PropertyList&) -> ReturnValue { |
| | | servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL); |
| | | return true; |
| | | }); |
| | | |
| | | // Example 2: with arguments - set RGB light color |
| | | mcp_server.AddTool("self.light.set_rgb", |
| | | "Set the RGB color of the light", |
| | | PropertyList({ |
| | | Property("r", kPropertyTypeInteger, 0, 255), |
| | | Property("g", kPropertyTypeInteger, 0, 255), |
| | | Property("b", kPropertyTypeInteger, 0, 255) |
| | | }), |
| | | [this](const PropertyList& properties) -> ReturnValue { |
| | | int r = properties["r"].value<int>(); |
| | | int g = properties["g"].value<int>(); |
| | | int b = properties["b"].value<int>(); |
| | | led_on_ = true; |
| | | SetLedColor(r, g, b); |
| | | return true; |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | ## Example - Registering a User-only Tool |
| | | |
| | | ```cpp |
| | | mcp_server.AddUserOnlyTool("self.display.clear_cache", |
| | | "Clear locally cached images. User-only action.", |
| | | PropertyList(), |
| | | [](const PropertyList&) -> ReturnValue { |
| | | ClearLocalCache(); |
| | | return true; |
| | | }); |
| | | ``` |
| | | |
| | | A tool registered this way will not appear in a regular `tools/list` response. The backend must set `params.withUserTools = true` to see it. |
| | | |
| | | ## Built-in Tools |
| | | |
| | | `McpServer::AddCommonTools` and `McpServer::AddUserOnlyTools` register a number of tools automatically: |
| | | |
| | | ### Default (AI-callable) tools - from `AddCommonTools` |
| | | |
| | | | Tool | Description | |
| | | |------|-------------| |
| | | | `self.get_device_status` | Returns the current volume, screen, battery, network, etc. | |
| | | | `self.audio_speaker.set_volume` | Set speaker volume (`volume`: 0-100). | |
| | | | `self.screen.set_brightness` | Set screen brightness when a backlight is available (`brightness`: 0-100). | |
| | | | `self.screen.set_theme` | Switch UI theme (`theme`: `"light"` or `"dark"`), when LVGL is enabled. | |
| | | | `self.camera.take_photo` | Take a picture with the on-board camera (when the board has one) and answer the given `question` about it. | |
| | | |
| | | Board-specific tools are appended after these by each board's `InitializeTools()`. |
| | | |
| | | ### User-only tools - from `AddUserOnlyTools` |
| | | |
| | | These tools are hidden by default. The backend must pass `withUserTools=true` to `tools/list` to see them. They are intended for companion apps / end users rather than the AI model. |
| | | |
| | | | Tool | Description | |
| | | |------|-------------| |
| | | | `self.get_system_info` | Return a JSON blob describing the system. | |
| | | | `self.reboot` | Reboot the device after a short delay. | |
| | | | `self.upgrade_firmware` | Download firmware from `url` and install it, then reboot. | |
| | | | `self.screen.get_info` | Return the current screen width, height, and whether it is monochrome (LVGL boards only). | |
| | | | `self.screen.snapshot` | Snapshot the screen as JPEG and upload it to `url` (LVGL boards, when `CONFIG_LV_USE_SNAPSHOT=y`). | |
| | | | `self.screen.preview_image` | Download and display an image from `url` on the screen. | |
| | | | `self.assets.set_download_url` | Set the download URL for the assets partition. | |
| | | |
| | | ## JSON-RPC Examples |
| | | |
| | | ### 1. Get the tools list |
| | | |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/list", |
| | | "params": { "cursor": "", "withUserTools": false }, |
| | | "id": 1 |
| | | } |
| | | ``` |
| | | |
| | | ### 2. Move the chassis forward |
| | | |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.chassis.go_forward", |
| | | "arguments": {} |
| | | }, |
| | | "id": 2 |
| | | } |
| | | ``` |
| | | |
| | | ### 3. Switch the light mode |
| | | |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.chassis.switch_light_mode", |
| | | "arguments": { "light_mode": 3 } |
| | | }, |
| | | "id": 3 |
| | | } |
| | | ``` |
| | | |
| | | ### 4. Reboot the device (user-only) |
| | | |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.reboot", |
| | | "arguments": {} |
| | | }, |
| | | "id": 4 |
| | | } |
| | | ``` |
| | | |
| | | ## Notes |
| | | |
| | | - Tool names, parameters, and return values must match what the device registers via `AddTool` / `AddUserOnlyTool`. |
| | | - Prefer MCP for any new IoT control. |
| | | - For the wire protocol and advanced topics, see [`mcp-protocol.md`](./mcp-protocol.md). |
| New file |
| | |
| | | # MCP åè®®ç©èç½æ§å¶ç¨æ³è¯´æ |
| | | |
| | | > æ¬ææ¡£ä»ç»å¦ä½åºäº MCP åè®®å®ç° ESP32 设å¤çç©èç½æ§å¶ã详ç»åè®®æµç¨è¯·åè [`mcp-protocol_zh.md`](./mcp-protocol_zh.md)ã |
| | | |
| | | ## ç®ä» |
| | | |
| | | MCPï¼Model Context Protocolï¼æ¯æ°ä¸ä»£æ¨èç¨äºç©èç½æ§å¶çåè®®ï¼éè¿æ å JSON-RPC 2.0 æ ¼å¼å¨åå°ä¸è®¾å¤é´åç°åè°ç¨"å·¥å
·"ï¼Toolï¼ï¼å®ç°çµæ´»çè®¾å¤æ§å¶ã |
| | | |
| | | ## å
¸åä½¿ç¨æµç¨ |
| | | |
| | | 1. 设å¤å¯å¨åéè¿åºç¡åè®®ï¼å¦ WebSocket/MQTTï¼ä¸åå°å»ºç«è¿æ¥ã |
| | | 2. åå°éè¿ MCP åè®®ç `initialize` æ¹æ³åå§åä¼è¯ã |
| | | 3. åå°éè¿ `tools/list` è·åè®¾å¤æ¯æçææå·¥å
·ï¼åè½ï¼ååæ°è¯´æã |
| | | 4. åå°éè¿ `tools/call` è°ç¨å
·ä½å·¥å
·ï¼å®ç°å¯¹è®¾å¤çæ§å¶ã |
| | | |
| | | 详ç»åè®®æ ¼å¼ä¸äº¤äºè¯·è§ [`mcp-protocol_zh.md`](./mcp-protocol_zh.md)ã |
| | | |
| | | ## 设å¤ç«¯å·¥å
·æ³¨åæ¹æ³è¯´æ |
| | | |
| | | 设å¤éè¿ `McpServer::AddTool` æ¹æ³æ³¨åå¯è¢«åå°è°ç¨ç"å·¥å
·"ãå
¶å¸¸ç¨å½æ°ç¾åå¦ä¸ï¼ |
| | | |
| | | ```cpp |
| | | void AddTool( |
| | | const std::string& name, // å·¥å
·åç§°ï¼å»ºè®®å¯ä¸ä¸æå±æ¬¡æï¼å¦ self.dog.forward |
| | | const std::string& description, // å·¥å
·æè¿°ï¼ç®æè¯´æåè½ï¼ä¾¿äºå¤§æ¨¡åçè§£ |
| | | const PropertyList& properties, // è¾å
¥åæ°å表ï¼å¯ä¸ºç©ºï¼ï¼æ¯æç±»åï¼å¸å°ãæ´æ°ãå符串 |
| | | std::function<ReturnValue(const PropertyList&)> callback // å·¥å
·è¢«è°ç¨æ¶çåè°å®ç° |
| | | ); |
| | | ``` |
| | | - nameï¼å·¥å
·å¯ä¸æ è¯ï¼å»ºè®®ç¨"模å.åè½"å½å飿 ¼ã |
| | | - descriptionï¼èªç¶è¯è¨æè¿°ï¼ä¾¿äº AI/ç¨æ·çè§£ã |
| | | - propertiesï¼åæ°åè¡¨ï¼æ¯æç±»åæå¸å°ãæ´æ°ãå符串ï¼å¯æå®èå´åé»è®¤å¼ã |
| | | - callbackï¼æ¶å°è°ç¨è¯·æ±æ¶çå®é
æ§è¡é»è¾ï¼è¿åå¼å¯ä¸º bool/int/stringã |
| | | |
| | | ## å
¸å注å示ä¾ï¼ä»¥ ESP-Hi 为ä¾ï¼ |
| | | |
| | | ```cpp |
| | | void InitializeTools() { |
| | | auto& mcp_server = McpServer::GetInstance(); |
| | | // ä¾1ï¼æ åæ°ï¼æ§å¶æºå¨äººåè¿ |
| | | mcp_server.AddTool("self.dog.forward", "æºå¨äººååç§»å¨", PropertyList(), [this](const PropertyList&) -> ReturnValue { |
| | | servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL); |
| | | return true; |
| | | }); |
| | | // ä¾2ï¼å¸¦åæ°ï¼è®¾ç½®ç¯å
RGB é¢è² |
| | | mcp_server.AddTool("self.light.set_rgb", "设置RGBé¢è²", PropertyList({ |
| | | Property("r", kPropertyTypeInteger, 0, 255), |
| | | Property("g", kPropertyTypeInteger, 0, 255), |
| | | Property("b", kPropertyTypeInteger, 0, 255) |
| | | }), [this](const PropertyList& properties) -> ReturnValue { |
| | | int r = properties["r"].value<int>(); |
| | | int g = properties["g"].value<int>(); |
| | | int b = properties["b"].value<int>(); |
| | | led_on_ = true; |
| | | SetLedColor(r, g, b); |
| | | return true; |
| | | }); |
| | | } |
| | | ``` |
| | | |
| | | ## 常è§å·¥å
·è°ç¨ JSON-RPC ç¤ºä¾ |
| | | |
| | | ### 1. è·åå·¥å
·å表 |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/list", |
| | | "params": { "cursor": "" }, |
| | | "id": 1 |
| | | } |
| | | ``` |
| | | |
| | | ### 2. æ§å¶åºçåè¿ |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.chassis.go_forward", |
| | | "arguments": {} |
| | | }, |
| | | "id": 2 |
| | | } |
| | | ``` |
| | | |
| | | ### 3. 忢ç¯å
æ¨¡å¼ |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.chassis.switch_light_mode", |
| | | "arguments": { "light_mode": 3 } |
| | | }, |
| | | "id": 3 |
| | | } |
| | | ``` |
| | | |
| | | ### 4. æå头翻转 |
| | | ```json |
| | | { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.camera.set_camera_flipped", |
| | | "arguments": {} |
| | | }, |
| | | "id": 4 |
| | | } |
| | | ``` |
| | | |
| | | ## 夿³¨ |
| | | - å·¥å
·åç§°ãåæ°åè¿åå¼è¯·ä»¥è®¾å¤ç«¯ `AddTool` 注å为åã |
| | | - æ¨èæææ°é¡¹ç®ç»ä¸éç¨ MCP åè®®è¿è¡ç©èç½æ§å¶ã |
| | | - 详ç»åè®®ä¸è¿é¶ç¨æ³è¯·æ¥é
[`mcp-protocol_zh.md`](./mcp-protocol_zh.md)ã |
| New file |
| | |
| | | # MQTT + UDP Hybrid Communication Protocol |
| | | |
| | | This document describes the MQTT + UDP hybrid protocol used between the device and the server, based on the current implementation: MQTT carries control messages, UDP carries real-time audio. |
| | | |
| | | --- |
| | | |
| | | ## 1. Overview |
| | | |
| | | The protocol uses two channels: |
| | | |
| | | - **MQTT** - control messages, state synchronization, JSON payloads. |
| | | - **UDP** - real-time audio, encrypted. |
| | | |
| | | ### 1.1 Key characteristics |
| | | |
| | | - **Dual channel design** - control is separated from data so audio has low latency. |
| | | - **Encrypted transport** - UDP audio is encrypted with AES-CTR. |
| | | - **Sequence numbers** - guard against replay and reordering. |
| | | - **Automatic reconnect** - MQTT reconnects on disconnect. |
| | | |
| | | --- |
| | | |
| | | ## 2. End-to-end Flow |
| | | |
| | | ```mermaid |
| | | sequenceDiagram |
| | | participant Device as ESP32 device |
| | | participant MQTT as MQTT broker |
| | | participant UDP as UDP server |
| | | |
| | | Note over Device, UDP: 1. Establish MQTT connection |
| | | Device->>MQTT: MQTT Connect |
| | | MQTT->>Device: Connected |
| | | |
| | | Note over Device, UDP: 2. Request audio channel |
| | | Device->>MQTT: Hello message (type: "hello", transport: "udp") |
| | | MQTT->>Device: Hello response (UDP endpoint + encryption keys) |
| | | |
| | | Note over Device, UDP: 3. Establish UDP connection |
| | | Device->>UDP: UDP Connect |
| | | UDP->>Device: Connected |
| | | |
| | | Note over Device, UDP: 4. Audio streaming |
| | | loop Audio stream |
| | | Device->>UDP: Encrypted audio (Opus) |
| | | UDP->>Device: Encrypted audio (Opus) |
| | | end |
| | | |
| | | Note over Device, UDP: 5. Control messages |
| | | par Control |
| | | Device->>MQTT: Listen / TTS / MCP messages |
| | | MQTT->>Device: STT / TTS / MCP / Alert responses |
| | | end |
| | | |
| | | Note over Device, UDP: 6. Teardown |
| | | Device->>MQTT: Goodbye |
| | | Device->>UDP: Disconnect |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 3. MQTT Control Channel |
| | | |
| | | ### 3.1 Connection |
| | | |
| | | The device connects to the broker using: |
| | | - **Endpoint** - broker host and port. |
| | | - **Client ID** - device identifier. |
| | | - **Username / Password** - credentials. |
| | | - **Keep Alive** - heartbeat interval (default 240 s). |
| | | |
| | | ### 3.2 Hello exchange |
| | | |
| | | #### 3.2.1 Device -> Server |
| | | |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 3, |
| | | "transport": "udp", |
| | | "features": { |
| | | "mcp": true, |
| | | "aec": true |
| | | }, |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | `features.mcp` is always set; `features.aec` is set when `CONFIG_USE_SERVER_AEC` is enabled. |
| | | |
| | | #### 3.2.2 Server -> Device |
| | | |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "transport": "udp", |
| | | "session_id": "xxx", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 24000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | }, |
| | | "udp": { |
| | | "server": "192.168.1.100", |
| | | "port": 8888, |
| | | "key": "0123456789ABCDEF0123456789ABCDEF", |
| | | "nonce": "0123456789ABCDEF0123456789ABCDEF" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | Field reference: |
| | | - `udp.server` - UDP server address. |
| | | - `udp.port` - UDP server port. |
| | | - `udp.key` - AES key, hex-encoded. |
| | | - `udp.nonce` - AES nonce, hex-encoded. |
| | | |
| | | ### 3.3 JSON message types |
| | | |
| | | #### 3.3.1 Device -> Server |
| | | |
| | | 1. **Listen** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "listen", |
| | | "state": "start", |
| | | "mode": "manual" |
| | | } |
| | | ``` |
| | | |
| | | 2. **Abort** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "abort", |
| | | "reason": "wake_word_detected" |
| | | } |
| | | ``` |
| | | |
| | | 3. **MCP** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "mcp", |
| | | "payload": { |
| | | "jsonrpc": "2.0", |
| | | "id": 1, |
| | | "result": {} |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 4. **Goodbye** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "goodbye" |
| | | } |
| | | ``` |
| | | |
| | | #### 3.3.2 Server -> Device |
| | | |
| | | Semantics match the WebSocket protocol. Supported types: |
| | | - **STT** - speech recognition result. |
| | | - **TTS** - TTS lifecycle (`start`, `stop`, `sentence_start`). |
| | | - **LLM** - emotion update for the UI. |
| | | - **MCP** - IoT control. |
| | | - **System** - system control, e.g. `"command": "reboot"`. |
| | | - **Alert** - show an alert on the UI; fields: `status`, `message`, `emotion`. |
| | | - **Goodbye** - server-initiated shutdown of the audio session. The device responds by closing the UDP channel without sending its own goodbye. |
| | | - **Custom** (optional, enabled via `CONFIG_RECEIVE_CUSTOM_MESSAGE`). |
| | | |
| | | Example alert: |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "alert", |
| | | "status": "Warning", |
| | | "message": "Battery low", |
| | | "emotion": "sad" |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 4. UDP Audio Channel |
| | | |
| | | ### 4.1 Establishing the channel |
| | | |
| | | After the device receives the MQTT hello response, it: |
| | | 1. Parses the UDP host and port. |
| | | 2. Parses the AES key and nonce. |
| | | 3. Initializes the AES-CTR context. |
| | | 4. Opens the UDP socket. |
| | | |
| | | ### 4.2 Audio packet format |
| | | |
| | | #### 4.2.1 Encrypted audio packet |
| | | |
| | | ``` |
| | | |type 1B|flags 1B|payload_len 2B|ssrc 4B|timestamp 4B|sequence 4B| |
| | | |payload payload_len bytes| |
| | | ``` |
| | | |
| | | Field reference: |
| | | - `type`: packet type, always `0x01`. |
| | | - `flags`: flags, currently unused. |
| | | - `payload_len`: payload length (network byte order). |
| | | - `ssrc`: synchronization source identifier. |
| | | - `timestamp`: timestamp (network byte order). |
| | | - `sequence`: sequence number (network byte order). |
| | | - `payload`: encrypted Opus audio data. |
| | | |
| | | #### 4.2.2 Encryption |
| | | |
| | | Uses **AES-CTR** with: |
| | | - **Key**: 128-bit, provided by the server. |
| | | - **Nonce**: 128-bit, provided by the server. |
| | | - **Counter**: built from the timestamp and sequence number. |
| | | |
| | | ### 4.3 Sequence number management |
| | | |
| | | - **Sender**: `local_sequence_` is incremented monotonically. |
| | | - **Receiver**: `remote_sequence_` validates continuity. |
| | | - **Anti-replay**: packets with sequence numbers below the expected value are dropped. |
| | | - **Tolerance**: small gaps are logged as warnings but still accepted. |
| | | |
| | | ### 4.4 Error handling |
| | | |
| | | 1. **Decryption failure** - log an error and drop the packet. |
| | | 2. **Sequence gap** - log a warning, continue processing the packet. |
| | | 3. **Malformed packet** - log an error and drop. |
| | | |
| | | --- |
| | | |
| | | ## 5. State Management |
| | | |
| | | ### 5.1 Connection state |
| | | |
| | | ```mermaid |
| | | stateDiagram |
| | | direction TB |
| | | [*] --> Disconnected |
| | | Disconnected --> MqttConnecting: StartMqttClient() |
| | | MqttConnecting --> MqttConnected: MQTT Connected |
| | | MqttConnecting --> Disconnected: Connect failed |
| | | MqttConnected --> RequestingChannel: OpenAudioChannel() |
| | | RequestingChannel --> ChannelOpened: Hello exchange success |
| | | RequestingChannel --> MqttConnected: Hello timeout / failed |
| | | ChannelOpened --> UdpConnected: UDP connect success |
| | | UdpConnected --> AudioStreaming: Start audio |
| | | AudioStreaming --> UdpConnected: Stop audio |
| | | UdpConnected --> ChannelOpened: UDP disconnect |
| | | ChannelOpened --> MqttConnected: CloseAudioChannel() |
| | | MqttConnected --> Disconnected: MQTT disconnect |
| | | ``` |
| | | |
| | | ### 5.2 State check |
| | | |
| | | The device determines whether the audio channel is available with: |
| | | ```cpp |
| | | bool IsAudioChannelOpened() const { |
| | | return udp_ != nullptr && !error_occurred_ && !IsTimeout(); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 6. Configuration Parameters |
| | | |
| | | ### 6.1 MQTT settings |
| | | |
| | | Read from storage: |
| | | - `endpoint` - broker address. |
| | | - `client_id` - client identifier. |
| | | - `username` - user name. |
| | | - `password` - password. |
| | | - `keepalive` - keep-alive interval (default 240 s). |
| | | - `publish_topic` - publish topic. |
| | | |
| | | ### 6.2 Audio parameters |
| | | |
| | | - **Format**: Opus |
| | | - **Sample rate**: 16 kHz device / 24 kHz server |
| | | - **Channels**: 1 (mono) |
| | | - **Frame duration**: 60 ms |
| | | |
| | | --- |
| | | |
| | | ## 7. Error Handling and Reconnection |
| | | |
| | | ### 7.1 MQTT reconnect |
| | | |
| | | - Automatic retry on connect failure. |
| | | - Optional error reporting. |
| | | - Clean-up runs on disconnect. |
| | | |
| | | ### 7.2 UDP connection |
| | | |
| | | - No automatic retry; depends on re-negotiation via MQTT. |
| | | - Status can be queried at any time. |
| | | |
| | | ### 7.3 Timeouts |
| | | |
| | | The base `Protocol` class provides timeout detection: |
| | | - Default timeout: 120 s. |
| | | - Based on the time since the last incoming packet. |
| | | - After a timeout the channel is marked unavailable. |
| | | |
| | | --- |
| | | |
| | | ## 8. Security |
| | | |
| | | ### 8.1 Transport encryption |
| | | |
| | | - **MQTT**: supports TLS/SSL (port 8883). |
| | | - **UDP**: AES-CTR on audio payloads. |
| | | |
| | | ### 8.2 Authentication |
| | | |
| | | - **MQTT**: user name / password. |
| | | - **UDP**: keys are distributed via the MQTT channel. |
| | | |
| | | ### 8.3 Anti-replay |
| | | |
| | | - Monotonically increasing sequence numbers. |
| | | - Stale packets are dropped. |
| | | - Timestamps are validated. |
| | | |
| | | --- |
| | | |
| | | ## 9. Performance Notes |
| | | |
| | | ### 9.1 Concurrency |
| | | |
| | | A mutex protects the UDP connection: |
| | | ```cpp |
| | | std::lock_guard<std::mutex> lock(channel_mutex_); |
| | | ``` |
| | | |
| | | ### 9.2 Memory management |
| | | |
| | | - Network objects are created and destroyed dynamically. |
| | | - Audio packets are managed with smart pointers. |
| | | - Encryption contexts are released promptly. |
| | | |
| | | ### 9.3 Network optimizations |
| | | |
| | | - UDP connection reuse. |
| | | - Reasonable packet sizes. |
| | | - Sequence continuity checks. |
| | | |
| | | --- |
| | | |
| | | ## 10. Comparison with WebSocket |
| | | |
| | | | Feature | MQTT + UDP | WebSocket | |
| | | |---------|------------|-----------| |
| | | | Control channel | MQTT | WebSocket | |
| | | | Audio channel | UDP (encrypted) | WebSocket (binary) | |
| | | | Latency | Low (UDP) | Medium | |
| | | | Reliability | Medium | High | |
| | | | Complexity | High | Low | |
| | | | Encryption | AES-CTR | TLS | |
| | | | Firewall friendliness | Low | High | |
| | | |
| | | --- |
| | | |
| | | ## 11. Deployment Notes |
| | | |
| | | ### 11.1 Network |
| | | |
| | | - Ensure UDP ports are reachable. |
| | | - Configure firewall rules accordingly. |
| | | - Plan for NAT traversal if needed. |
| | | |
| | | ### 11.2 Server infrastructure |
| | | |
| | | - MQTT broker configuration. |
| | | - UDP server deployment. |
| | | - Key management. |
| | | |
| | | ### 11.3 Monitoring |
| | | |
| | | - Connection success rate. |
| | | - Audio transmission latency. |
| | | - Packet loss. |
| | | - Decryption failures. |
| | | |
| | | --- |
| | | |
| | | ## 12. Summary |
| | | |
| | | The MQTT + UDP hybrid protocol achieves efficient audio communication through: |
| | | |
| | | - **Split architecture** - separate control and data channels with clear responsibilities. |
| | | - **Encryption** - AES-CTR protects audio payloads. |
| | | - **Sequence management** - prevents replay and reordering. |
| | | - **Automatic recovery** - MQTT reconnects on failure. |
| | | - **Performance** - UDP keeps audio latency low. |
| | | |
| | | The protocol is a good fit for low-latency voice interaction, at the cost of higher network complexity than pure WebSocket. |
| New file |
| | |
| | | # MQTT + UDP æ··åéä¿¡åè®®ææ¡£ |
| | | |
| | | åºäºä»£ç å®ç°æ´çç MQTT + UDP æ··åéä¿¡åè®®ææ¡£ï¼æ¦è¿°è®¾å¤ç«¯ä¸æå¡å¨ä¹é´å¦ä½éè¿ MQTT è¿è¡æ§å¶æ¶æ¯ä¼ è¾ï¼éè¿ UDP è¿è¡é³é¢æ°æ®ä¼ è¾çäº¤äºæ¹å¼ã |
| | | |
| | | --- |
| | | |
| | | ## 1. åè®®æ¦è§ |
| | | |
| | | æ¬åè®®éç¨æ··åä¼ è¾æ¹å¼ï¼ |
| | | - **MQTT**ï¼ç¨äºæ§å¶æ¶æ¯ãç¶æåæ¥ãJSON æ°æ®äº¤æ¢ |
| | | - **UDP**ï¼ç¨äºå®æ¶é³é¢æ°æ®ä¼ è¾ï¼æ¯æå å¯ |
| | | |
| | | ### 1.1 åè®®ç¹ç¹ |
| | | |
| | | - **åéé设计**ï¼æ§å¶ä¸æ°æ®å离ï¼ç¡®ä¿å®æ¶æ§ |
| | | - **å å¯ä¼ è¾**ï¼UDP é³é¢æ°æ®ä½¿ç¨ AES-CTR å å¯ |
| | | - **åºåå·ä¿æ¤**ï¼é²æ¢æ°æ®å
éæ¾åä¹±åº |
| | | - **èªå¨éè¿**ï¼MQTT è¿æ¥æå¼æ¶èªå¨éè¿ |
| | | |
| | | --- |
| | | |
| | | ## 2. æ»ä½æµç¨æ¦è§ |
| | | |
| | | ```mermaid |
| | | sequenceDiagram |
| | | participant Device as ESP32 è®¾å¤ |
| | | participant MQTT as MQTT æå¡å¨ |
| | | participant UDP as UDP æå¡å¨ |
| | | |
| | | Note over Device, UDP: 1. å»ºç« MQTT è¿æ¥ |
| | | Device->>MQTT: MQTT Connect |
| | | MQTT->>Device: Connected |
| | | |
| | | Note over Device, UDP: 2. 请æ±é³é¢éé |
| | | Device->>MQTT: Hello Message (type: "hello", transport: "udp") |
| | | MQTT->>Device: Hello Response (UDP è¿æ¥ä¿¡æ¯ + å å¯å¯é¥) |
| | | |
| | | Note over Device, UDP: 3. å»ºç« UDP è¿æ¥ |
| | | Device->>UDP: UDP Connect |
| | | UDP->>Device: Connected |
| | | |
| | | Note over Device, UDP: 4. é³é¢æ°æ®ä¼ è¾ |
| | | loop é³é¢æµä¼ è¾ |
| | | Device->>UDP: å å¯é³é¢æ°æ® (Opus) |
| | | UDP->>Device: å å¯é³é¢æ°æ® (Opus) |
| | | end |
| | | |
| | | Note over Device, UDP: 5. æ§å¶æ¶æ¯äº¤æ¢ |
| | | par æ§å¶æ¶æ¯ |
| | | Device->>MQTT: Listen/TTS/MCP æ¶æ¯ |
| | | MQTT->>Device: STT/TTS/MCP ååº |
| | | end |
| | | |
| | | Note over Device, UDP: 6. å
³éè¿æ¥ |
| | | Device->>MQTT: Goodbye Message |
| | | Device->>UDP: Disconnect |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 3. MQTT æ§å¶éé |
| | | |
| | | ### 3.1 è¿æ¥å»ºç« |
| | | |
| | | 设å¤éè¿ MQTT è¿æ¥å°æå¡å¨ï¼è¿æ¥åæ°å
æ¬ï¼ |
| | | - **Endpoint**ï¼MQTT æå¡å¨å°ååç«¯å£ |
| | | - **Client ID**ï¼è®¾å¤å¯ä¸æ è¯ç¬¦ |
| | | - **Username/Password**ï¼è®¤è¯åæ® |
| | | - **Keep Alive**ï¼å¿è·³é´éï¼é»è®¤240ç§ï¼ |
| | | |
| | | ### 3.2 Hello æ¶æ¯äº¤æ¢ |
| | | |
| | | #### 3.2.1 设å¤ç«¯åé Hello |
| | | |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 3, |
| | | "transport": "udp", |
| | | "features": { |
| | | "mcp": true |
| | | }, |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | #### 3.2.2 æå¡å¨ååº Hello |
| | | |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "transport": "udp", |
| | | "session_id": "xxx", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 24000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | }, |
| | | "udp": { |
| | | "server": "192.168.1.100", |
| | | "port": 8888, |
| | | "key": "0123456789ABCDEF0123456789ABCDEF", |
| | | "nonce": "0123456789ABCDEF0123456789ABCDEF" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | **åæ®µè¯´æï¼** |
| | | - `udp.server`ï¼UDP æå¡å¨å°å |
| | | - `udp.port`ï¼UDP æå¡å¨ç«¯å£ |
| | | - `udp.key`ï¼AES å å¯å¯é¥ï¼åå
è¿å¶åç¬¦ä¸²ï¼ |
| | | - `udp.nonce`ï¼AES å å¯éæºæ°ï¼åå
è¿å¶åç¬¦ä¸²ï¼ |
| | | |
| | | ### 3.3 JSON æ¶æ¯ç±»å |
| | | |
| | | #### 3.3.1 设å¤ç«¯âæå¡å¨ |
| | | |
| | | 1. **Listen æ¶æ¯** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "listen", |
| | | "state": "start", |
| | | "mode": "manual" |
| | | } |
| | | ``` |
| | | |
| | | 2. **Abort æ¶æ¯** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "abort", |
| | | "reason": "wake_word_detected" |
| | | } |
| | | ``` |
| | | |
| | | 3. **MCP æ¶æ¯** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "mcp", |
| | | "payload": { |
| | | "jsonrpc": "2.0", |
| | | "id": 1, |
| | | "result": {...} |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 4. **Goodbye æ¶æ¯** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "goodbye" |
| | | } |
| | | ``` |
| | | |
| | | #### 3.3.2 æå¡å¨â设å¤ç«¯ |
| | | |
| | | æ¯æçæ¶æ¯ç±»åä¸ WebSocket åè®®ä¸è´ï¼å
æ¬ï¼ |
| | | - **STT**ï¼è¯é³è¯å«ç»æ |
| | | - **TTS**ï¼è¯é³åææ§å¶ |
| | | - **LLM**ï¼æ
æè¡¨è¾¾æ§å¶ |
| | | - **MCP**ï¼ç©èç½æ§å¶ |
| | | - **System**ï¼ç³»ç»æ§å¶ |
| | | - **Custom**ï¼èªå®ä¹æ¶æ¯ï¼å¯éï¼ |
| | | |
| | | --- |
| | | |
| | | ## 4. UDP é³é¢éé |
| | | |
| | | ### 4.1 è¿æ¥å»ºç« |
| | | |
| | | è®¾å¤æ¶å° MQTT Hello ååºåï¼ä½¿ç¨å
¶ä¸ç UDP è¿æ¥ä¿¡æ¯å»ºç«é³é¢ééï¼ |
| | | 1. è§£æ UDP æå¡å¨å°ååç«¯å£ |
| | | 2. è§£æå å¯å¯é¥åéæºæ° |
| | | 3. åå§å AES-CTR å å¯ä¸ä¸æ |
| | | 4. å»ºç« UDP è¿æ¥ |
| | | |
| | | ### 4.2 é³é¢æ°æ®æ ¼å¼ |
| | | |
| | | #### 4.2.1 å å¯é³é¢å
ç»æ |
| | | |
| | | ``` |
| | | |type 1byte|flags 1byte|payload_len 2bytes|ssrc 4bytes|timestamp 4bytes|sequence 4bytes| |
| | | |payload payload_len bytes| |
| | | ``` |
| | | |
| | | **åæ®µè¯´æï¼** |
| | | - `type`ï¼æ°æ®å
ç±»åï¼åºå®ä¸º 0x01 |
| | | - `flags`ï¼æ å¿ä½ï¼å½åæªä½¿ç¨ |
| | | - `payload_len`ï¼è´è½½é¿åº¦ï¼ç½ç»åèåºï¼ |
| | | - `ssrc`ï¼åæ¥æºæ è¯ç¬¦ |
| | | - `timestamp`ï¼æ¶é´æ³ï¼ç½ç»åèåºï¼ |
| | | - `sequence`ï¼åºåå·ï¼ç½ç»åèåºï¼ |
| | | - `payload`ï¼å å¯ç Opus é³é¢æ°æ® |
| | | |
| | | #### 4.2.2 å å¯ç®æ³ |
| | | |
| | | ä½¿ç¨ **AES-CTR** 模å¼å å¯ï¼ |
| | | - **å¯é¥**ï¼128ä½ï¼ç±æå¡å¨æä¾ |
| | | - **éæºæ°**ï¼128ä½ï¼ç±æå¡å¨æä¾ |
| | | - **计æ°å¨**ï¼å
嫿¶é´æ³ååºåå·ä¿¡æ¯ |
| | | |
| | | ### 4.3 åºåå·ç®¡ç |
| | | |
| | | - **åé端**ï¼`local_sequence_` åè°éå¢ |
| | | - **æ¥æ¶ç«¯**ï¼`remote_sequence_` éªè¯è¿ç»æ§ |
| | | - **é²éæ¾**ï¼æç»åºåå·å°äºææå¼çæ°æ®å
|
| | | - **容éå¤ç**ï¼å
许轻微çåºåå·è·³è·ï¼è®°å½è¦å |
| | | |
| | | ### 4.4 é误å¤ç |
| | | |
| | | 1. **è§£å¯å¤±è´¥**ï¼è®°å½é误ï¼ä¸¢å¼æ°æ®å
|
| | | 2. **åºåå·å¼å¸¸**ï¼è®°å½è¦åï¼ä½ä»å¤çæ°æ®å
|
| | | 3. **æ°æ®å
æ ¼å¼é误**ï¼è®°å½é误ï¼ä¸¢å¼æ°æ®å
|
| | | |
| | | --- |
| | | |
| | | ## 5. ç¶æç®¡ç |
| | | |
| | | ### 5.1 è¿æ¥ç¶æ |
| | | |
| | | ```mermaid |
| | | stateDiagram |
| | | direction TB |
| | | [*] --> Disconnected |
| | | Disconnected --> MqttConnecting: StartMqttClient() |
| | | MqttConnecting --> MqttConnected: MQTT Connected |
| | | MqttConnecting --> Disconnected: Connect Failed |
| | | MqttConnected --> RequestingChannel: OpenAudioChannel() |
| | | RequestingChannel --> ChannelOpened: Hello Exchange Success |
| | | RequestingChannel --> MqttConnected: Hello Timeout/Failed |
| | | ChannelOpened --> UdpConnected: UDP Connect Success |
| | | UdpConnected --> AudioStreaming: Start Audio Transfer |
| | | AudioStreaming --> UdpConnected: Stop Audio Transfer |
| | | UdpConnected --> ChannelOpened: UDP Disconnect |
| | | ChannelOpened --> MqttConnected: CloseAudioChannel() |
| | | MqttConnected --> Disconnected: MQTT Disconnect |
| | | ``` |
| | | |
| | | ### 5.2 ç¶ææ£æ¥ |
| | | |
| | | 设å¤éè¿ä»¥ä¸æ¡ä»¶å¤æé³é¢é鿝å¦å¯ç¨ï¼ |
| | | ```cpp |
| | | bool IsAudioChannelOpened() const { |
| | | return udp_ != nullptr && !error_occurred_ && !IsTimeout(); |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 6. é
ç½®åæ° |
| | | |
| | | ### 6.1 MQTT é
ç½® |
| | | |
| | | ä»è®¾ç½®ä¸è¯»åçé
ç½®é¡¹ï¼ |
| | | - `endpoint`ï¼MQTT æå¡å¨å°å |
| | | - `client_id`ï¼å®¢æ·ç«¯æ è¯ç¬¦ |
| | | - `username`ï¼ç¨æ·å |
| | | - `password`ï¼å¯ç |
| | | - `keepalive`ï¼å¿è·³é´éï¼é»è®¤240ç§ï¼ |
| | | - `publish_topic`ï¼åå¸ä¸»é¢ |
| | | |
| | | ### 6.2 é³é¢åæ° |
| | | |
| | | - **æ ¼å¼**ï¼Opus |
| | | - **éæ ·ç**ï¼16000 Hzï¼è®¾å¤ç«¯ï¼/ 24000 Hzï¼æå¡å¨ç«¯ï¼ |
| | | - **声鿰**ï¼1ï¼å声éï¼ |
| | | - **帧æ¶é¿**ï¼60ms |
| | | |
| | | --- |
| | | |
| | | ## 7. é误å¤çä¸éè¿ |
| | | |
| | | ### 7.1 MQTT éè¿æºå¶ |
| | | |
| | | - è¿æ¥å¤±è´¥æ¶èªå¨éè¯ |
| | | - æ¯æéè¯¯ä¸æ¥æ§å¶ |
| | | - æçº¿æ¶è§¦åæ¸
çæµç¨ |
| | | |
| | | ### 7.2 UDP è¿æ¥ç®¡ç |
| | | |
| | | - è¿æ¥å¤±è´¥æ¶ä¸èªå¨éè¯ |
| | | - ä¾èµ MQTT éééæ°åå |
| | | - æ¯æè¿æ¥ç¶ææ¥è¯¢ |
| | | |
| | | ### 7.3 è¶
æ¶å¤ç |
| | | |
| | | åºç±» `Protocol` æä¾è¶
æ¶æ£æµï¼ |
| | | - é»è®¤è¶
æ¶æ¶é´ï¼120 ç§ |
| | | - åºäºæåæ¥æ¶æ¶é´è®¡ç® |
| | | - è¶
æ¶æ¶èªå¨æ 记为ä¸å¯ç¨ |
| | | |
| | | --- |
| | | |
| | | ## 8. å®å
¨èè |
| | | |
| | | ### 8.1 ä¼ è¾å å¯ |
| | | |
| | | - **MQTT**ï¼æ¯æ TLS/SSL å å¯ï¼ç«¯å£8883ï¼ |
| | | - **UDP**ï¼ä½¿ç¨ AES-CTR å å¯é³é¢æ°æ® |
| | | |
| | | ### 8.2 è®¤è¯æºå¶ |
| | | |
| | | - **MQTT**ï¼ç¨æ·å/å¯ç è®¤è¯ |
| | | - **UDP**ï¼éè¿ MQTT ééååå¯é¥ |
| | | |
| | | ### 8.3 é²éæ¾æ»å» |
| | | |
| | | - åºåå·åè°éå¢ |
| | | - æç»è¿ææ°æ®å
|
| | | - æ¶é´æ³éªè¯ |
| | | |
| | | --- |
| | | |
| | | ## 9. æ§è½ä¼å |
| | | |
| | | ### 9.1 å¹¶åæ§å¶ |
| | | |
| | | 使ç¨äºæ¥éä¿æ¤ UDP è¿æ¥ï¼ |
| | | ```cpp |
| | | std::lock_guard<std::mutex> lock(channel_mutex_); |
| | | ``` |
| | | |
| | | ### 9.2 å
å管ç |
| | | |
| | | - 卿å建/鿝ç½ç»å¯¹è±¡ |
| | | - æºè½æé管çé³é¢æ°æ®å
|
| | | - åæ¶éæ¾å å¯ä¸ä¸æ |
| | | |
| | | ### 9.3 ç½ç»ä¼å |
| | | |
| | | - UDP è¿æ¥å¤ç¨ |
| | | - æ°æ®å
大å°ä¼å |
| | | - åºåå·è¿ç»æ§æ£æ¥ |
| | | |
| | | --- |
| | | |
| | | ## 10. ä¸ WebSocket åè®®çæ¯è¾ |
| | | |
| | | | ç¹æ§ | MQTT + UDP | WebSocket | |
| | | |------|------------|-----------| |
| | | | æ§å¶éé | MQTT | WebSocket | |
| | | | é³é¢éé | UDP (å å¯) | WebSocket (äºè¿å¶) | |
| | | | 宿¶æ§ | é« (UDP) | ä¸ç | |
| | | | å¯é æ§ | ä¸ç | é« | |
| | | | å¤æåº¦ | é« | ä½ | |
| | | | å å¯ | AES-CTR | TLS | |
| | | | é²ç«å¢å好度 | ä½ | é« | |
| | | |
| | | --- |
| | | |
| | | ## 11. é¨ç½²å»ºè®® |
| | | |
| | | ### 11.1 ç½ç»ç¯å¢ |
| | | |
| | | - ç¡®ä¿ UDP 端å£å¯è¾¾ |
| | | - é
ç½®é²ç«å¢è§å |
| | | - èè NAT ç©¿é |
| | | |
| | | ### 11.2 æå¡å¨é
ç½® |
| | | |
| | | - MQTT Broker é
ç½® |
| | | - UDP æå¡å¨é¨ç½² |
| | | - å¯é¥ç®¡çç³»ç» |
| | | |
| | | ### 11.3 çæ§ææ |
| | | |
| | | - è¿æ¥æåç |
| | | - é³é¢ä¼ è¾å»¶è¿ |
| | | - æ°æ®å
丢失ç |
| | | - è§£å¯å¤±è´¥ç |
| | | |
| | | --- |
| | | |
| | | ## 12. æ»ç» |
| | | |
| | | MQTT + UDP æ··ååè®®éè¿ä»¥ä¸è®¾è®¡å®ç°é«æçé³è§é¢éä¿¡ï¼ |
| | | |
| | | - **åç¦»å¼æ¶æ**ï¼æ§å¶ä¸æ°æ®ééå离ï¼åå¸å
¶è |
| | | - **å å¯ä¿æ¤**ï¼AES-CTR ç¡®ä¿é³é¢æ°æ®å®å
¨ä¼ è¾ |
| | | - **åºåå管ç**ï¼é²æ¢éæ¾æ»å»åæ°æ®ä¹±åº |
| | | - **èªå¨æ¢å¤**ï¼æ¯æè¿æ¥æå¼åçèªå¨éè¿ |
| | | - **æ§è½ä¼å**ï¼UDP ä¼ è¾ä¿è¯é³é¢æ°æ®ç宿¶æ§ |
| | | |
| | | 该åè®®éç¨äºå¯¹å®æ¶æ§è¦æ±è¾é«çè¯é³äº¤äºåºæ¯ï¼ä½éè¦å¨ç½ç»å¤æåº¦åä¼ è¾æ§è½ä¹é´ååºæè¡¡ã |
| New file |
| | |
| | | # WebSocket Communication Protocol |
| | | |
| | | This document describes the WebSocket communication protocol between the device and the server, based on the current code. When implementing a server, please cross-check with the actual implementation. |
| | | |
| | | --- |
| | | |
| | | ## 1. Overall Flow |
| | | |
| | | 1. **Device initialization** |
| | | - The device boots and initializes `Application`: |
| | | - Initializes the audio codec, display, LEDs, etc. |
| | | - Connects to the network. |
| | | - Creates a WebSocket protocol instance (`WebsocketProtocol`) that implements the `Protocol` interface. |
| | | - Enters the main loop and waits for events (audio input, audio output, scheduled tasks, etc.). |
| | | |
| | | 2. **Opening the WebSocket connection** |
| | | - When the device needs to start a voice session (wake-up, button press, etc.), it calls `OpenAudioChannel()`: |
| | | - Reads the WebSocket URL from settings. |
| | | - Sets the request headers (`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`). |
| | | - Calls `Connect()` to establish the WebSocket connection. |
| | | |
| | | 3. **Device sends a "hello" message** |
| | | - Once connected, the device sends a JSON message. Example: |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 1, |
| | | "features": { |
| | | "mcp": true, |
| | | "aec": true |
| | | }, |
| | | "transport": "websocket", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | - `features` is optional and generated from compile-time configuration. For example, `"mcp": true` means the device supports MCP, and `"aec": true` is emitted when `CONFIG_USE_SERVER_AEC` is enabled. |
| | | - `frame_duration` matches `OPUS_FRAME_DURATION_MS` (typically 60 ms). |
| | | |
| | | 4. **Server replies with "hello"** |
| | | - The device waits for a JSON message whose `"type"` is `"hello"` and whose `"transport"` is `"websocket"`. |
| | | - The server may include a `session_id`; the device will store it. |
| | | - Example: |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "transport": "websocket", |
| | | "session_id": "xxx", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 24000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | - If `transport` matches, the device marks the audio channel as opened. |
| | | - If no valid hello arrives within the timeout (default 10 seconds), the connection is considered failed and the network error callback is fired. |
| | | |
| | | 5. **Subsequent exchanges** |
| | | - Two kinds of data are sent in either direction: |
| | | 1. **Binary audio data** (Opus encoded) |
| | | 2. **Text JSON messages** (chat state, TTS/STT events, MCP messages, etc.) |
| | | |
| | | - In the code, the receive callback splits traffic as follows: |
| | | - `OnData(...)`: |
| | | - If `binary` is `true`, the payload is treated as an Opus frame and decoded. |
| | | - If `binary` is `false`, the payload is parsed as JSON and dispatched by `type`. |
| | | |
| | | - When the server or network drops, `OnDisconnected()` fires: |
| | | - The device invokes `on_audio_channel_closed_()` and eventually returns to the idle state. |
| | | |
| | | 6. **Closing the WebSocket connection** |
| | | - When the device wants to end the session, it calls `CloseAudioChannel()` to tear down the socket and returns to idle. |
| | | - The same callback chain runs if the server closes the socket first. |
| | | |
| | | --- |
| | | |
| | | ## 2. Common Request Headers |
| | | |
| | | When establishing the WebSocket connection, the device sets the following headers: |
| | | |
| | | - `Authorization`: access token, usually formatted as `"Bearer <token>"`. |
| | | - `Protocol-Version`: the protocol version number, matching the `version` field in the hello message. |
| | | - `Device-Id`: the physical MAC address of the device. |
| | | - `Client-Id`: a software-generated UUID (reset when NVS is erased or the full firmware is re-flashed). |
| | | |
| | | These headers are sent with the WebSocket handshake; the server can use them for authentication or bookkeeping. |
| | | |
| | | --- |
| | | |
| | | ## 3. Binary Protocol Versions |
| | | |
| | | The device supports several binary protocol versions, selected by the `version` field in settings: |
| | | |
| | | ### 3.1 Version 1 (default) |
| | | Raw Opus frames with no extra metadata. The WebSocket layer already distinguishes text and binary frames. |
| | | |
| | | ### 3.2 Version 2 |
| | | Uses the `BinaryProtocol2` structure: |
| | | ```c |
| | | struct BinaryProtocol2 { |
| | | uint16_t version; // protocol version |
| | | uint16_t type; // message type (0: OPUS, 1: JSON) |
| | | uint32_t reserved; // reserved |
| | | uint32_t timestamp; // timestamp in milliseconds (useful for server-side AEC) |
| | | uint32_t payload_size; // payload size in bytes |
| | | uint8_t payload[]; // payload |
| | | } __attribute__((packed)); |
| | | ``` |
| | | |
| | | ### 3.3 Version 3 |
| | | Uses the `BinaryProtocol3` structure: |
| | | ```c |
| | | struct BinaryProtocol3 { |
| | | uint8_t type; // message type |
| | | uint8_t reserved; // reserved |
| | | uint16_t payload_size; // payload size |
| | | uint8_t payload[]; // payload |
| | | } __attribute__((packed)); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 4. JSON Message Structure |
| | | |
| | | WebSocket text frames carry JSON. The most common `"type"` values and their semantics are listed below. Fields that are not listed may be implementation-specific or optional. |
| | | |
| | | ### 4.1 Device -> Server |
| | | |
| | | 1. **Hello** |
| | | - Sent once the connection is established; announces the device parameters. |
| | | - Example: |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 1, |
| | | "features": { |
| | | "mcp": true, |
| | | "aec": true |
| | | }, |
| | | "transport": "websocket", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 2. **Listen** |
| | | - Tells the server that the device is starting or stopping microphone capture. |
| | | - Common fields: |
| | | - `"session_id"`: session identifier. |
| | | - `"type": "listen"` |
| | | - `"state"`: `"start"`, `"stop"`, or `"detect"` (wake word detected). |
| | | - `"mode"`: `"auto"`, `"manual"`, or `"realtime"`. |
| | | - Example (start listening): |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "listen", |
| | | "state": "start", |
| | | "mode": "manual" |
| | | } |
| | | ``` |
| | | |
| | | 3. **Abort** |
| | | - Aborts the current TTS playback or the voice channel. |
| | | - Example: |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "abort", |
| | | "reason": "wake_word_detected" |
| | | } |
| | | ``` |
| | | - `reason` may be `"wake_word_detected"` or other implementation-defined values. |
| | | |
| | | 4. **Wake Word Detected** |
| | | - Sent by the device when the local wake word detector fires. |
| | | - Opus audio containing the wake word may be streamed before this message to let the server run voice-print verification. |
| | | - Example: |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "listen", |
| | | "state": "detect", |
| | | "text": "Hi XiaoZhi" |
| | | } |
| | | ``` |
| | | |
| | | 5. **MCP** |
| | | - The recommended channel for IoT control. Device capability discovery and tool invocation all flow through `type: "mcp"` messages whose `payload` is JSON-RPC 2.0 (see [MCP protocol document](./mcp-protocol.md)). |
| | | - Device-to-server response example: |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "mcp", |
| | | "payload": { |
| | | "jsonrpc": "2.0", |
| | | "id": 1, |
| | | "result": { |
| | | "content": [ |
| | | { "type": "text", "text": "true" } |
| | | ], |
| | | "isError": false |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 4.2 Server -> Device |
| | | |
| | | 1. **Hello** |
| | | - The handshake acknowledgement. |
| | | - Must include `"type": "hello"` and `"transport": "websocket"`. |
| | | - May include `audio_params`, meaning the audio parameters the server expects / the canonical set agreed with the device. |
| | | - May include a `session_id` which the device records. |
| | | - Once received, the device sets the "audio channel open" event. |
| | | |
| | | 2. **STT** |
| | | - `{"session_id": "xxx", "type": "stt", "text": "..."}` |
| | | - The speech-to-text result for the user utterance. Typically shown on the display before moving to the response. |
| | | |
| | | 3. **LLM** |
| | | - `{"session_id": "xxx", "type": "llm", "emotion": "happy", "text": "ð"}` |
| | | - Tells the device to update the emotion / facial expression on the UI. |
| | | |
| | | 4. **TTS** |
| | | - `{"session_id": "xxx", "type": "tts", "state": "start"}`: the server is about to stream TTS audio. The device transitions to the speaking state. |
| | | - `{"session_id": "xxx", "type": "tts", "state": "stop"}`: the TTS segment is finished. |
| | | - `{"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}`: show the current sentence on the UI (for example, subtitle display). |
| | | |
| | | 5. **MCP** |
| | | - The server sends IoT-related commands or receives tool-call results. The `payload` structure follows JSON-RPC 2.0. |
| | | - Server-to-device `tools/call` example: |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "mcp", |
| | | "payload": { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.light.set_rgb", |
| | | "arguments": { "r": 255, "g": 0, "b": 0 } |
| | | }, |
| | | "id": 1 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 6. **System** |
| | | - System-level control, often used for remote upgrades / management. |
| | | - Example: |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "system", |
| | | "command": "reboot" |
| | | } |
| | | ``` |
| | | - Supported commands: |
| | | - `"reboot"`: reboot the device. |
| | | |
| | | 7. **Alert** |
| | | - Instructs the device to show an alert and play a vibration sound. Handled in `Application::OnIncomingJson`. |
| | | - Example: |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "alert", |
| | | "status": "Warning", |
| | | "message": "Battery low", |
| | | "emotion": "sad" |
| | | } |
| | | ``` |
| | | - Fields: |
| | | - `status`: short title displayed on screen. |
| | | - `message`: detailed message. |
| | | - `emotion`: emotion shown while alerting (e.g. `"sad"`, `"neutral"`). |
| | | |
| | | 8. **Custom** (optional) |
| | | - Available when `CONFIG_RECEIVE_CUSTOM_MESSAGE` is enabled. |
| | | - Example: |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "custom", |
| | | "payload": { |
| | | "message": "anything you want" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 9. **Binary audio frames** |
| | | - When the server pushes Opus-encoded audio as binary frames, the device decodes and plays them. |
| | | - Frames received while the device is in the `listening` state are dropped to avoid conflicts with the microphone stream. |
| | | |
| | | --- |
| | | |
| | | ## 5. Audio Codec |
| | | |
| | | 1. **Device uploads microphone audio** |
| | | - After optional AEC / NR / AGC processing, the audio is Opus-encoded and sent as binary frames. |
| | | - Depending on the protocol version, the frames may be raw Opus (v1) or wrapped in the metadata structures (v2/v3). |
| | | |
| | | 2. **Device plays server audio** |
| | | - Incoming binary frames are also treated as Opus. |
| | | - The device decodes and sends them to the audio output. |
| | | - If the sample rate differs from the device's output, it is resampled after decoding. |
| | | |
| | | --- |
| | | |
| | | ## 6. Device States |
| | | |
| | | ### 6.1 Main states |
| | | |
| | | The device state machine is defined in [`main/device_state.h`](../main/device_state.h) and includes: |
| | | |
| | | - `kDeviceStateUnknown` |
| | | - `kDeviceStateStarting` |
| | | - `kDeviceStateWifiConfiguring` |
| | | - `kDeviceStateIdle` |
| | | - `kDeviceStateConnecting` |
| | | - `kDeviceStateListening` |
| | | - `kDeviceStateSpeaking` |
| | | - `kDeviceStateUpgrading` |
| | | - `kDeviceStateActivating` |
| | | - `kDeviceStateAudioTesting` (factory / bring-up audio testing) |
| | | - `kDeviceStateFatalError` (non-recoverable error requiring user action) |
| | | |
| | | ### 6.2 Typical transitions |
| | | |
| | | 1. **Idle -> Connecting** |
| | | - Triggered by wake word or button press. The device calls `OpenAudioChannel()`, sets up the WebSocket, and sends `"type":"hello"`. |
| | | |
| | | 2. **Connecting -> Listening** |
| | | - Once connected, `SendStartListening(...)` is called and microphone streaming begins. |
| | | |
| | | 3. **Listening -> Speaking** |
| | | - Server sends `{"type":"tts","state":"start"}`; the device stops sending mic audio and plays incoming TTS. |
| | | |
| | | 4. **Speaking -> Idle** |
| | | - Server sends `{"type":"tts","state":"stop"}`. When auto-continue is enabled the device transitions back to Listening; otherwise it returns to Idle. |
| | | |
| | | 5. **Listening / Speaking -> Idle** (abort) |
| | | - `SendAbortSpeaking(...)` or `CloseAudioChannel()` interrupts the session and closes the WebSocket. |
| | | |
| | | ### 6.3 Auto-mode state diagram |
| | | |
| | | ```mermaid |
| | | stateDiagram |
| | | direction TB |
| | | [*] --> kDeviceStateUnknown |
| | | kDeviceStateUnknown --> kDeviceStateStarting: Initialize |
| | | kDeviceStateStarting --> kDeviceStateWifiConfiguring: Configure WiFi |
| | | kDeviceStateStarting --> kDeviceStateActivating: Activate device |
| | | kDeviceStateActivating --> kDeviceStateUpgrading: New firmware detected |
| | | kDeviceStateActivating --> kDeviceStateIdle: Activation complete |
| | | kDeviceStateIdle --> kDeviceStateConnecting: Start connecting |
| | | kDeviceStateConnecting --> kDeviceStateIdle: Connection failed |
| | | kDeviceStateConnecting --> kDeviceStateListening: Connection succeeded |
| | | kDeviceStateListening --> kDeviceStateSpeaking: TTS start |
| | | kDeviceStateSpeaking --> kDeviceStateListening: TTS stop |
| | | kDeviceStateListening --> kDeviceStateIdle: Manual abort |
| | | kDeviceStateSpeaking --> kDeviceStateIdle: Auto stop |
| | | kDeviceStateStarting --> kDeviceStateAudioTesting: Factory audio test |
| | | kDeviceStateStarting --> kDeviceStateFatalError: Fatal error |
| | | ``` |
| | | |
| | | ### 6.4 Manual-mode state diagram |
| | | |
| | | ```mermaid |
| | | stateDiagram |
| | | direction TB |
| | | [*] --> kDeviceStateUnknown |
| | | kDeviceStateUnknown --> kDeviceStateStarting: Initialize |
| | | kDeviceStateStarting --> kDeviceStateWifiConfiguring: Configure WiFi |
| | | kDeviceStateStarting --> kDeviceStateActivating: Activate device |
| | | kDeviceStateActivating --> kDeviceStateUpgrading: New firmware detected |
| | | kDeviceStateActivating --> kDeviceStateIdle: Activation complete |
| | | kDeviceStateIdle --> kDeviceStateConnecting: Start connecting |
| | | kDeviceStateConnecting --> kDeviceStateIdle: Connection failed |
| | | kDeviceStateConnecting --> kDeviceStateListening: Connection succeeded |
| | | kDeviceStateIdle --> kDeviceStateListening: Start listening |
| | | kDeviceStateListening --> kDeviceStateIdle: Stop listening |
| | | kDeviceStateIdle --> kDeviceStateSpeaking: Start speaking |
| | | kDeviceStateSpeaking --> kDeviceStateIdle: Stop speaking |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 7. Error Handling |
| | | |
| | | 1. **Connection failure** |
| | | - If `Connect(url)` fails or the server hello is not received before the timeout, `on_network_error_()` is invoked and the device shows a "cannot connect" alert. |
| | | |
| | | 2. **Server disconnect** |
| | | - If the WebSocket drops unexpectedly, `OnDisconnected()` is called: |
| | | - `on_audio_channel_closed_()` runs. |
| | | - The device returns to Idle (or retries, depending on policy). |
| | | |
| | | --- |
| | | |
| | | ## 8. Other Notes |
| | | |
| | | 1. **Authentication** |
| | | - The device supplies `Authorization: Bearer <token>`; the server must validate it. |
| | | - If the token is missing or invalid the server may reject the handshake or terminate the session later. |
| | | |
| | | 2. **Session scope** |
| | | - Many messages carry a `session_id`, useful when the server serves multiple concurrent interactions. |
| | | |
| | | 3. **Audio payload** |
| | | - Default audio format is Opus at 16 kHz, mono. The frame duration is controlled by `OPUS_FRAME_DURATION_MS` (typically 60 ms). The server may use 24 kHz on the downlink for better music playback. |
| | | |
| | | 4. **Binary protocol version selection** |
| | | - Configured through the `version` setting: |
| | | - v1: raw Opus |
| | | - v2: metadata + timestamp (useful for server-side AEC) |
| | | - v3: lightweight header |
| | | - The value is echoed back in the `Protocol-Version` header and the hello message. |
| | | |
| | | 5. **IoT control via MCP** |
| | | - All IoT capability discovery and control flows through MCP (`type: "mcp"`). The legacy `type: "iot"` protocol is deprecated. |
| | | - MCP works over both WebSocket and MQTT, giving better standardization and extensibility. |
| | | - See [MCP protocol document](./mcp-protocol.md) and [MCP IoT control usage](./mcp-usage.md) for details. |
| | | |
| | | 6. **Malformed JSON** |
| | | - When a required field such as `type` is missing, the device logs `ESP_LOGE(TAG, "Missing message type, data: %s", data);` and ignores the message. |
| | | |
| | | --- |
| | | |
| | | ## 9. Example Message Flow |
| | | |
| | | A simplified two-way exchange: |
| | | |
| | | 1. **Device -> Server** (handshake) |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 1, |
| | | "features": { |
| | | "mcp": true, |
| | | "aec": true |
| | | }, |
| | | "transport": "websocket", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 2. **Server -> Device** (handshake ack) |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "transport": "websocket", |
| | | "session_id": "xxx", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 3. **Device -> Server** (start listening) |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "listen", |
| | | "state": "start", |
| | | "mode": "auto" |
| | | } |
| | | ``` |
| | | The device begins streaming binary Opus frames. |
| | | |
| | | 4. **Server -> Device** (ASR result) |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "stt", |
| | | "text": "what the user said" |
| | | } |
| | | ``` |
| | | |
| | | 5. **Server -> Device** (TTS start) |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "tts", |
| | | "state": "start" |
| | | } |
| | | ``` |
| | | The server follows up with binary Opus frames for the device to play. |
| | | |
| | | 6. **Server -> Device** (TTS stop) |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "tts", |
| | | "state": "stop" |
| | | } |
| | | ``` |
| | | The device stops playback and, if no further instructions arrive, returns to idle. |
| | | |
| | | --- |
| | | |
| | | ## 10. Summary |
| | | |
| | | This protocol carries JSON text and binary Opus frames over a WebSocket connection to implement audio streaming, TTS playback, speech recognition, device state management, MCP dispatch, and more. Key traits: |
| | | |
| | | - **Handshake**: send `"type":"hello"` and wait for the server reply. |
| | | - **Audio channel**: bidirectional Opus streaming, with three binary framing variants. |
| | | - **JSON messages**: dispatched by `"type"` (TTS, STT, MCP, WakeWord, System, Alert, Custom, ...). |
| | | - **Extensibility**: extra fields in JSON, additional headers for authentication. |
| | | |
| | | Server and device must agree on the meaning, timing, and error handling of each message type so the session runs smoothly. The text above provides the baseline for integration, debugging, and extension. |
| New file |
| | |
| | | 以䏿¯ä¸ä»½åºäºä»£ç å®ç°æ´çç WebSocket éä¿¡åè®®ææ¡£ï¼æ¦è¿°è®¾å¤ç«¯ä¸æå¡å¨ä¹é´å¦ä½éè¿ WebSocket è¿è¡äº¤äºã |
| | | |
| | | è¯¥ææ¡£ä»
åºäºææä¾çä»£ç æ¨æï¼å®é
é¨ç½²æ¶å¯è½éè¦ç»åæå¡å¨ç«¯å®ç°è¿è¡è¿ä¸æ¥ç¡®è®¤æè¡¥å
ã |
| | | |
| | | --- |
| | | |
| | | ## 1. æ»ä½æµç¨æ¦è§ |
| | | |
| | | 1. **设å¤ç«¯åå§å** |
| | | - 设å¤ä¸çµãåå§å `Application`ï¼ |
| | | - åå§åé³é¢ç¼è§£ç å¨ãæ¾ç¤ºå±ãLED ç |
| | | - è¿æ¥ç½ç» |
| | | - å建并åå§åå®ç° `Protocol` æ¥å£ç WebSocket åè®®å®ä¾ï¼`WebsocketProtocol`ï¼ |
| | | - è¿å
¥ä¸»å¾ªç¯çå¾
äºä»¶ï¼é³é¢è¾å
¥ãé³é¢è¾åºãè°åº¦ä»»å¡çï¼ã |
| | | |
| | | 2. **å»ºç« WebSocket è¿æ¥** |
| | | - å½è®¾å¤éè¦å¼å§è¯é³ä¼è¯æ¶ï¼ä¾å¦ç¨æ·å¤éãæå¨æé®è§¦åçï¼ï¼è°ç¨ `OpenAudioChannel()`ï¼ |
| | | - æ ¹æ®é
ç½®è·å WebSocket URL |
| | | - 设置è¥å¹²è¯·æ±å¤´ï¼`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`ï¼ |
| | | - è°ç¨ `Connect()` 䏿å¡å¨å»ºç« WebSocket è¿æ¥ |
| | | |
| | | 3. **设å¤ç«¯åé "hello" æ¶æ¯** |
| | | - è¿æ¥æååï¼è®¾å¤ä¼åé䏿¡ JSON æ¶æ¯ï¼ç¤ºä¾ç»æå¦ä¸ï¼ |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 1, |
| | | "features": { |
| | | "mcp": true |
| | | }, |
| | | "transport": "websocket", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | - å
¶ä¸ `features` åæ®µä¸ºå¯éï¼å
å®¹æ ¹æ®è®¾å¤ç¼è¯é
ç½®èªå¨çæãä¾å¦ï¼`"mcp": true` è¡¨ç¤ºæ¯æ MCP åè®®ã |
| | | - `frame_duration` çå¼å¯¹åº `OPUS_FRAME_DURATION_MS`ï¼ä¾å¦ 60msï¼ã |
| | | |
| | | 4. **æå¡å¨åå¤ "hello"** |
| | | - 设å¤çå¾
æå¡å¨è¿å䏿¡å
å« `"type": "hello"` ç JSON æ¶æ¯ï¼å¹¶æ£æ¥ `"transport": "websocket"` æ¯å¦å¹é
ã |
| | | - æå¡å¨å¯éä¸å `session_id` åæ®µï¼è®¾å¤ç«¯æ¶å°åä¼èªå¨è®°å½ã |
| | | - 示ä¾ï¼ |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "transport": "websocket", |
| | | "session_id": "xxx", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 24000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | - 妿å¹é
ï¼å认为æå¡å¨å·²å°±ç»ªï¼æ è®°é³é¢ééæå¼æåã |
| | | - 妿å¨è¶
æ¶æ¶é´ï¼é»è®¤ 10 ç§ï¼å
æªæ¶å°æ£ç¡®åå¤ï¼è®¤ä¸ºè¿æ¥å¤±è´¥å¹¶è§¦åç½ç»é误åè°ã |
| | | |
| | | 5. **åç»æ¶æ¯äº¤äº** |
| | | - 设å¤ç«¯åæå¡å¨ç«¯ä¹é´å¯åé两ç§ä¸»è¦ç±»åçæ°æ®ï¼ |
| | | 1. **äºè¿å¶é³é¢æ°æ®**ï¼Opus ç¼ç ï¼ |
| | | 2. **ææ¬ JSON æ¶æ¯**ï¼ç¨äºä¼ è¾èå¤©ç¶æãTTS/STT äºä»¶ãMCP åè®®æ¶æ¯çï¼ |
| | | |
| | | - å¨ä»£ç éï¼æ¥æ¶åè°ä¸»è¦åä¸ºï¼ |
| | | - `OnData(...)`: |
| | | - å½ `binary` 为 `true` æ¶ï¼è®¤ä¸ºæ¯é³é¢å¸§ï¼è®¾å¤ä¼å°å
¶å½ä½ Opus æ°æ®è¿è¡è§£ç ã |
| | | - å½ `binary` 为 `false` æ¶ï¼è®¤ä¸ºæ¯ JSON ææ¬ï¼éè¦å¨è®¾å¤ç«¯ç¨ cJSON è¿è¡è§£æå¹¶åç¸åºä¸å¡é»è¾å¤çï¼å¦è天ãTTSãMCP åè®®æ¶æ¯çï¼ã |
| | | |
| | | - 彿å¡å¨æç½ç»åºç°æè¿ï¼åè° `OnDisconnected()` 被触åï¼ |
| | | - 设å¤ä¼è°ç¨ `on_audio_channel_closed_()`ï¼å¹¶æç»åå°ç©ºé²ç¶æã |
| | | |
| | | 6. **å
³é WebSocket è¿æ¥** |
| | | - 设å¤å¨éè¦ç»æè¯é³ä¼è¯æ¶ï¼ä¼è°ç¨ `CloseAudioChannel()` 䏻卿å¼è¿æ¥ï¼å¹¶åå°ç©ºé²ç¶æã |
| | | - æè
妿æå¡å¨ç«¯ä¸»å¨æå¼ï¼ä¹ä¼å¼ååæ ·çåè°æµç¨ã |
| | | |
| | | --- |
| | | |
| | | ## 2. éç¨è¯·æ±å¤´ |
| | | |
| | | å¨å»ºç« WebSocket è¿æ¥æ¶ï¼ä»£ç 示ä¾ä¸è®¾ç½®äºä»¥ä¸è¯·æ±å¤´ï¼ |
| | | |
| | | - `Authorization`: ç¨äºåæ¾è®¿é®ä»¤çï¼å½¢å¦ `"Bearer <token>"` |
| | | - `Protocol-Version`: åè®®çæ¬å·ï¼ä¸ hello æ¶æ¯ä½å
ç `version` åæ®µä¿æä¸è´ |
| | | - `Device-Id`: 设å¤ç©çç½å¡ MAC å°å |
| | | - `Client-Id`: 软件çæç UUIDï¼æ¦é¤ NVS æéæ°ç§å½å®æ´åºä»¶ä¼éç½®ï¼ |
| | | |
| | | è¿äºå¤´ä¼éç WebSocket æ¡æä¸èµ·åéå°æå¡å¨ï¼æå¡å¨å¯æ ¹æ®éæ±è¿è¡æ ¡éªã认è¯çã |
| | | |
| | | --- |
| | | |
| | | ## 3. äºè¿å¶åè®®çæ¬ |
| | | |
| | | è®¾å¤æ¯æå¤ç§äºè¿å¶åè®®çæ¬ï¼éè¿é
ç½®ä¸ç `version` åæ®µæå®ï¼ |
| | | |
| | | ### 3.1 çæ¬1ï¼é»è®¤ï¼ |
| | | ç´æ¥åé Opus é³é¢æ°æ®ï¼æ é¢å¤å
æ°æ®ãWebsocket åè®®ä¼åºå text ä¸ binaryã |
| | | |
| | | ### 3.2 çæ¬2 |
| | | ä½¿ç¨ `BinaryProtocol2` ç»æï¼ |
| | | ```c |
| | | struct BinaryProtocol2 { |
| | | uint16_t version; // åè®®çæ¬ |
| | | uint16_t type; // æ¶æ¯ç±»å (0: OPUS, 1: JSON) |
| | | uint32_t reserved; // ä¿çåæ®µ |
| | | uint32_t timestamp; // æ¶é´æ³ï¼æ¯«ç§ï¼ç¨äºæå¡å¨ç«¯AECï¼ |
| | | uint32_t payload_size; // è´è½½å¤§å°ï¼åèï¼ |
| | | uint8_t payload[]; // è´è½½æ°æ® |
| | | } __attribute__((packed)); |
| | | ``` |
| | | |
| | | ### 3.3 çæ¬3 |
| | | ä½¿ç¨ `BinaryProtocol3` ç»æï¼ |
| | | ```c |
| | | struct BinaryProtocol3 { |
| | | uint8_t type; // æ¶æ¯ç±»å |
| | | uint8_t reserved; // ä¿çåæ®µ |
| | | uint16_t payload_size; // è´è½½å¤§å° |
| | | uint8_t payload[]; // è´è½½æ°æ® |
| | | } __attribute__((packed)); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 4. JSON æ¶æ¯ç»æ |
| | | |
| | | WebSocket ææ¬å¸§ä»¥ JSON æ¹å¼ä¼ è¾ï¼ä»¥ä¸ä¸ºå¸¸è§ç `"type"` åæ®µåå
¶å¯¹åºä¸å¡é»è¾ãè¥æ¶æ¯éå
嫿ªååºçåæ®µï¼å¯è½ä¸ºå¯éæç¹å®å®ç°ç»èã |
| | | |
| | | ### 4.1 设å¤ç«¯âæå¡å¨ |
| | | |
| | | 1. **Hello** |
| | | - è¿æ¥æååï¼ç±è®¾å¤ç«¯åéï¼åç¥æå¡å¨åºæ¬åæ°ã |
| | | - ä¾ï¼ |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 1, |
| | | "features": { |
| | | "mcp": true |
| | | }, |
| | | "transport": "websocket", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 2. **Listen** |
| | | - 表示设å¤ç«¯å¼å§æåæ¢å½é³çå¬ã |
| | | - 常è§åæ®µï¼ |
| | | - `"session_id"`ï¼ä¼è¯æ è¯ |
| | | - `"type": "listen"` |
| | | - `"state"`ï¼`"start"`, `"stop"`, `"detect"`ï¼å¤éæ£æµå·²è§¦åï¼ |
| | | - `"mode"`ï¼`"auto"`, `"manual"` æ `"realtime"`ï¼è¡¨ç¤ºè¯å«æ¨¡å¼ã |
| | | - ä¾ï¼å¼å§çå¬ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "listen", |
| | | "state": "start", |
| | | "mode": "manual" |
| | | } |
| | | ``` |
| | | |
| | | 3. **Abort** |
| | | - ç»æ¢å½å说è¯ï¼TTS ææ¾ï¼æè¯é³ééã |
| | | - ä¾ï¼ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "abort", |
| | | "reason": "wake_word_detected" |
| | | } |
| | | ``` |
| | | - `reason` å¼å¯ä¸º `"wake_word_detected"` æå
¶ä»ã |
| | | |
| | | 4. **Wake Word Detected** |
| | | - ç¨äºè®¾å¤ç«¯åæå¡å¨åç¥æ£æµå°å¤éè¯ã |
| | | - å¨åéè¯¥æ¶æ¯ä¹åï¼å¯æååéå¤éè¯ç Opus é³é¢æ°æ®ï¼ç¨äºæå¡å¨è¿è¡å£°çº¹æ£æµã |
| | | - ä¾ï¼ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "listen", |
| | | "state": "detect", |
| | | "text": "ä½ å¥½å°æ" |
| | | } |
| | | ``` |
| | | |
| | | 5. **MCP** |
| | | - æ¨èç¨äºç©èç½æ§å¶çæ°ä¸ä»£åè®®ãææè®¾å¤è½ååç°ãå·¥å
·è°ç¨çåéè¿ type: "mcp" çæ¶æ¯è¿è¡ï¼payload å
é¨ä¸ºæ å JSON-RPC 2.0ï¼è¯¦è§ [MCP åè®®ææ¡£](./mcp-protocol_zh.md)ï¼ã |
| | | |
| | | - **设å¤ç«¯å°æå¡å¨åé result çä¾åï¼** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "mcp", |
| | | "payload": { |
| | | "jsonrpc": "2.0", |
| | | "id": 1, |
| | | "result": { |
| | | "content": [ |
| | | { "type": "text", "text": "true" } |
| | | ], |
| | | "isError": false |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 4.2 æå¡å¨â设å¤ç«¯ |
| | | |
| | | 1. **Hello** |
| | | - æå¡å¨ç«¯è¿åçæ¡æç¡®è®¤æ¶æ¯ã |
| | | - å¿
é¡»å
å« `"type": "hello"` å `"transport": "websocket"`ã |
| | | - å¯è½ä¼å¸¦æ `audio_params`ï¼è¡¨ç¤ºæå¡å¨ææçé³é¢åæ°ï¼æä¸è®¾å¤ç«¯å¯¹é½çé
ç½®ã |
| | | - æå¡å¨å¯éä¸å `session_id` åæ®µï¼è®¾å¤ç«¯æ¶å°åä¼èªå¨è®°å½ã |
| | | - æåæ¥æ¶å设å¤ç«¯ä¼è®¾ç½®äºä»¶æ å¿ï¼è¡¨ç¤º WebSocket éé就绪ã |
| | | |
| | | 2. **STT** |
| | | - `{"session_id": "xxx", "type": "stt", "text": "..."}` |
| | | - 表示æå¡å¨ç«¯è¯å«å°äºç¨æ·è¯é³ãï¼ä¾å¦è¯é³è½¬ææ¬ç»æï¼ |
| | | - 设å¤å¯è½å°æ¤ææ¬æ¾ç¤ºå°å±å¹ä¸ï¼åç»åè¿å
¥åççæµç¨ã |
| | | |
| | | 3. **LLM** |
| | | - `{"session_id": "xxx", "type": "llm", "emotion": "happy", "text": "ð"}` |
| | | - æå¡å¨æç¤ºè®¾å¤è°æ´è¡¨æ
å¨ç» / UI 表达ã |
| | | |
| | | 4. **TTS** |
| | | - `{"session_id": "xxx", "type": "tts", "state": "start"}`ï¼æå¡å¨åå¤ä¸å TTS é³é¢ï¼è®¾å¤ç«¯è¿å
¥ "speaking" ææ¾ç¶æã |
| | | - `{"session_id": "xxx", "type": "tts", "state": "stop"}`ï¼è¡¨ç¤ºæ¬æ¬¡ TTS ç»æã |
| | | - `{"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}` |
| | | - 让设å¤å¨çé¢ä¸æ¾ç¤ºå½åè¦ææ¾ææè¯»çææ¬çæ®µï¼ä¾å¦ç¨äºæ¾ç¤ºç»ç¨æ·ï¼ã |
| | | |
| | | 5. **MCP** |
| | | - æå¡å¨éè¿ type: "mcp" çæ¶æ¯ä¸åç©èç½ç¸å
³çæ§å¶æä»¤æè¿åè°ç¨ç»æï¼payload ç»æåä¸ã |
| | | |
| | | - **æå¡å¨å°è®¾å¤ç«¯åé tools/call çä¾åï¼** |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "mcp", |
| | | "payload": { |
| | | "jsonrpc": "2.0", |
| | | "method": "tools/call", |
| | | "params": { |
| | | "name": "self.light.set_rgb", |
| | | "arguments": { "r": 255, "g": 0, "b": 0 } |
| | | }, |
| | | "id": 1 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 6. **System** |
| | | - ç³»ç»æ§å¶å½ä»¤ï¼å¸¸ç¨äºè¿ç¨åçº§æ´æ°ã |
| | | - ä¾ï¼ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "system", |
| | | "command": "reboot" |
| | | } |
| | | ``` |
| | | - æ¯æçå½ä»¤ï¼ |
| | | - `"reboot"`ï¼éå¯è®¾å¤ |
| | | |
| | | 7. **Custom**ï¼å¯éï¼ |
| | | - èªå®ä¹æ¶æ¯ï¼å½ `CONFIG_RECEIVE_CUSTOM_MESSAGE` å¯ç¨æ¶æ¯æã |
| | | - ä¾ï¼ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "custom", |
| | | "payload": { |
| | | "message": "èªå®ä¹å
容" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 8. **é³é¢æ°æ®ï¼äºè¿å¶å¸§** |
| | | - 彿å¡å¨åéé³é¢äºè¿å¶å¸§ï¼Opus ç¼ç ï¼æ¶ï¼è®¾å¤ç«¯è§£ç å¹¶ææ¾ã |
| | | - è¥è®¾å¤ç«¯æ£å¨å¤äº "listening" ï¼å½é³ï¼ç¶æï¼æ¶å°çé³é¢å¸§ä¼è¢«å¿½ç¥ææ¸
空以é²å²çªã |
| | | |
| | | --- |
| | | |
| | | ## 5. é³é¢ç¼è§£ç |
| | | |
| | | 1. **设å¤ç«¯åéå½é³æ°æ®** |
| | | - é³é¢è¾å
¥ç»è¿å¯è½çå声æ¶é¤ãéåªæé³éå¢çåï¼éè¿ Opus ç¼ç æå
为äºè¿å¶å¸§åéç»æå¡å¨ã |
| | | - æ ¹æ®åè®®çæ¬ï¼å¯è½ç´æ¥åé Opus æ°æ®ï¼çæ¬1ï¼æä½¿ç¨å¸¦å
æ°æ®çäºè¿å¶åè®®ï¼çæ¬2/3ï¼ã |
| | | |
| | | 2. **设å¤ç«¯ææ¾æ¶å°çé³é¢** |
| | | - æ¶å°æå¡å¨çäºè¿å¶å¸§æ¶ï¼åæ ·è®¤å®æ¯ Opus æ°æ®ã |
| | | - 设å¤ç«¯ä¼è¿è¡è§£ç ï¼ç¶å交ç±é³é¢è¾åºæ¥å£ææ¾ã |
| | | - 妿æå¡å¨çé³é¢éæ ·çä¸è®¾å¤ä¸ä¸è´ï¼ä¼å¨è§£ç ååè¿è¡ééæ ·ã |
| | | |
| | | --- |
| | | |
| | | ## 6. 常è§ç¶ææµè½¬ |
| | | |
| | | 以ä¸ä¸ºå¸¸è§è®¾å¤ç«¯å
³é®ç¶ææµè½¬ï¼ä¸ WebSocket æ¶æ¯å¯¹åºï¼ |
| | | |
| | | 1. **Idle** â **Connecting** |
| | | - ç¨æ·è§¦åæå¤éåï¼è®¾å¤è°ç¨ `OpenAudioChannel()` â å»ºç« WebSocket è¿æ¥ â åé `"type":"hello"`ã |
| | | |
| | | 2. **Connecting** â **Listening** |
| | | - æå建ç«è¿æ¥åï¼è¥ç»§ç»æ§è¡ `SendStartListening(...)`ï¼åè¿å
¥å½é³ç¶æãæ¤æ¶è®¾å¤ä¼æç»ç¼ç 麦å
飿°æ®å¹¶åéå°æå¡å¨ã |
| | | |
| | | 3. **Listening** â **Speaking** |
| | | - æ¶å°æå¡å¨ TTS Start æ¶æ¯ (`{"type":"tts","state":"start"}`) â 忢å½é³å¹¶ææ¾æ¥æ¶å°çé³é¢ã |
| | | |
| | | 4. **Speaking** â **Idle** |
| | | - æå¡å¨ TTS Stop (`{"type":"tts","state":"stop"}`) â é³é¢ææ¾ç»æãè¥æªç»§ç»è¿å
¥èªå¨çå¬ï¼åè¿å Idleï¼å¦æé
ç½®äºèªå¨å¾ªç¯ï¼åå度è¿å
¥ Listeningã |
| | | |
| | | 5. **Listening** / **Speaking** â **Idle**ï¼éå°å¼å¸¸æä¸»å¨ä¸æï¼ |
| | | - è°ç¨ `SendAbortSpeaking(...)` æ `CloseAudioChannel()` â 䏿ä¼è¯ â å
³é WebSocket â ç¶æåå° Idleã |
| | | |
| | | ### èªå¨æ¨¡å¼ç¶ææµè½¬å¾ |
| | | |
| | | ```mermaid |
| | | stateDiagram |
| | | direction TB |
| | | [*] --> kDeviceStateUnknown |
| | | kDeviceStateUnknown --> kDeviceStateStarting:åå§å |
| | | kDeviceStateStarting --> kDeviceStateWifiConfiguring:é
ç½®WiFi |
| | | kDeviceStateStarting --> kDeviceStateActivating:æ¿æ´»è®¾å¤ |
| | | kDeviceStateActivating --> kDeviceStateUpgrading:æ£æµå°æ°çæ¬ |
| | | kDeviceStateActivating --> kDeviceStateIdle:æ¿æ´»å®æ |
| | | kDeviceStateIdle --> kDeviceStateConnecting:å¼å§è¿æ¥ |
| | | kDeviceStateConnecting --> kDeviceStateIdle:è¿æ¥å¤±è´¥ |
| | | kDeviceStateConnecting --> kDeviceStateListening:è¿æ¥æå |
| | | kDeviceStateListening --> kDeviceStateSpeaking:å¼å§è¯´è¯ |
| | | kDeviceStateSpeaking --> kDeviceStateListening:ç»æè¯´è¯ |
| | | kDeviceStateListening --> kDeviceStateIdle:æå¨ç»æ¢ |
| | | kDeviceStateSpeaking --> kDeviceStateIdle:èªå¨ç»æ¢ |
| | | ``` |
| | | |
| | | ### æå¨æ¨¡å¼ç¶ææµè½¬å¾ |
| | | |
| | | ```mermaid |
| | | stateDiagram |
| | | direction TB |
| | | [*] --> kDeviceStateUnknown |
| | | kDeviceStateUnknown --> kDeviceStateStarting:åå§å |
| | | kDeviceStateStarting --> kDeviceStateWifiConfiguring:é
ç½®WiFi |
| | | kDeviceStateStarting --> kDeviceStateActivating:æ¿æ´»è®¾å¤ |
| | | kDeviceStateActivating --> kDeviceStateUpgrading:æ£æµå°æ°çæ¬ |
| | | kDeviceStateActivating --> kDeviceStateIdle:æ¿æ´»å®æ |
| | | kDeviceStateIdle --> kDeviceStateConnecting:å¼å§è¿æ¥ |
| | | kDeviceStateConnecting --> kDeviceStateIdle:è¿æ¥å¤±è´¥ |
| | | kDeviceStateConnecting --> kDeviceStateListening:è¿æ¥æå |
| | | kDeviceStateIdle --> kDeviceStateListening:å¼å§çå¬ |
| | | kDeviceStateListening --> kDeviceStateIdle:忢çå¬ |
| | | kDeviceStateIdle --> kDeviceStateSpeaking:å¼å§è¯´è¯ |
| | | kDeviceStateSpeaking --> kDeviceStateIdle:ç»æè¯´è¯ |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 7. é误å¤ç |
| | | |
| | | 1. **è¿æ¥å¤±è´¥** |
| | | - 妿 `Connect(url)` è¿å失败æå¨çå¾
æå¡å¨ "hello" æ¶æ¯æ¶è¶
æ¶ï¼è§¦å `on_network_error_()` åè°ã设å¤ä¼æç¤º"æ æ³è¿æ¥å°æå¡"æç±»ä¼¼é误信æ¯ã |
| | | |
| | | 2. **æå¡å¨æå¼** |
| | | - 妿 WebSocket å¼å¸¸æå¼ï¼åè° `OnDisconnected()`ï¼ |
| | | - 设å¤åè° `on_audio_channel_closed_()` |
| | | - åæ¢å° Idle æå
¶ä»éè¯é»è¾ã |
| | | |
| | | --- |
| | | |
| | | ## 8. å
¶å®æ³¨æäºé¡¹ |
| | | |
| | | 1. **é´æ** |
| | | - 设å¤éè¿è®¾ç½® `Authorization: Bearer <token>` æä¾é´æï¼æå¡å¨ç«¯ééªè¯æ¯å¦ææã |
| | | - 妿令çè¿æææ æï¼æå¡å¨å¯æç»æ¡ææå¨åç»æå¼ã |
| | | |
| | | 2. **ä¼è¯æ§å¶** |
| | | - 代ç ä¸é¨åæ¶æ¯å
å« `session_id`ï¼ç¨äºåºåç¬ç«çå¯¹è¯ææä½ãæå¡ç«¯å¯æ ¹æ®éè¦å¯¹ä¸åä¼è¯åå离å¤çã |
| | | |
| | | 3. **é³é¢è´è½½** |
| | | - 代ç éé»è®¤ä½¿ç¨ Opus æ ¼å¼ï¼å¹¶è®¾ç½® `sample_rate = 16000`ï¼å声éã帧æ¶é¿ç± `OPUS_FRAME_DURATION_MS` æ§å¶ï¼ä¸è¬ä¸º 60msã坿 ¹æ®å¸¦å®½ææ§è½åéå½è°æ´ã为äºè·å¾æ´å¥½çé³ä¹ææ¾ææï¼æå¡å¨ä¸è¡é³é¢å¯è½ä½¿ç¨ 24000 éæ ·çã |
| | | |
| | | 4. **åè®®çæ¬é
ç½®** |
| | | - éè¿è®¾ç½®ä¸ç `version` åæ®µé
ç½®äºè¿å¶åè®®çæ¬ï¼1ã2 æ 3ï¼ |
| | | - çæ¬1ï¼ç´æ¥åé Opus æ°æ® |
| | | - çæ¬2ï¼ä½¿ç¨å¸¦æ¶é´æ³çäºè¿å¶åè®®ï¼éç¨äºæå¡å¨ç«¯ AEC |
| | | - çæ¬3ï¼ä½¿ç¨ç®åçäºè¿å¶åè®® |
| | | |
| | | 5. **ç©èç½æ§å¶æ¨è MCP åè®®** |
| | | - 设å¤ä¸æå¡å¨ä¹é´çç©èç½è½ååç°ãç¶æåæ¥ãæ§å¶æä»¤çï¼å»ºè®®å
¨é¨éè¿ MCP åè®®ï¼type: "mcp"ï¼å®ç°ãåæç type: "iot" æ¹æ¡å·²åºå¼ã |
| | | - MCP åè®®å¯å¨ WebSocketãMQTT çå¤ç§åºå±åè®®ä¸ä¼ è¾ï¼å
·å¤æ´å¥½çæ©å±æ§åæ ååè½åã |
| | | - 详ç»ç¨æ³è¯·åè [MCP åè®®ææ¡£](./mcp-protocol_zh.md) å [MCP ç©èç½æ§å¶ç¨æ³](./mcp-usage_zh.md)ã |
| | | |
| | | 6. **é误æå¼å¸¸ JSON** |
| | | - å½ JSON ä¸ç¼ºå°å¿
è¦å段ï¼ä¾å¦ `{"type": ...}`ï¼è®¾å¤ç«¯ä¼è®°å½é误æ¥å¿ï¼`ESP_LOGE(TAG, "Missing message type, data: %s", data);`ï¼ï¼ä¸ä¼æ§è¡ä»»ä½ä¸å¡ã |
| | | |
| | | --- |
| | | |
| | | ## 9. æ¶æ¯ç¤ºä¾ |
| | | |
| | | ä¸é¢ç»åºä¸ä¸ªå
¸åçååæ¶æ¯ç¤ºä¾ï¼æµç¨ç®å示æï¼ï¼ |
| | | |
| | | 1. **设å¤ç«¯ â æå¡å¨**ï¼æ¡æï¼ |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "version": 1, |
| | | "features": { |
| | | "mcp": true |
| | | }, |
| | | "transport": "websocket", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000, |
| | | "channels": 1, |
| | | "frame_duration": 60 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 2. **æå¡å¨ â 设å¤ç«¯**ï¼æ¡æåºçï¼ |
| | | ```json |
| | | { |
| | | "type": "hello", |
| | | "transport": "websocket", |
| | | "session_id": "xxx", |
| | | "audio_params": { |
| | | "format": "opus", |
| | | "sample_rate": 16000 |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | 3. **设å¤ç«¯ â æå¡å¨**ï¼å¼å§çå¬ï¼ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "listen", |
| | | "state": "start", |
| | | "mode": "auto" |
| | | } |
| | | ``` |
| | | åæ¶è®¾å¤ç«¯å¼å§åéäºè¿å¶å¸§ï¼Opus æ°æ®ï¼ã |
| | | |
| | | 4. **æå¡å¨ â 设å¤ç«¯**ï¼ASR ç»æï¼ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "stt", |
| | | "text": "ç¨æ·è¯´çè¯" |
| | | } |
| | | ``` |
| | | |
| | | 5. **æå¡å¨ â 设å¤ç«¯**ï¼TTSå¼å§ï¼ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "tts", |
| | | "state": "start" |
| | | } |
| | | ``` |
| | | æ¥çæå¡å¨åéäºè¿å¶é³é¢å¸§ç»è®¾å¤ç«¯ææ¾ã |
| | | |
| | | 6. **æå¡å¨ â 设å¤ç«¯**ï¼TTSç»æï¼ |
| | | ```json |
| | | { |
| | | "session_id": "xxx", |
| | | "type": "tts", |
| | | "state": "stop" |
| | | } |
| | | ``` |
| | | 设å¤ç«¯åæ¢ææ¾é³é¢ï¼è¥æ æ´å¤æä»¤ï¼ååå°ç©ºé²ç¶æã |
| | | |
| | | --- |
| | | |
| | | ## 10. æ»ç» |
| | | |
| | | æ¬åè®®éè¿å¨ WebSocket ä¸å±ä¼ è¾ JSON ææ¬ä¸äºè¿å¶é³é¢å¸§ï¼å®æåè½å
æ¬é³é¢æµä¸ä¼ ãTTS é³é¢ææ¾ãè¯é³è¯å«ä¸ç¶æç®¡çãMCP æä»¤ä¸åçãå
¶æ ¸å¿ç¹å¾ï¼ |
| | | |
| | | - **æ¡æé¶æ®µ**ï¼åé `"type":"hello"`ï¼çå¾
æå¡å¨è¿åã |
| | | - **é³é¢éé**ï¼éç¨ Opus ç¼ç çäºè¿å¶å¸§ååä¼ è¾è¯é³æµï¼æ¯æå¤ç§åè®®çæ¬ã |
| | | - **JSON æ¶æ¯**ï¼ä½¿ç¨ `"type"` ä¸ºæ ¸å¿å段æ è¯ä¸åä¸å¡é»è¾ï¼å
æ¬ TTSãSTTãMCPãWakeWordãSystemãCustom çã |
| | | - **æ©å±æ§**ï¼å¯æ ¹æ®å®é
éæ±å¨ JSON æ¶æ¯ä¸æ·»å åæ®µï¼æå¨ headers éè¿è¡é¢å¤é´æã |
| | | |
| | | æå¡å¨ä¸è®¾å¤ç«¯éæå约å®åç±»æ¶æ¯çåæ®µå«ä¹ãæ¶åºé»è¾ä»¥åé误å¤çè§åï¼æ¹è½ä¿è¯é信顺ç
ãä¸è¿°ä¿¡æ¯å¯ä½ä¸ºåºç¡ææ¡£ï¼ä¾¿äºåç»å¯¹æ¥ãå¼åææ©å±ã |
| New file |
| | |
| | | # Define source files |
| | | set(SOURCES "audio/audio_codec.cc" |
| | | "audio/audio_service.cc" |
| | | "audio/demuxer/ogg_demuxer.cc" |
| | | "audio/codecs/no_audio_codec.cc" |
| | | "audio/codecs/box_audio_codec.cc" |
| | | "audio/codecs/es8311_audio_codec.cc" |
| | | "audio/codecs/es8374_audio_codec.cc" |
| | | "audio/codecs/es8388_audio_codec.cc" |
| | | "audio/codecs/es8389_audio_codec.cc" |
| | | "audio/codecs/dummy_audio_codec.cc" |
| | | "audio/processors/audio_debugger.cc" |
| | | "led/single_led.cc" |
| | | "led/circular_strip.cc" |
| | | "led/gpio_led.cc" |
| | | "display/display.cc" |
| | | "display/lcd_display.cc" |
| | | "display/oled_display.cc" |
| | | "display/lvgl_display/lvgl_display.cc" |
| | | "display/emote_display.cc" |
| | | "display/lvgl_display/emoji_collection.cc" |
| | | "display/lvgl_display/lvgl_theme.cc" |
| | | "display/lvgl_display/lvgl_font.cc" |
| | | "display/lvgl_display/lvgl_image.cc" |
| | | "display/lvgl_display/gif/lvgl_gif.cc" |
| | | "display/lvgl_display/gif/gifdec.c" |
| | | "display/lvgl_display/jpg/image_to_jpeg.cpp" |
| | | "display/lvgl_display/jpg/jpeg_to_image.c" |
| | | "protocols/protocol.cc" |
| | | "protocols/mqtt_protocol.cc" |
| | | "protocols/websocket_protocol.cc" |
| | | "mcp_server.cc" |
| | | "system_info.cc" |
| | | "application.cc" |
| | | "ota.cc" |
| | | "settings.cc" |
| | | "device_state_machine.cc" |
| | | "assets.cc" |
| | | "main.cc" |
| | | ) |
| | | |
| | | set(INCLUDE_DIRS "." "display" "display/lvgl_display" "display/lvgl_display/jpg" "audio" "audio/demuxer" "protocols") |
| | | |
| | | # Add board common files |
| | | list(APPEND SOURCES |
| | | "boards/common/board.cc" |
| | | "boards/common/wifi_board.cc" |
| | | "boards/common/ml307_board.cc" |
| | | "boards/common/nt26_board.cc" |
| | | "boards/common/dual_network_board.cc" |
| | | "boards/common/adc_battery_monitor.cc" |
| | | "boards/common/afsk_demod.cc" |
| | | "boards/common/axp2101.cc" |
| | | "boards/common/backlight.cc" |
| | | "boards/common/button.cc" |
| | | "boards/common/i2c_device.cc" |
| | | "boards/common/knob.cc" |
| | | "boards/common/power_save_timer.cc" |
| | | "boards/common/press_to_talk_mcp_tool.cc" |
| | | "boards/common/sleep_timer.cc" |
| | | "boards/common/sy6970.cc" |
| | | "boards/common/system_reset.cc" |
| | | ) |
| | | list(APPEND INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/boards/common) |
| | | |
| | | |
| | | idf_build_get_property(build_components BUILD_COMPONENTS) |
| | | # Function to find component dynamically by pattern |
| | | function(find_component_by_pattern PATTERN COMPONENT_VAR PATH_VAR) |
| | | foreach(COMPONENT ${build_components}) |
| | | if(COMPONENT MATCHES "${PATTERN}") |
| | | set(${COMPONENT_VAR} ${COMPONENT} PARENT_SCOPE) |
| | | idf_component_get_property(COMPONENT_PATH ${COMPONENT} COMPONENT_DIR) |
| | | set(${PATH_VAR} "${COMPONENT_PATH}" PARENT_SCOPE) |
| | | break() |
| | | endif() |
| | | endforeach() |
| | | endfunction() |
| | | |
| | | # Set default BUILTIN_TEXT_FONT and BUILTIN_ICON_FONT |
| | | set(BUILTIN_TEXT_FONT font_puhui_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | |
| | | set(EMOTE_RESOLUTION "320_240") |
| | | |
| | | # Add board files according to BOARD_TYPE |
| | | # Set default assets if the board uses partition table V2 |
| | | if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI) |
| | | set(BOARD_TYPE "bread-compact-wifi") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307) |
| | | set(BOARD_TYPE "bread-compact-ml307") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_NT26) |
| | | set(BOARD_TYPE "bread-compact-nt26") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32) |
| | | set(BOARD_TYPE "bread-compact-esp32") |
| | | elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32_LCD) |
| | | set(BOARD_TYPE "bread-compact-esp32-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_DF_K10) |
| | | set(BOARD_TYPE "df-k10") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_DF_S3_AI_CAM) |
| | | set(BOARD_TYPE "df-s3-ai-cam") |
| | | elseif(CONFIG_BOARD_TYPE_ESP_BOX_3) |
| | | set(BOARD_TYPE "esp-box-3") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | set(EMOTE_RESOLUTION "320_240") |
| | | elseif(CONFIG_BOARD_TYPE_ESP_BOX) |
| | | set(BOARD_TYPE "esp-box") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | set(EMOTE_RESOLUTION "320_240") |
| | | elseif(CONFIG_BOARD_TYPE_ESP_BOX_LITE) |
| | | set(BOARD_TYPE "esp-box-lite") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_2) |
| | | set(BOARD_TYPE "kevin-box-2") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_KEVIN_C3) |
| | | set(BOARD_TYPE "kevin-c3") |
| | | elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V3_DEV) |
| | | set(BOARD_TYPE "kevin-sp-v3-dev") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V4_DEV) |
| | | set(BOARD_TYPE "kevin-sp-v4-dev") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD) |
| | | set(BOARD_TYPE "kevin-yuying-313lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_S3) |
| | | set(BOARD_TYPE "lichuang-dev") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_C3) |
| | | set(BOARD_TYPE "lichuang-c3-dev") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_EDA_TV_PRO) |
| | | set(MANUFACTURER "lceda-course-examples") |
| | | set(BOARD_TYPE "eda-tv-pro") |
| | | elseif(CONFIG_BOARD_TYPE_EDA_ROBOT_PRO) |
| | | set(MANUFACTURER "lceda-course-examples") |
| | | set(BOARD_TYPE "eda-robot-pro") |
| | | set(BUILTIN_TEXT_FONT font_puhui_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | elseif(CONFIG_BOARD_TYPE_EDA_SUPER_BEAR) |
| | | set(MANUFACTURER "lceda-course-examples") |
| | | set(BOARD_TYPE "eda-super-bear") |
| | | elseif(CONFIG_BOARD_TYPE_MAGICLICK_S3_2P4) |
| | | set(BOARD_TYPE "magiclick-2p4") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_MAGICLICK_S3_2P5) |
| | | set(BOARD_TYPE "magiclick-2p5") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3) |
| | | set(BOARD_TYPE "magiclick-c3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3_V2) |
| | | set(BOARD_TYPE "magiclick-c3-v2") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_S3) |
| | | set(BOARD_TYPE "m5stack-core-s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_TAB5) |
| | | set(BOARD_TYPE "m5stack-tab5") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3_ECHO_BASE) |
| | | set(BOARD_TYPE "atoms3-echo-base") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_ECHO_BASE) |
| | | set(BOARD_TYPE "atoms3r-echo-base") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_CAM_M12_ECHO_BASE) |
| | | set(BOARD_TYPE "atoms3r-cam-m12-echo-base") |
| | | elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_ECHOS3R) |
| | | set(BOARD_TYPE "atom-echos3r") |
| | | elseif(CONFIG_BOARD_TYPE_M5STACK_CARDPUTER_ADV) |
| | | set(BOARD_TYPE "m5stack-cardputer-adv") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_MATRIX_ECHO_BASE) |
| | | set(BOARD_TYPE "atommatrix-echo-base") |
| | | elseif(CONFIG_BOARD_TYPE_XMINI_C3_V3) |
| | | set(BOARD_TYPE "xmini-c3-v3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_XMINI_C3_4G) |
| | | set(BOARD_TYPE "xmini-c3-4g") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_XMINI_C3) |
| | | set(BOARD_TYPE "xmini-c3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_ESP_KORVO2_V3) |
| | | set(BOARD_TYPE "esp32s3-korvo2-v3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ESP_KORVO2_V3_RNDIS) |
| | | set(BOARD_TYPE "esp32s3-korvo2-v3-rndis") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT) |
| | | set(BOARD_TYPE "esp-sparkbot") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3) |
| | | set(BOARD_TYPE "esp-spot") |
| | | elseif(CONFIG_BOARD_TYPE_ESP_SPOT_C5) |
| | | set(BOARD_TYPE "esp-spot") |
| | | elseif(CONFIG_BOARD_TYPE_ESP_HI) |
| | | set(BOARD_TYPE "esp-hi") |
| | | # Set ESP_HI emoji directory for DEFAULT_ASSETS_EXTRA_FILES |
| | | set(DEFAULT_ASSETS_EXTRA_FILES "${CMAKE_BINARY_DIR}/emoji") |
| | | elseif(CONFIG_BOARD_TYPE_ESP_VOCAT) |
| | | set(BOARD_TYPE "esp-vocat") |
| | | set(BUILTIN_TEXT_FONT font_puhui_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | set(EMOTE_RESOLUTION "360_360") |
| | | # set(EMOTE_EXTERNAL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/boards/esp-vocat/assets") |
| | | elseif(CONFIG_BOARD_TYPE_ESP_SENSAIRSHUTTLE) |
| | | set(BOARD_TYPE "esp-sensairshuttle") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_TOUCH_LCD_3_5) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-touch-lcd-3.5") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_CAM_XXXX) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-cam") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_AUDIO_BOARD) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-audio-board") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_8) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-amoled-1.8") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_8) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-c6-touch-amoled-1.8") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_2_06) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-amoled-2.06") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_2_06) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-c6-touch-amoled-2.06") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_2_16) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-c6-touch-amoled-2.16") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_43C) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-amoled-1.43c") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4B) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-4b") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4_3C) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-4.3c") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-amoled-1.75") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75C) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-amoled-1.75") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_83) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-1.83") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85C) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-1.85c") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-1.85") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_46) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-1.46") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_5) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-3.5") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_5B) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-3.5b") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_1_54_v1) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-epaper-1.54") |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_1_54_v2) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-epaper-1.54") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_3_97) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-epaper-3.97") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_RLCD_4_2) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-rlcd-4.2") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_49) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-3.49") |
| | | set(LVGL_TEXT_FONT font_puhui_basic_30_4) |
| | | set(LVGL_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_54) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-lcd-1.54") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_LCD_0_85) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-lcd-0.85") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_LCD_1_69) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-c6-lcd-1.69") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_LCD_1_83) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-c6-touch-lcd-1.83") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_43) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-c6-touch-amoled-1.43") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_32) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-c6-touch-amoled-1.32") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_32) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-s3-touch-amoled-1.32") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_NANO) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-nano") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4B) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4_3) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7B) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_3_4C) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_3_5) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd-3.5") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4C) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_8) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_10_1) |
| | | set(MANUFACTURER "waveshare") |
| | | set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ESP_P4_FUNCTION_EV_BOARD) |
| | | set(BOARD_TYPE "esp-p4-function-ev-board") |
| | | elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD) |
| | | set(BOARD_TYPE "bread-compact-wifi-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_TUDOUZI) |
| | | set(BOARD_TYPE "tudouzi") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_LILYGO_T_CIRCLE_S3) |
| | | set(BOARD_TYPE "lilygo-t-circle-s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_0_V1_1) |
| | | set(BOARD_TYPE "lilygo-t-cameraplus-s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_2) |
| | | set(BOARD_TYPE "lilygo-t-cameraplus-s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA) |
| | | set(BOARD_TYPE "lilygo-t-display-s3-pro-mvsrlora") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA_NO_BATTERY) |
| | | set(BOARD_TYPE "lilygo-t-display-s3-pro-mvsrlora") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_P4) |
| | | set(BOARD_TYPE "lilygo-t-display-p4") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3) |
| | | set(BOARD_TYPE "movecall-moji-esp32s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI2_ESP32C5) |
| | | set(BOARD_TYPE "movecall-moji2-esp32c5") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3) |
| | | set(BOARD_TYPE "movecall-cuican-esp32s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3) |
| | | set(BOARD_TYPE "atk-dnesp32s3") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX) |
| | | set(BOARD_TYPE "atk-dnesp32s3-box") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX0) |
| | | set(BOARD_TYPE "atk-dnesp32s3-box0") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI) |
| | | set(BOARD_TYPE "atk-dnesp32s3-box2-wifi") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_4G) |
| | | set(BOARD_TYPE "atk-dnesp32s3-box2-4g") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_WIFI) |
| | | set(BOARD_TYPE "atk-dnesp32s3m-wifi") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_4G) |
| | | set(BOARD_TYPE "atk-dnesp32s3m-4g") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX3) |
| | | set(BOARD_TYPE "atk-dnesp32s3-box3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_DU_CHATX) |
| | | set(BOARD_TYPE "du-chatx") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_TAIJI_PI_S3) |
| | | set(BOARD_TYPE "taiji-pi-s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_85TFT_WIFI) |
| | | set(BOARD_TYPE "xingzhi-cube-0.85tft-wifi") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_85TFT_ML307) |
| | | set(BOARD_TYPE "xingzhi-cube-0.85tft-ml307") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_96OLED_WIFI) |
| | | set(BOARD_TYPE "xingzhi-cube-0.96oled-wifi") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_96OLED_ML307) |
| | | set(BOARD_TYPE "xingzhi-cube-0.96oled-ml307") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_WIFI) |
| | | set(BOARD_TYPE "xingzhi-cube-1.54tft-wifi") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307) |
| | | set(BOARD_TYPE "xingzhi-cube-1.54tft-ml307") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_XINGZHI_METAL_1_54_WIFI) |
| | | set(BOARD_TYPE "xingzhi-metal-1.54-wifi") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_XINGZHI_ABS_2_0) |
| | | set(BOARD_TYPE "xingzhi-abs-2.0") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER) |
| | | set(BOARD_TYPE "sensecap-watcher") |
| | | set(BUILTIN_TEXT_FONT font_noto_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) |
| | | elseif(CONFIG_BOARD_TYPE_DOIT_S3_AIBOX) |
| | | set(BOARD_TYPE "doit-s3-aibox") |
| | | elseif(CONFIG_BOARD_TYPE_MIXGO_NOVA) |
| | | set(BOARD_TYPE "mixgo-nova") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_GENJUTECH_S3_1_54TFT) |
| | | set(BOARD_TYPE "genjutech-s3-1.54tft") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_CGC) |
| | | set(BOARD_TYPE "esp32-cgc") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_CGC_144) |
| | | set(BOARD_TYPE "esp32-cgc-144") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_ESP_S3_LCD_EV_Board) |
| | | set(BOARD_TYPE "esp-s3-lcd-ev-board") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ESP_S3_LCD_EV_Board_2) |
| | | set(BOARD_TYPE "esp-s3-lcd-ev-board-2") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_1_54TFT_WIFI) |
| | | set(BOARD_TYPE "zhengchen-1.54tft-wifi") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_MINSI_K08_DUAL) |
| | | set(BOARD_TYPE "minsi-k08-dual") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_1_54TFT_ML307) |
| | | set(BOARD_TYPE "zhengchen-1.54tft-ml307") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM) |
| | | set(BOARD_TYPE "zhengchen-cam") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM_ML307) |
| | | set(BOARD_TYPE "zhengchen-cam-ml307") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_SPOTPEAR_ESP32_S3_1_54_MUMA) |
| | | set(BOARD_TYPE "sp-esp32-s3-1.54-muma") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_SPOTPEAR_ESP32_S3_1_28_BOX) |
| | | set(BOARD_TYPE "sp-esp32-s3-1.28-box") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_OTTO_ROBOT) |
| | | set(BOARD_TYPE "otto-robot") |
| | | set(BUILTIN_TEXT_FONT font_puhui_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION otto-gif) |
| | | elseif(CONFIG_BOARD_TYPE_ELECTRON_BOT) |
| | | set(BOARD_TYPE "electron-bot") |
| | | set(BUILTIN_TEXT_FONT font_puhui_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION otto-gif) |
| | | elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_CAM) |
| | | set(BOARD_TYPE "bread-compact-wifi-s3cam") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_16_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_JIUCHUAN) |
| | | set(BOARD_TYPE "jiuchuan-s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_LABPLUS_MPYTHON_V3) |
| | | set(BOARD_TYPE "labplus-mpython-v3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_LABPLUS_LEDONG_V2) |
| | | set(BOARD_TYPE "labplus-ledong-v2") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_SURFER_C3_1_14TFT) |
| | | set(BOARD_TYPE "surfer-c3-1.14tft") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_YUNLIAO_S3) |
| | | set(BOARD_TYPE "yunliao-s3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_WIRELESS_TAG_WTP4C5MP07S) |
| | | set(BOARD_TYPE "wireless-tag-wtp4c5mp07s") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_30_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_AIPI_LITE) |
| | | set(BOARD_TYPE "aipi-lite") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_32) |
| | | elseif(CONFIG_BOARD_TYPE_HU_087) |
| | | set(BOARD_TYPE "hu-087") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) |
| | | set(BUILTIN_ICON_FONT font_awesome_14_1) |
| | | elseif(CONFIG_BOARD_TYPE_Freenove_ESP32S3_DISPLAY_2_8_LCD) |
| | | set(BOARD_TYPE "freenove-esp32s3-display-2.8-lcd") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | elseif(CONFIG_BOARD_TYPE_AI_VOX3) |
| | | set(BOARD_TYPE "nulllab-ai-vox-v3") |
| | | set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) |
| | | set(BUILTIN_ICON_FONT font_awesome_20_4) |
| | | set(DEFAULT_EMOJI_COLLECTION twemoji_64) |
| | | endif() |
| | | |
| | | if(MANUFACTURER) |
| | | file(GLOB BOARD_SOURCES |
| | | ${CMAKE_CURRENT_SOURCE_DIR}/boards/${MANUFACTURER}/${BOARD_TYPE}/*.cc |
| | | ${CMAKE_CURRENT_SOURCE_DIR}/boards/${MANUFACTURER}/${BOARD_TYPE}/*.c |
| | | ) |
| | | else() |
| | | file(GLOB BOARD_SOURCES |
| | | ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc |
| | | ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.c |
| | | ) |
| | | endif() |
| | | list(APPEND SOURCES ${BOARD_SOURCES}) |
| | | |
| | | # Select audio processor according to Kconfig |
| | | if(CONFIG_USE_AUDIO_PROCESSOR) |
| | | list(APPEND SOURCES "audio/processors/afe_audio_processor.cc") |
| | | else() |
| | | list(APPEND SOURCES "audio/processors/no_audio_processor.cc") |
| | | endif() |
| | | if(CONFIG_IDF_TARGET_ESP32S3 OR CONFIG_IDF_TARGET_ESP32P4) |
| | | list(APPEND SOURCES "audio/wake_words/afe_wake_word.cc") |
| | | list(APPEND SOURCES "audio/wake_words/custom_wake_word.cc") |
| | | else() |
| | | list(APPEND SOURCES "audio/wake_words/esp_wake_word.cc") |
| | | endif() |
| | | |
| | | # Auto Select Additional Sources |
| | | if (CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING) |
| | | list(APPEND SOURCES "boards/common/blufi.cpp") |
| | | endif () |
| | | # Select language directory according to Kconfig |
| | | if(CONFIG_LANGUAGE_ZH_CN) |
| | | set(LANG_DIR "zh-CN") |
| | | elseif(CONFIG_LANGUAGE_ZH_TW) |
| | | set(LANG_DIR "zh-TW") |
| | | elseif(CONFIG_LANGUAGE_EN_US) |
| | | set(LANG_DIR "en-US") |
| | | elseif(CONFIG_LANGUAGE_JA_JP) |
| | | set(LANG_DIR "ja-JP") |
| | | elseif(CONFIG_LANGUAGE_KO_KR) |
| | | set(LANG_DIR "ko-KR") |
| | | elseif(CONFIG_LANGUAGE_VI_VN) |
| | | set(LANG_DIR "vi-VN") |
| | | elseif(CONFIG_LANGUAGE_TH_TH) |
| | | set(LANG_DIR "th-TH") |
| | | elseif(CONFIG_LANGUAGE_DE_DE) |
| | | set(LANG_DIR "de-DE") |
| | | elseif(CONFIG_LANGUAGE_FR_FR) |
| | | set(LANG_DIR "fr-FR") |
| | | elseif(CONFIG_LANGUAGE_ES_ES) |
| | | set(LANG_DIR "es-ES") |
| | | elseif(CONFIG_LANGUAGE_IT_IT) |
| | | set(LANG_DIR "it-IT") |
| | | elseif(CONFIG_LANGUAGE_RU_RU) |
| | | set(LANG_DIR "ru-RU") |
| | | elseif(CONFIG_LANGUAGE_AR_SA) |
| | | set(LANG_DIR "ar-SA") |
| | | elseif(CONFIG_LANGUAGE_HI_IN) |
| | | set(LANG_DIR "hi-IN") |
| | | elseif(CONFIG_LANGUAGE_PT_PT) |
| | | set(LANG_DIR "pt-PT") |
| | | elseif(CONFIG_LANGUAGE_PL_PL) |
| | | set(LANG_DIR "pl-PL") |
| | | elseif(CONFIG_LANGUAGE_CS_CZ) |
| | | set(LANG_DIR "cs-CZ") |
| | | elseif(CONFIG_LANGUAGE_FI_FI) |
| | | set(LANG_DIR "fi-FI") |
| | | elseif(CONFIG_LANGUAGE_TR_TR) |
| | | set(LANG_DIR "tr-TR") |
| | | elseif(CONFIG_LANGUAGE_ID_ID) |
| | | set(LANG_DIR "id-ID") |
| | | elseif(CONFIG_LANGUAGE_UK_UA) |
| | | set(LANG_DIR "uk-UA") |
| | | elseif(CONFIG_LANGUAGE_RO_RO) |
| | | set(LANG_DIR "ro-RO") |
| | | elseif(CONFIG_LANGUAGE_BG_BG) |
| | | set(LANG_DIR "bg-BG") |
| | | elseif(CONFIG_LANGUAGE_CA_ES) |
| | | set(LANG_DIR "ca-ES") |
| | | elseif(CONFIG_LANGUAGE_DA_DK) |
| | | set(LANG_DIR "da-DK") |
| | | elseif(CONFIG_LANGUAGE_EL_GR) |
| | | set(LANG_DIR "el-GR") |
| | | elseif(CONFIG_LANGUAGE_FA_IR) |
| | | set(LANG_DIR "fa-IR") |
| | | elseif(CONFIG_LANGUAGE_FIL_PH) |
| | | set(LANG_DIR "fil-PH") |
| | | elseif(CONFIG_LANGUAGE_HE_IL) |
| | | set(LANG_DIR "he-IL") |
| | | elseif(CONFIG_LANGUAGE_HR_HR) |
| | | set(LANG_DIR "hr-HR") |
| | | elseif(CONFIG_LANGUAGE_HU_HU) |
| | | set(LANG_DIR "hu-HU") |
| | | elseif(CONFIG_LANGUAGE_MS_MY) |
| | | set(LANG_DIR "ms-MY") |
| | | elseif(CONFIG_LANGUAGE_NB_NO) |
| | | set(LANG_DIR "nb-NO") |
| | | elseif(CONFIG_LANGUAGE_NL_NL) |
| | | set(LANG_DIR "nl-NL") |
| | | elseif(CONFIG_LANGUAGE_SK_SK) |
| | | set(LANG_DIR "sk-SK") |
| | | elseif(CONFIG_LANGUAGE_SL_SI) |
| | | set(LANG_DIR "sl-SI") |
| | | elseif(CONFIG_LANGUAGE_SV_SE) |
| | | set(LANG_DIR "sv-SE") |
| | | elseif(CONFIG_LANGUAGE_SR_RS) |
| | | set(LANG_DIR "sr-RS") |
| | | endif() |
| | | |
| | | # Define generation path |
| | | set(LANG_JSON "${CMAKE_CURRENT_SOURCE_DIR}/assets/locales/${LANG_DIR}/language.json") |
| | | set(LANG_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/assets/lang_config.h") |
| | | |
| | | # Collect current language audio files |
| | | file(GLOB LANG_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/locales/${LANG_DIR}/*.ogg) |
| | | |
| | | # If not en-US, collect en-US audio files as fallback for missing files |
| | | if(NOT LANG_DIR STREQUAL "en-US") |
| | | file(GLOB EN_US_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/locales/en-US/*.ogg) |
| | | |
| | | # Extract filenames (without path) from current language |
| | | set(EXISTING_NAMES "") |
| | | foreach(SOUND_FILE ${LANG_SOUNDS}) |
| | | get_filename_component(FILENAME ${SOUND_FILE} NAME) |
| | | list(APPEND EXISTING_NAMES ${FILENAME}) |
| | | endforeach() |
| | | |
| | | # Only add en-US audio files that are missing in current language |
| | | foreach(EN_SOUND ${EN_US_SOUNDS}) |
| | | get_filename_component(FILENAME ${EN_SOUND} NAME) |
| | | if(NOT ${FILENAME} IN_LIST EXISTING_NAMES) |
| | | list(APPEND LANG_SOUNDS ${EN_SOUND}) |
| | | message(STATUS "Using en-US fallback for missing audio: ${FILENAME}") |
| | | endif() |
| | | endforeach() |
| | | endif() |
| | | |
| | | file(GLOB COMMON_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/common/*.ogg) |
| | | |
| | | # If target chip is ESP32, exclude specific files to avoid build errors |
| | | if(CONFIG_IDF_TARGET_ESP32) |
| | | list(REMOVE_ITEM SOURCES "audio/codecs/box_audio_codec.cc" |
| | | "audio/codecs/es8388_audio_codec.cc" |
| | | "audio/codecs/es8389_audio_codec.cc" |
| | | "led/gpio_led.cc" |
| | | "display/lvgl_display/jpg/image_to_jpeg.cpp" |
| | | "display/lvgl_display/jpg/jpeg_to_image.c" |
| | | "boards/common/nt26_board.cc" |
| | | "boards/common/ml307_board.cc" |
| | | "boards/common/dual_network_board.cc" |
| | | ) |
| | | endif() |
| | | |
| | | # Include EspVideo if target is ESP32S3 or ESP32P4 |
| | | if(CONFIG_IDF_TARGET_ESP32S3 OR CONFIG_IDF_TARGET_ESP32P4) |
| | | list(APPEND SOURCES "boards/common/esp_video.cc" |
| | | "boards/common/rndis_board.cc" |
| | | ) |
| | | endif() |
| | | |
| | | # Include Esp32Camera if target is ESP32S3 |
| | | if(CONFIG_IDF_TARGET_ESP32S3) |
| | | list(APPEND SOURCES "boards/common/esp32_camera.cc") |
| | | endif() |
| | | |
| | | set(MAIN_PRIV_REQUIRES_EXTRA "") |
| | | if(CONFIG_BOARD_TYPE_ESP_VOCAT) |
| | | list(APPEND MAIN_PRIV_REQUIRES_EXTRA |
| | | espressif__touch_slider_sensor |
| | | espressif__touch_button_sensor |
| | | ) |
| | | endif() |
| | | |
| | | idf_component_register(SRCS ${SOURCES} |
| | | EMBED_FILES ${LANG_SOUNDS} ${COMMON_SOUNDS} |
| | | INCLUDE_DIRS ${INCLUDE_DIRS} |
| | | WHOLE_ARCHIVE |
| | | PRIV_REQUIRES |
| | | esp_pm |
| | | esp_psram |
| | | esp_netif |
| | | esp_driver_gpio |
| | | esp_driver_uart |
| | | esp_driver_spi |
| | | esp_driver_i2c |
| | | esp_driver_i2s |
| | | esp_driver_jpeg |
| | | esp_driver_ppa |
| | | esp_app_format |
| | | app_update |
| | | spi_flash |
| | | console |
| | | efuse |
| | | bt |
| | | fatfs |
| | | ${MAIN_PRIV_REQUIRES_EXTRA} |
| | | ) |
| | | |
| | | # Use target_compile_definitions to define BOARD_TYPE, BOARD_NAME |
| | | # If BOARD_NAME is empty, use BOARD_TYPE |
| | | if(NOT BOARD_NAME) |
| | | set(BOARD_NAME ${BOARD_TYPE}) |
| | | endif() |
| | | target_compile_definitions(${COMPONENT_LIB} |
| | | PRIVATE BOARD_TYPE=\"${BOARD_TYPE}\" BOARD_NAME=\"${BOARD_NAME}\" |
| | | PRIVATE BUILTIN_TEXT_FONT=${BUILTIN_TEXT_FONT} BUILTIN_ICON_FONT=${BUILTIN_ICON_FONT} |
| | | ) |
| | | |
| | | # Add generation rules |
| | | add_custom_command( |
| | | OUTPUT ${LANG_HEADER} |
| | | COMMAND python ${PROJECT_DIR}/scripts/gen_lang.py |
| | | --language "${LANG_DIR}" |
| | | --output "${LANG_HEADER}" |
| | | DEPENDS |
| | | ${LANG_JSON} |
| | | ${PROJECT_DIR}/scripts/gen_lang.py |
| | | COMMENT "Generating ${LANG_DIR} language config" |
| | | ) |
| | | |
| | | # Force build generation dependencies |
| | | add_custom_target(lang_header ALL |
| | | DEPENDS ${LANG_HEADER} |
| | | ) |
| | | |
| | | # Find ESP-SR component dynamically |
| | | find_component_by_pattern("espressif__esp-sr" ESP_SR_COMPONENT ESP_SR_COMPONENT_PATH) |
| | | if(ESP_SR_COMPONENT_PATH) |
| | | set(ESP_SR_MODEL_PATH "${ESP_SR_COMPONENT_PATH}/model") |
| | | endif() |
| | | |
| | | # Find xiaozhi-fonts component dynamically |
| | | find_component_by_pattern("xiaozhi-fonts" XIAOZHI_FONTS_COMPONENT XIAOZHI_FONTS_COMPONENT_PATH) |
| | | if(XIAOZHI_FONTS_COMPONENT_PATH) |
| | | set(XIAOZHI_FONTS_PATH "${XIAOZHI_FONTS_COMPONENT_PATH}") |
| | | endif() |
| | | |
| | | if(CONFIG_BOARD_TYPE_ESP_HI) |
| | | set(URL "https://github.com/espressif2022/image_player/raw/main/test_apps/test_8bit") |
| | | set(EMOJI_DIR "${CMAKE_BINARY_DIR}/emoji") |
| | | file(MAKE_DIRECTORY ${EMOJI_DIR}) |
| | | |
| | | # List all files to download |
| | | set(FILES_TO_DOWNLOAD "") |
| | | list(APPEND FILES_TO_DOWNLOAD "Anger_enter.aaf" "Anger_loop.aaf" "Anger_return.aaf") |
| | | list(APPEND FILES_TO_DOWNLOAD "happy_enter.aaf" "happy_loop.aaf" "happ_return.aaf") |
| | | list(APPEND FILES_TO_DOWNLOAD "sad_enter.aaf" "sad_loop.aaf" "sad_return.aaf") |
| | | list(APPEND FILES_TO_DOWNLOAD "scorn_enter.aaf" "scorn_loop.aaf" "scorn_return.aaf") |
| | | list(APPEND FILES_TO_DOWNLOAD "left_enter.aaf" "left_loop.aaf" "left_return.aaf") |
| | | list(APPEND FILES_TO_DOWNLOAD "right_enter.aaf" "right_loop.aaf" "right_return.aaf") |
| | | list(APPEND FILES_TO_DOWNLOAD "asking.aaf" "blink_once.aaf" "blink_quick.aaf") |
| | | list(APPEND FILES_TO_DOWNLOAD "connecting.aaf" "panic_enter.aaf" "panic_loop.aaf") |
| | | list(APPEND FILES_TO_DOWNLOAD "panic_return.aaf" "wake.aaf") |
| | | |
| | | foreach(FILENAME IN LISTS FILES_TO_DOWNLOAD) |
| | | set(REMOTE_FILE "${URL}/${FILENAME}") |
| | | set(LOCAL_FILE "${EMOJI_DIR}/${FILENAME}") |
| | | |
| | | # Check if local file exists |
| | | if(EXISTS ${LOCAL_FILE}) |
| | | message(STATUS "File ${FILENAME} already exists, skipping download") |
| | | else() |
| | | message(STATUS "Downloading ${FILENAME}") |
| | | file(DOWNLOAD ${REMOTE_FILE} ${LOCAL_FILE} |
| | | STATUS DOWNLOAD_STATUS) |
| | | list(GET DOWNLOAD_STATUS 0 STATUS_CODE) |
| | | if(NOT STATUS_CODE EQUAL 0) |
| | | message(FATAL_ERROR "Failed to download ${FILENAME} from ${URL}") |
| | | endif() |
| | | endif() |
| | | endforeach() |
| | | |
| | | endif() |
| | | |
| | | |
| | | # Function to build default assets based on configuration |
| | | function(build_default_assets_bin) |
| | | # Set output path for generated assets.bin |
| | | set(GENERATED_ASSETS_BIN "${CMAKE_BINARY_DIR}/generated_assets.bin") |
| | | |
| | | # Prepare arguments for build script |
| | | set(BUILD_ARGS |
| | | "--sdkconfig" "${SDKCONFIG}" |
| | | "--output" "${GENERATED_ASSETS_BIN}" |
| | | ) |
| | | |
| | | # Add builtin text font if defined |
| | | if(BUILTIN_TEXT_FONT) |
| | | list(APPEND BUILD_ARGS "--builtin_text_font" "${BUILTIN_TEXT_FONT}") |
| | | endif() |
| | | |
| | | # Add default emoji collection if defined |
| | | if(DEFAULT_EMOJI_COLLECTION) |
| | | list(APPEND BUILD_ARGS "--emoji_collection" "${DEFAULT_EMOJI_COLLECTION}") |
| | | endif() |
| | | |
| | | # Add default assets extra files if defined |
| | | if(DEFAULT_ASSETS_EXTRA_FILES) |
| | | list(APPEND BUILD_ARGS "--extra_files" "${DEFAULT_ASSETS_EXTRA_FILES}") |
| | | endif() |
| | | |
| | | list(APPEND BUILD_ARGS "--esp_sr_model_path" "${ESP_SR_MODEL_PATH}") |
| | | list(APPEND BUILD_ARGS "--xiaozhi_fonts_path" "${XIAOZHI_FONTS_PATH}") |
| | | |
| | | # Create custom command to build assets |
| | | add_custom_command( |
| | | OUTPUT ${GENERATED_ASSETS_BIN} |
| | | COMMAND python ${PROJECT_DIR}/scripts/build_default_assets.py ${BUILD_ARGS} |
| | | DEPENDS |
| | | ${SDKCONFIG} |
| | | ${PROJECT_DIR}/scripts/build_default_assets.py |
| | | COMMENT "Building default assets.bin based on configuration" |
| | | VERBATIM |
| | | ) |
| | | |
| | | # Create target for generated assets |
| | | add_custom_target(generated_default_assets ALL |
| | | DEPENDS ${GENERATED_ASSETS_BIN} |
| | | ) |
| | | |
| | | # Set the generated file path in parent scope |
| | | set(GENERATED_ASSETS_LOCAL_FILE ${GENERATED_ASSETS_BIN} PARENT_SCOPE) |
| | | |
| | | message(STATUS "Default assets build configured: ${GENERATED_ASSETS_BIN}") |
| | | endfunction() |
| | | |
| | | |
| | | # Function to get local assets file path (handles both URL and local file) |
| | | function(get_assets_local_file assets_source assets_local_file_var) |
| | | # Check if it's a URL (starts with http:// or https://) |
| | | if(assets_source MATCHES "^https?://") |
| | | # It's a URL, download it |
| | | get_filename_component(ASSETS_FILENAME "${assets_source}" NAME) |
| | | set(ASSETS_LOCAL_FILE "${CMAKE_BINARY_DIR}/${ASSETS_FILENAME}") |
| | | set(ASSETS_TEMP_FILE "${CMAKE_BINARY_DIR}/${ASSETS_FILENAME}.tmp") |
| | | |
| | | # Check if local file exists |
| | | if(EXISTS ${ASSETS_LOCAL_FILE}) |
| | | message(STATUS "Assets file ${ASSETS_FILENAME} already exists, skipping download") |
| | | else() |
| | | message(STATUS "Downloading ${ASSETS_FILENAME}") |
| | | |
| | | # Clean up any existing temp file |
| | | if(EXISTS ${ASSETS_TEMP_FILE}) |
| | | file(REMOVE ${ASSETS_TEMP_FILE}) |
| | | endif() |
| | | |
| | | # Download to temporary file first |
| | | file(DOWNLOAD ${assets_source} ${ASSETS_TEMP_FILE} |
| | | STATUS DOWNLOAD_STATUS) |
| | | list(GET DOWNLOAD_STATUS 0 STATUS_CODE) |
| | | if(NOT STATUS_CODE EQUAL 0) |
| | | # Clean up temp file on failure |
| | | if(EXISTS ${ASSETS_TEMP_FILE}) |
| | | file(REMOVE ${ASSETS_TEMP_FILE}) |
| | | endif() |
| | | message(FATAL_ERROR "Failed to download ${ASSETS_FILENAME} from ${assets_source}") |
| | | endif() |
| | | |
| | | # Move temp file to final location (atomic operation) |
| | | file(RENAME ${ASSETS_TEMP_FILE} ${ASSETS_LOCAL_FILE}) |
| | | message(STATUS "Successfully downloaded ${ASSETS_FILENAME}") |
| | | endif() |
| | | else() |
| | | # It's a local file path |
| | | if(IS_ABSOLUTE "${assets_source}") |
| | | set(ASSETS_LOCAL_FILE "${assets_source}") |
| | | else() |
| | | set(ASSETS_LOCAL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/${assets_source}") |
| | | endif() |
| | | |
| | | # Check if local file exists |
| | | if(NOT EXISTS ${ASSETS_LOCAL_FILE}) |
| | | message(FATAL_ERROR "Assets file not found: ${ASSETS_LOCAL_FILE}") |
| | | endif() |
| | | |
| | | message(STATUS "Using assets file: ${ASSETS_LOCAL_FILE}") |
| | | endif() |
| | | |
| | | set(${assets_local_file_var} ${ASSETS_LOCAL_FILE} PARENT_SCOPE) |
| | | endfunction() |
| | | |
| | | |
| | | partition_table_get_partition_info(size "--partition-name assets" "size") |
| | | partition_table_get_partition_info(offset "--partition-name assets" "offset") |
| | | if ("${size}" AND "${offset}") |
| | | # Flash assets based on configuration |
| | | if(CONFIG_FLASH_DEFAULT_ASSETS) |
| | | # Build default assets based on configuration |
| | | build_default_assets_bin() |
| | | esptool_py_flash_to_partition(flash "assets" "${GENERATED_ASSETS_LOCAL_FILE}") |
| | | message(STATUS "Generated default assets flash configured: ${GENERATED_ASSETS_LOCAL_FILE} -> assets partition") |
| | | elseif(CONFIG_FLASH_CUSTOM_ASSETS) |
| | | # Flash custom assets |
| | | get_assets_local_file("${CONFIG_CUSTOM_ASSETS_FILE}" ASSETS_LOCAL_FILE) |
| | | esptool_py_flash_to_partition(flash "assets" "${ASSETS_LOCAL_FILE}") |
| | | message(STATUS "Custom assets flash configured: ${ASSETS_LOCAL_FILE} -> assets partition") |
| | | elseif(CONFIG_FLASH_EXPRESSION_ASSETS) |
| | | set(ASSETS_NAME "expression_assets") |
| | | set(ASSETS_PARTITION "assets") |
| | | set(ASSETS_FILE "${CMAKE_BINARY_DIR}/${ASSETS_NAME}.bin") |
| | | |
| | | build_speaker_assets_bin("${ASSETS_PARTITION}" ${EMOTE_RESOLUTION} ${ASSETS_FILE} ${CONFIG_MMAP_FILE_NAME_LENGTH}) |
| | | message(STATUS "Generated emote assets: ${ASSETS_FILE} -> ${ASSETS_PARTITION} partition") |
| | | esptool_py_flash_to_partition(flash "${ASSETS_PARTITION}" "${ASSETS_FILE}") |
| | | elseif(CONFIG_FLASH_NONE_ASSETS) |
| | | message(STATUS "Assets flashing disabled (FLASH_NONE_ASSETS)") |
| | | endif() |
| | | else() |
| | | message(STATUS "Assets partition not found, using v1 partition table") |
| | | endif() |
| New file |
| | |
| | | menu "Xiaozhi Assistant" |
| | | |
| | | config OTA_URL |
| | | string "Default OTA URL" |
| | | default "https://api.tenclass.net/xiaozhi/ota/" |
| | | help |
| | | The application will access this URL to check for new firmwares and server address. |
| | | |
| | | choice |
| | | prompt "Flash Assets" |
| | | default FLASH_DEFAULT_ASSETS if !USE_EMOTE_MESSAGE_STYLE |
| | | default FLASH_EXPRESSION_ASSETS if USE_EMOTE_MESSAGE_STYLE |
| | | help |
| | | Select the assets to flash. |
| | | |
| | | config FLASH_NONE_ASSETS |
| | | bool "Do not flash assets" |
| | | config FLASH_DEFAULT_ASSETS |
| | | bool "Flash Default Assets" |
| | | depends on !USE_EMOTE_MESSAGE_STYLE |
| | | config FLASH_CUSTOM_ASSETS |
| | | bool "Flash Custom Assets" |
| | | config FLASH_EXPRESSION_ASSETS |
| | | bool "Flash Emote Assets" |
| | | depends on USE_EMOTE_MESSAGE_STYLE |
| | | endchoice |
| | | |
| | | config CUSTOM_ASSETS_FILE |
| | | depends on FLASH_CUSTOM_ASSETS |
| | | string "Custom Assets File" |
| | | default "assets.bin" |
| | | help |
| | | The custom assets file to flash. |
| | | It can be a local file relative to the project directory or a remote url. |
| | | |
| | | choice |
| | | prompt "Default Language" |
| | | default LANGUAGE_ZH_CN |
| | | help |
| | | Select device display language |
| | | |
| | | config LANGUAGE_ZH_CN |
| | | bool "Chinese" |
| | | config LANGUAGE_ZH_TW |
| | | bool "Chinese Traditional" |
| | | config LANGUAGE_EN_US |
| | | bool "English" |
| | | config LANGUAGE_JA_JP |
| | | bool "Japanese" |
| | | config LANGUAGE_KO_KR |
| | | bool "Korean" |
| | | config LANGUAGE_VI_VN |
| | | bool "Vietnamese" |
| | | config LANGUAGE_TH_TH |
| | | bool "Thai" |
| | | config LANGUAGE_DE_DE |
| | | bool "German" |
| | | config LANGUAGE_FR_FR |
| | | bool "French" |
| | | config LANGUAGE_ES_ES |
| | | bool "Spanish" |
| | | config LANGUAGE_IT_IT |
| | | bool "Italian" |
| | | config LANGUAGE_RU_RU |
| | | bool "Russian" |
| | | config LANGUAGE_AR_SA |
| | | bool "Arabic" |
| | | config LANGUAGE_HI_IN |
| | | bool "Hindi" |
| | | config LANGUAGE_PT_PT |
| | | bool "Portuguese" |
| | | config LANGUAGE_PL_PL |
| | | bool "Polish" |
| | | config LANGUAGE_CS_CZ |
| | | bool "Czech" |
| | | config LANGUAGE_FI_FI |
| | | bool "Finnish" |
| | | config LANGUAGE_TR_TR |
| | | bool "Turkish" |
| | | config LANGUAGE_ID_ID |
| | | bool "Indonesian" |
| | | config LANGUAGE_UK_UA |
| | | bool "Ukrainian" |
| | | config LANGUAGE_RO_RO |
| | | bool "Romanian" |
| | | config LANGUAGE_BG_BG |
| | | bool "Bulgarian" |
| | | config LANGUAGE_CA_ES |
| | | bool "Catalan" |
| | | config LANGUAGE_DA_DK |
| | | bool "Danish" |
| | | config LANGUAGE_EL_GR |
| | | bool "Greek" |
| | | config LANGUAGE_FA_IR |
| | | bool "Persian" |
| | | config LANGUAGE_FIL_PH |
| | | bool "Filipino" |
| | | config LANGUAGE_HE_IL |
| | | bool "Hebrew" |
| | | config LANGUAGE_HR_HR |
| | | bool "Croatian" |
| | | config LANGUAGE_HU_HU |
| | | bool "Hungarian" |
| | | config LANGUAGE_MS_MY |
| | | bool "Malay" |
| | | config LANGUAGE_NB_NO |
| | | bool "Norwegian" |
| | | config LANGUAGE_NL_NL |
| | | bool "Dutch" |
| | | config LANGUAGE_SK_SK |
| | | bool "Slovak" |
| | | config LANGUAGE_SL_SI |
| | | bool "Slovenian" |
| | | config LANGUAGE_SV_SE |
| | | bool "Swedish" |
| | | config LANGUAGE_SR_RS |
| | | bool "Serbian" |
| | | endchoice |
| | | |
| | | choice BOARD_TYPE |
| | | prompt "Board Type" |
| | | default BOARD_TYPE_BREAD_COMPACT_WIFI |
| | | help |
| | | Board type. å¼åæ¿ç±»å |
| | | config BOARD_TYPE_BREAD_COMPACT_WIFI |
| | | bool "Bread Compact WiFi (é¢å
æ¿)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_BREAD_COMPACT_WIFI_LCD |
| | | bool "Bread Compact WiFi + LCD (é¢å
æ¿)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_BREAD_COMPACT_WIFI_CAM |
| | | bool "Bread Compact WiFi + LCD + Camera (é¢å
æ¿)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_BREAD_COMPACT_ML307 |
| | | bool "Bread Compact ML307/EC801E (é¢å
æ¿ 4G)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_BREAD_COMPACT_NT26 |
| | | bool "Bread Compact NT26 (é¢å
æ¿ 4G)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_BREAD_COMPACT_ESP32 |
| | | bool "Bread Compact ESP32 DevKit (é¢å
æ¿)" |
| | | depends on IDF_TARGET_ESP32 |
| | | config BOARD_TYPE_BREAD_COMPACT_ESP32_LCD |
| | | bool "Bread Compact ESP32 DevKit + LCD (é¢å
æ¿)" |
| | | depends on IDF_TARGET_ESP32 |
| | | config BOARD_TYPE_XMINI_C3_V3 |
| | | bool "Xmini C3 V3" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_XMINI_C3_4G |
| | | bool "Xmini C3 4G" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_XMINI_C3 |
| | | bool "Xmini C3" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_ESP_KORVO2_V3 |
| | | bool "Espressif Korvo2 V3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_KORVO2_V3_RNDIS |
| | | bool "Espressif Korvo2 V3 RNDIS" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_SPARKBOT |
| | | bool "Espressif SparkBot" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_SENSAIRSHUTTLE |
| | | bool "Espressif ESP-SensairShuttle" |
| | | depends on IDF_TARGET_ESP32C5 |
| | | config BOARD_TYPE_ESP_SPOT_S3 |
| | | bool "Espressif Spot-S3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_SPOT_C5 |
| | | bool "Espressif Spot-C5" |
| | | depends on IDF_TARGET_ESP32C5 |
| | | config BOARD_TYPE_ESP_HI |
| | | bool "Espressif ESP-HI" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_ESP_BOX_3 |
| | | bool "Espressif ESP-BOX-3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_BOX |
| | | bool "Espressif ESP-BOX" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_BOX_LITE |
| | | bool "Espressif ESP-BOX-Lite" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_P4_FUNCTION_EV_BOARD |
| | | bool "Espressif ESP-P4-Function-EV-Board" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_ESP_VOCAT |
| | | bool "Espressif ESP-VoCat" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_KEVIN_BOX_2 |
| | | bool "Kevin Box 2" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_KEVIN_C3 |
| | | bool "Kevin C3" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_KEVIN_SP_V3_DEV |
| | | bool "Kevin SP V3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_KEVIN_SP_V4_DEV |
| | | bool "Kevin SP V4" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_KEVIN_YUYING_313LCD |
| | | bool "é±¼é¹°ç§æ 3.13LCD" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_CGC |
| | | bool "CGC" |
| | | depends on IDF_TARGET_ESP32 |
| | | config BOARD_TYPE_CGC_144 |
| | | bool "CGC 144" |
| | | depends on IDF_TARGET_ESP32 |
| | | config BOARD_TYPE_LICHUANG_DEV_S3 |
| | | bool "ç«åÂ·å®ææ´¾ ESP32-S3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LICHUANG_DEV_C3 |
| | | bool "ç«åÂ·å®ææ´¾ ESP32-C3" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_EDA_TV_PRO |
| | | bool "EDAè¯¾ç¨æ¡ä¾ EDA-TV-Pro" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_EDA_ROBOT_PRO |
| | | bool "EDAè¯¾ç¨æ¡ä¾ EDA-Robot-Pro" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_EDA_SUPER_BEAR |
| | | bool "EDAè¯¾ç¨æ¡ä¾ EDA-Super-Bear" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_DF_K10 |
| | | bool "DFRobot è¡ç©ºæ¿ k10" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_DF_S3_AI_CAM |
| | | bool "DFRobot ESP32-S3 AIæºè½æå头模å" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_MAGICLICK_S3_2P4 |
| | | bool "ç¥å¥æé® Magiclick_2.4" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_MAGICLICK_S3_2P5 |
| | | bool "ç¥å¥æé® Magiclick_2.5" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_MAGICLICK_C3 |
| | | bool "ç¥å¥æé® Magiclick_C3" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_MAGICLICK_C3_V2 |
| | | bool "ç¥å¥æé® Magiclick_C3_v2" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_M5STACK_CORE_S3 |
| | | bool "M5Stack CoreS3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_M5STACK_CORE_TAB5 |
| | | bool "M5Stack Tab5" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_M5STACK_ATOM_S3_ECHO_BASE |
| | | bool "M5Stack AtomS3 + Echo Base" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_M5STACK_ATOM_S3R_ECHO_BASE |
| | | bool "M5Stack AtomS3R + Echo Base" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_M5STACK_ATOM_S3R_CAM_M12_ECHO_BASE |
| | | bool "M5Stack AtomS3R CAM/M12 + Echo Base" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_M5STACK_ATOM_ECHOS3R |
| | | bool "M5Stack AtomEchoS3R" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_M5STACK_CARDPUTER_ADV |
| | | bool "M5Stack Cardputer Adv" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_M5STACK_ATOM_MATRIX_ECHO_BASE |
| | | bool "M5Stack AtomMatrix + Echo Base" |
| | | depends on IDF_TARGET_ESP32 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_TOUCH_LCD_3_5 |
| | | bool "Waveshare ESP32-Touch-LCD-3.5" |
| | | depends on IDF_TARGET_ESP32 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_CAM_XXXX |
| | | bool "Waveshare ESP32-S3-CAM" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_AUDIO_BOARD |
| | | bool "Waveshare ESP32-S3-Audio-Board" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_8 |
| | | bool "Waveshare ESP32-S3-Touch-AMOLED-1.8" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_43C |
| | | bool "Waveshare ESP32-S3-Touch-AMOLED-1.43C" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_2_06 |
| | | bool "Waveshare ESP32-S3-Touch-AMOLED-2.06" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_2_06 |
| | | bool "Waveshare ESP32-C6-Touch-AMOLED-2.06" |
| | | depends on IDF_TARGET_ESP32C6 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_2_16 |
| | | bool "Waveshare ESP32-C6-Touch-AMOLED-2.16" |
| | | depends on IDF_TARGET_ESP32C6 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75 |
| | | bool "Waveshare ESP32-S3-Touch-AMOLED-1.75" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75C |
| | | bool "Waveshare ESP32-S3-Touch-AMOLED-1.75C" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_83 |
| | | bool "Waveshare ESP32-S3-Touch-LCD-1.83" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4B |
| | | bool "Waveshare ESP32-S3-Touch-LCD-4B" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4_3C |
| | | bool "Waveshare ESP32-S3-Touch-LCD-4.3C" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85C |
| | | bool "Waveshare ESP32-S3-Touch-LCD-1.85C" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85 |
| | | bool "Waveshare ESP32-S3-Touch-LCD-1.85" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_46 |
| | | bool "Waveshare ESP32-S3-Touch-LCD-1.46" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_C6_LCD_1_69 |
| | | bool "Waveshare ESP32-C6-LCD-1.69" |
| | | depends on IDF_TARGET_ESP32C6 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_LCD_1_83 |
| | | bool "Waveshare ESP32-C6-Touch-LCD-1.83" |
| | | depends on IDF_TARGET_ESP32C6 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_43 |
| | | bool "Waveshare ESP32-C6-Touch-AMOLOED-1.43" |
| | | depends on IDF_TARGET_ESP32C6 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_32 |
| | | bool "Waveshare ESP32-C6-Touch-AMOLOED-1.32" |
| | | depends on IDF_TARGET_ESP32C6 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_8 |
| | | bool "Waveshare ESP32-C6-Touch-AMOLED-1.8" |
| | | depends on IDF_TARGET_ESP32C6 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_32 |
| | | bool "Waveshare ESP32-S3-Touch-AMOLOED-1.32" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_49 |
| | | bool "Waveshare ESP32-S3-Touch-LCD-3.49" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_5 |
| | | bool "Waveshare ESP32-S3-Touch-LCD-3.5" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_1_54_v1 |
| | | bool "Waveshare ESP32-S3-ePaper-1.54_v1" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_1_54_v2 |
| | | bool "Waveshare ESP32-S3-ePaper-1.54_v2" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_3_97 |
| | | bool "Waveshare ESP32-S3-ePaper-3.97" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_RLCD_4_2 |
| | | bool "Waveshare ESP32-S3-RLCD-4.2" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_5B |
| | | bool "Waveshare ESP32-S3-Touch-LCD-3.5B" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_54 |
| | | bool "Waveshare ESP32-S3-Touch-LCD-1.54" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_S3_LCD_0_85 |
| | | bool "Waveshare ESP32-S3-LCD-0.85" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_NANO |
| | | bool "Waveshare ESP32-P4-NANO" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4B |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-4B" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4_3 |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7B |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-7B" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_3_4C |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-3.4C" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_3_5 |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-3.5" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4C |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-4C" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7 |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-7" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_8 |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-8" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_10_1 |
| | | bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-10.1" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_TUDOUZI |
| | | bool "åè±å" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LILYGO_T_CIRCLE_S3 |
| | | bool "LILYGO T-Circle-S3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_0_V1_1 |
| | | bool "LILYGO T-CameraPlus-S3_V1_0_V1_1" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_2 |
| | | bool "LILYGO T-CameraPlus-S3_V1_2" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA |
| | | bool "LILYGO T-Display-S3-Pro-MVSRLora" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA_NO_BATTERY |
| | | bool "LILYGO T-Display-S3-Pro-MVSRLora_No_Battery" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LILYGO_T_DISPLAY_P4 |
| | | bool "LILYGO T-Display-P4" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_MOVECALL_MOJI_ESP32S3 |
| | | bool "Movecall Moji å°æºAIè¡çç" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_MOVECALL_MOJI2_ESP32C5 |
| | | bool "Movecall Moji2.0 å°æºAIè¡çç" |
| | | depends on IDF_TARGET_ESP32C5 |
| | | config BOARD_TYPE_MOVECALL_CUICAN_ESP32S3 |
| | | bool "Movecall CuiCan çç¨Â·AIåå " |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ATK_DNESP32S3 |
| | | bool "æ£ç¹ååDNESP32S3å¼åæ¿" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ATK_DNESP32S3_BOX |
| | | bool "æ£ç¹ååDNESP32S3-BOX" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ATK_DNESP32S3_BOX0 |
| | | bool "æ£ç¹ååDNESP32S3-BOX0" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI |
| | | bool "æ£ç¹ååDNESP32S3-BOX2-WIFI" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ATK_DNESP32S3_BOX2_4G |
| | | bool "æ£ç¹ååDNESP32S3-BOX2-4G" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ATK_DNESP32S3M_WIFI |
| | | bool "æ£ç¹ååDNESP32S3M-WIFI" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ATK_DNESP32S3M_4G |
| | | bool "æ£ç¹ååDNESP32S3M-4G" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ATK_DNESP32S3_BOX3 |
| | | bool "æ£ç¹ååDNESP32S3-BOX3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_DU_CHATX |
| | | bool "ååå¼åæ¿CHATX(wifi)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_TAIJI_PI_S3 |
| | | bool "太æå°æ´¾esp32s3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_XINGZHI_CUBE_0_85TFT_WIFI |
| | | bool "æ åç§æææº0.85(WIFI)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_XINGZHI_CUBE_0_85TFT_ML307 |
| | | bool "æ åç§æææº0.85(ML307)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_XINGZHI_CUBE_0_96OLED_WIFI |
| | | bool "æ åç§æææº0.96(WIFI)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_XINGZHI_CUBE_0_96OLED_ML307 |
| | | bool "æ åç§æææº0.96(ML307)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_XINGZHI_CUBE_1_54TFT_WIFI |
| | | bool "æ åç§æææº1.54(WIFI)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307 |
| | | bool "æ åç§æææº1.54(ML307)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_XINGZHI_METAL_1_54_WIFI |
| | | bool "æ åç§æææº1.54 METAL(wifi)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_XINGZHI_ABS_2_0 |
| | | bool "æ åç§æææºABS 2.0" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER |
| | | bool "Seeed Studio SenseCAP Watcher" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_DOIT_S3_AIBOX |
| | | bool "ååæºèAIéªä¼´çå" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_MIXGO_NOVA |
| | | bool "å
æ§Â·éæ¥" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_GENJUTECH_S3_1_54TFT |
| | | bool "äºå
·ç§æ1.54(s3)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_S3_LCD_EV_Board |
| | | bool "ä¹é«ESP S3 LCD EV Boardå¼åæ¿" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ESP_S3_LCD_EV_Board_2 |
| | | bool "ä¹é«ESP S3 LCD EV Board 2å¼åæ¿" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ZHENGCHEN_1_54TFT_WIFI |
| | | bool "å¾è¾°ç§æ1.54(WIFI)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ZHENGCHEN_1_54TFT_ML307 |
| | | bool "å¾è¾°ç§æ1.54(ML307)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ZHENGCHEN_CAM |
| | | bool "å¾è¾°ç§æAI Camera" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ZHENGCHEN_CAM_ML307 |
| | | bool "å¾è¾°ç§æAI Camera(ML307)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_MINSI_K08_DUAL |
| | | bool "ææç§æK08(DUAL)" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_SPOTPEAR_ESP32_S3_1_54_MUMA |
| | | bool "Spotpear ESP32-S3-1.54-MUMA" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_SPOTPEAR_ESP32_S3_1_28_BOX |
| | | bool "Spotpear ESP32-S3-1.28-BOX" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_OTTO_ROBOT |
| | | bool "ottoRobot" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_ELECTRON_BOT |
| | | bool "electronBot" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_JIUCHUAN |
| | | bool "ä¹å·æºè½" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LABPLUS_MPYTHON_V3 |
| | | bool "labplus mpython_v3 board" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_LABPLUS_LEDONG_V2 |
| | | bool "labplus ledong_v2 board" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_SURFER_C3_1_14TFT |
| | | bool "Surfer-C3-1.14TFT" |
| | | depends on IDF_TARGET_ESP32C3 |
| | | config BOARD_TYPE_YUNLIAO_S3 |
| | | bool "å°æºäºè-S3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_WIRELESS_TAG_WTP4C5MP07S |
| | | bool "Wireless-Tag WTP4C5MP07S" |
| | | depends on IDF_TARGET_ESP32P4 |
| | | config BOARD_TYPE_AIPI_LITE |
| | | bool "AIPI-Lite" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_HU_087 |
| | | bool "HU-087" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_Freenove_ESP32S3_DISPLAY_2_8_LCD |
| | | bool "Freenove ESP32S3 Display 2.8 LCD" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | config BOARD_TYPE_AI_VOX3 |
| | | bool "NULLLAB-AI-VOX3" |
| | | depends on IDF_TARGET_ESP32S3 |
| | | endchoice |
| | | |
| | | choice |
| | | depends on BOARD_TYPE_LILYGO_T_DISPLAY_P4 |
| | | prompt "Select the screen type" |
| | | default SCREEN_TYPE_HI8561 |
| | | config SCREEN_TYPE_HI8561 |
| | | bool "HI8561" |
| | | config SCREEN_TYPE_RM69A10 |
| | | bool "RM69A10" |
| | | endchoice |
| | | |
| | | choice |
| | | depends on BOARD_TYPE_LILYGO_T_DISPLAY_P4 |
| | | prompt "Select the color format of the screen" |
| | | default SCREEN_PIXEL_FORMAT_RGB565 |
| | | config SCREEN_PIXEL_FORMAT_RGB565 |
| | | bool "RGB565" |
| | | config SCREEN_PIXEL_FORMAT_RGB888 |
| | | bool "RGB888" |
| | | endchoice |
| | | |
| | | choice ESP_S3_LCD_EV_Board_Version_TYPE |
| | | depends on BOARD_TYPE_ESP_S3_LCD_EV_Board |
| | | prompt "EV_BOARD Type" |
| | | default ESP_S3_LCD_EV_Board_1p4 |
| | | config ESP_S3_LCD_EV_Board_1p4 |
| | | bool "ä¹é«ESP32_S3_LCD_EV_Board-MB_V1.4" |
| | | config ESP_S3_LCD_EV_Board_1p5 |
| | | bool "ä¹é«ESP32_S3_LCD_EV_Board-MB_V1.5" |
| | | endchoice |
| | | |
| | | choice DISPLAY_OLED_TYPE |
| | | depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_NT26 || BOARD_TYPE_BREAD_COMPACT_ESP32 || BOARD_TYPE_HU_087 |
| | | prompt "OLED Type" |
| | | default OLED_SSD1306_128X32 |
| | | help |
| | | OLED Monochrome Display Type |
| | | config OLED_SSD1306_128X32 |
| | | bool "SSD1306 128*32" |
| | | config OLED_SSD1306_128X64 |
| | | bool "SSD1306 128*64" |
| | | config OLED_SH1106_128X64 |
| | | bool "SH1106 128*64" |
| | | endchoice |
| | | |
| | | choice DISPLAY_LCD_TYPE |
| | | depends on BOARD_TYPE_BREAD_COMPACT_WIFI_LCD || BOARD_TYPE_BREAD_COMPACT_ESP32_LCD || BOARD_TYPE_CGC || BOARD_TYPE_BREAD_COMPACT_WIFI_CAM |
| | | prompt "LCD Type" |
| | | default LCD_ST7789_240X320 |
| | | help |
| | | LCD Display Type |
| | | config LCD_ST7789_240X320 |
| | | bool "ST7789 240*320, IPS" |
| | | config LCD_ST7789_240X320_NO_IPS |
| | | bool "ST7789 240*320, Non-IPS" |
| | | config LCD_ST7789_170X320 |
| | | bool "ST7789 170*320" |
| | | config LCD_ST7789_172X320 |
| | | bool "ST7789 172*320" |
| | | config LCD_ST7789_240X280 |
| | | bool "ST7789 240*280" |
| | | config LCD_ST7789_240X240 |
| | | bool "ST7789 240*240" |
| | | config LCD_ST7789_240X240_7PIN |
| | | bool "ST7789 240*240, 7PIN" |
| | | config LCD_ST7789_240X135 |
| | | bool "ST7789 240*135" |
| | | config LCD_ST7735_128X160 |
| | | bool "ST7735 128*160" |
| | | config LCD_ST7735_128X128 |
| | | bool "ST7735 128*128" |
| | | config LCD_ST7796_320X480 |
| | | bool "ST7796 320*480 IPS" |
| | | config LCD_ST7796_320X480_NO_IPS |
| | | bool "ST7796 320*480, Non-IPS" |
| | | config LCD_ILI9341_240X320 |
| | | bool "ILI9341 240*320" |
| | | config LCD_ILI9341_240X320_NO_IPS |
| | | bool "ILI9341 240*320, Non-IPS" |
| | | config LCD_GC9A01_240X240 |
| | | bool "GC9A01 240*240 Circle" |
| | | config LCD_CUSTOM |
| | | bool "Custom LCD (èªå®ä¹å±å¹åæ°)" |
| | | endchoice |
| | | |
| | | choice DISPLAY_ESP32S3_KORVO2_V3 |
| | | depends on BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_ESP_KORVO2_V3_RNDIS |
| | | prompt "ESP32S3_KORVO2_V3 LCD Type" |
| | | default ESP32S3_KORVO2_V3_LCD_ST7789 |
| | | help |
| | | LCD Display Type |
| | | config ESP32S3_KORVO2_V3_LCD_ST7789 |
| | | bool "ST7789 240*280" |
| | | config ESP32S3_KORVO2_V3_LCD_ILI9341 |
| | | bool "ILI9341 240*320" |
| | | endchoice |
| | | |
| | | choice DISPLAY_ESP32S3_CAM_XXXX |
| | | depends on BOARD_TYPE_WAVESHARE_ESP32_S3_CAM_XXXX |
| | | prompt "ESP32S3_CAM LCD Type" |
| | | default BSP_LCD_SIZE_2INCH |
| | | help |
| | | LCD Display Type |
| | | config BSP_LCD_SIZE_2INCH |
| | | bool "2inch Touch LCD Module" |
| | | |
| | | config BSP_LCD_SIZE_2_8INCH |
| | | bool "2.8inch Touch LCD Module" |
| | | |
| | | config BSP_LCD_SIZE_3_5INCH |
| | | bool "3.5inch Touch LCD Module" |
| | | |
| | | config BSP_LCD_SIZE_1_83INCH |
| | | bool "1.83inch Touch LCD Module" |
| | | endchoice |
| | | |
| | | choice DISPLAY_ESP32S3_AUDIO_BOARD |
| | | depends on BOARD_TYPE_WAVESHARE_ESP32_S3_AUDIO_BOARD |
| | | prompt "ESP32S3_AUDIO_BOARD LCD Type" |
| | | default AUDIO_BOARD_LCD_JD9853 |
| | | help |
| | | LCD Display Type |
| | | config AUDIO_BOARD_LCD_JD9853 |
| | | bool "JD9853 320*172" |
| | | config AUDIO_BOARD_LCD_ST7789 |
| | | bool "ST7789 240*320" |
| | | endchoice |
| | | |
| | | choice DISPLAY_ESP32S3_TOUCH_LCD_1_85C |
| | | depends on BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85C |
| | | prompt "ESP32S3_TOUCH_LCD_1_85C version" |
| | | default VERSION_2_0 |
| | | help |
| | | hardware version |
| | | config VERSION_1_0 |
| | | bool "version 1.0" |
| | | config VERSION_2_0 |
| | | bool "version 2.0" |
| | | endchoice |
| | | |
| | | |
| | | choice DISPLAY_STYLE |
| | | prompt "Select display style" |
| | | default USE_DEFAULT_MESSAGE_STYLE |
| | | help |
| | | Select display style for Xiaozhi device |
| | | |
| | | config USE_DEFAULT_MESSAGE_STYLE |
| | | bool "Enable default message style" |
| | | |
| | | config USE_WECHAT_MESSAGE_STYLE |
| | | bool "Enable WeChat Message Style" |
| | | |
| | | config USE_EMOTE_MESSAGE_STYLE |
| | | bool "Emote animation style" |
| | | depends on BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_3 \ |
| | | || BOARD_TYPE_ESP_VOCAT || BOARD_TYPE_LICHUANG_DEV_S3 \ |
| | | || BOARD_TYPE_ESP_SENSAIRSHUTTLE |
| | | endchoice |
| | | |
| | | config USE_MULTILINE_CHAT_MESSAGE |
| | | bool "Use multiline chat message display (default mode only)" |
| | | depends on USE_DEFAULT_MESSAGE_STYLE |
| | | default n |
| | | help |
| | | When enabled, the chat message area in the default display mode shows |
| | | multiple wrapped lines that grow upward from the bottom of the screen, |
| | | with auto-adaptive height. |
| | | When disabled (default), a single-line horizontally scrolling label |
| | | is shown at the bottom of the screen. |
| | | |
| | | choice WAKE_WORD_TYPE |
| | | prompt "Wake Word Implementation Type" |
| | | default USE_AFE_WAKE_WORD if (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM |
| | | default WAKE_WORD_DISABLED |
| | | help |
| | | Choose the type of wake word implementation to use |
| | | |
| | | config WAKE_WORD_DISABLED |
| | | bool "Disabled" |
| | | help |
| | | Disable wake word detection |
| | | |
| | | config USE_ESP_WAKE_WORD |
| | | bool "Wakenet model without AFE" |
| | | depends on IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C5 || IDF_TARGET_ESP32C6 || (IDF_TARGET_ESP32 && SPIRAM) |
| | | help |
| | | Support ESP32 C3ãESP32 C5 ä¸ ESP32 C6, and (ESP32 with PSRAM) |
| | | |
| | | config USE_AFE_WAKE_WORD |
| | | bool "Wakenet model with AFE" |
| | | depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM |
| | | help |
| | | Support AEC if available, requires ESP32 S3 and PSRAM |
| | | |
| | | config USE_CUSTOM_WAKE_WORD |
| | | bool "Multinet model (Custom Wake Word)" |
| | | depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM |
| | | help |
| | | Requires ESP32 S3 and PSRAM |
| | | |
| | | endchoice |
| | | |
| | | config CUSTOM_WAKE_WORD |
| | | string "Custom Wake Word" |
| | | default "xiao tu dou" |
| | | depends on USE_CUSTOM_WAKE_WORD |
| | | help |
| | | Custom Wake Word, use pinyin for Chinese, separated by spaces |
| | | |
| | | config CUSTOM_WAKE_WORD_DISPLAY |
| | | string "Custom Wake Word Display" |
| | | default "å°åè±" |
| | | depends on USE_CUSTOM_WAKE_WORD |
| | | help |
| | | Greeting sent to the server after wake word detection |
| | | |
| | | config CUSTOM_WAKE_WORD_THRESHOLD |
| | | int "Custom Wake Word Threshold (%)" |
| | | default 20 |
| | | range 1 99 |
| | | depends on USE_CUSTOM_WAKE_WORD |
| | | help |
| | | Custom Wake Word Threshold, range 1-99, the smaller the more sensitive, default 20 |
| | | |
| | | config SEND_WAKE_WORD_DATA |
| | | bool "Send Wake Word Data" |
| | | default y |
| | | depends on USE_AFE_WAKE_WORD || USE_CUSTOM_WAKE_WORD |
| | | help |
| | | Send wake word data to the server as the first message of the conversation and wait for response |
| | | |
| | | config WAKE_WORD_DETECTION_IN_LISTENING |
| | | bool "Enable Wake Word Detection in Listening Mode" |
| | | default n |
| | | depends on USE_AFE_WAKE_WORD || USE_CUSTOM_WAKE_WORD |
| | | help |
| | | Enable wake word detection while in listening mode. |
| | | When enabled, the device can detect wake word during listening, |
| | | which allows interrupting the current conversation. |
| | | When disabled (default), wake word detection is turned off during listening. |
| | | |
| | | config USE_AUDIO_PROCESSOR |
| | | bool "Enable Audio Noise Reduction" |
| | | default y |
| | | depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM |
| | | help |
| | | Requires ESP32 S3 and PSRAM |
| | | |
| | | config USE_DEVICE_AEC |
| | | bool "Enable Device-Side AEC" |
| | | default n |
| | | depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE \ |
| | | || BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75|| BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75C || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_83\ |
| | | || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_2_06 || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4B || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4B || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4_3 \ |
| | | || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7B \ |
| | | || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_3_4C || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4C || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3 \ |
| | | || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7 || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_8 || BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_10_1 \ |
| | | || BOARD_TYPE_ESP_VOCAT || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_49 || BOARD_TYPE_WAVESHARE_ESP32_S3_RLCD_4_2 || BOARD_TYPE_ZHENGCHEN_CAM || BOARD_TYPE_ZHENGCHEN_CAM_ML307 \ |
| | | || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4_3C || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_54 || BOARD_TYPE_WAVESHARE_ESP32_S3_LCD_0_85 || BOARD_TYPE_AI_VOX3 || BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_43C) |
| | | help |
| | | To work properly, device-side AEC requires a clean output reference path from the speaker signal and physical acoustic isolation between the microphone and speaker. |
| | | |
| | | config USE_SERVER_AEC |
| | | bool "Enable Server-Side AEC (Unstable)" |
| | | default n |
| | | depends on USE_AUDIO_PROCESSOR |
| | | help |
| | | To work perperly, server-side AEC requires server support |
| | | |
| | | config USE_AUDIO_DEBUGGER |
| | | bool "Enable Audio Debugger" |
| | | default n |
| | | help |
| | | Enable audio debugger, send audio data through UDP to the host machine |
| | | |
| | | menu "WiFi Configuration Method" |
| | | help |
| | | WiFi Configuration Method Selection |
| | | config USE_HOTSPOT_WIFI_PROVISIONING |
| | | bool "Hotspot" |
| | | default y |
| | | help |
| | | Use WiFi Hotspot to transmit WiFi configuration data |
| | | config USE_ACOUSTIC_WIFI_PROVISIONING |
| | | bool "Acoustic" |
| | | help |
| | | Use audio signal to transmit WiFi configuration data |
| | | |
| | | config USE_ESP_BLUFI_WIFI_PROVISIONING |
| | | bool "Esp Blufi" |
| | | help |
| | | Use esp blufi protocol to transmit WiFi configuration data |
| | | select BT_ENABLED |
| | | select BT_BLE_42_FEATURES_SUPPORTED |
| | | select BT_BLE_BLUFI_ENABLE |
| | | select MBEDTLS_DHM_C |
| | | endmenu |
| | | |
| | | config AUDIO_DEBUG_UDP_SERVER |
| | | string "Audio Debug UDP Server Address" |
| | | default "192.168.2.100:8000" |
| | | depends on USE_AUDIO_DEBUGGER |
| | | help |
| | | UDP server address, format: IP:PORT, used to receive audio debugging data |
| | | |
| | | config RECEIVE_CUSTOM_MESSAGE |
| | | bool "Enable Custom Message Reception" |
| | | default n |
| | | help |
| | | Enable custom message reception, allow the device to receive custom messages from the server (preferably through the MQTT protocol) |
| | | |
| | | menu "Camera Configuration" |
| | | depends on !IDF_TARGET_ESP32 |
| | | |
| | | comment "Warning: Please read the help text before modifying these settings." |
| | | |
| | | config XIAOZHI_CAMERA_ALLOW_JPEG_INPUT |
| | | bool "Allow JPEG Input" |
| | | default n |
| | | help |
| | | Allow JPEG Input format for the camera. |
| | | |
| | | This option may need to be enabled when using a USB camera. |
| | | |
| | | Not currently supported when used simultaneously with XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE. |
| | | |
| | | config XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER |
| | | bool "Enable Hardware JPEG Encoder" |
| | | default y |
| | | depends on SOC_JPEG_ENCODE_SUPPORTED |
| | | help |
| | | Use hardware JPEG encoder on ESP32-P4 to encode image to JPEG. |
| | | See https://docs.espressif.com/projects/esp-idf/en/stable/esp32p4/api-reference/peripherals/jpeg.html for more details. |
| | | |
| | | config XIAOZHI_ENABLE_HARDWARE_JPEG_DECODER |
| | | bool "Enable Hardware JPEG Decoder" |
| | | default n |
| | | depends on SOC_JPEG_DECODE_SUPPORTED && XIAOZHI_CAMERA_ALLOW_JPEG_INPUT |
| | | help |
| | | Use hardware JPEG decoder on ESP32-P4 to decode JPEG to image. |
| | | See https://docs.espressif.com/projects/esp-idf/en/stable/esp32p4/api-reference/peripherals/jpeg.html for more details. |
| | | |
| | | config XIAOZHI_ENABLE_CAMERA_DEBUG_MODE |
| | | bool "Enable Camera Debug Mode" |
| | | default n |
| | | help |
| | | Enable camera debug mode, print camera debug information to the console. |
| | | Only works on boards that support camera. |
| | | |
| | | config XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP |
| | | bool "Enable software camera buffer endianness swapping" |
| | | default n |
| | | depends on !CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER |
| | | help |
| | | This option treats the camera buffer as a uint16_t[] array and performs byte-swapping (endianness conversion) on each element. |
| | | |
| | | Should only be modified by development board integration engineers. |
| | | |
| | | **Incorrect usage may result in incorrect image colors!** |
| | | |
| | | ATTENTION: If the option CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER is available for your sensor, please use that instead. |
| | | |
| | | menuconfig XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE |
| | | bool "Enable Camera Image Rotation" |
| | | default n |
| | | depends on !XIAOZHI_CAMERA_ALLOW_JPEG_INPUT |
| | | help |
| | | Enable camera image rotation, rotate the camera image to the correct orientation. |
| | | - On ESP32-P4, rotation is handled by PPA hardware. |
| | | - On other chips, rotation is done in software with performance cost. |
| | | - For 180° rotation, use HFlip + VFlip instead of this option. |
| | | |
| | | Not currently supported when used simultaneously with XIAOZHI_CAMERA_ALLOW_JPEG_INPUT. |
| | | |
| | | if XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE |
| | | choice XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE |
| | | prompt "Camera Image Rotation Angle (clockwise)" |
| | | default XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_90 |
| | | help |
| | | Camera image rotation angle. |
| | | config XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_90 |
| | | bool "90°" |
| | | config XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_270 |
| | | bool "270°" |
| | | comment "For 180° rotation, use HFlip + VFlip instead of this option" |
| | | endchoice |
| | | endif |
| | | endmenu |
| | | |
| | | menu "TAIJIPAI_S3_CONFIG" |
| | | depends on BOARD_TYPE_TAIJI_PI_S3 |
| | | choice I2S_TYPE_TAIJIPI_S3 |
| | | prompt "taiji-pi-S3 I2S Type" |
| | | default TAIJIPAI_I2S_TYPE_STD |
| | | help |
| | | I2S ç±»åéæ© |
| | | config TAIJIPAI_I2S_TYPE_STD |
| | | bool "I2S Type STD" |
| | | config TAIJIPAI_I2S_TYPE_PDM |
| | | bool "I2S Type PDM" |
| | | endchoice |
| | | |
| | | config I2S_USE_2SLOT |
| | | bool "Enable I2S 2 Slot" |
| | | default y |
| | | help |
| | | å¯å¨å声é |
| | | endmenu |
| | | |
| | | endmenu |
| New file |
| | |
| | | #include "application.h" |
| | | #include "board.h" |
| | | #include "display.h" |
| | | #include "system_info.h" |
| | | #include "audio_codec.h" |
| | | #include "mqtt_protocol.h" |
| | | #include "websocket_protocol.h" |
| | | #include "assets/lang_config.h" |
| | | #include "mcp_server.h" |
| | | #include "assets.h" |
| | | #include "settings.h" |
| | | |
| | | #include <cstring> |
| | | #include <esp_log.h> |
| | | #include <cJSON.h> |
| | | #include <driver/gpio.h> |
| | | #include <arpa/inet.h> |
| | | #include <font_awesome.h> |
| | | |
| | | #define TAG "Application" |
| | | |
| | | |
| | | Application::Application() { |
| | | event_group_ = xEventGroupCreate(); |
| | | |
| | | #if CONFIG_USE_DEVICE_AEC && CONFIG_USE_SERVER_AEC |
| | | #error "CONFIG_USE_DEVICE_AEC and CONFIG_USE_SERVER_AEC cannot be enabled at the same time" |
| | | #elif CONFIG_USE_DEVICE_AEC |
| | | aec_mode_ = kAecOnDeviceSide; |
| | | #elif CONFIG_USE_SERVER_AEC |
| | | aec_mode_ = kAecOnServerSide; |
| | | #else |
| | | aec_mode_ = kAecOff; |
| | | #endif |
| | | |
| | | esp_timer_create_args_t clock_timer_args = { |
| | | .callback = [](void* arg) { |
| | | Application* app = (Application*)arg; |
| | | xEventGroupSetBits(app->event_group_, MAIN_EVENT_CLOCK_TICK); |
| | | }, |
| | | .arg = this, |
| | | .dispatch_method = ESP_TIMER_TASK, |
| | | .name = "clock_timer", |
| | | .skip_unhandled_events = true |
| | | }; |
| | | esp_timer_create(&clock_timer_args, &clock_timer_handle_); |
| | | } |
| | | |
| | | Application::~Application() { |
| | | if (clock_timer_handle_ != nullptr) { |
| | | esp_timer_stop(clock_timer_handle_); |
| | | esp_timer_delete(clock_timer_handle_); |
| | | } |
| | | vEventGroupDelete(event_group_); |
| | | } |
| | | |
| | | bool Application::SetDeviceState(DeviceState state) { |
| | | return state_machine_.TransitionTo(state); |
| | | } |
| | | |
| | | void Application::Initialize() { |
| | | auto& board = Board::GetInstance(); |
| | | SetDeviceState(kDeviceStateStarting); |
| | | |
| | | // Setup the display |
| | | auto display = board.GetDisplay(); |
| | | display->SetupUI(); |
| | | // Print board name/version info |
| | | display->SetChatMessage("system", SystemInfo::GetUserAgent().c_str()); |
| | | |
| | | // Setup the audio service |
| | | auto codec = board.GetAudioCodec(); |
| | | audio_service_.Initialize(codec); |
| | | audio_service_.Start(); |
| | | |
| | | AudioServiceCallbacks callbacks; |
| | | callbacks.on_send_queue_available = [this]() { |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_SEND_AUDIO); |
| | | }; |
| | | callbacks.on_wake_word_detected = [this](const std::string& wake_word) { |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_WAKE_WORD_DETECTED); |
| | | }; |
| | | callbacks.on_vad_change = [this](bool speaking) { |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_VAD_CHANGE); |
| | | }; |
| | | audio_service_.SetCallbacks(callbacks); |
| | | |
| | | // Add state change listeners |
| | | state_machine_.AddStateChangeListener([this](DeviceState old_state, DeviceState new_state) { |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_STATE_CHANGED); |
| | | }); |
| | | |
| | | // Start the clock timer to update the status bar |
| | | esp_timer_start_periodic(clock_timer_handle_, 1000000); |
| | | |
| | | // Add MCP common tools (only once during initialization) |
| | | auto& mcp_server = McpServer::GetInstance(); |
| | | mcp_server.AddCommonTools(); |
| | | mcp_server.AddUserOnlyTools(); |
| | | |
| | | // Set network event callback for UI updates and network state handling |
| | | board.SetNetworkEventCallback([this](NetworkEvent event, const std::string& data) { |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | |
| | | switch (event) { |
| | | case NetworkEvent::Scanning: |
| | | display->ShowNotification(Lang::Strings::SCANNING_WIFI, 30000); |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_NETWORK_DISCONNECTED); |
| | | break; |
| | | case NetworkEvent::Connecting: { |
| | | if (data.empty()) { |
| | | // Cellular network - registering without carrier info yet |
| | | display->SetStatus(Lang::Strings::REGISTERING_NETWORK); |
| | | } else { |
| | | // WiFi or cellular with carrier info |
| | | std::string msg = Lang::Strings::CONNECT_TO; |
| | | msg += data; |
| | | msg += "..."; |
| | | display->ShowNotification(msg.c_str(), 30000); |
| | | } |
| | | break; |
| | | } |
| | | case NetworkEvent::Connected: { |
| | | std::string msg = Lang::Strings::CONNECTED_TO; |
| | | msg += data; |
| | | display->ShowNotification(msg.c_str(), 30000); |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_NETWORK_CONNECTED); |
| | | break; |
| | | } |
| | | case NetworkEvent::Disconnected: |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_NETWORK_DISCONNECTED); |
| | | break; |
| | | case NetworkEvent::WifiConfigModeEnter: |
| | | // WiFi config mode enter is handled by WifiBoard internally |
| | | break; |
| | | case NetworkEvent::WifiConfigModeExit: |
| | | // WiFi config mode exit is handled by WifiBoard internally |
| | | break; |
| | | // Cellular modem specific events |
| | | case NetworkEvent::ModemDetecting: |
| | | display->SetStatus(Lang::Strings::DETECTING_MODULE); |
| | | break; |
| | | case NetworkEvent::ModemErrorNoSim: |
| | | Alert(Lang::Strings::ERROR, Lang::Strings::PIN_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_PIN); |
| | | break; |
| | | case NetworkEvent::ModemErrorRegDenied: |
| | | Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_REG); |
| | | break; |
| | | case NetworkEvent::ModemErrorInitFailed: |
| | | Alert(Lang::Strings::ERROR, Lang::Strings::MODEM_INIT_ERROR, "triangle_exclamation", Lang::Sounds::OGG_EXCLAMATION); |
| | | break; |
| | | case NetworkEvent::ModemErrorTimeout: |
| | | display->SetStatus(Lang::Strings::REGISTERING_NETWORK); |
| | | break; |
| | | } |
| | | }); |
| | | |
| | | // Start network asynchronously |
| | | board.StartNetwork(); |
| | | |
| | | // Update the status bar immediately to show the network state |
| | | display->UpdateStatusBar(true); |
| | | } |
| | | |
| | | void Application::Run() { |
| | | // Set the priority of the main task to 10 |
| | | vTaskPrioritySet(nullptr, 10); |
| | | |
| | | const EventBits_t ALL_EVENTS = |
| | | MAIN_EVENT_SCHEDULE | |
| | | MAIN_EVENT_SEND_AUDIO | |
| | | MAIN_EVENT_WAKE_WORD_DETECTED | |
| | | MAIN_EVENT_VAD_CHANGE | |
| | | MAIN_EVENT_CLOCK_TICK | |
| | | MAIN_EVENT_ERROR | |
| | | MAIN_EVENT_NETWORK_CONNECTED | |
| | | MAIN_EVENT_NETWORK_DISCONNECTED | |
| | | MAIN_EVENT_TOGGLE_CHAT | |
| | | MAIN_EVENT_START_LISTENING | |
| | | MAIN_EVENT_STOP_LISTENING | |
| | | MAIN_EVENT_ACTIVATION_DONE | |
| | | MAIN_EVENT_STATE_CHANGED; |
| | | |
| | | while (true) { |
| | | auto bits = xEventGroupWaitBits(event_group_, ALL_EVENTS, pdTRUE, pdFALSE, portMAX_DELAY); |
| | | |
| | | if (bits & MAIN_EVENT_ERROR) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | Alert(Lang::Strings::ERROR, last_error_message_.c_str(), "circle_xmark", Lang::Sounds::OGG_EXCLAMATION); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_NETWORK_CONNECTED) { |
| | | HandleNetworkConnectedEvent(); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_NETWORK_DISCONNECTED) { |
| | | HandleNetworkDisconnectedEvent(); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_ACTIVATION_DONE) { |
| | | HandleActivationDoneEvent(); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_STATE_CHANGED) { |
| | | HandleStateChangedEvent(); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_TOGGLE_CHAT) { |
| | | HandleToggleChatEvent(); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_START_LISTENING) { |
| | | HandleStartListeningEvent(); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_STOP_LISTENING) { |
| | | HandleStopListeningEvent(); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_SEND_AUDIO) { |
| | | while (auto packet = audio_service_.PopPacketFromSendQueue()) { |
| | | if (protocol_ && !protocol_->SendAudio(std::move(packet))) { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_WAKE_WORD_DETECTED) { |
| | | HandleWakeWordDetectedEvent(); |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_VAD_CHANGE) { |
| | | if (GetDeviceState() == kDeviceStateListening) { |
| | | auto led = Board::GetInstance().GetLed(); |
| | | led->OnStateChanged(); |
| | | } |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_SCHEDULE) { |
| | | std::unique_lock<std::mutex> lock(mutex_); |
| | | auto tasks = std::move(main_tasks_); |
| | | lock.unlock(); |
| | | for (auto& task : tasks) { |
| | | task(); |
| | | } |
| | | } |
| | | |
| | | if (bits & MAIN_EVENT_CLOCK_TICK) { |
| | | clock_ticks_++; |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | display->UpdateStatusBar(); |
| | | |
| | | // Print debug info every 10 seconds |
| | | if (clock_ticks_ % 10 == 0) { |
| | | SystemInfo::PrintHeapStats(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | void Application::HandleNetworkConnectedEvent() { |
| | | ESP_LOGI(TAG, "Network connected"); |
| | | auto state = GetDeviceState(); |
| | | |
| | | if (state == kDeviceStateStarting || state == kDeviceStateWifiConfiguring) { |
| | | // Network is ready, start activation |
| | | SetDeviceState(kDeviceStateActivating); |
| | | if (activation_task_handle_ != nullptr) { |
| | | ESP_LOGW(TAG, "Activation task already running"); |
| | | return; |
| | | } |
| | | |
| | | xTaskCreate([](void* arg) { |
| | | Application* app = static_cast<Application*>(arg); |
| | | app->ActivationTask(); |
| | | app->activation_task_handle_ = nullptr; |
| | | vTaskDelete(NULL); |
| | | }, "activation", 4096 * 2, this, 2, &activation_task_handle_); |
| | | } |
| | | |
| | | // Update the status bar immediately to show the network state |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | display->UpdateStatusBar(true); |
| | | } |
| | | |
| | | void Application::HandleNetworkDisconnectedEvent() { |
| | | // Close current conversation when network disconnected |
| | | auto state = GetDeviceState(); |
| | | if (state == kDeviceStateConnecting || state == kDeviceStateListening || state == kDeviceStateSpeaking) { |
| | | ESP_LOGI(TAG, "Closing audio channel due to network disconnection"); |
| | | protocol_->CloseAudioChannel(); |
| | | } |
| | | |
| | | // Update the status bar immediately to show the network state |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | display->UpdateStatusBar(true); |
| | | } |
| | | |
| | | void Application::HandleActivationDoneEvent() { |
| | | ESP_LOGI(TAG, "Activation done"); |
| | | |
| | | SystemInfo::PrintHeapStats(); |
| | | SetDeviceState(kDeviceStateIdle); |
| | | |
| | | has_server_time_ = ota_->HasServerTime(); |
| | | |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | std::string message = std::string(Lang::Strings::VERSION) + ota_->GetCurrentVersion(); |
| | | display->ShowNotification(message.c_str()); |
| | | display->SetChatMessage("system", ""); |
| | | |
| | | // Release OTA object after activation is complete |
| | | ota_.reset(); |
| | | auto& board = Board::GetInstance(); |
| | | board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); |
| | | |
| | | Schedule([this]() { |
| | | // Play the success sound to indicate the device is ready |
| | | audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS); |
| | | }); |
| | | } |
| | | |
| | | void Application::ActivationTask() { |
| | | // Create OTA object for activation process |
| | | ota_ = std::make_unique<Ota>(); |
| | | |
| | | // Check for new assets version |
| | | CheckAssetsVersion(); |
| | | |
| | | // Check for new firmware version |
| | | CheckNewVersion(); |
| | | |
| | | // Initialize the protocol |
| | | InitializeProtocol(); |
| | | |
| | | // Signal completion to main loop |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_ACTIVATION_DONE); |
| | | } |
| | | |
| | | void Application::CheckAssetsVersion() { |
| | | // Only allow CheckAssetsVersion to be called once |
| | | if (assets_version_checked_) { |
| | | return; |
| | | } |
| | | assets_version_checked_ = true; |
| | | |
| | | auto& board = Board::GetInstance(); |
| | | auto display = board.GetDisplay(); |
| | | auto& assets = Assets::GetInstance(); |
| | | |
| | | if (!assets.partition_valid()) { |
| | | ESP_LOGW(TAG, "Assets partition is disabled for board %s", BOARD_NAME); |
| | | return; |
| | | } |
| | | |
| | | Settings settings("assets", true); |
| | | // Check if there is a new assets need to be downloaded |
| | | std::string download_url = settings.GetString("download_url"); |
| | | |
| | | if (!download_url.empty()) { |
| | | settings.EraseKey("download_url"); |
| | | |
| | | char message[256]; |
| | | snprintf(message, sizeof(message), Lang::Strings::FOUND_NEW_ASSETS, download_url.c_str()); |
| | | Alert(Lang::Strings::LOADING_ASSETS, message, "cloud_arrow_down", Lang::Sounds::OGG_UPGRADE); |
| | | |
| | | // Wait for the audio service to be idle for 3 seconds |
| | | vTaskDelay(pdMS_TO_TICKS(3000)); |
| | | SetDeviceState(kDeviceStateUpgrading); |
| | | board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | display->SetChatMessage("system", Lang::Strings::PLEASE_WAIT); |
| | | |
| | | bool success = assets.Download(download_url, [this, display](int progress, size_t speed) -> void { |
| | | char buffer[32]; |
| | | snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024); |
| | | Schedule([display, message = std::string(buffer)]() { |
| | | display->SetChatMessage("system", message.c_str()); |
| | | }); |
| | | }); |
| | | |
| | | board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); |
| | | vTaskDelay(pdMS_TO_TICKS(1000)); |
| | | |
| | | if (!success) { |
| | | Alert(Lang::Strings::ERROR, Lang::Strings::DOWNLOAD_ASSETS_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION); |
| | | vTaskDelay(pdMS_TO_TICKS(2000)); |
| | | SetDeviceState(kDeviceStateActivating); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // Apply assets |
| | | assets.Apply(); |
| | | display->SetChatMessage("system", ""); |
| | | display->SetEmotion("microchip_ai"); |
| | | } |
| | | |
| | | void Application::CheckNewVersion() { |
| | | const int MAX_RETRY = 10; |
| | | int retry_count = 0; |
| | | int retry_delay = 10; // Initial retry delay in seconds |
| | | |
| | | auto& board = Board::GetInstance(); |
| | | while (true) { |
| | | auto display = board.GetDisplay(); |
| | | display->SetStatus(Lang::Strings::CHECKING_NEW_VERSION); |
| | | |
| | | esp_err_t err = ota_->CheckVersion(); |
| | | if (err != ESP_OK) { |
| | | retry_count++; |
| | | if (retry_count >= MAX_RETRY) { |
| | | ESP_LOGE(TAG, "Too many retries, exit version check"); |
| | | return; |
| | | } |
| | | |
| | | char error_message[128]; |
| | | snprintf(error_message, sizeof(error_message), "code=%d, url=%s", err, ota_->GetCheckVersionUrl().c_str()); |
| | | char buffer[256]; |
| | | snprintf(buffer, sizeof(buffer), Lang::Strings::CHECK_NEW_VERSION_FAILED, retry_delay, error_message); |
| | | Alert(Lang::Strings::ERROR, buffer, "cloud_slash", Lang::Sounds::OGG_EXCLAMATION); |
| | | |
| | | ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", retry_delay, retry_count, MAX_RETRY); |
| | | for (int i = 0; i < retry_delay; i++) { |
| | | vTaskDelay(pdMS_TO_TICKS(1000)); |
| | | if (GetDeviceState() == kDeviceStateIdle) { |
| | | break; |
| | | } |
| | | } |
| | | retry_delay *= 2; // Double the retry delay |
| | | continue; |
| | | } |
| | | retry_count = 0; |
| | | retry_delay = 10; // Reset retry delay |
| | | |
| | | if (ota_->HasNewVersion()) { |
| | | if (UpgradeFirmware(ota_->GetFirmwareUrl(), ota_->GetFirmwareVersion())) { |
| | | return; // This line will never be reached after reboot |
| | | } |
| | | // If upgrade failed, continue to normal operation |
| | | } |
| | | |
| | | // No new version, mark the current version as valid |
| | | ota_->MarkCurrentVersionValid(); |
| | | if (!ota_->HasActivationCode() && !ota_->HasActivationChallenge()) { |
| | | // Exit the loop if done checking new version |
| | | break; |
| | | } |
| | | |
| | | display->SetStatus(Lang::Strings::ACTIVATION); |
| | | // Activation code is shown to the user and waiting for the user to input |
| | | if (ota_->HasActivationCode()) { |
| | | ShowActivationCode(ota_->GetActivationCode(), ota_->GetActivationMessage()); |
| | | } |
| | | |
| | | // This will block the loop until the activation is done or timeout |
| | | for (int i = 0; i < 10; ++i) { |
| | | ESP_LOGI(TAG, "Activating... %d/%d", i + 1, 10); |
| | | esp_err_t err = ota_->Activate(); |
| | | if (err == ESP_OK) { |
| | | break; |
| | | } else if (err == ESP_ERR_TIMEOUT) { |
| | | vTaskDelay(pdMS_TO_TICKS(3000)); |
| | | } else { |
| | | vTaskDelay(pdMS_TO_TICKS(10000)); |
| | | } |
| | | if (GetDeviceState() == kDeviceStateIdle) { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | void Application::InitializeProtocol() { |
| | | auto& board = Board::GetInstance(); |
| | | auto display = board.GetDisplay(); |
| | | auto codec = board.GetAudioCodec(); |
| | | |
| | | display->SetStatus(Lang::Strings::LOADING_PROTOCOL); |
| | | |
| | | if (ota_->HasMqttConfig()) { |
| | | protocol_ = std::make_unique<MqttProtocol>(); |
| | | } else if (ota_->HasWebsocketConfig()) { |
| | | protocol_ = std::make_unique<WebsocketProtocol>(); |
| | | } else { |
| | | ESP_LOGW(TAG, "No protocol specified in the OTA config, using MQTT"); |
| | | protocol_ = std::make_unique<MqttProtocol>(); |
| | | } |
| | | |
| | | protocol_->OnConnected([this]() { |
| | | DismissAlert(); |
| | | }); |
| | | |
| | | protocol_->OnNetworkError([this](const std::string& message) { |
| | | last_error_message_ = message; |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_ERROR); |
| | | }); |
| | | |
| | | protocol_->OnIncomingAudio([this](std::unique_ptr<AudioStreamPacket> packet) { |
| | | if (GetDeviceState() == kDeviceStateSpeaking) { |
| | | audio_service_.PushPacketToDecodeQueue(std::move(packet)); |
| | | } |
| | | }); |
| | | |
| | | protocol_->OnAudioChannelOpened([this, codec, &board]() { |
| | | board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | if (protocol_->server_sample_rate() != codec->output_sample_rate()) { |
| | | ESP_LOGW(TAG, "Server sample rate %d does not match device output sample rate %d, resampling may cause distortion", |
| | | protocol_->server_sample_rate(), codec->output_sample_rate()); |
| | | } |
| | | }); |
| | | |
| | | protocol_->OnAudioChannelClosed([this, &board]() { |
| | | board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); |
| | | Schedule([this]() { |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | display->SetChatMessage("system", ""); |
| | | SetDeviceState(kDeviceStateIdle); |
| | | }); |
| | | }); |
| | | |
| | | protocol_->OnIncomingJson([this, display](const cJSON* root) { |
| | | // Parse JSON data |
| | | auto type = cJSON_GetObjectItem(root, "type"); |
| | | if (strcmp(type->valuestring, "tts") == 0) { |
| | | auto state = cJSON_GetObjectItem(root, "state"); |
| | | if (strcmp(state->valuestring, "start") == 0) { |
| | | Schedule([this]() { |
| | | aborted_ = false; |
| | | SetDeviceState(kDeviceStateSpeaking); |
| | | }); |
| | | } else if (strcmp(state->valuestring, "stop") == 0) { |
| | | Schedule([this]() { |
| | | if (GetDeviceState() == kDeviceStateSpeaking) { |
| | | if (listening_mode_ == kListeningModeManualStop) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } else { |
| | | SetDeviceState(kDeviceStateListening); |
| | | } |
| | | } |
| | | }); |
| | | } else if (strcmp(state->valuestring, "sentence_start") == 0) { |
| | | auto text = cJSON_GetObjectItem(root, "text"); |
| | | if (cJSON_IsString(text)) { |
| | | ESP_LOGI(TAG, "<< %s", text->valuestring); |
| | | Schedule([display, message = std::string(text->valuestring)]() { |
| | | display->SetChatMessage("assistant", message.c_str()); |
| | | }); |
| | | } |
| | | } |
| | | } else if (strcmp(type->valuestring, "stt") == 0) { |
| | | auto text = cJSON_GetObjectItem(root, "text"); |
| | | if (cJSON_IsString(text)) { |
| | | ESP_LOGI(TAG, ">> %s", text->valuestring); |
| | | Schedule([display, message = std::string(text->valuestring)]() { |
| | | display->SetChatMessage("user", message.c_str()); |
| | | }); |
| | | } |
| | | } else if (strcmp(type->valuestring, "llm") == 0) { |
| | | auto emotion = cJSON_GetObjectItem(root, "emotion"); |
| | | if (cJSON_IsString(emotion)) { |
| | | Schedule([display, emotion_str = std::string(emotion->valuestring)]() { |
| | | display->SetEmotion(emotion_str.c_str()); |
| | | }); |
| | | } |
| | | } else if (strcmp(type->valuestring, "mcp") == 0) { |
| | | auto payload = cJSON_GetObjectItem(root, "payload"); |
| | | if (cJSON_IsObject(payload)) { |
| | | McpServer::GetInstance().ParseMessage(payload); |
| | | } |
| | | } else if (strcmp(type->valuestring, "system") == 0) { |
| | | auto command = cJSON_GetObjectItem(root, "command"); |
| | | if (cJSON_IsString(command)) { |
| | | ESP_LOGI(TAG, "System command: %s", command->valuestring); |
| | | if (strcmp(command->valuestring, "reboot") == 0) { |
| | | // Do a reboot if user requests a OTA update |
| | | Schedule([this]() { |
| | | Reboot(); |
| | | }); |
| | | } else { |
| | | ESP_LOGW(TAG, "Unknown system command: %s", command->valuestring); |
| | | } |
| | | } |
| | | } else if (strcmp(type->valuestring, "alert") == 0) { |
| | | auto status = cJSON_GetObjectItem(root, "status"); |
| | | auto message = cJSON_GetObjectItem(root, "message"); |
| | | auto emotion = cJSON_GetObjectItem(root, "emotion"); |
| | | if (cJSON_IsString(status) && cJSON_IsString(message) && cJSON_IsString(emotion)) { |
| | | Alert(status->valuestring, message->valuestring, emotion->valuestring, Lang::Sounds::OGG_VIBRATION); |
| | | } else { |
| | | ESP_LOGW(TAG, "Alert command requires status, message and emotion"); |
| | | } |
| | | #if CONFIG_RECEIVE_CUSTOM_MESSAGE |
| | | } else if (strcmp(type->valuestring, "custom") == 0) { |
| | | auto payload = cJSON_GetObjectItem(root, "payload"); |
| | | ESP_LOGI(TAG, "Received custom message: %s", cJSON_PrintUnformatted(root)); |
| | | if (cJSON_IsObject(payload)) { |
| | | Schedule([this, display, payload_str = std::string(cJSON_PrintUnformatted(payload))]() { |
| | | display->SetChatMessage("system", payload_str.c_str()); |
| | | }); |
| | | } else { |
| | | ESP_LOGW(TAG, "Invalid custom message format: missing payload"); |
| | | } |
| | | #endif |
| | | } else { |
| | | ESP_LOGW(TAG, "Unknown message type: %s", type->valuestring); |
| | | } |
| | | }); |
| | | |
| | | protocol_->Start(); |
| | | } |
| | | |
| | | void Application::ShowActivationCode(const std::string& code, const std::string& message) { |
| | | struct digit_sound { |
| | | char digit; |
| | | const std::string_view& sound; |
| | | }; |
| | | static const std::array<digit_sound, 10> digit_sounds{{ |
| | | digit_sound{'0', Lang::Sounds::OGG_0}, |
| | | digit_sound{'1', Lang::Sounds::OGG_1}, |
| | | digit_sound{'2', Lang::Sounds::OGG_2}, |
| | | digit_sound{'3', Lang::Sounds::OGG_3}, |
| | | digit_sound{'4', Lang::Sounds::OGG_4}, |
| | | digit_sound{'5', Lang::Sounds::OGG_5}, |
| | | digit_sound{'6', Lang::Sounds::OGG_6}, |
| | | digit_sound{'7', Lang::Sounds::OGG_7}, |
| | | digit_sound{'8', Lang::Sounds::OGG_8}, |
| | | digit_sound{'9', Lang::Sounds::OGG_9} |
| | | }}; |
| | | |
| | | // This sentence uses 9KB of SRAM, so we need to wait for it to finish |
| | | Alert(Lang::Strings::ACTIVATION, message.c_str(), "link", Lang::Sounds::OGG_ACTIVATION); |
| | | |
| | | for (const auto& digit : code) { |
| | | auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(), |
| | | [digit](const digit_sound& ds) { return ds.digit == digit; }); |
| | | if (it != digit_sounds.end()) { |
| | | audio_service_.PlaySound(it->sound); |
| | | } |
| | | } |
| | | } |
| | | |
| | | void Application::Alert(const char* status, const char* message, const char* emotion, const std::string_view& sound) { |
| | | ESP_LOGW(TAG, "Alert [%s] %s: %s", emotion, status, message); |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | display->SetStatus(status); |
| | | display->SetEmotion(emotion); |
| | | display->SetChatMessage("system", message); |
| | | if (!sound.empty()) { |
| | | audio_service_.PlaySound(sound); |
| | | } |
| | | } |
| | | |
| | | void Application::DismissAlert() { |
| | | if (GetDeviceState() == kDeviceStateIdle) { |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | display->SetStatus(Lang::Strings::STANDBY); |
| | | display->SetEmotion("neutral"); |
| | | display->SetChatMessage("system", ""); |
| | | } |
| | | } |
| | | |
| | | void Application::ToggleChatState() { |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_TOGGLE_CHAT); |
| | | } |
| | | |
| | | void Application::StartListening() { |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_START_LISTENING); |
| | | } |
| | | |
| | | void Application::StopListening() { |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_STOP_LISTENING); |
| | | } |
| | | |
| | | void Application::HandleToggleChatEvent() { |
| | | auto state = GetDeviceState(); |
| | | |
| | | if (state == kDeviceStateActivating) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | return; |
| | | } else if (state == kDeviceStateWifiConfiguring) { |
| | | audio_service_.EnableAudioTesting(true); |
| | | SetDeviceState(kDeviceStateAudioTesting); |
| | | return; |
| | | } else if (state == kDeviceStateAudioTesting) { |
| | | audio_service_.EnableAudioTesting(false); |
| | | SetDeviceState(kDeviceStateWifiConfiguring); |
| | | return; |
| | | } |
| | | |
| | | if (!protocol_) { |
| | | ESP_LOGE(TAG, "Protocol not initialized"); |
| | | return; |
| | | } |
| | | |
| | | if (state == kDeviceStateIdle) { |
| | | ListeningMode mode = GetDefaultListeningMode(); |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | | SetDeviceState(kDeviceStateConnecting); |
| | | // Schedule to let the state change be processed first (UI update) |
| | | Schedule([this, mode]() { |
| | | ContinueOpenAudioChannel(mode); |
| | | }); |
| | | return; |
| | | } |
| | | SetListeningMode(mode); |
| | | } else if (state == kDeviceStateSpeaking) { |
| | | AbortSpeaking(kAbortReasonNone); |
| | | } else if (state == kDeviceStateListening) { |
| | | protocol_->CloseAudioChannel(); |
| | | } |
| | | } |
| | | |
| | | void Application::ContinueOpenAudioChannel(ListeningMode mode) { |
| | | // Check state again in case it was changed during scheduling |
| | | if (GetDeviceState() != kDeviceStateConnecting) { |
| | | return; |
| | | } |
| | | |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | | if (!protocol_->OpenAudioChannel()) { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | SetListeningMode(mode); |
| | | } |
| | | |
| | | void Application::HandleStartListeningEvent() { |
| | | auto state = GetDeviceState(); |
| | | |
| | | if (state == kDeviceStateActivating) { |
| | | SetDeviceState(kDeviceStateIdle); |
| | | return; |
| | | } else if (state == kDeviceStateWifiConfiguring) { |
| | | audio_service_.EnableAudioTesting(true); |
| | | SetDeviceState(kDeviceStateAudioTesting); |
| | | return; |
| | | } |
| | | |
| | | if (!protocol_) { |
| | | ESP_LOGE(TAG, "Protocol not initialized"); |
| | | return; |
| | | } |
| | | |
| | | if (state == kDeviceStateIdle) { |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | | SetDeviceState(kDeviceStateConnecting); |
| | | // Schedule to let the state change be processed first (UI update) |
| | | Schedule([this]() { |
| | | ContinueOpenAudioChannel(kListeningModeManualStop); |
| | | }); |
| | | return; |
| | | } |
| | | SetListeningMode(kListeningModeManualStop); |
| | | } else if (state == kDeviceStateSpeaking) { |
| | | AbortSpeaking(kAbortReasonNone); |
| | | SetListeningMode(kListeningModeManualStop); |
| | | } |
| | | } |
| | | |
| | | void Application::HandleStopListeningEvent() { |
| | | auto state = GetDeviceState(); |
| | | |
| | | if (state == kDeviceStateAudioTesting) { |
| | | audio_service_.EnableAudioTesting(false); |
| | | SetDeviceState(kDeviceStateWifiConfiguring); |
| | | return; |
| | | } else if (state == kDeviceStateListening) { |
| | | if (protocol_) { |
| | | protocol_->SendStopListening(); |
| | | } |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | } |
| | | |
| | | void Application::HandleWakeWordDetectedEvent() { |
| | | if (!protocol_) { |
| | | return; |
| | | } |
| | | |
| | | auto state = GetDeviceState(); |
| | | auto wake_word = audio_service_.GetLastWakeWord(); |
| | | ESP_LOGI(TAG, "Wake word detected: %s (state: %d)", wake_word.c_str(), (int)state); |
| | | |
| | | if (state == kDeviceStateIdle) { |
| | | audio_service_.EncodeWakeWord(); |
| | | auto wake_word = audio_service_.GetLastWakeWord(); |
| | | |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | | SetDeviceState(kDeviceStateConnecting); |
| | | // Schedule to let the state change be processed first (UI update), |
| | | // then continue with OpenAudioChannel which may block for ~1 second |
| | | Schedule([this, wake_word]() { |
| | | ContinueWakeWordInvoke(wake_word); |
| | | }); |
| | | return; |
| | | } |
| | | // Channel already opened, continue directly |
| | | ContinueWakeWordInvoke(wake_word); |
| | | } else if (state == kDeviceStateSpeaking || state == kDeviceStateListening) { |
| | | AbortSpeaking(kAbortReasonWakeWordDetected); |
| | | // Clear send queue to avoid sending residues to server |
| | | while (audio_service_.PopPacketFromSendQueue()); |
| | | |
| | | if (state == kDeviceStateListening) { |
| | | protocol_->SendStartListening(GetDefaultListeningMode()); |
| | | audio_service_.ResetDecoder(); |
| | | audio_service_.PlaySound(Lang::Sounds::OGG_POPUP); |
| | | // Re-enable wake word detection as it was stopped by the detection itself |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | } else { |
| | | // Play popup sound and start listening again |
| | | play_popup_on_listening_ = true; |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | } |
| | | } else if (state == kDeviceStateActivating) { |
| | | // Restart the activation check if the wake word is detected during activation |
| | | SetDeviceState(kDeviceStateIdle); |
| | | } |
| | | } |
| | | |
| | | void Application::ContinueWakeWordInvoke(const std::string& wake_word) { |
| | | // Check state again in case it was changed during scheduling |
| | | if (GetDeviceState() != kDeviceStateConnecting) { |
| | | return; |
| | | } |
| | | |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | | if (!protocol_->OpenAudioChannel()) { |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str()); |
| | | #if CONFIG_SEND_WAKE_WORD_DATA |
| | | // Encode and send the wake word data to the server |
| | | while (auto packet = audio_service_.PopWakeWordPacket()) { |
| | | protocol_->SendAudio(std::move(packet)); |
| | | } |
| | | // Set the chat state to wake word detected |
| | | protocol_->SendWakeWordDetected(wake_word); |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | #else |
| | | // Set flag to play popup sound after state changes to listening |
| | | // (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing) |
| | | play_popup_on_listening_ = true; |
| | | SetListeningMode(GetDefaultListeningMode()); |
| | | #endif |
| | | } |
| | | |
| | | void Application::HandleStateChangedEvent() { |
| | | DeviceState new_state = state_machine_.GetState(); |
| | | clock_ticks_ = 0; |
| | | |
| | | auto& board = Board::GetInstance(); |
| | | auto display = board.GetDisplay(); |
| | | auto led = board.GetLed(); |
| | | led->OnStateChanged(); |
| | | |
| | | switch (new_state) { |
| | | case kDeviceStateUnknown: |
| | | case kDeviceStateIdle: |
| | | display->SetStatus(Lang::Strings::STANDBY); |
| | | display->ClearChatMessages(); // Clear messages first |
| | | display->SetEmotion("neutral"); // Then set emotion (wechat mode checks child count) |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(true); |
| | | break; |
| | | case kDeviceStateConnecting: |
| | | display->SetStatus(Lang::Strings::CONNECTING); |
| | | display->SetEmotion("neutral"); |
| | | display->SetChatMessage("system", ""); |
| | | break; |
| | | case kDeviceStateListening: |
| | | display->SetStatus(Lang::Strings::LISTENING); |
| | | display->SetEmotion("neutral"); |
| | | |
| | | // Make sure the audio processor is running |
| | | if (play_popup_on_listening_ || !audio_service_.IsAudioProcessorRunning()) { |
| | | // For auto mode, wait for playback queue to be empty before enabling voice processing |
| | | // This prevents audio truncation when STOP arrives late due to network jitter |
| | | if (listening_mode_ == kListeningModeAutoStop) { |
| | | audio_service_.WaitForPlaybackQueueEmpty(); |
| | | } |
| | | |
| | | // Send the start listening command |
| | | protocol_->SendStartListening(listening_mode_); |
| | | audio_service_.EnableVoiceProcessing(true); |
| | | } |
| | | |
| | | #ifdef CONFIG_WAKE_WORD_DETECTION_IN_LISTENING |
| | | // Enable wake word detection in listening mode (configured via Kconfig) |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | #else |
| | | // Disable wake word detection in listening mode |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | #endif |
| | | |
| | | // Play popup sound after ResetDecoder (in EnableVoiceProcessing) has been called |
| | | if (play_popup_on_listening_) { |
| | | play_popup_on_listening_ = false; |
| | | audio_service_.PlaySound(Lang::Sounds::OGG_POPUP); |
| | | } |
| | | break; |
| | | case kDeviceStateSpeaking: |
| | | display->SetStatus(Lang::Strings::SPEAKING); |
| | | |
| | | if (listening_mode_ != kListeningModeRealtime) { |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | // Only AFE wake word can be detected in speaking mode |
| | | audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord()); |
| | | } |
| | | audio_service_.ResetDecoder(); |
| | | break; |
| | | case kDeviceStateWifiConfiguring: |
| | | audio_service_.EnableVoiceProcessing(false); |
| | | audio_service_.EnableWakeWordDetection(false); |
| | | break; |
| | | default: |
| | | // Do nothing |
| | | break; |
| | | } |
| | | } |
| | | |
| | | void Application::Schedule(std::function<void()>&& callback) { |
| | | { |
| | | std::lock_guard<std::mutex> lock(mutex_); |
| | | main_tasks_.push_back(std::move(callback)); |
| | | } |
| | | xEventGroupSetBits(event_group_, MAIN_EVENT_SCHEDULE); |
| | | } |
| | | |
| | | void Application::AbortSpeaking(AbortReason reason) { |
| | | ESP_LOGI(TAG, "Abort speaking"); |
| | | aborted_ = true; |
| | | if (protocol_) { |
| | | protocol_->SendAbortSpeaking(reason); |
| | | } |
| | | } |
| | | |
| | | void Application::SetListeningMode(ListeningMode mode) { |
| | | listening_mode_ = mode; |
| | | SetDeviceState(kDeviceStateListening); |
| | | } |
| | | |
| | | ListeningMode Application::GetDefaultListeningMode() const { |
| | | return aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime; |
| | | } |
| | | |
| | | void Application::Reboot() { |
| | | ESP_LOGI(TAG, "Rebooting..."); |
| | | // Disconnect the audio channel |
| | | if (protocol_ && protocol_->IsAudioChannelOpened()) { |
| | | protocol_->CloseAudioChannel(); |
| | | } |
| | | protocol_.reset(); |
| | | audio_service_.Stop(); |
| | | |
| | | vTaskDelay(pdMS_TO_TICKS(1000)); |
| | | esp_restart(); |
| | | } |
| | | |
| | | bool Application::UpgradeFirmware(const std::string& url, const std::string& version) { |
| | | auto& board = Board::GetInstance(); |
| | | auto display = board.GetDisplay(); |
| | | |
| | | std::string upgrade_url = url; |
| | | std::string version_info = version.empty() ? "(Manual upgrade)" : version; |
| | | |
| | | // Close audio channel if it's open |
| | | if (protocol_ && protocol_->IsAudioChannelOpened()) { |
| | | ESP_LOGI(TAG, "Closing audio channel before firmware upgrade"); |
| | | protocol_->CloseAudioChannel(); |
| | | } |
| | | ESP_LOGI(TAG, "Starting firmware upgrade from URL: %s", upgrade_url.c_str()); |
| | | |
| | | Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "download", Lang::Sounds::OGG_UPGRADE); |
| | | vTaskDelay(pdMS_TO_TICKS(3000)); |
| | | |
| | | SetDeviceState(kDeviceStateUpgrading); |
| | | |
| | | std::string message = std::string(Lang::Strings::NEW_VERSION) + version_info; |
| | | display->SetChatMessage("system", message.c_str()); |
| | | |
| | | board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); |
| | | audio_service_.Stop(); |
| | | vTaskDelay(pdMS_TO_TICKS(1000)); |
| | | |
| | | bool upgrade_success = Ota::Upgrade(upgrade_url, [this, display](int progress, size_t speed) { |
| | | char buffer[32]; |
| | | snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024); |
| | | Schedule([display, message = std::string(buffer)]() { |
| | | display->SetChatMessage("system", message.c_str()); |
| | | }); |
| | | }); |
| | | |
| | | if (!upgrade_success) { |
| | | // Upgrade failed, restart audio service and continue running |
| | | ESP_LOGE(TAG, "Firmware upgrade failed, restarting audio service and continuing operation..."); |
| | | audio_service_.Start(); // Restart audio service |
| | | board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); // Restore power save level |
| | | Alert(Lang::Strings::ERROR, Lang::Strings::UPGRADE_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION); |
| | | vTaskDelay(pdMS_TO_TICKS(3000)); |
| | | return false; |
| | | } else { |
| | | // Upgrade success, reboot immediately |
| | | ESP_LOGI(TAG, "Firmware upgrade successful, rebooting..."); |
| | | display->SetChatMessage("system", "Upgrade successful, rebooting..."); |
| | | vTaskDelay(pdMS_TO_TICKS(1000)); // Brief pause to show message |
| | | Reboot(); |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | void Application::WakeWordInvoke(const std::string& wake_word) { |
| | | if (!protocol_) { |
| | | return; |
| | | } |
| | | |
| | | auto state = GetDeviceState(); |
| | | |
| | | if (state == kDeviceStateIdle) { |
| | | audio_service_.EncodeWakeWord(); |
| | | |
| | | if (!protocol_->IsAudioChannelOpened()) { |
| | | SetDeviceState(kDeviceStateConnecting); |
| | | // Schedule to let the state change be processed first (UI update) |
| | | Schedule([this, wake_word]() { |
| | | ContinueWakeWordInvoke(wake_word); |
| | | }); |
| | | return; |
| | | } |
| | | // Channel already opened, continue directly |
| | | ContinueWakeWordInvoke(wake_word); |
| | | } else if (state == kDeviceStateSpeaking) { |
| | | Schedule([this]() { |
| | | AbortSpeaking(kAbortReasonNone); |
| | | }); |
| | | } else if (state == kDeviceStateListening) { |
| | | Schedule([this]() { |
| | | if (protocol_) { |
| | | protocol_->CloseAudioChannel(); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | bool Application::CanEnterSleepMode() { |
| | | if (GetDeviceState() != kDeviceStateIdle) { |
| | | return false; |
| | | } |
| | | |
| | | if (protocol_ && protocol_->IsAudioChannelOpened()) { |
| | | return false; |
| | | } |
| | | |
| | | if (!audio_service_.IsIdle()) { |
| | | return false; |
| | | } |
| | | |
| | | // Now it is safe to enter sleep mode |
| | | return true; |
| | | } |
| | | |
| | | void Application::SendMcpMessage(const std::string& payload) { |
| | | // Always schedule to run in main task for thread safety |
| | | Schedule([this, payload = std::move(payload)]() { |
| | | if (protocol_) { |
| | | protocol_->SendMcpMessage(payload); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | void Application::SetAecMode(AecMode mode) { |
| | | aec_mode_ = mode; |
| | | Schedule([this]() { |
| | | auto& board = Board::GetInstance(); |
| | | auto display = board.GetDisplay(); |
| | | switch (aec_mode_) { |
| | | case kAecOff: |
| | | audio_service_.EnableDeviceAec(false); |
| | | display->ShowNotification(Lang::Strings::RTC_MODE_OFF); |
| | | break; |
| | | case kAecOnServerSide: |
| | | audio_service_.EnableDeviceAec(false); |
| | | display->ShowNotification(Lang::Strings::RTC_MODE_ON); |
| | | break; |
| | | case kAecOnDeviceSide: |
| | | audio_service_.EnableDeviceAec(true); |
| | | display->ShowNotification(Lang::Strings::RTC_MODE_ON); |
| | | break; |
| | | } |
| | | |
| | | // If the AEC mode is changed, close the audio channel |
| | | if (protocol_ && protocol_->IsAudioChannelOpened()) { |
| | | protocol_->CloseAudioChannel(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | void Application::PlaySound(const std::string_view& sound) { |
| | | audio_service_.PlaySound(sound); |
| | | } |
| | | |
| | | void Application::ResetProtocol() { |
| | | Schedule([this]() { |
| | | // Close audio channel if opened |
| | | if (protocol_ && protocol_->IsAudioChannelOpened()) { |
| | | protocol_->CloseAudioChannel(); |
| | | } |
| | | // Reset protocol |
| | | protocol_.reset(); |
| | | }); |
| | | } |
| | | |
| New file |
| | |
| | | #ifndef _APPLICATION_H_ |
| | | #define _APPLICATION_H_ |
| | | |
| | | #include <freertos/FreeRTOS.h> |
| | | #include <freertos/event_groups.h> |
| | | #include <freertos/task.h> |
| | | #include <esp_timer.h> |
| | | |
| | | #include <string> |
| | | #include <mutex> |
| | | #include <deque> |
| | | #include <memory> |
| | | |
| | | #include "protocol.h" |
| | | #include "ota.h" |
| | | #include "audio_service.h" |
| | | #include "device_state.h" |
| | | #include "device_state_machine.h" |
| | | |
| | | // Main event bits |
| | | #define MAIN_EVENT_SCHEDULE (1 << 0) |
| | | #define MAIN_EVENT_SEND_AUDIO (1 << 1) |
| | | #define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2) |
| | | #define MAIN_EVENT_VAD_CHANGE (1 << 3) |
| | | #define MAIN_EVENT_ERROR (1 << 4) |
| | | #define MAIN_EVENT_ACTIVATION_DONE (1 << 5) |
| | | #define MAIN_EVENT_CLOCK_TICK (1 << 6) |
| | | #define MAIN_EVENT_NETWORK_CONNECTED (1 << 7) |
| | | #define MAIN_EVENT_NETWORK_DISCONNECTED (1 << 8) |
| | | #define MAIN_EVENT_TOGGLE_CHAT (1 << 9) |
| | | #define MAIN_EVENT_START_LISTENING (1 << 10) |
| | | #define MAIN_EVENT_STOP_LISTENING (1 << 11) |
| | | #define MAIN_EVENT_STATE_CHANGED (1 << 12) |
| | | |
| | | |
| | | enum AecMode { |
| | | kAecOff, |
| | | kAecOnDeviceSide, |
| | | kAecOnServerSide, |
| | | }; |
| | | |
| | | class Application { |
| | | public: |
| | | static Application& GetInstance() { |
| | | static Application instance; |
| | | return instance; |
| | | } |
| | | // Delete copy constructor and assignment operator |
| | | Application(const Application&) = delete; |
| | | Application& operator=(const Application&) = delete; |
| | | |
| | | /** |
| | | * Initialize the application |
| | | * This sets up display, audio, network callbacks, etc. |
| | | * Network connection starts asynchronously. |
| | | */ |
| | | void Initialize(); |
| | | |
| | | /** |
| | | * Run the main event loop |
| | | * This function runs in the main task and never returns. |
| | | * It handles all events including network, state changes, and user interactions. |
| | | */ |
| | | void Run(); |
| | | |
| | | DeviceState GetDeviceState() const { return state_machine_.GetState(); } |
| | | bool IsVoiceDetected() const { return audio_service_.IsVoiceDetected(); } |
| | | |
| | | /** |
| | | * Request state transition |
| | | * Returns true if transition was successful |
| | | */ |
| | | bool SetDeviceState(DeviceState state); |
| | | |
| | | /** |
| | | * Schedule a callback to be executed in the main task |
| | | */ |
| | | void Schedule(std::function<void()>&& callback); |
| | | |
| | | /** |
| | | * Alert with status, message, emotion and optional sound |
| | | */ |
| | | void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = ""); |
| | | void DismissAlert(); |
| | | |
| | | void AbortSpeaking(AbortReason reason); |
| | | |
| | | /** |
| | | * Toggle chat state (event-based, thread-safe) |
| | | * Sends MAIN_EVENT_TOGGLE_CHAT to be handled in Run() |
| | | */ |
| | | void ToggleChatState(); |
| | | |
| | | /** |
| | | * Start listening (event-based, thread-safe) |
| | | * Sends MAIN_EVENT_START_LISTENING to be handled in Run() |
| | | */ |
| | | void StartListening(); |
| | | |
| | | /** |
| | | * Stop listening (event-based, thread-safe) |
| | | * Sends MAIN_EVENT_STOP_LISTENING to be handled in Run() |
| | | */ |
| | | void StopListening(); |
| | | |
| | | void Reboot(); |
| | | void WakeWordInvoke(const std::string& wake_word); |
| | | bool UpgradeFirmware(const std::string& url, const std::string& version = ""); |
| | | bool CanEnterSleepMode(); |
| | | void SendMcpMessage(const std::string& payload); |
| | | void SetAecMode(AecMode mode); |
| | | AecMode GetAecMode() const { return aec_mode_; } |
| | | void PlaySound(const std::string_view& sound); |
| | | AudioService& GetAudioService() { return audio_service_; } |
| | | |
| | | /** |
| | | * Reset protocol resources (thread-safe) |
| | | * Can be called from any task to release resources allocated after network connected |
| | | * This includes closing audio channel, resetting protocol and ota objects |
| | | */ |
| | | void ResetProtocol(); |
| | | |
| | | private: |
| | | Application(); |
| | | ~Application(); |
| | | |
| | | std::mutex mutex_; |
| | | std::deque<std::function<void()>> main_tasks_; |
| | | std::unique_ptr<Protocol> protocol_; |
| | | EventGroupHandle_t event_group_ = nullptr; |
| | | esp_timer_handle_t clock_timer_handle_ = nullptr; |
| | | DeviceStateMachine state_machine_; |
| | | ListeningMode listening_mode_ = kListeningModeAutoStop; |
| | | AecMode aec_mode_ = kAecOff; |
| | | std::string last_error_message_; |
| | | AudioService audio_service_; |
| | | std::unique_ptr<Ota> ota_; |
| | | |
| | | bool has_server_time_ = false; |
| | | bool aborted_ = false; |
| | | bool assets_version_checked_ = false; |
| | | bool play_popup_on_listening_ = false; // Flag to play popup sound after state changes to listening |
| | | int clock_ticks_ = 0; |
| | | TaskHandle_t activation_task_handle_ = nullptr; |
| | | |
| | | |
| | | // Event handlers |
| | | void HandleStateChangedEvent(); |
| | | void HandleToggleChatEvent(); |
| | | void HandleStartListeningEvent(); |
| | | void HandleStopListeningEvent(); |
| | | void HandleNetworkConnectedEvent(); |
| | | void HandleNetworkDisconnectedEvent(); |
| | | void HandleActivationDoneEvent(); |
| | | void HandleWakeWordDetectedEvent(); |
| | | void ContinueOpenAudioChannel(ListeningMode mode); |
| | | void ContinueWakeWordInvoke(const std::string& wake_word); |
| | | |
| | | // Activation task (runs in background) |
| | | void ActivationTask(); |
| | | |
| | | // Helper methods |
| | | void CheckAssetsVersion(); |
| | | void CheckNewVersion(); |
| | | void InitializeProtocol(); |
| | | void ShowActivationCode(const std::string& code, const std::string& message); |
| | | void SetListeningMode(ListeningMode mode); |
| | | ListeningMode GetDefaultListeningMode() const; |
| | | |
| | | // State change handler called by state machine |
| | | void OnStateChanged(DeviceState old_state, DeviceState new_state); |
| | | }; |
| | | |
| | | |
| | | class TaskPriorityReset { |
| | | public: |
| | | TaskPriorityReset(BaseType_t priority) { |
| | | original_priority_ = uxTaskPriorityGet(NULL); |
| | | vTaskPrioritySet(NULL, priority); |
| | | } |
| | | ~TaskPriorityReset() { |
| | | vTaskPrioritySet(NULL, original_priority_); |
| | | } |
| | | |
| | | private: |
| | | BaseType_t original_priority_; |
| | | }; |
| | | |
| | | #endif // _APPLICATION_H_ |
| New file |
| | |
| | | #include "assets.h" |
| | | #include "board.h" |
| | | #include "display.h" |
| | | #include "application.h" |
| | | #include "lvgl_theme.h" |
| | | #include "emote_display.h" |
| | | #include "expression_emote.h" |
| | | #if HAVE_LVGL |
| | | #include "display/lcd_display.h" |
| | | #include <spi_flash_mmap.h> |
| | | #endif |
| | | |
| | | #include <esp_log.h> |
| | | #include <esp_timer.h> |
| | | #include <esp_heap_caps.h> |
| | | #include <cbin_font.h> |
| | | |
| | | |
| | | #define TAG "Assets" |
| | | #define PARTITION_LABEL "assets" |
| | | |
| | | struct mmap_assets_table { |
| | | char asset_name[32]; /*!< Name of the asset */ |
| | | uint32_t asset_size; /*!< Size of the asset */ |
| | | uint32_t asset_offset; /*!< Offset of the asset */ |
| | | uint16_t asset_width; /*!< Width of the asset */ |
| | | uint16_t asset_height; /*!< Height of the asset */ |
| | | }; |
| | | |
| | | Assets::Assets() { |
| | | #if HAVE_LVGL |
| | | strategy_ = std::make_unique<Assets::LvglStrategy>(); |
| | | #else |
| | | strategy_ = std::make_unique<Assets::EmoteStrategy>(); |
| | | #endif |
| | | // Initialize the partition |
| | | InitializePartition(); |
| | | } |
| | | |
| | | Assets::~Assets() { |
| | | UnApplyPartition(); |
| | | } |
| | | |
| | | bool Assets::FindPartition(Assets* assets) { |
| | | assets->partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, PARTITION_LABEL); |
| | | if (assets->partition_ == nullptr) { |
| | | ESP_LOGI(TAG, "No assets partition found"); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | bool Assets::Apply(bool refresh_display_theme) { |
| | | return strategy_ ? strategy_->Apply(this, refresh_display_theme) : false; |
| | | } |
| | | |
| | | bool Assets::InitializePartition() { |
| | | return strategy_ ? strategy_->InitializePartition(this) : false; |
| | | } |
| | | |
| | | void Assets::UnApplyPartition() { |
| | | if (strategy_) { |
| | | strategy_->UnApplyPartition(this); |
| | | } |
| | | } |
| | | |
| | | bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) { |
| | | return strategy_ ? strategy_->GetAssetData(this, name, ptr, size) : false; |
| | | } |
| | | |
| | | bool Assets::LoadSrmodelsFromIndex(Assets* assets, cJSON* root) { |
| | | void* ptr = nullptr; |
| | | size_t size = 0; |
| | | bool need_delete_root = false; |
| | | |
| | | // If root is not provided, parse index.json |
| | | if (root == nullptr) { |
| | | if (!assets->GetAssetData("index.json", ptr, size)) { |
| | | ESP_LOGE(TAG, "The index.json file is not found"); |
| | | return false; |
| | | } |
| | | |
| | | root = cJSON_ParseWithLength(static_cast<char*>(ptr), size); |
| | | if (root == nullptr) { |
| | | ESP_LOGE(TAG, "The index.json file is not valid"); |
| | | return false; |
| | | } |
| | | need_delete_root = true; |
| | | } |
| | | |
| | | cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels"); |
| | | if (cJSON_IsString(srmodels)) { |
| | | std::string srmodels_file = srmodels->valuestring; |
| | | if (assets->GetAssetData(srmodels_file, ptr, size)) { |
| | | if (assets->models_list_ != nullptr) { |
| | | esp_srmodel_deinit(assets->models_list_); |
| | | assets->models_list_ = nullptr; |
| | | } |
| | | assets->models_list_ = srmodel_load(static_cast<uint8_t*>(ptr)); |
| | | if (assets->models_list_ != nullptr) { |
| | | auto& app = Application::GetInstance(); |
| | | app.GetAudioService().SetModelsList(assets->models_list_); |
| | | if (need_delete_root) { |
| | | cJSON_Delete(root); |
| | | } |
| | | return true; |
| | | } else { |
| | | ESP_LOGE(TAG, "Failed to load srmodels.bin"); |
| | | } |
| | | } else { |
| | | ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str()); |
| | | } |
| | | } |
| | | |
| | | if (need_delete_root) { |
| | | cJSON_Delete(root); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | #if HAVE_LVGL |
| | | uint32_t Assets::LvglStrategy::CalculateChecksum(const char* data, uint32_t length) { |
| | | uint32_t checksum = 0; |
| | | for (uint32_t i = 0; i < length; i++) { |
| | | checksum += data[i]; |
| | | } |
| | | return checksum & 0xFFFF; |
| | | } |
| | | |
| | | bool Assets::LvglStrategy::InitializePartition(Assets* assets) { |
| | | assets->partition_valid_ = false; |
| | | assets_.clear(); |
| | | |
| | | if (!Assets::FindPartition(assets)) { |
| | | return false; |
| | | } |
| | | |
| | | int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA); |
| | | uint32_t storage_size = free_pages * 64 * 1024; |
| | | ESP_LOGI(TAG, "The storage free size is %ld KB", storage_size / 1024); |
| | | ESP_LOGI(TAG, "The partition size is %ld KB", assets->partition_->size / 1024); |
| | | if (storage_size < assets->partition_->size) { |
| | | ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, assets->partition_->size / 1024); |
| | | return false; |
| | | } |
| | | |
| | | esp_err_t err = esp_partition_mmap(assets->partition_, 0, assets->partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_); |
| | | if (err != ESP_OK) { |
| | | ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err)); |
| | | return false; |
| | | } |
| | | |
| | | assets->partition_valid_ = true; |
| | | |
| | | uint32_t stored_files = *(uint32_t*)(mmap_root_ + 0); |
| | | uint32_t stored_chksum = *(uint32_t*)(mmap_root_ + 4); |
| | | uint32_t stored_len = *(uint32_t*)(mmap_root_ + 8); |
| | | |
| | | if (stored_len > assets->partition_->size - 12) { |
| | | ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, assets->partition_->size); |
| | | return false; |
| | | } |
| | | |
| | | auto start_time = esp_timer_get_time(); |
| | | uint32_t calculated_checksum = CalculateChecksum(mmap_root_ + 12, stored_len); |
| | | auto end_time = esp_timer_get_time(); |
| | | ESP_LOGI(TAG, "The checksum calculation time is %d ms", int((end_time - start_time) / 1000)); |
| | | |
| | | if (calculated_checksum != stored_chksum) { |
| | | ESP_LOGE(TAG, "The calculated checksum (0x%lx) does not match the stored checksum (0x%lx)", calculated_checksum, stored_chksum); |
| | | return false; |
| | | } |
| | | |
| | | checksum_valid_ = true; |
| | | |
| | | for (uint32_t i = 0; i < stored_files; i++) { |
| | | auto item = (const mmap_assets_table*)(mmap_root_ + 12 + i * sizeof(mmap_assets_table)); |
| | | auto asset = Asset{ |
| | | .size = static_cast<size_t>(item->asset_size), |
| | | .offset = static_cast<size_t>(12 + sizeof(mmap_assets_table) * stored_files + item->asset_offset) |
| | | }; |
| | | assets_[item->asset_name] = asset; |
| | | } |
| | | return checksum_valid_; |
| | | } |
| | | |
| | | void Assets::LvglStrategy::UnApplyPartition(Assets* assets) { |
| | | if (mmap_handle_ != 0) { |
| | | esp_partition_munmap(mmap_handle_); |
| | | mmap_handle_ = 0; |
| | | mmap_root_ = nullptr; |
| | | } |
| | | checksum_valid_ = false; |
| | | assets_.clear(); |
| | | (void)assets; // Unused parameter |
| | | } |
| | | |
| | | bool Assets::LvglStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) { |
| | | auto asset = assets_.find(name); |
| | | if (asset == assets_.end()) { |
| | | return false; |
| | | } |
| | | auto data = (const char*)(mmap_root_ + asset->second.offset); |
| | | if (data[0] != 'Z' || data[1] != 'Z') { |
| | | ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]); |
| | | return false; |
| | | } |
| | | |
| | | ptr = static_cast<void*>(const_cast<char*>(data + 2)); |
| | | size = asset->second.size; |
| | | return true; |
| | | } |
| | | |
| | | bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) { |
| | | void* ptr = nullptr; |
| | | size_t size = 0; |
| | | if (!assets->GetAssetData("index.json", ptr, size)) { |
| | | ESP_LOGE(TAG, "The index.json file is not found"); |
| | | return false; |
| | | } |
| | | |
| | | cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size); |
| | | if (root == nullptr) { |
| | | ESP_LOGE(TAG, "The index.json file is not valid"); |
| | | return false; |
| | | } |
| | | |
| | | cJSON* version = cJSON_GetObjectItem(root, "version"); |
| | | if (cJSON_IsNumber(version)) { |
| | | if (version->valuedouble > 1) { |
| | | ESP_LOGE(TAG, "The assets version %d is not supported, please upgrade the firmware", version->valueint); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | Assets::LoadSrmodelsFromIndex(assets, root); |
| | | |
| | | auto& theme_manager = LvglThemeManager::GetInstance(); |
| | | auto light_theme = theme_manager.GetTheme("light"); |
| | | auto dark_theme = theme_manager.GetTheme("dark"); |
| | | |
| | | cJSON* font = cJSON_GetObjectItem(root, "text_font"); |
| | | if (cJSON_IsString(font)) { |
| | | std::string fonts_text_file = font->valuestring; |
| | | if (assets->GetAssetData(fonts_text_file, ptr, size)) { |
| | | auto text_font = std::make_shared<LvglCBinFont>(ptr); |
| | | if (text_font->font() == nullptr) { |
| | | ESP_LOGE(TAG, "Failed to load fonts.bin"); |
| | | return false; |
| | | } |
| | | if (light_theme != nullptr) { |
| | | light_theme->set_text_font(text_font); |
| | | } |
| | | if (dark_theme != nullptr) { |
| | | dark_theme->set_text_font(text_font); |
| | | } |
| | | } else { |
| | | ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str()); |
| | | } |
| | | } |
| | | |
| | | cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection"); |
| | | if (cJSON_IsArray(emoji_collection)) { |
| | | auto custom_emoji_collection = std::make_shared<EmojiCollection>(); |
| | | int emoji_count = cJSON_GetArraySize(emoji_collection); |
| | | for (int i = 0; i < emoji_count; i++) { |
| | | cJSON* emoji = cJSON_GetArrayItem(emoji_collection, i); |
| | | if (cJSON_IsObject(emoji)) { |
| | | cJSON* name = cJSON_GetObjectItem(emoji, "name"); |
| | | cJSON* file = cJSON_GetObjectItem(emoji, "file"); |
| | | cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf"); |
| | | if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL== eaf)) { |
| | | if (!assets->GetAssetData(file->valuestring, ptr, size)) { |
| | | ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring); |
| | | continue; |
| | | } |
| | | custom_emoji_collection->AddEmoji(name->valuestring, new LvglRawImage(ptr, size)); |
| | | } |
| | | } |
| | | } |
| | | if (light_theme != nullptr) { |
| | | light_theme->set_emoji_collection(custom_emoji_collection); |
| | | } |
| | | if (dark_theme != nullptr) { |
| | | dark_theme->set_emoji_collection(custom_emoji_collection); |
| | | } |
| | | } |
| | | |
| | | cJSON* skin = cJSON_GetObjectItem(root, "skin"); |
| | | if (cJSON_IsObject(skin)) { |
| | | cJSON* light_skin = cJSON_GetObjectItem(skin, "light"); |
| | | if (cJSON_IsObject(light_skin) && light_theme != nullptr) { |
| | | cJSON* text_color = cJSON_GetObjectItem(light_skin, "text_color"); |
| | | cJSON* background_color = cJSON_GetObjectItem(light_skin, "background_color"); |
| | | cJSON* background_image = cJSON_GetObjectItem(light_skin, "background_image"); |
| | | if (cJSON_IsString(text_color)) { |
| | | light_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring)); |
| | | } |
| | | if (cJSON_IsString(background_color)) { |
| | | light_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring)); |
| | | light_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring)); |
| | | } |
| | | if (cJSON_IsString(background_image)) { |
| | | if (!assets->GetAssetData(background_image->valuestring, ptr, size)) { |
| | | ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring); |
| | | return false; |
| | | } |
| | | auto background_image = std::make_shared<LvglCBinImage>(ptr); |
| | | light_theme->set_background_image(background_image); |
| | | } |
| | | } |
| | | cJSON* dark_skin = cJSON_GetObjectItem(skin, "dark"); |
| | | if (cJSON_IsObject(dark_skin) && dark_theme != nullptr) { |
| | | cJSON* text_color = cJSON_GetObjectItem(dark_skin, "text_color"); |
| | | cJSON* background_color = cJSON_GetObjectItem(dark_skin, "background_color"); |
| | | cJSON* background_image = cJSON_GetObjectItem(dark_skin, "background_image"); |
| | | if (cJSON_IsString(text_color)) { |
| | | dark_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring)); |
| | | } |
| | | if (cJSON_IsString(background_color)) { |
| | | dark_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring)); |
| | | dark_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring)); |
| | | } |
| | | if (cJSON_IsString(background_image)) { |
| | | if (!assets->GetAssetData(background_image->valuestring, ptr, size)) { |
| | | ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring); |
| | | return false; |
| | | } |
| | | auto background_image = std::make_shared<LvglCBinImage>(ptr); |
| | | dark_theme->set_background_image(background_image); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (refresh_display_theme) { |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | ESP_LOGI(TAG, "Refreshing display theme..."); |
| | | |
| | | auto current_theme = display->GetTheme(); |
| | | if (current_theme != nullptr) { |
| | | display->SetTheme(current_theme); |
| | | } |
| | | |
| | | // Parse hide_subtitle configuration |
| | | cJSON* hide_subtitle = cJSON_GetObjectItem(root, "hide_subtitle"); |
| | | if (cJSON_IsBool(hide_subtitle)) { |
| | | bool hide = cJSON_IsTrue(hide_subtitle); |
| | | auto lcd_display = dynamic_cast<LcdDisplay*>(display); |
| | | if (lcd_display != nullptr) { |
| | | lcd_display->SetHideSubtitle(hide); |
| | | ESP_LOGI(TAG, "Set hide_subtitle to %s", hide ? "true" : "false"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | cJSON_Delete(root); |
| | | return true; |
| | | } |
| | | #endif // HAVE_LVGL |
| | | |
| | | bool Assets::EmoteStrategy::InitializePartition(Assets* assets) { |
| | | assets->partition_valid_ = false; |
| | | |
| | | if (!Assets::FindPartition(assets)) { |
| | | return false; |
| | | } |
| | | |
| | | esp_err_t ret = ESP_ERR_INVALID_STATE; |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display); |
| | | if (emote_display && emote_display->GetEmoteHandle() != nullptr) { |
| | | const emote_data_t data = { |
| | | .type = EMOTE_SOURCE_PARTITION, |
| | | .source = { |
| | | .partition_label = PARTITION_LABEL, |
| | | }, |
| | | .flags = { |
| | | .mmap_enable = true, //must be true here!!! |
| | | }, |
| | | }; |
| | | ret = emote_mount_assets(emote_display->GetEmoteHandle(), &data); |
| | | } else { |
| | | ESP_LOGE(TAG, "Emote display is not initialized"); |
| | | } |
| | | assets->partition_valid_ = ((ret == ESP_OK) ? true : false); |
| | | return assets->partition_valid_; |
| | | } |
| | | |
| | | void Assets::EmoteStrategy::UnApplyPartition(Assets* assets) { |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display); |
| | | if (emote_display && emote_display->GetEmoteHandle() != nullptr) { |
| | | emote_unmount_assets(emote_display->GetEmoteHandle()); |
| | | } |
| | | (void)assets; // Unused parameter |
| | | } |
| | | |
| | | bool Assets::EmoteStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) { |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display); |
| | | if (emote_display && emote_display->GetEmoteHandle() != nullptr) { |
| | | const uint8_t* data = nullptr; |
| | | size_t data_size = 0; |
| | | if (ESP_OK == emote_get_asset_data_by_name(emote_display->GetEmoteHandle(), name.c_str(), &data, &data_size)) { |
| | | ptr = const_cast<void*>(static_cast<const void*>(data)); |
| | | size = data_size; |
| | | return true; |
| | | } |
| | | ESP_LOGE(TAG, "Failed to get asset data by name: %s", name.c_str()); |
| | | return false; |
| | | } |
| | | (void)assets; // Unused parameter |
| | | return false; |
| | | } |
| | | |
| | | bool Assets::EmoteStrategy::Apply(Assets* assets, bool refresh_display_theme) { |
| | | Assets::LoadSrmodelsFromIndex(assets); |
| | | |
| | | auto display = Board::GetInstance().GetDisplay(); |
| | | auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display); |
| | | |
| | | if (emote_display && emote_display->GetEmoteHandle() != nullptr) { |
| | | emote_load_assets(emote_display->GetEmoteHandle()); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | bool Assets::Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback) { |
| | | ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str()); |
| | | |
| | | // åæ¶å½åèµæºååºçå
åæ å° |
| | | UnApplyPartition(); |
| | | |
| | | // ä¸è½½æ°çèµæºæä»¶ |
| | | auto network = Board::GetInstance().GetNetwork(); |
| | | auto http = network->CreateHttp(0); |
| | | |
| | | if (!http->Open("GET", url)) { |
| | | ESP_LOGE(TAG, "Failed to open HTTP connection"); |
| | | return false; |
| | | } |
| | | |
| | | if (http->GetStatusCode() != 200) { |
| | | ESP_LOGE(TAG, "Failed to get assets, status code: %d", http->GetStatusCode()); |
| | | return false; |
| | | } |
| | | |
| | | size_t content_length = http->GetBodyLength(); |
| | | if (content_length == 0) { |
| | | ESP_LOGE(TAG, "Failed to get content length"); |
| | | return false; |
| | | } |
| | | |
| | | if (content_length > partition_->size) { |
| | | ESP_LOGE(TAG, "Assets file size (%u) is larger than partition size (%lu)", content_length, partition_->size); |
| | | return false; |
| | | } |
| | | |
| | | // å®ä¹æåºå¤§å°ä¸º4KBï¼ESP32çæ åæåºå¤§å°ï¼ |
| | | const size_t SECTOR_SIZE = esp_partition_get_main_flash_sector_size(); |
| | | |
| | | // 计ç®éè¦æ¦é¤çæåºæ°é |
| | | size_t sectors_to_erase = (content_length + SECTOR_SIZE - 1) / SECTOR_SIZE; // åä¸åæ´ |
| | | size_t total_erase_size = sectors_to_erase * SECTOR_SIZE; |
| | | |
| | | ESP_LOGI(TAG, "Sector size: %u, content length: %u, sectors to erase: %u, total erase size: %u", |
| | | SECTOR_SIZE, content_length, sectors_to_erase, total_erase_size); |
| | | |
| | | // åå
¥æ°çèµæºæä»¶å°ååºï¼ä¸è¾¹eraseä¸è¾¹åå
¥ |
| | | char* buffer = (char*)heap_caps_malloc(SECTOR_SIZE, MALLOC_CAP_INTERNAL); |
| | | if (buffer == nullptr) { |
| | | ESP_LOGE(TAG, "Failed to allocate buffer"); |
| | | return false; |
| | | } |
| | | size_t total_written = 0; |
| | | size_t recent_written = 0; |
| | | size_t current_sector = 0; |
| | | auto last_calc_time = esp_timer_get_time(); |
| | | |
| | | while (true) { |
| | | int ret = http->Read(buffer, SECTOR_SIZE); |
| | | if (ret < 0) { |
| | | ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret)); |
| | | heap_caps_free(buffer); |
| | | return false; |
| | | } |
| | | |
| | | if (ret == 0) { |
| | | break; |
| | | } |
| | | |
| | | // æ£æ¥æ¯å¦éè¦æ¦é¤æ°çæåº |
| | | size_t write_end_offset = total_written + ret; |
| | | size_t needed_sectors = (write_end_offset + SECTOR_SIZE - 1) / SECTOR_SIZE; |
| | | |
| | | // æ¦é¤éè¦çæ°æåº |
| | | while (current_sector < needed_sectors) { |
| | | size_t sector_start = current_sector * SECTOR_SIZE; |
| | | size_t sector_end = (current_sector + 1) * SECTOR_SIZE; |
| | | |
| | | // ç¡®ä¿æ¦é¤èå´ä¸è¶
è¿ååºå¤§å° |
| | | if (sector_end > partition_->size) { |
| | | ESP_LOGE(TAG, "Sector end (%u) exceeds partition size (%lu)", sector_end, partition_->size); |
| | | heap_caps_free(buffer); |
| | | return false; |
| | | } |
| | | |
| | | ESP_LOGD(TAG, "Erasing sector %u (offset: %u, size: %u)", current_sector, sector_start, SECTOR_SIZE); |
| | | esp_err_t err = esp_partition_erase_range(partition_, sector_start, SECTOR_SIZE); |
| | | if (err != ESP_OK) { |
| | | ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", current_sector, sector_start, esp_err_to_name(err)); |
| | | heap_caps_free(buffer); |
| | | return false; |
| | | } |
| | | |
| | | current_sector++; |
| | | } |
| | | |
| | | // åå
¥æ°æ®å°ååº |
| | | esp_err_t err = esp_partition_write(partition_, total_written, buffer, ret); |
| | | if (err != ESP_OK) { |
| | | ESP_LOGE(TAG, "Failed to write to assets partition at offset %u: %s", total_written, esp_err_to_name(err)); |
| | | heap_caps_free(buffer); |
| | | return false; |
| | | } |
| | | |
| | | total_written += ret; |
| | | recent_written += ret; |
| | | |
| | | // 计ç®è¿åº¦åé度 |
| | | if (esp_timer_get_time() - last_calc_time >= 1000000 || total_written == content_length || ret == 0) { |
| | | size_t progress = total_written * 100 / content_length; |
| | | size_t speed = recent_written; // æ¯ç§çåèæ° |
| | | ESP_LOGI(TAG, "Progress: %u%% (%u/%u), Speed: %u B/s, Sectors erased: %u", |
| | | progress, total_written, content_length, speed, current_sector); |
| | | if (progress_callback) { |
| | | progress_callback(progress, speed); |
| | | } |
| | | last_calc_time = esp_timer_get_time(); |
| | | recent_written = 0; // éç½®æè¿åå
¥çåèæ° |
| | | } |
| | | } |
| | | |
| | | http->Close(); |
| | | heap_caps_free(buffer); |
| | | |
| | | if (total_written != content_length) { |
| | | ESP_LOGE(TAG, "Downloaded size (%u) does not match expected size (%u)", total_written, content_length); |
| | | return false; |
| | | } |
| | | |
| | | ESP_LOGI(TAG, "Assets download completed, total written: %u bytes, total sectors erased: %u", |
| | | total_written, current_sector); |
| | | |
| | | // éæ°åå§åèµæºååº |
| | | if (!InitializePartition()) { |
| | | ESP_LOGE(TAG, "Failed to re-initialize assets partition"); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | } |
| New file |
| | |
| | | #ifndef ASSETS_H |
| | | #define ASSETS_H |
| | | |
| | | #include <string> |
| | | #include <functional> |
| | | #include <memory> |
| | | |
| | | #include <cJSON.h> |
| | | #include <esp_partition.h> |
| | | #include <model_path.h> |
| | | #include <map> |
| | | #include <string> |
| | | |
| | | #if HAVE_LVGL |
| | | #include <spi_flash_mmap.h> |
| | | #endif |
| | | |
| | | struct Asset { |
| | | size_t size; |
| | | size_t offset; |
| | | }; |
| | | |
| | | class Assets { |
| | | public: |
| | | static Assets& GetInstance() { |
| | | static Assets instance; |
| | | return instance; |
| | | } |
| | | ~Assets(); |
| | | |
| | | bool Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback); |
| | | bool Apply(bool refresh_display_theme = true); |
| | | bool GetAssetData(const std::string& name, void*& ptr, size_t& size); |
| | | |
| | | inline bool partition_valid() const { return partition_valid_; } |
| | | inline std::string default_assets_url() const { return default_assets_url_; } |
| | | |
| | | private: |
| | | Assets(); |
| | | Assets(const Assets&) = delete; |
| | | Assets& operator=(const Assets&) = delete; |
| | | |
| | | bool InitializePartition(); |
| | | void UnApplyPartition(); |
| | | static bool FindPartition(Assets* assets); |
| | | static bool LoadSrmodelsFromIndex(Assets* assets, cJSON* root = nullptr); |
| | | |
| | | class AssetStrategy { |
| | | public: |
| | | virtual ~AssetStrategy() = default; |
| | | virtual bool Apply(Assets* assets, bool refresh_display_theme = true) = 0; |
| | | virtual bool InitializePartition(Assets* assets) = 0; |
| | | virtual void UnApplyPartition(Assets* assets) = 0; |
| | | virtual bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) = 0; |
| | | }; |
| | | |
| | | class LvglStrategy : public AssetStrategy { |
| | | public: |
| | | bool Apply(Assets* assets, bool refresh_display_theme = true) override; |
| | | bool InitializePartition(Assets* assets) override; |
| | | void UnApplyPartition(Assets* assets) override; |
| | | bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) override; |
| | | private: |
| | | static uint32_t CalculateChecksum(const char* data, uint32_t length); |
| | | std::map<std::string, Asset> assets_; |
| | | esp_partition_mmap_handle_t mmap_handle_ = 0; |
| | | const char* mmap_root_ = nullptr; |
| | | bool checksum_valid_ = false; |
| | | }; |
| | | |
| | | class EmoteStrategy : public AssetStrategy { |
| | | public: |
| | | bool Apply(Assets* assets, bool refresh_display_theme = true) override; |
| | | bool InitializePartition(Assets* assets) override; |
| | | void UnApplyPartition(Assets* assets) override; |
| | | bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) override; |
| | | }; |
| | | |
| | | // Strategy instance |
| | | std::unique_ptr<AssetStrategy> strategy_; |
| | | |
| | | protected: |
| | | const esp_partition_t* partition_ = nullptr; |
| | | bool partition_valid_ = false; |
| | | std::string default_assets_url_; |
| | | srmodel_list_t* models_list_ = nullptr; |
| | | }; |
| | | |
| | | #endif |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "ar-SA" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "ØªØØ°Ùر", |
| | | "INFO": "Ù
عÙÙÙ
ات", |
| | | "ERROR": "خطأ", |
| | | "VERSION": "Ø§ÙØ¥ØµØ¯Ø§Ø± ", |
| | | "LOADING_PROTOCOL": "Ø§ÙØ§ØªØµØ§Ù Ø¨Ø§ÙØ®Ø§Ø¯Ù
...", |
| | | "INITIALIZING": "Ø§ÙØªÙÙØ¦Ø©...", |
| | | "PIN_ERROR": "ÙØ±Ø¬Ù Ø¥Ø¯Ø®Ø§Ù Ø¨Ø·Ø§ÙØ© SIM", |
| | | "REG_ERROR": "ÙØ§ ÙÙ
Ù٠اÙÙØµÙ٠إÙÙ Ø§ÙØ´Ø¨ÙØ©Ø ÙØ±Ø¬Ù Ø§ÙØªØÙÙ Ù
Ù ØØ§ÙØ© Ø¨Ø·Ø§ÙØ© Ø§ÙØ¨ÙØ§ÙØ§Øª", |
| | | "DETECTING_MODULE": "Ø§ÙØªØ´Ø§Ù اÙÙØØ¯Ø©...", |
| | | "REGISTERING_NETWORK": "Ø§ÙØªØ¸Ø§Ø± Ø§ÙØ´Ø¨ÙØ©...", |
| | | "CHECKING_NEW_VERSION": "ÙØØµ Ø§ÙØ¥ØµØ¯Ø§Ø± Ø§ÙØ¬Ø¯Ùد...", |
| | | "CHECK_NEW_VERSION_FAILED": "ÙØ´Ù ÙØØµ Ø§ÙØ¥ØµØ¯Ø§Ø± Ø§ÙØ¬Ø¯ÙØ¯Ø Ø³ÙØªÙ
اÙÙ
ØØ§ÙÙØ© Ø®ÙØ§Ù %d ثاÙÙØ©: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Ø§ÙØªØ¨Ø¯Ù٠إÙÙ Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Ø§ÙØªØ¨Ø¯Ù٠إÙÙ 4G...", |
| | | "STANDBY": "ÙÙ Ø§ÙØ§Ùتظار", |
| | | "CONNECT_TO": "Ø§ÙØ§ØªØµØ§Ù ب٠", |
| | | "CONNECTING": "Ø¬Ø§Ø±Ù Ø§ÙØ§ØªØµØ§Ù...", |
| | | "CONNECTED_TO": "Ù
تص٠ب٠", |
| | | "LISTENING": "Ø§ÙØ§Ø³ØªÙ
اع...", |
| | | "SPEAKING": "Ø§ÙØªØØ¯Ø«...", |
| | | "SERVER_NOT_FOUND": "Ø§ÙØ¨ØØ« ع٠خدÙ
Ø© Ù
ØªØ§ØØ©", |
| | | "SERVER_NOT_CONNECTED": "ÙØ§ ÙÙ
ÙÙ Ø§ÙØ§ØªØµØ§Ù Ø¨Ø§ÙØ®Ø¯Ù
Ø©Ø ÙØ±Ø¬Ù اÙÙ
ØØ§ÙÙØ© ÙØ§ØÙاÙ", |
| | | "SERVER_TIMEOUT": "Ø§ÙØªÙت Ù
ÙÙØ© Ø§ÙØ§Ø³ØªØ¬Ø§Ø¨Ø©", |
| | | "SERVER_ERROR": "ÙØ´Ù Ø§ÙØ¥Ø±Ø³Ø§ÙØ ÙØ±Ø¬Ù Ø§ÙØªØÙÙ Ù
Ù Ø§ÙØ´Ø¨ÙØ©", |
| | | "CONNECT_TO_HOTSPOT": "اتص٠اÙÙØ§ØªÙ بÙÙØ·Ø© Ø§ÙØ§ØªØµØ§Ù ", |
| | | "ACCESS_VIA_BROWSER": "ØØ§ÙÙØµÙ٠عبر اÙÙ
ØªØµÙØ ", |
| | | "WIFI_CONFIG_MODE": "ÙØ¶Ø¹ تÙÙÙÙ Ø§ÙØ´Ø¨ÙØ©", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Ø§ÙØ¯Ø®ÙÙ ÙÙ ÙØ¶Ø¹ تÙÙÙÙ Ø§ÙØ´Ø¨ÙØ©...", |
| | | "SCANNING_WIFI": "ÙØØµ Wi-Fi...", |
| | | "NEW_VERSION": "إصدار Ø¬Ø¯ÙØ¯ ", |
| | | "OTA_UPGRADE": "ØªØØ¯ÙØ« OTA", |
| | | "UPGRADING": "ØªØØ¯ÙØ« اÙÙØ¸Ø§Ù
...", |
| | | "UPGRADE_FAILED": "ÙØ´Ù Ø§ÙØªØØ¯ÙØ«", |
| | | "ACTIVATION": "ØªÙØ¹ÙÙ Ø§ÙØ¬Ùاز", |
| | | "BATTERY_LOW": "Ø§ÙØ¨Ø·Ø§Ø±ÙØ© Ù
ÙØ®Ùضة", |
| | | "BATTERY_CHARGING": "Ø¬Ø§Ø±Ù Ø§ÙØ´ØÙ", |
| | | "BATTERY_FULL": "Ø§ÙØ¨Ø·Ø§Ø±ÙØ© Ù
Ù
ØªÙØ¦Ø©", |
| | | "BATTERY_NEED_CHARGE": "Ø§ÙØ¨Ø·Ø§Ø±ÙØ© Ù
ÙØ®ÙØ¶Ø©Ø ÙØ±Ø¬Ù Ø§ÙØ´ØÙ", |
| | | "VOLUME": "Ø§ÙØµÙت ", |
| | | "MUTED": "صاÙ
ت", |
| | | "MAX_VOLUME": "Ø£ÙØµÙ ØµÙØª", |
| | | "RTC_MODE_OFF": "AEC Ù
ÙÙÙÙ", |
| | | "RTC_MODE_ON": "AEC Ù
ÙØ´ØºÙ", |
| | | "DOWNLOAD_ASSETS_FAILED": "ÙØ´Ù ÙÙ ØªÙØ²Ù٠اÙÙ
ÙØ§Ø±Ø¯", |
| | | "LOADING_ASSETS": "جار٠تØÙ
Ù٠اÙÙ
ÙØ§Ø±Ø¯...", |
| | | "PLEASE_WAIT": "ÙØ±Ø¬Ù Ø§ÙØ§Ùتظار...", |
| | | "FOUND_NEW_ASSETS": "تÙ
Ø§ÙØ¹Ø«Ùر عÙÙ Ù
ÙØ§Ø±Ø¯ Ø¬Ø¯ÙØ¯Ø©: %s", |
| | | "HELLO_MY_FRIEND": "Ù
Ø±ØØ¨Ø§ÙØ ØµØ¯ÙÙÙ!", |
| | | "CONNECTION_SUCCESSFUL": "تÙ
Ø§ÙØ§ØªØµØ§Ù Ø¨ÙØ¬Ø§Ø", |
| | | "FLIGHT_MODE_OFF": "ÙØ¶Ø¹ Ø§ÙØ·Ùرا٠Ù
عطÙ", |
| | | "FLIGHT_MODE_ON": "ÙØ¶Ø¹ Ø§ÙØ·Ùرا٠ÙÙØ¯ Ø§ÙØªØ´ØºÙÙ", |
| | | "MODEM_INIT_ERROR": "ÙØ´Ù تÙÙØ¦Ø© اÙÙ
ÙØ¯Ù
" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "bg-BG" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "ÐÑедÑпÑеждение", |
| | | "INFO": "ÐнÑоÑмаÑиÑ", |
| | | "ERROR": "ÐÑеÑка", |
| | | "VERSION": "ÐеÑÑÐ¸Ñ ", |
| | | "LOADING_PROTOCOL": "Ðлизане в ÑиÑÑемаÑа...", |
| | | "INITIALIZING": "ÐниÑиализаÑиÑ...", |
| | | "PIN_ERROR": "ÐолÑ, поÑÑавеÑе SIM каÑÑа", |
| | | "REG_ERROR": "Ðе може да Ñе оÑÑÑеÑÑви доÑÑÑп до мÑежаÑа, Ð¼Ð¾Ð»Ñ Ð¿ÑовеÑеÑе ÑÑаÑÑÑа на SIM каÑÑаÑа", |
| | | "DETECTING_MODULE": "ÐÑкÑиване на модÑл...", |
| | | "REGISTERING_NETWORK": "ÐзÑакване на мÑежаÑа...", |
| | | "CHECKING_NEW_VERSION": "ÐÑовеÑка за нова веÑÑиÑ...", |
| | | "CHECK_NEW_VERSION_FAILED": "ÐÑовеÑкаÑа за нова веÑÑÐ¸Ñ Ðµ неÑÑпеÑна, Ñе Ñе опиÑа оÑново Ñлед %d ÑекÑнди: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "ÐÑевклÑÑване кÑм Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "ÐÑевклÑÑване кÑм 4G...", |
| | | "STANDBY": "Режим на гоÑовноÑÑ", |
| | | "CONNECT_TO": "СвÑÑзване кÑм ", |
| | | "CONNECTING": "СвÑÑзване...", |
| | | "CONNECTION_SUCCESSFUL": "УÑпеÑно ÑвÑÑзване", |
| | | "CONNECTED_TO": "СвÑÑзан кÑм ", |
| | | "LISTENING": "СлÑÑане...", |
| | | "SPEAKING": "ÐовоÑене...", |
| | | "SERVER_NOT_FOUND": "ТÑÑÑене на налиÑна ÑÑлÑга", |
| | | "SERVER_NOT_CONNECTED": "Ðе може да Ñе ÑвÑÑже Ñ ÑÑлÑгаÑа, Ð¼Ð¾Ð»Ñ Ð¾Ð¿Ð¸ÑайÑе по-кÑÑно", |
| | | "SERVER_TIMEOUT": "ÐÑемеÑо за изÑакване на оÑÐ³Ð¾Ð²Ð¾Ñ Ð¸Ð·ÑеÑе", |
| | | "SERVER_ERROR": "ÐеÑÑпеÑно изпÑаÑане, Ð¼Ð¾Ð»Ñ Ð¿ÑовеÑеÑе мÑежаÑа", |
| | | "CONNECT_TO_HOTSPOT": "ÐоÑеÑa ÑоÑка: ", |
| | | "ACCESS_VIA_BROWSER": " ÐонÑигÑÑаÑионен URL: ", |
| | | "WIFI_CONFIG_MODE": "Режим на конÑигÑÑиÑане на Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Ðлизане в Ñежим на конÑигÑÑиÑане на Wi-Fi...", |
| | | "SCANNING_WIFI": "СканиÑане на Wi-Fi...", |
| | | "NEW_VERSION": "Ðова веÑÑÐ¸Ñ ", |
| | | "OTA_UPGRADE": "OTA надÑÑÑойка", |
| | | "UPGRADING": "СиÑÑемаÑа Ñе надÑÑÑойва...", |
| | | "UPGRADE_FAILED": "ÐадÑÑÑойкаÑа е неÑÑпеÑна", |
| | | "ACTIVATION": "ÐкÑиваÑиÑ", |
| | | "BATTERY_LOW": "Слаба баÑеÑиÑ", |
| | | "BATTERY_CHARGING": "ÐаÑÑдна", |
| | | "BATTERY_FULL": "ÐаÑеÑиÑÑа е пÑлна", |
| | | "BATTERY_NEED_CHARGE": "Слаба баÑеÑиÑ, Ð¼Ð¾Ð»Ñ Ð·Ð°ÑедеÑе", |
| | | "VOLUME": "Сила на звÑка ", |
| | | "MUTED": "ÐаглÑÑено", |
| | | "MAX_VOLUME": "ÐакÑимална Ñила на звÑка", |
| | | "RTC_MODE_OFF": "AEC изклÑÑен", |
| | | "RTC_MODE_ON": "AEC вклÑÑен", |
| | | "PLEASE_WAIT": "ÐолÑ, изÑакайÑе...", |
| | | "FOUND_NEW_ASSETS": "ÐамеÑени нови ÑеÑÑÑÑи: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "ÐеÑÑпеÑно изÑеглÑне на ÑеÑÑÑÑи", |
| | | "LOADING_ASSETS": "ÐаÑеждане на ÑеÑÑÑÑи...", |
| | | "HELLO_MY_FRIEND": "ÐдÑавей, мой пÑиÑÑел!", |
| | | "FLIGHT_MODE_OFF": "Режим на ÑÐ°Ð¼Ð¾Ð»ÐµÑ Ðµ изклÑÑен", |
| | | "FLIGHT_MODE_ON": "Режим на ÑÐ°Ð¼Ð¾Ð»ÐµÑ Ðµ вклÑÑен", |
| | | "MODEM_INIT_ERROR": "ÐеÑÑпеÑна иниÑиализаÑÐ¸Ñ Ð½Ð° модема" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "ca-ES" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Advertència", |
| | | "INFO": "Informació", |
| | | "ERROR": "Error", |
| | | "VERSION": "Versió ", |
| | | "LOADING_PROTOCOL": "Iniciant sessió...", |
| | | "INITIALIZING": "Inicialitzant...", |
| | | "PIN_ERROR": "Si us plau, inseriu la targeta SIM", |
| | | "REG_ERROR": "No es pot accedir a la xarxa, si us plau comproveu l'estat de la targeta SIM", |
| | | "DETECTING_MODULE": "Detectant mòdul...", |
| | | "REGISTERING_NETWORK": "Esperant la xarxa...", |
| | | "CHECKING_NEW_VERSION": "Comprovant nova versió...", |
| | | "CHECK_NEW_VERSION_FAILED": "La comprovació de nova versió ha fallat, es tornarà a intentar en %d segons: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Canviant a Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Canviant a 4G...", |
| | | "STANDBY": "En espera", |
| | | "CONNECT_TO": "Connectar a ", |
| | | "CONNECTING": "Connectant...", |
| | | "CONNECTION_SUCCESSFUL": "Connexió exitosa", |
| | | "CONNECTED_TO": "Connectat a ", |
| | | "LISTENING": "Escoltant...", |
| | | "SPEAKING": "Parlant...", |
| | | "SERVER_NOT_FOUND": "Buscant servei disponible", |
| | | "SERVER_NOT_CONNECTED": "No es pot connectar al servei, si us plau intenteu-ho més tard", |
| | | "SERVER_TIMEOUT": "Temps d'espera de resposta exhaurit", |
| | | "SERVER_ERROR": "L'enviament ha fallat, si us plau comproveu la xarxa", |
| | | "CONNECT_TO_HOTSPOT": "Punt d'accés: ", |
| | | "ACCESS_VIA_BROWSER": " URL de configuració: ", |
| | | "WIFI_CONFIG_MODE": "Mode de configuració Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Entrant en mode de configuració Wi-Fi...", |
| | | "SCANNING_WIFI": "Escanejant Wi-Fi...", |
| | | "NEW_VERSION": "Nova versió ", |
| | | "OTA_UPGRADE": "Actualització OTA", |
| | | "UPGRADING": "El sistema s'està actualitzant...", |
| | | "UPGRADE_FAILED": "L'actualització ha fallat", |
| | | "ACTIVATION": "Activació", |
| | | "BATTERY_LOW": "Bateria baixa", |
| | | "BATTERY_CHARGING": "Carregant", |
| | | "BATTERY_FULL": "Bateria plena", |
| | | "BATTERY_NEED_CHARGE": "Bateria baixa, si us plau carregueu", |
| | | "VOLUME": "Volum ", |
| | | "MUTED": "Silenciat", |
| | | "MAX_VOLUME": "Volum mà xim", |
| | | "RTC_MODE_OFF": "AEC desactivat", |
| | | "RTC_MODE_ON": "AEC activat", |
| | | "PLEASE_WAIT": "Si us plau, espereu...", |
| | | "FOUND_NEW_ASSETS": "S'han trobat nous recursos: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "No s'han pogut descarregar els recursos", |
| | | "LOADING_ASSETS": "Carregant recursos...", |
| | | "HELLO_MY_FRIEND": "Hola, amic meu!", |
| | | "FLIGHT_MODE_OFF": "El mode avió està desactivat", |
| | | "FLIGHT_MODE_ON": "El mode avió està activat", |
| | | "MODEM_INIT_ERROR": "Error d'inicialització del mòdem" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "cs-CZ" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "VarovánÃ", |
| | | "INFO": "Informace", |
| | | "ERROR": "Chyba", |
| | | "VERSION": "Verze ", |
| | | "LOADING_PROTOCOL": "PÅipojovánà k serveru...", |
| | | "INITIALIZING": "Inicializace...", |
| | | "PIN_ERROR": "ProsÃm vložte SIM kartu", |
| | | "REG_ERROR": "Nelze se pÅipojit k sÃti, zkontrolujte stav datové karty", |
| | | "DETECTING_MODULE": "Detekce modulu...", |
| | | "REGISTERING_NETWORK": "Äekánà na sÃÅ¥...", |
| | | "CHECKING_NEW_VERSION": "Kontrola nové verze...", |
| | | "CHECK_NEW_VERSION_FAILED": "Kontrola nové verze selhala, opakovánà za %d sekund: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "PÅepÃnánà na Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "PÅepÃnánà na 4G...", |
| | | "STANDBY": "Pohotovost", |
| | | "CONNECT_TO": "PÅipojit k ", |
| | | "CONNECTING": "PÅipojovánÃ...", |
| | | "CONNECTED_TO": "PÅipojeno k ", |
| | | "LISTENING": "NaslouchánÃ...", |
| | | "SPEAKING": "MluvenÃ...", |
| | | "SERVER_NOT_FOUND": "Hledánà dostupné služby", |
| | | "SERVER_NOT_CONNECTED": "Nelze se pÅipojit ke službÄ, zkuste to pozdÄji", |
| | | "SERVER_TIMEOUT": "Äas odpovÄdi vyprÅ¡el", |
| | | "SERVER_ERROR": "Odeslánà selhalo, zkontrolujte sÃÅ¥", |
| | | "CONNECT_TO_HOTSPOT": "PÅipojte telefon k hotspotu ", |
| | | "ACCESS_VIA_BROWSER": "ï¼pÅÃstup pÅes prohlÞeÄ ", |
| | | "WIFI_CONFIG_MODE": "Režim konfigurace sÃtÄ", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Vstup do režimu konfigurace sÃtÄ...", |
| | | "SCANNING_WIFI": "Skenovánà Wi-Fi...", |
| | | "NEW_VERSION": "Nová verze ", |
| | | "OTA_UPGRADE": "OTA upgrade", |
| | | "UPGRADING": "Aktualizace systému...", |
| | | "UPGRADE_FAILED": "Upgrade selhal", |
| | | "ACTIVATION": "Aktivace zaÅÃzenÃ", |
| | | "BATTERY_LOW": "Slabá baterie", |
| | | "BATTERY_CHARGING": "NabÃjenÃ", |
| | | "BATTERY_FULL": "Baterie plná", |
| | | "BATTERY_NEED_CHARGE": "Slabá baterie, prosÃm nabijte", |
| | | "VOLUME": "Hlasitost ", |
| | | "MUTED": "Ztlumeno", |
| | | "MAX_VOLUME": "Maximálnà hlasitost", |
| | | "RTC_MODE_OFF": "AEC vypnuto", |
| | | "RTC_MODE_ON": "AEC zapnuto", |
| | | "DOWNLOAD_ASSETS_FAILED": "NepodaÅilo se stáhnout prostÅedky", |
| | | "LOADING_ASSETS": "NaÄÃtánà prostÅedků...", |
| | | "PLEASE_WAIT": "ProsÃm Äekejte...", |
| | | "FOUND_NEW_ASSETS": "Nalezeny nové prostÅedky: %s", |
| | | "HELLO_MY_FRIEND": "Ahoj, můj pÅÃteli!", |
| | | "CONNECTION_SUCCESSFUL": "PÅipojenà úspÄÅ¡né", |
| | | "FLIGHT_MODE_OFF": "Letecký režim je vypnutý", |
| | | "FLIGHT_MODE_ON": "Letecký režim je zapnutý", |
| | | "MODEM_INIT_ERROR": "Chyba inicializace modemu" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "da-DK" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Advarsel", |
| | | "INFO": "Information", |
| | | "ERROR": "Fejl", |
| | | "VERSION": "Version ", |
| | | "LOADING_PROTOCOL": "Logger ind...", |
| | | "INITIALIZING": "Initialiserer...", |
| | | "PIN_ERROR": "Indsæt venligst SIM-kort", |
| | | "REG_ERROR": "Kan ikke få adgang til netværket, tjek venligst SIM-kortets status", |
| | | "DETECTING_MODULE": "Detekterer modul...", |
| | | "REGISTERING_NETWORK": "Venter på netværk...", |
| | | "CHECKING_NEW_VERSION": "Tjekker for ny version...", |
| | | "CHECK_NEW_VERSION_FAILED": "Tjek for ny version mislykkedes, prøver igen om %d sekunder: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Skifter til Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Skifter til 4G...", |
| | | "STANDBY": "Standby", |
| | | "CONNECT_TO": "Forbind til ", |
| | | "CONNECTING": "Forbinder...", |
| | | "CONNECTION_SUCCESSFUL": "Forbindelse lykkedes", |
| | | "CONNECTED_TO": "Forbundet til ", |
| | | "LISTENING": "Lytter...", |
| | | "SPEAKING": "Taler...", |
| | | "SERVER_NOT_FOUND": "Søger efter tilgængelig tjeneste", |
| | | "SERVER_NOT_CONNECTED": "Kan ikke forbinde til tjeneste, prøv venligst igen senere", |
| | | "SERVER_TIMEOUT": "Timeout ved venten på svar", |
| | | "SERVER_ERROR": "Afsendelse mislykkedes, tjek venligst netværket", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " Konfigurations-URL: ", |
| | | "WIFI_CONFIG_MODE": "Wi-Fi-konfigurationstilstand", |
| | | "ENTERING_WIFI_CONFIG_MODE": "GÃ¥r ind i Wi-Fi-konfigurationstilstand...", |
| | | "SCANNING_WIFI": "Scanner Wi-Fi...", |
| | | "NEW_VERSION": "Ny version ", |
| | | "OTA_UPGRADE": "OTA-opgradering", |
| | | "UPGRADING": "Systemet opgraderes...", |
| | | "UPGRADE_FAILED": "Opgradering mislykkedes", |
| | | "ACTIVATION": "Aktivering", |
| | | "BATTERY_LOW": "Lavt batteri", |
| | | "BATTERY_CHARGING": "Oplader", |
| | | "BATTERY_FULL": "Batteriet er fuldt", |
| | | "BATTERY_NEED_CHARGE": "Lavt batteri, oplad venligst", |
| | | "VOLUME": "Lydstyrke ", |
| | | "MUTED": "Lydløs", |
| | | "MAX_VOLUME": "Maksimal lydstyrke", |
| | | "RTC_MODE_OFF": "AEC slukket", |
| | | "RTC_MODE_ON": "AEC tændt", |
| | | "PLEASE_WAIT": "Vent venligst...", |
| | | "FOUND_NEW_ASSETS": "Fandt nye ressourcer: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Download af ressourcer mislykkedes", |
| | | "LOADING_ASSETS": "Indlæser ressourcer...", |
| | | "HELLO_MY_FRIEND": "Hej, min ven!", |
| | | "FLIGHT_MODE_OFF": "Flytilstand er slukket", |
| | | "FLIGHT_MODE_ON": "Flytilstand er tændt", |
| | | "MODEM_INIT_ERROR": "Modeminitialisering mislykkedes" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "de-DE" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Warnung", |
| | | "INFO": "Information", |
| | | "ERROR": "Fehler", |
| | | "VERSION": "Version ", |
| | | "LOADING_PROTOCOL": "Verbindung zum Server...", |
| | | "INITIALIZING": "Initialisierung...", |
| | | "PIN_ERROR": "Bitte SIM-Karte einlegen", |
| | | "REG_ERROR": "Netzwerkverbindung fehlgeschlagen, bitte Datenkartenstatus prüfen", |
| | | "DETECTING_MODULE": "Modul erkennen...", |
| | | "REGISTERING_NETWORK": "Auf Netzwerk warten...", |
| | | "CHECKING_NEW_VERSION": "Neue Version prüfen...", |
| | | "CHECK_NEW_VERSION_FAILED": "Neue Version prüfen fehlgeschlagen, Wiederholung in %d Sekunden: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Zu Wi-Fi wechseln...", |
| | | "SWITCH_TO_4G_NETWORK": "Zu 4G wechseln...", |
| | | "STANDBY": "Bereitschaft", |
| | | "CONNECT_TO": "Verbinden zu ", |
| | | "CONNECTING": "Verbindung wird hergestellt...", |
| | | "CONNECTED_TO": "Verbunden mit ", |
| | | "LISTENING": "Zuhören...", |
| | | "SPEAKING": "Sprechen...", |
| | | "SERVER_NOT_FOUND": "Verfügbaren Service suchen", |
| | | "SERVER_NOT_CONNECTED": "Service-Verbindung fehlgeschlagen, bitte später versuchen", |
| | | "SERVER_TIMEOUT": "Antwort-Timeout", |
| | | "SERVER_ERROR": "Senden fehlgeschlagen, bitte Netzwerk prüfen", |
| | | "CONNECT_TO_HOTSPOT": "Handy mit Hotspot verbinden ", |
| | | "ACCESS_VIA_BROWSER": "ï¼Browser öffnen ", |
| | | "WIFI_CONFIG_MODE": "Netzwerkkonfigurationsmodus", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Netzwerkkonfigurationsmodus eingeben...", |
| | | "SCANNING_WIFI": "Wi-Fi scannen...", |
| | | "NEW_VERSION": "Neue Version ", |
| | | "OTA_UPGRADE": "OTA-Upgrade", |
| | | "UPGRADING": "System wird aktualisiert...", |
| | | "UPGRADE_FAILED": "Upgrade fehlgeschlagen", |
| | | "ACTIVATION": "Gerät aktivieren", |
| | | "BATTERY_LOW": "Niedriger Batteriestand", |
| | | "BATTERY_CHARGING": "Wird geladen", |
| | | "BATTERY_FULL": "Batterie voll", |
| | | "BATTERY_NEED_CHARGE": "Niedriger Batteriestand, bitte aufladen", |
| | | "VOLUME": "Lautstärke ", |
| | | "MUTED": "Stummgeschaltet", |
| | | "MAX_VOLUME": "Maximale Lautstärke", |
| | | "RTC_MODE_OFF": "AEC aus", |
| | | "RTC_MODE_ON": "AEC ein", |
| | | "DOWNLOAD_ASSETS_FAILED": "Fehler beim Herunterladen der Ressourcen", |
| | | "LOADING_ASSETS": "Ressourcen werden geladen...", |
| | | "PLEASE_WAIT": "Bitte warten...", |
| | | "FOUND_NEW_ASSETS": "Neue Ressourcen gefunden: %s", |
| | | "HELLO_MY_FRIEND": "Hallo, mein Freund!", |
| | | "CONNECTION_SUCCESSFUL": "Verbindung erfolgreich", |
| | | "FLIGHT_MODE_OFF": "Flugmodus ist deaktiviert", |
| | | "FLIGHT_MODE_ON": "Flugmodus ist aktiviert", |
| | | "MODEM_INIT_ERROR": "Modem-Initialisierung fehlgeschlagen" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "el-GR" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Î ÏοειδοÏοίηÏη", |
| | | "INFO": "ΠληÏοÏοÏίεÏ", |
| | | "ERROR": "ΣÏάλμα", |
| | | "VERSION": "ÎκδοÏη ", |
| | | "LOADING_PROTOCOL": "ΣÏνδεÏη...", |
| | | "INITIALIZING": "ÎÏÏικοÏοίηÏη...", |
| | | "PIN_ERROR": "ΠαÏÎ±ÎºÎ±Î»Ï ÎµÎ¹ÏαγάγεÏε κάÏÏα SIM", |
| | | "REG_ERROR": "ÎδÏ
ναμία ÏÏÏÏβαÏÎ·Ï ÏÏο δίκÏÏ
ο, ελÎγξÏε Ïην καÏάÏÏαÏη ÏÎ·Ï ÎºÎ¬ÏÏÎ±Ï SIM", |
| | | "DETECTING_MODULE": "ÎνίÏνεÏ
Ïη μονάδαÏ...", |
| | | "REGISTERING_NETWORK": "Îναμονή δικÏÏοÏ
...", |
| | | "CHECKING_NEW_VERSION": "ÎλεγÏÎ¿Ï Î³Î¹Î± νÎα ÎκδοÏη...", |
| | | "CHECK_NEW_VERSION_FAILED": "Î ÎλεγÏÎ¿Ï Î³Î¹Î± νÎα ÎκδοÏη αÏÎÏÏ
Ïε, θα εÏαναληÏθεί Ïε %d δεÏ
ÏεÏÏλεÏÏα: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "ÎεÏάβαÏη Ïε Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "ÎεÏάβαÏη Ïε 4G...", |
| | | "STANDBY": "Îναμονή", |
| | | "CONNECT_TO": "ΣÏνδεÏη Ïε ", |
| | | "CONNECTING": "ΣÏνδεÏη...", |
| | | "CONNECTION_SUCCESSFUL": "ÎÏιÏÏ
ÏÎ®Ï ÏÏνδεÏη", |
| | | "CONNECTED_TO": "ΣÏ
νδÎθηκε Ïε ", |
| | | "LISTENING": "ÎκÏÏαÏη...", |
| | | "SPEAKING": "Îμιλία...", |
| | | "SERVER_NOT_FOUND": "ÎναζήÏηÏη διαθÎÏÎ¹Î¼Î·Ï Ï
ÏηÏεÏίαÏ", |
| | | "SERVER_NOT_CONNECTED": "ÎδÏ
ναμία ÏÏνδεÏÎ·Ï ÏÏην Ï
ÏηÏεÏία, ÏαÏÎ±ÎºÎ±Î»Ï Î´Î¿ÎºÎ¹Î¼Î¬ÏÏε αÏγÏÏεÏα", |
| | | "SERVER_TIMEOUT": "Îήξη ÏÏÏνοÏ
Î±Î½Î±Î¼Î¿Î½Î®Ï Î±ÏÏκÏιÏηÏ", |
| | | "SERVER_ERROR": "ΠαÏοÏÏολή αÏÎÏÏ
Ïε, ελÎγξÏε Ïο δίκÏÏ
ο", |
| | | "CONNECT_TO_HOTSPOT": "Σημείο ÏÏÏÏβαÏηÏ: ", |
| | | "ACCESS_VIA_BROWSER": " URL διαμÏÏÏÏÏηÏ: ", |
| | | "WIFI_CONFIG_MODE": "ÎειÏοÏ
Ïγία διαμÏÏÏÏÏÎ·Ï Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "ÎίÏÎ¿Î´Î¿Ï Ïε λειÏοÏ
Ïγία διαμÏÏÏÏÏÎ·Ï Wi-Fi...", |
| | | "SCANNING_WIFI": "ΣάÏÏÏη Wi-Fi...", |
| | | "NEW_VERSION": "ÎÎα ÎκδοÏη ", |
| | | "OTA_UPGRADE": "ÎναβάθμιÏη OTA", |
| | | "UPGRADING": "Το ÏÏÏÏημα αναβαθμίζεÏαι...", |
| | | "UPGRADE_FAILED": "ΠαναβάθμιÏη αÏÎÏÏ
Ïε", |
| | | "ACTIVATION": "ÎνεÏγοÏοίηÏη", |
| | | "BATTERY_LOW": "Χαμηλή μÏαÏαÏία", |
| | | "BATTERY_CHARGING": "ΦÏÏÏιÏη", |
| | | "BATTERY_FULL": "ΠλήÏÎ·Ï Î¼ÏαÏαÏία", |
| | | "BATTERY_NEED_CHARGE": "Χαμηλή μÏαÏαÏία, ÏαÏÎ±ÎºÎ±Î»Ï ÏοÏÏίÏÏε", |
| | | "VOLUME": "ÎνÏαÏη ", |
| | | "MUTED": "ΣίγαÏη", |
| | | "MAX_VOLUME": "ÎÎγιÏÏη ÎνÏαÏη", |
| | | "RTC_MODE_OFF": "AEC αÏενεÏγοÏοιημÎνο", |
| | | "RTC_MODE_ON": "AEC ενεÏγοÏοιημÎνο", |
| | | "PLEASE_WAIT": "ΠαÏÎ±ÎºÎ±Î»Ï ÏεÏιμÎνεÏε...", |
| | | "FOUND_NEW_ASSETS": "ÎÏÎθηκαν νÎοι ÏÏÏοι: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "ÎÏοÏÏ
Ïία λήÏÎ·Ï ÏÏÏÏν", |
| | | "LOADING_ASSETS": "ΦÏÏÏÏÏη ÏÏÏÏν...", |
| | | "HELLO_MY_FRIEND": "Îεια ÏοÏ
, Ïίλε μοÏ
!", |
| | | "FLIGHT_MODE_OFF": "ΠλειÏοÏ
Ïγία ÏÏήÏÎ·Ï ÎµÎ¯Î½Î±Î¹ αÏενεÏγοÏοιημÎνη", |
| | | "FLIGHT_MODE_ON": "ΠλειÏοÏ
Ïγία ÏÏήÏÎ·Ï ÎµÎ¯Î½Î±Î¹ ενεÏγή", |
| | | "MODEM_INIT_ERROR": "ÎÏοÏÏ
Ïία αÏÏικοÏοίηÏÎ·Ï modem" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "en-US" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Warning", |
| | | "INFO": "Information", |
| | | "ERROR": "Error", |
| | | "VERSION": "Ver ", |
| | | "LOADING_PROTOCOL": "Logging in...", |
| | | "INITIALIZING": "Initializing...", |
| | | "PIN_ERROR": "Please insert SIM card", |
| | | "REG_ERROR": "Unable to access network, please check SIM card status", |
| | | "MODEM_INIT_ERROR": "Modem initialization failed", |
| | | "DETECTING_MODULE": "Detecting module...", |
| | | "FLIGHT_MODE_ON": "Flight mode is on", |
| | | "FLIGHT_MODE_OFF": "Flight mode is off", |
| | | "REGISTERING_NETWORK": "Waiting for network...", |
| | | "CHECKING_NEW_VERSION": "Checking for new version...", |
| | | "CHECK_NEW_VERSION_FAILED": "Check for new version failed, will retry in %d seconds: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Switching to Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Switching to 4G...", |
| | | "STANDBY": "Standby", |
| | | "CONNECT_TO": "Connect to ", |
| | | "CONNECTING": "Connecting...", |
| | | "CONNECTION_SUCCESSFUL": "Connection Successful", |
| | | "CONNECTED_TO": "Connected to ", |
| | | "LISTENING": "Listening...", |
| | | "SPEAKING": "Speaking...", |
| | | "SERVER_NOT_FOUND": "Looking for available service", |
| | | "SERVER_NOT_CONNECTED": "Unable to connect to service, please try again later", |
| | | "SERVER_TIMEOUT": "Waiting for response timeout", |
| | | "SERVER_ERROR": "Sending failed, please check the network", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " Config URL: ", |
| | | "WIFI_CONFIG_MODE": "Wi-Fi Configuration Mode", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Entering Wi-Fi configuration mode...", |
| | | "SCANNING_WIFI": "Scanning Wi-Fi...", |
| | | "NEW_VERSION": "New version ", |
| | | "OTA_UPGRADE": "OTA Upgrade", |
| | | "UPGRADING": "System is upgrading...", |
| | | "UPGRADE_FAILED": "Upgrade failed", |
| | | "ACTIVATION": "Activation", |
| | | "BATTERY_LOW": "Low battery", |
| | | "BATTERY_CHARGING": "Charging", |
| | | "BATTERY_FULL": "Battery full", |
| | | "BATTERY_NEED_CHARGE": "Low battery, please charge", |
| | | "VOLUME": "Volume ", |
| | | "MUTED": "Muted", |
| | | "MAX_VOLUME": "Max volume", |
| | | "RTC_MODE_OFF": "AEC Off", |
| | | "RTC_MODE_ON": "AEC On", |
| | | "PLEASE_WAIT": "Please wait...", |
| | | "FOUND_NEW_ASSETS": "Found new assets: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Failed to download assets", |
| | | "LOADING_ASSETS": "Loading assets...", |
| | | "HELLO_MY_FRIEND": "Hello, my friend!" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "es-ES" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Advertencia", |
| | | "INFO": "Información", |
| | | "ERROR": "Error", |
| | | "VERSION": "Versión ", |
| | | "LOADING_PROTOCOL": "Conectando al servidor...", |
| | | "INITIALIZING": "Inicializando...", |
| | | "PIN_ERROR": "Por favor inserte la tarjeta SIM", |
| | | "REG_ERROR": "No se puede acceder a la red, verifique el estado de la tarjeta de datos", |
| | | "DETECTING_MODULE": "Detectando módulo...", |
| | | "REGISTERING_NETWORK": "Esperando red...", |
| | | "CHECKING_NEW_VERSION": "Verificando nueva versión...", |
| | | "CHECK_NEW_VERSION_FAILED": "Error al verificar nueva versión, reintentando en %d segundos: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Cambiando a Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Cambiando a 4G...", |
| | | "STANDBY": "En espera", |
| | | "CONNECT_TO": "Conectar a ", |
| | | "CONNECTING": "Conectando...", |
| | | "CONNECTED_TO": "Conectado a ", |
| | | "LISTENING": "Escuchando...", |
| | | "SPEAKING": "Hablando...", |
| | | "SERVER_NOT_FOUND": "Buscando servicio disponible", |
| | | "SERVER_NOT_CONNECTED": "No se puede conectar al servicio, inténtelo más tarde", |
| | | "SERVER_TIMEOUT": "Tiempo de espera agotado", |
| | | "SERVER_ERROR": "Error de envÃo, verifique la red", |
| | | "CONNECT_TO_HOTSPOT": "Conectar teléfono al punto de acceso ", |
| | | "ACCESS_VIA_BROWSER": "ï¼acceder mediante navegador ", |
| | | "WIFI_CONFIG_MODE": "Modo configuración de red", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Entrando en modo configuración de red...", |
| | | "SCANNING_WIFI": "Escaneando Wi-Fi...", |
| | | "NEW_VERSION": "Nueva versión ", |
| | | "OTA_UPGRADE": "Actualización OTA", |
| | | "UPGRADING": "Actualizando sistema...", |
| | | "UPGRADE_FAILED": "Actualización fallida", |
| | | "ACTIVATION": "Activación del dispositivo", |
| | | "BATTERY_LOW": "BaterÃa baja", |
| | | "BATTERY_CHARGING": "Cargando", |
| | | "BATTERY_FULL": "BaterÃa llena", |
| | | "BATTERY_NEED_CHARGE": "BaterÃa baja, por favor cargar", |
| | | "VOLUME": "Volumen ", |
| | | "MUTED": "Silenciado", |
| | | "MAX_VOLUME": "Volumen máximo", |
| | | "RTC_MODE_OFF": "AEC desactivado", |
| | | "RTC_MODE_ON": "AEC activado", |
| | | "DOWNLOAD_ASSETS_FAILED": "Error al descargar recursos", |
| | | "LOADING_ASSETS": "Cargando recursos...", |
| | | "PLEASE_WAIT": "Por favor espere...", |
| | | "FOUND_NEW_ASSETS": "Encontrados nuevos recursos: %s", |
| | | "HELLO_MY_FRIEND": "¡Hola, mi amigo!", |
| | | "CONNECTION_SUCCESSFUL": "Conexión exitosa", |
| | | "FLIGHT_MODE_OFF": "El modo avión está desactivado", |
| | | "FLIGHT_MODE_ON": "El modo avión está activado", |
| | | "MODEM_INIT_ERROR": "Error de inicialización del módem" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "fa-IR" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "ÙØ´Ø¯Ø§Ø±", |
| | | "INFO": "Ø§Ø·ÙØ§Ø¹Ø§Øª", |
| | | "ERROR": "خطا", |
| | | "VERSION": "ÙØ³Ø®Ù ", |
| | | "LOADING_PROTOCOL": "ÙØ±Ùد Ø¨Ù Ø³ÛØ³ØªÙ
...", |
| | | "INITIALIZING": "در ØØ§Ù راÙâØ§ÙØ¯Ø§Ø²Û...", |
| | | "PIN_ERROR": "ÙØ·Ùا٠سÛÙ
کارت را ÙØ§Ø±Ø¯ Ú©ÙÛØ¯", |
| | | "REG_ERROR": "عدÙ
Ø¯Ø³ØªØ±Ø³Û Ø¨Ù Ø´Ø¨Ú©ÙØ ÙØ·ÙØ§Ù ÙØ¶Ø¹Ûت سÛÙ
کارت را Ø¨Ø±Ø±Ø³Û Ú©ÙÛØ¯", |
| | | "DETECTING_MODULE": "Ø´ÙØ§Ø³Ø§ÛÛ Ù
اÚÙÙ...", |
| | | "REGISTERING_NETWORK": "در Ø§ÙØªØ¸Ø§Ø± شبکÙ...", |
| | | "CHECKING_NEW_VERSION": "Ø¨Ø±Ø±Ø³Û ÙØ³Ø®Ù Ø¬Ø¯ÛØ¯...", |
| | | "CHECK_NEW_VERSION_FAILED": "Ø¨Ø±Ø±Ø³Û ÙØ³Ø®Ù Ø¬Ø¯ÛØ¯ ÙØ§Ù
ÙÙÙ Ø¨ÙØ¯Ø پس از %d ثاÙÛÙ Ù
Ø¬Ø¯Ø¯Ø§Ù ØªÙØ§Ø´ Ù
ÛâØ´ÙØ¯: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "تغÛÛØ± ب٠Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "تغÛÛØ± ب٠4G...", |
| | | "STANDBY": "Ø¢Ù
اد٠ب٠کار", |
| | | "CONNECT_TO": "اتصا٠ب٠", |
| | | "CONNECTING": "در ØØ§Ù اتصاÙ...", |
| | | "CONNECTION_SUCCESSFUL": "اتصا٠Ù
ÙÙÙ", |
| | | "CONNECTED_TO": "Ù
تص٠ب٠", |
| | | "LISTENING": "در ØØ§Ù Ú¯ÙØ´ دادÙ...", |
| | | "SPEAKING": "در ØØ§Ù ØµØØ¨Øª...", |
| | | "SERVER_NOT_FOUND": "جستجÙÛ Ø³Ø±ÙÛØ³ در دسترس", |
| | | "SERVER_NOT_CONNECTED": "اتصا٠ب٠سرÙÛØ³ Ø¨Ø±ÙØ±Ø§Ø± ÙØ´Ø¯Ø ÙØ·ÙØ§Ù Ø¨Ø¹Ø¯Ø§Ù ØªÙØ§Ø´ Ú©ÙÛØ¯", |
| | | "SERVER_TIMEOUT": "زÙ
Ø§Ù Ø§ÙØªØ¸Ø§Ø± Ø¨Ø±Ø§Û Ù¾Ø§Ø³Ø® Ø¨Ù Ù¾Ø§ÛØ§Ù Ø±Ø³ÛØ¯", |
| | | "SERVER_ERROR": "Ø§Ø±Ø³Ø§Ù ÙØ§Ù
ÙÙÙØ ÙØ·Ùا٠شبک٠را Ø¨Ø±Ø±Ø³Û Ú©ÙÛØ¯", |
| | | "CONNECT_TO_HOTSPOT": "ÙÙØ·Ù اتصاÙ: ", |
| | | "ACCESS_VIA_BROWSER": " آدرس Ù¾ÛÚ©Ø±Ø¨ÙØ¯Û: ", |
| | | "WIFI_CONFIG_MODE": "ØØ§Ùت Ù¾ÛÚ©Ø±Ø¨ÙØ¯Û Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "ÙØ±Ùد Ø¨Ù ØØ§Ùت Ù¾ÛÚ©Ø±Ø¨ÙØ¯Û Wi-Fi...", |
| | | "SCANNING_WIFI": "جستجÙÛ Wi-Fi...", |
| | | "NEW_VERSION": "ÙØ³Ø®Ù Ø¬Ø¯ÛØ¯ ", |
| | | "OTA_UPGRADE": "بÙâØ±ÙØ²Ø±Ø³Ø§ÙÛ OTA", |
| | | "UPGRADING": "Ø³ÛØ³ØªÙ
در ØØ§Ù بÙâØ±ÙØ²Ø±Ø³Ø§ÙÛ Ø§Ø³Øª...", |
| | | "UPGRADE_FAILED": "بÙâØ±ÙØ²Ø±Ø³Ø§ÙÛ ÙØ§Ù
ÙÙÙ Ø¨ÙØ¯", |
| | | "ACTIVATION": "ÙØ¹Ø§ÙâØ³Ø§Ø²Û", |
| | | "BATTERY_LOW": "Ø´Ø§Ø±Ú Ø¨Ø§ØªØ±Û Ú©Ù
", |
| | | "BATTERY_CHARGING": "در ØØ§Ù شارÚ", |
| | | "BATTERY_FULL": "Ø¨Ø§ØªØ±Û Ù¾Ø± است", |
| | | "BATTERY_NEED_CHARGE": "Ø´Ø§Ø±Ú Ø¨Ø§ØªØ±Û Ú©Ù
Ø ÙØ·ÙØ§Ù Ø´Ø§Ø±Ú Ú©ÙÛØ¯", |
| | | "VOLUME": "صدا ", |
| | | "MUTED": "بÛâØµØ¯Ø§", |
| | | "MAX_VOLUME": "ØØ¯Ø§Ú©Ø«Ø± صدا", |
| | | "RTC_MODE_OFF": "AEC خاÙ
ÙØ´", |
| | | "RTC_MODE_ON": "AEC Ø±ÙØ´Ù", |
| | | "PLEASE_WAIT": "ÙØ·Ùا٠صبر Ú©ÙÛØ¯...", |
| | | "FOUND_NEW_ASSETS": "Ù
ÙØ§Ø¨Ø¹ Ø¬Ø¯ÛØ¯ ÛØ§Ùت شد: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "داÙÙÙØ¯ Ù
ÙØ§Ø¨Ø¹ ÙØ§Ù
ÙÙÙ Ø¨ÙØ¯", |
| | | "LOADING_ASSETS": "Ø¨Ø§Ø±Ú¯Ø°Ø§Ø±Û Ù
ÙØ§Ø¨Ø¹...", |
| | | "HELLO_MY_FRIEND": "Ø³ÙØ§Ù
Ø Ø¯ÙØ³Øª Ù
Ù!", |
| | | "FLIGHT_MODE_OFF": "ØØ§Ùت Ù¾Ø±ÙØ§Ø² خاÙ
ÙØ´ است", |
| | | "FLIGHT_MODE_ON": "ØØ§Ùت Ù¾Ø±ÙØ§Ø² Ø±ÙØ´Ù است", |
| | | "MODEM_INIT_ERROR": "خطا در راÙâØ§ÙØ¯Ø§Ø²Û Ù
ÙØ¯Ù
" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "fi-FI" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Varoitus", |
| | | "INFO": "Tieto", |
| | | "ERROR": "Virhe", |
| | | "VERSION": "Versio ", |
| | | "LOADING_PROTOCOL": "Yhdistetään palvelimeen...", |
| | | "INITIALIZING": "Alustetaan...", |
| | | "PIN_ERROR": "Ole hyvä ja aseta SIM-kortti", |
| | | "REG_ERROR": "Ei voi muodostaa yhteyttä verkkoon, tarkista datakortin tila", |
| | | "DETECTING_MODULE": "Tunnistetaan moduuli...", |
| | | "REGISTERING_NETWORK": "Odotetaan verkkoa...", |
| | | "CHECKING_NEW_VERSION": "Tarkistetaan uutta versiota...", |
| | | "CHECK_NEW_VERSION_FAILED": "Uuden version tarkistus epäonnistui, yritetään uudelleen %d sekunnin kuluttua: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Vaihdetaan Wi-Fi:hin...", |
| | | "SWITCH_TO_4G_NETWORK": "Vaihdetaan 4G:hen...", |
| | | "STANDBY": "Valmiustila", |
| | | "CONNECT_TO": "Yhdistä ", |
| | | "CONNECTING": "Yhdistetään...", |
| | | "CONNECTED_TO": "Yhdistetty ", |
| | | "LISTENING": "Kuunnellaan...", |
| | | "SPEAKING": "Puhutaan...", |
| | | "SERVER_NOT_FOUND": "Etsitään käytettävissä olevaa palvelua", |
| | | "SERVER_NOT_CONNECTED": "Ei voi yhdistää palveluun, yritä myöhemmin", |
| | | "SERVER_TIMEOUT": "Vastauksen aikakatkaisu", |
| | | "SERVER_ERROR": "Lähetys epäonnistui, tarkista verkko", |
| | | "CONNECT_TO_HOTSPOT": "Yhdistä puhelin hotspottiin ", |
| | | "ACCESS_VIA_BROWSER": "ï¼pääsy selaimen kautta ", |
| | | "WIFI_CONFIG_MODE": "Verkon konfigurointitila", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Siirrytään verkon konfigurointitilaan...", |
| | | "SCANNING_WIFI": "Skannataan Wi-Fi...", |
| | | "NEW_VERSION": "Uusi versio ", |
| | | "OTA_UPGRADE": "OTA-päivitys", |
| | | "UPGRADING": "Päivitetään järjestelmää...", |
| | | "UPGRADE_FAILED": "Päivitys epäonnistui", |
| | | "ACTIVATION": "Laitteen aktivointi", |
| | | "BATTERY_LOW": "Akku vähissä", |
| | | "BATTERY_CHARGING": "Ladataan", |
| | | "BATTERY_FULL": "Akku täynnä", |
| | | "BATTERY_NEED_CHARGE": "Akku vähissä, ole hyvä ja lataa", |
| | | "VOLUME": "Ãänenvoimakkuus ", |
| | | "MUTED": "Mykistetty", |
| | | "MAX_VOLUME": "Maksimi äänenvoimakkuus", |
| | | "RTC_MODE_OFF": "AEC pois päältä", |
| | | "RTC_MODE_ON": "AEC päällä", |
| | | "DOWNLOAD_ASSETS_FAILED": "Resurssien lataaminen epäonnistui", |
| | | "LOADING_ASSETS": "Ladataan resursseja...", |
| | | "PLEASE_WAIT": "Odota hetki...", |
| | | "FOUND_NEW_ASSETS": "Löydetty uusia resursseja: %s", |
| | | "HELLO_MY_FRIEND": "Hei, ystäväni!", |
| | | "CONNECTION_SUCCESSFUL": "Yhteys onnistui", |
| | | "FLIGHT_MODE_OFF": "Lentotila on pois päältä", |
| | | "FLIGHT_MODE_ON": "Lentotila on päällä", |
| | | "MODEM_INIT_ERROR": "Modeemin alustus epäonnistui" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "fil-PH" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Babala", |
| | | "INFO": "Impormasyon", |
| | | "ERROR": "Error", |
| | | "VERSION": "Bersyon ", |
| | | "LOADING_PROTOCOL": "Nagla-log in...", |
| | | "INITIALIZING": "Nag-i-initialize...", |
| | | "PIN_ERROR": "Mangyaring ilagay ang SIM card", |
| | | "REG_ERROR": "Hindi ma-access ang network, mangyaring tingnan ang estado ng SIM card", |
| | | "DETECTING_MODULE": "Tinutuklas ang module...", |
| | | "REGISTERING_NETWORK": "Naghihintay ng network...", |
| | | "CHECKING_NEW_VERSION": "Sinusuri ang bagong bersyon...", |
| | | "CHECK_NEW_VERSION_FAILED": "Nabigo ang pagsuri ng bagong bersyon, susubukan muli sa %d segundo: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Lumilipat sa Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Lumilipat sa 4G...", |
| | | "STANDBY": "Standby", |
| | | "CONNECT_TO": "Kumonekta sa ", |
| | | "CONNECTING": "Kumokonekta...", |
| | | "CONNECTION_SUCCESSFUL": "Matagumpay na koneksyon", |
| | | "CONNECTED_TO": "Nakakonekta sa ", |
| | | "LISTENING": "Nakikinig...", |
| | | "SPEAKING": "Nagsasalita...", |
| | | "SERVER_NOT_FOUND": "Naghahanap ng available na serbisyo", |
| | | "SERVER_NOT_CONNECTED": "Hindi makakonekta sa serbisyo, mangyaring subukan muli mamaya", |
| | | "SERVER_TIMEOUT": "Nag-timeout ang paghihintay ng tugon", |
| | | "SERVER_ERROR": "Nabigo ang pagpapadala, mangyaring tingnan ang network", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " URL ng configuration: ", |
| | | "WIFI_CONFIG_MODE": "Wi-Fi Configuration Mode", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Pumapasok sa Wi-Fi configuration mode...", |
| | | "SCANNING_WIFI": "Nag-i-scan ng Wi-Fi...", |
| | | "NEW_VERSION": "Bagong bersyon ", |
| | | "OTA_UPGRADE": "OTA Upgrade", |
| | | "UPGRADING": "Nag-a-upgrade ang system...", |
| | | "UPGRADE_FAILED": "Nabigo ang upgrade", |
| | | "ACTIVATION": "Activation", |
| | | "BATTERY_LOW": "Mababang baterya", |
| | | "BATTERY_CHARGING": "Nagcha-charge", |
| | | "BATTERY_FULL": "Puno ang baterya", |
| | | "BATTERY_NEED_CHARGE": "Mababang baterya, mangyaring mag-charge", |
| | | "VOLUME": "Volume ", |
| | | "MUTED": "Naka-mute", |
| | | "MAX_VOLUME": "Maximum volume", |
| | | "RTC_MODE_OFF": "AEC Off", |
| | | "RTC_MODE_ON": "AEC On", |
| | | "PLEASE_WAIT": "Mangyaring maghintay...", |
| | | "FOUND_NEW_ASSETS": "Nakahanap ng mga bagong assets: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Nabigo ang pag-download ng mga assets", |
| | | "LOADING_ASSETS": "Nilo-load ang mga assets...", |
| | | "HELLO_MY_FRIEND": "Kumusta, kaibigan ko!", |
| | | "FLIGHT_MODE_OFF": "Naka-off ang flight mode", |
| | | "FLIGHT_MODE_ON": "Naka-on ang flight mode", |
| | | "MODEM_INIT_ERROR": "Nabigo ang pag-initialize ng modem" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "fr-FR" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Avertissement", |
| | | "INFO": "Information", |
| | | "ERROR": "Erreur", |
| | | "VERSION": "Version ", |
| | | "LOADING_PROTOCOL": "Connexion au serveur...", |
| | | "INITIALIZING": "Initialisation...", |
| | | "PIN_ERROR": "Veuillez insérer la carte SIM", |
| | | "REG_ERROR": "Impossible d'accéder au réseau, veuillez vérifier l'état de la carte de données", |
| | | "DETECTING_MODULE": "Détection du module...", |
| | | "REGISTERING_NETWORK": "En attente du réseau...", |
| | | "CHECKING_NEW_VERSION": "Vérification de nouvelle version...", |
| | | "CHECK_NEW_VERSION_FAILED": "Ãchec de vérification de nouvelle version, nouvelle tentative dans %d secondes : %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Basculer vers Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Basculer vers 4G...", |
| | | "STANDBY": "En attente", |
| | | "CONNECT_TO": "Se connecter à ", |
| | | "CONNECTING": "Connexion en cours...", |
| | | "CONNECTED_TO": "Connecté à ", |
| | | "LISTENING": "Ãcoute...", |
| | | "SPEAKING": "Parole...", |
| | | "SERVER_NOT_FOUND": "Recherche d'un service disponible", |
| | | "SERVER_NOT_CONNECTED": "Impossible de se connecter au service, veuillez réessayer plus tard", |
| | | "SERVER_TIMEOUT": "Délai d'attente de réponse", |
| | | "SERVER_ERROR": "Ãchec d'envoi, veuillez vérifier le réseau", |
| | | "CONNECT_TO_HOTSPOT": "Connecter le téléphone au point d'accès ", |
| | | "ACCESS_VIA_BROWSER": "ï¼accéder via le navigateur ", |
| | | "WIFI_CONFIG_MODE": "Mode configuration réseau", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Entrer en mode configuration réseau...", |
| | | "SCANNING_WIFI": "Scan Wi-Fi...", |
| | | "NEW_VERSION": "Nouvelle version ", |
| | | "OTA_UPGRADE": "Mise à jour OTA", |
| | | "UPGRADING": "Mise à jour du système...", |
| | | "UPGRADE_FAILED": "Ãchec de mise à jour", |
| | | "ACTIVATION": "Activation de l'appareil", |
| | | "BATTERY_LOW": "Batterie faible", |
| | | "BATTERY_CHARGING": "En charge", |
| | | "BATTERY_FULL": "Batterie pleine", |
| | | "BATTERY_NEED_CHARGE": "Batterie faible, veuillez charger", |
| | | "VOLUME": "Volume ", |
| | | "MUTED": "Muet", |
| | | "MAX_VOLUME": "Volume maximum", |
| | | "RTC_MODE_OFF": "AEC désactivé", |
| | | "RTC_MODE_ON": "AEC activé", |
| | | "DOWNLOAD_ASSETS_FAILED": "Ãchec du téléchargement des ressources", |
| | | "LOADING_ASSETS": "Chargement des ressources...", |
| | | "PLEASE_WAIT": "Veuillez patienter...", |
| | | "FOUND_NEW_ASSETS": "Nouvelles ressources trouvées: %s", |
| | | "HELLO_MY_FRIEND": "Bonjour, mon ami !", |
| | | "CONNECTION_SUCCESSFUL": "Connexion réussie", |
| | | "FLIGHT_MODE_OFF": "Le mode avion est désactivé", |
| | | "FLIGHT_MODE_ON": "Le mode avion est activé", |
| | | "MODEM_INIT_ERROR": "Ãchec de l'initialisation du modem" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "he-IL" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "×××ר×", |
| | | "INFO": "××××¢", |
| | | "ERROR": "ש××××", |
| | | "VERSION": "××¨×¡× ", |
| | | "LOADING_PROTOCOL": "×ת××ר...", |
| | | "INITIALIZING": "××ת××...", |
| | | "PIN_ERROR": "×× × ××× ×¡ ×ר××ס SIM", |
| | | "REG_ERROR": "×× × ××ª× ××שת ×רשת, ×× × ××××§ ×ת ××¦× ×ר××ס ×-SIM", |
| | | "DETECTING_MODULE": "×××× ×××××...", |
| | | "REGISTERING_NETWORK": "××ת×× ×רשת...", |
| | | "CHECKING_NEW_VERSION": "××××§ ××¨×¡× ××ש×...", |
| | | "CHECK_NEW_VERSION_FAILED": "×××קת ××¨×¡× ×××©× × ×ש××, ×× ×¡× ×©×× ××¢×× %d ×©× ××ת: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "×¢××ר ×-Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "×¢××ר ×-4G...", |
| | | "STANDBY": "×××ª× ×", |
| | | "CONNECT_TO": "×ת××ר ×-", |
| | | "CONNECTING": "×ת××ר...", |
| | | "CONNECTION_SUCCESSFUL": "×ת××ר×ת ×צ××××", |
| | | "CONNECTED_TO": "××××ר ×-", |
| | | "LISTENING": "×קש××...", |
| | | "SPEAKING": "×××ר...", |
| | | "SERVER_NOT_FOUND": "××פש ש×ר×ת ××××", |
| | | "SERVER_NOT_CONNECTED": "×× × ××ª× ××ת××ר ×ש×ר×ת, ×× × × ×¡× ×©×× ××××ר ××תר", |
| | | "SERVER_TIMEOUT": "×¤× ××× ××××ª× × ×ת××××", |
| | | "SERVER_ERROR": "×ש×××× × ×ש××, ×× × ××××§ ×ת ×רשת", |
| | | "CONNECT_TO_HOTSPOT": "× ×§××× ×××: ", |
| | | "ACCESS_VIA_BROWSER": " ×ת××ת תצ×ר×: ", |
| | | "WIFI_CONFIG_MODE": "××¦× ×ª×¦×רת Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "× ×× ×¡ ×××¦× ×ª×¦×רת Wi-Fi...", |
| | | "SCANNING_WIFI": "ס×רק Wi-Fi...", |
| | | "NEW_VERSION": "××¨×¡× ×××©× ", |
| | | "OTA_UPGRADE": "ש×ר×× OTA", |
| | | "UPGRADING": "××ער×ת ×שת×ר×ת...", |
| | | "UPGRADE_FAILED": "×ש×ר×× × ×ש×", |
| | | "ACTIVATION": "×פע××", |
| | | "BATTERY_LOW": "ס×××× ××ש×", |
| | | "BATTERY_CHARGING": "× ××¢×", |
| | | "BATTERY_FULL": "×ס×××× ××××", |
| | | "BATTERY_NEED_CHARGE": "ס×××× ××ש×, ×× × ××¢×", |
| | | "VOLUME": "×¢×צ×ת ×§×× ", |
| | | "MUTED": "××שתק", |
| | | "MAX_VOLUME": "×¢×צ×ת ×§×× ×קס××××ת", |
| | | "RTC_MODE_OFF": "AEC ××××", |
| | | "RTC_MODE_ON": "AEC ××××§", |
| | | "PLEASE_WAIT": "×× × ××ת×...", |
| | | "FOUND_NEW_ASSETS": "× ×צ×× ×ש×××× ××ש××: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "××ר×ת ×ש×××× × ×ש××", |
| | | "LOADING_ASSETS": "×××¢× ×ש××××...", |
| | | "HELLO_MY_FRIEND": "ש×××, ×××××!", |
| | | "FLIGHT_MODE_OFF": "××¦× ×××¡× ××××", |
| | | "FLIGHT_MODE_ON": "××¦× ×××¡× ××פע×", |
| | | "MODEM_INIT_ERROR": "×ת××× ××××× × ×ש×" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "hi-IN" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "à¤à¥à¤¤à¤¾à¤µà¤¨à¥", |
| | | "INFO": "à¤à¤¾à¤¨à¤à¤¾à¤°à¥", |
| | | "ERROR": "तà¥à¤°à¥à¤à¤¿", |
| | | "VERSION": "सà¤à¤¸à¥à¤à¤°à¤£ ", |
| | | "LOADING_PROTOCOL": "सरà¥à¤µà¤° सॠà¤à¤¨à¥à¤à¥à¤ हॠरहॠहà¥à¤...", |
| | | "INITIALIZING": "à¤à¤°à¤à¤à¥à¤à¤°à¤£...", |
| | | "PIN_ERROR": "à¤à¥à¤ªà¤¯à¤¾ सिम à¤à¤¾à¤°à¥à¤¡ डालà¥à¤", |
| | | "REG_ERROR": "नà¥à¤à¤µà¤°à¥à¤ तठपहà¥à¤à¤ नहà¥à¤ हॠसà¤à¤¤à¥, à¤à¥à¤ªà¤¯à¤¾ डà¥à¤à¤¾ à¤à¤¾à¤°à¥à¤¡ सà¥à¤¥à¤¿à¤¤à¤¿ à¤à¤¾à¤à¤à¥à¤", |
| | | "DETECTING_MODULE": "मà¥à¤¡à¥à¤¯à¥à¤² à¤à¤¾ पता लà¤à¤¾à¤¯à¤¾ à¤à¤¾ रहा हà¥...", |
| | | "REGISTERING_NETWORK": "नà¥à¤à¤µà¤°à¥à¤ à¤à¥ पà¥à¤°à¤¤à¥à¤à¥à¤·à¤¾...", |
| | | "CHECKING_NEW_VERSION": "नया सà¤à¤¸à¥à¤à¤°à¤£ à¤à¤¾à¤à¤ रहॠहà¥à¤...", |
| | | "CHECK_NEW_VERSION_FAILED": "नया सà¤à¤¸à¥à¤à¤°à¤£ à¤à¤¾à¤à¤à¤¨à¤¾ à¤
सफल, %d सà¥à¤à¤à¤¡ मà¥à¤ पà¥à¤¨à¤ पà¥à¤°à¤¯à¤¾à¤¸: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Wi-Fi पर सà¥à¤µà¤¿à¤ à¤à¤° रहॠहà¥à¤...", |
| | | "SWITCH_TO_4G_NETWORK": "4G पर सà¥à¤µà¤¿à¤ à¤à¤° रहॠहà¥à¤...", |
| | | "STANDBY": "सà¥à¤à¥à¤à¤¡à¤¬à¤¾à¤¯", |
| | | "CONNECT_TO": "à¤à¤¨à¥à¤à¥à¤ à¤à¤°à¥à¤ ", |
| | | "CONNECTING": "à¤à¤¨à¥à¤à¥à¤ हॠरहॠहà¥à¤...", |
| | | "CONNECTED_TO": "à¤à¤¨à¥à¤à¥à¤ हॠà¤à¤ ", |
| | | "LISTENING": "सà¥à¤¨ रहॠहà¥à¤...", |
| | | "SPEAKING": "बà¥à¤² रहॠहà¥à¤...", |
| | | "SERVER_NOT_FOUND": "à¤à¤ªà¤²à¤¬à¥à¤§ सà¥à¤µà¤¾ à¤à¥à¤ रहॠहà¥à¤", |
| | | "SERVER_NOT_CONNECTED": "सà¥à¤µà¤¾ सॠà¤à¤¨à¥à¤à¥à¤ नहà¥à¤ हॠसà¤à¤¤à¥, à¤à¥à¤ªà¤¯à¤¾ बाद मà¥à¤ à¤à¥à¤¶à¤¿à¤¶ à¤à¤°à¥à¤", |
| | | "SERVER_TIMEOUT": "पà¥à¤°à¤¤à¤¿à¤à¥à¤°à¤¿à¤¯à¤¾ à¤à¤¾ समय समापà¥à¤¤", |
| | | "SERVER_ERROR": "à¤à¥à¤à¤¨à¤¾ à¤
सफल, à¤à¥à¤ªà¤¯à¤¾ नà¥à¤à¤µà¤°à¥à¤ à¤à¤¾à¤à¤à¥à¤", |
| | | "CONNECT_TO_HOTSPOT": "फà¥à¤¨ à¤à¥ हà¥à¤à¤¸à¥à¤ªà¥à¤ सॠà¤à¤¨à¥à¤à¥à¤ à¤à¤°à¥à¤ ", |
| | | "ACCESS_VIA_BROWSER": "ï¼à¤¬à¥à¤°à¤¾à¤à¤à¤¼à¤° à¤à¥ माधà¥à¤¯à¤® सॠपहà¥à¤à¤à¥à¤ ", |
| | | "WIFI_CONFIG_MODE": "नà¥à¤à¤µà¤°à¥à¤ à¤à¥à¤¨à¥à¤«à¤¼à¤¿à¤à¤°à¥à¤¶à¤¨ मà¥à¤¡", |
| | | "ENTERING_WIFI_CONFIG_MODE": "नà¥à¤à¤µà¤°à¥à¤ à¤à¥à¤¨à¥à¤«à¤¼à¤¿à¤à¤°à¥à¤¶à¤¨ मà¥à¤¡ मà¥à¤ पà¥à¤°à¤µà¥à¤¶...", |
| | | "SCANNING_WIFI": "Wi-Fi सà¥à¤à¥à¤¨ à¤à¤° रहॠहà¥à¤...", |
| | | "NEW_VERSION": "नया सà¤à¤¸à¥à¤à¤°à¤£ ", |
| | | "OTA_UPGRADE": "OTA à¤
पà¤à¥à¤°à¥à¤¡", |
| | | "UPGRADING": "सिसà¥à¤à¤® à¤
पà¤à¥à¤°à¥à¤¡ हॠरहा हà¥...", |
| | | "UPGRADE_FAILED": "à¤
पà¤à¥à¤°à¥à¤¡ à¤
सफल", |
| | | "ACTIVATION": "डिवाà¤à¤¸ सà¤à¥à¤°à¤¿à¤¯à¤£", |
| | | "BATTERY_LOW": "बà¥à¤à¤°à¥ à¤à¤®", |
| | | "BATTERY_CHARGING": "à¤à¤¾à¤°à¥à¤ हॠरहॠहà¥", |
| | | "BATTERY_FULL": "बà¥à¤à¤°à¥ फà¥à¤²", |
| | | "BATTERY_NEED_CHARGE": "बà¥à¤à¤°à¥ à¤à¤® हà¥, à¤à¥à¤ªà¤¯à¤¾ à¤à¤¾à¤°à¥à¤ à¤à¤°à¥à¤", |
| | | "VOLUME": "à¤à¤µà¤¾à¤à¤¼ ", |
| | | "MUTED": "मà¥à¤¯à¥à¤", |
| | | "MAX_VOLUME": "à¤
धिà¤à¤¤à¤® à¤à¤µà¤¾à¤à¤¼", |
| | | "RTC_MODE_OFF": "AEC बà¤à¤¦", |
| | | "RTC_MODE_ON": "AEC à¤à¤¾à¤²à¥", |
| | | "DOWNLOAD_ASSETS_FAILED": "सà¤à¤¸à¤¾à¤§à¤¨ डाà¤à¤¨à¤²à¥à¤¡ à¤à¤°à¤¨à¥ मà¥à¤ विफल", |
| | | "LOADING_ASSETS": "सà¤à¤¸à¤¾à¤§à¤¨ लà¥à¤¡ हॠरहॠहà¥à¤...", |
| | | "PLEASE_WAIT": "à¤à¥à¤ªà¤¯à¤¾ पà¥à¤°à¤¤à¥à¤à¥à¤·à¤¾ à¤à¤°à¥à¤...", |
| | | "FOUND_NEW_ASSETS": "नठसà¤à¤¸à¤¾à¤§à¤¨ मिलà¥: %s", |
| | | "HELLO_MY_FRIEND": "नमसà¥à¤¤à¥, मà¥à¤°à¥ दà¥à¤¸à¥à¤¤!", |
| | | "CONNECTION_SUCCESSFUL": "à¤à¤¨à¥à¤à¥à¤¶à¤¨ सफल", |
| | | "FLIGHT_MODE_OFF": "फ़à¥à¤²à¤¾à¤à¤ मà¥à¤¡ बà¤à¤¦ हà¥", |
| | | "FLIGHT_MODE_ON": "फ़à¥à¤²à¤¾à¤à¤ मà¥à¤¡ à¤à¤¾à¤²à¥ हà¥", |
| | | "MODEM_INIT_ERROR": "मà¥à¤¡à¥à¤® à¤à¤°à¤à¤à¥à¤à¤°à¤£ विफल" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "hr-HR" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Upozorenje", |
| | | "INFO": "Informacije", |
| | | "ERROR": "Greška", |
| | | "VERSION": "Verzija ", |
| | | "LOADING_PROTOCOL": "Prijava...", |
| | | "INITIALIZING": "Inicijalizacija...", |
| | | "PIN_ERROR": "Molimo umetnite SIM karticu", |
| | | "REG_ERROR": "Nije moguÄ pristup mreži, molimo provjerite status SIM kartice", |
| | | "DETECTING_MODULE": "Otkrivanje modula...", |
| | | "REGISTERING_NETWORK": "Äekanje mreže...", |
| | | "CHECKING_NEW_VERSION": "Provjera nove verzije...", |
| | | "CHECK_NEW_VERSION_FAILED": "Provjera nove verzije nije uspjela, ponovit Äe se za %d sekundi: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Prebacivanje na Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Prebacivanje na 4G...", |
| | | "STANDBY": "Pripravnost", |
| | | "CONNECT_TO": "Poveži se s ", |
| | | "CONNECTING": "Povezivanje...", |
| | | "CONNECTION_SUCCESSFUL": "Uspješno povezivanje", |
| | | "CONNECTED_TO": "Povezano s ", |
| | | "LISTENING": "Slušanje...", |
| | | "SPEAKING": "Govorenje...", |
| | | "SERVER_NOT_FOUND": "Traženje dostupne usluge", |
| | | "SERVER_NOT_CONNECTED": "Nije moguÄe povezati se s uslugom, molimo pokuÅ¡ajte kasnije", |
| | | "SERVER_TIMEOUT": "Istek vremena Äekanja odgovora", |
| | | "SERVER_ERROR": "Slanje nije uspjelo, molimo provjerite mrežu", |
| | | "CONNECT_TO_HOTSPOT": "Pristupna toÄka: ", |
| | | "ACCESS_VIA_BROWSER": " URL konfiguracije: ", |
| | | "WIFI_CONFIG_MODE": "NaÄin konfiguracije Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Ulazak u naÄin konfiguracije Wi-Fi...", |
| | | "SCANNING_WIFI": "Skeniranje Wi-Fi...", |
| | | "NEW_VERSION": "Nova verzija ", |
| | | "OTA_UPGRADE": "OTA nadogradnja", |
| | | "UPGRADING": "Sustav se nadograÄuje...", |
| | | "UPGRADE_FAILED": "Nadogradnja nije uspjela", |
| | | "ACTIVATION": "Aktivacija", |
| | | "BATTERY_LOW": "Niska baterija", |
| | | "BATTERY_CHARGING": "Punjenje", |
| | | "BATTERY_FULL": "Baterija puna", |
| | | "BATTERY_NEED_CHARGE": "Niska baterija, molimo napunite", |
| | | "VOLUME": "GlasnoÄa ", |
| | | "MUTED": "Utišano", |
| | | "MAX_VOLUME": "Maksimalna glasnoÄa", |
| | | "RTC_MODE_OFF": "AEC iskljuÄen", |
| | | "RTC_MODE_ON": "AEC ukljuÄen", |
| | | "PLEASE_WAIT": "Molimo priÄekajte...", |
| | | "FOUND_NEW_ASSETS": "PronaÄeni novi resursi: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Preuzimanje resursa nije uspjelo", |
| | | "LOADING_ASSETS": "UÄitavanje resursa...", |
| | | "HELLO_MY_FRIEND": "Bok, moj prijatelju!", |
| | | "FLIGHT_MODE_OFF": "NaÄin rada u zrakoplovu je iskljuÄen", |
| | | "FLIGHT_MODE_ON": "NaÄin rada u zrakoplovu je ukljuÄen", |
| | | "MODEM_INIT_ERROR": "Neuspjela inicijalizacija modema" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "hu-HU" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Figyelmeztetés", |
| | | "INFO": "Információ", |
| | | "ERROR": "Hiba", |
| | | "VERSION": "Verzió ", |
| | | "LOADING_PROTOCOL": "Bejelentkezés...", |
| | | "INITIALIZING": "Inicializálás...", |
| | | "PIN_ERROR": "Kérjük, helyezze be a SIM kártyát", |
| | | "REG_ERROR": "A hálózat nem érhetÅ el, kérjük, ellenÅrizze a SIM kártya állapotát", |
| | | "DETECTING_MODULE": "Modul észlelése...", |
| | | "REGISTERING_NETWORK": "Várakozás a hálózatra...", |
| | | "CHECKING_NEW_VERSION": "Ãj verzió keresése...", |
| | | "CHECK_NEW_VERSION_FAILED": "Az új verzió keresése sikertelen, újrapróbálás %d másodperc múlva: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Váltás Wi-Fi-re...", |
| | | "SWITCH_TO_4G_NETWORK": "Váltás 4G-re...", |
| | | "STANDBY": "Készenlét", |
| | | "CONNECT_TO": "Csatlakozás: ", |
| | | "CONNECTING": "Csatlakozás...", |
| | | "CONNECTION_SUCCESSFUL": "Sikeres csatlakozás", |
| | | "CONNECTED_TO": "Csatlakozva: ", |
| | | "LISTENING": "Figyelés...", |
| | | "SPEAKING": "Beszéd...", |
| | | "SERVER_NOT_FOUND": "ElérhetŠszolgáltatás keresése", |
| | | "SERVER_NOT_CONNECTED": "A szolgáltatáshoz nem sikerült csatlakozni, kérjük, próbálja újra késÅbb", |
| | | "SERVER_TIMEOUT": "A várakozási idŠlejárt", |
| | | "SERVER_ERROR": "A küldés sikertelen, kérjük, ellenÅrizze a hálózatot", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " Konfigurációs URL: ", |
| | | "WIFI_CONFIG_MODE": "Wi-Fi konfigurációs mód", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Belépés Wi-Fi konfigurációs módba...", |
| | | "SCANNING_WIFI": "Wi-Fi keresése...", |
| | | "NEW_VERSION": "Ãj verzió ", |
| | | "OTA_UPGRADE": "OTA frissÃtés", |
| | | "UPGRADING": "A rendszer frissÃtése folyamatban...", |
| | | "UPGRADE_FAILED": "A frissÃtés sikertelen", |
| | | "ACTIVATION": "Aktiválás", |
| | | "BATTERY_LOW": "Alacsony akkumulátor", |
| | | "BATTERY_CHARGING": "Töltés", |
| | | "BATTERY_FULL": "Akkumulátor tele", |
| | | "BATTERY_NEED_CHARGE": "Alacsony akkumulátor, kérjük, töltse fel", |
| | | "VOLUME": "HangerÅ ", |
| | | "MUTED": "NémÃtva", |
| | | "MAX_VOLUME": "Maximális hangerÅ", |
| | | "RTC_MODE_OFF": "AEC kikapcsolva", |
| | | "RTC_MODE_ON": "AEC bekapcsolva", |
| | | "PLEASE_WAIT": "Kérjük, várjon...", |
| | | "FOUND_NEW_ASSETS": "Ãj erÅforrások találva: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Az erÅforrások letöltése sikertelen", |
| | | "LOADING_ASSETS": "ErÅforrások betöltése...", |
| | | "HELLO_MY_FRIEND": "Helló, barátom!", |
| | | "FLIGHT_MODE_OFF": "A repülési mód ki van kapcsolva", |
| | | "FLIGHT_MODE_ON": "A repülési mód be van kapcsolva", |
| | | "MODEM_INIT_ERROR": "A modem inicializálása sikertelen" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "id-ID" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Peringatan", |
| | | "INFO": "Informasi", |
| | | "ERROR": "Kesalahan", |
| | | "VERSION": "Versi ", |
| | | "LOADING_PROTOCOL": "Menghubungkan ke server...", |
| | | "INITIALIZING": "Menginisialisasi...", |
| | | "PIN_ERROR": "Silakan masukkan kartu SIM", |
| | | "REG_ERROR": "Tidak dapat mengakses jaringan, periksa status kartu data", |
| | | "DETECTING_MODULE": "Mendeteksi modul...", |
| | | "REGISTERING_NETWORK": "Menunggu jaringan...", |
| | | "CHECKING_NEW_VERSION": "Memeriksa versi baru...", |
| | | "CHECK_NEW_VERSION_FAILED": "Pemeriksaan versi baru gagal, mencoba lagi dalam %d detik: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Beralih ke Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Beralih ke 4G...", |
| | | "STANDBY": "Siaga", |
| | | "CONNECT_TO": "Hubungkan ke ", |
| | | "CONNECTING": "Menghubungkan...", |
| | | "CONNECTED_TO": "Terhubung ke ", |
| | | "LISTENING": "Mendengarkan...", |
| | | "SPEAKING": "Berbicara...", |
| | | "SERVER_NOT_FOUND": "Mencari layanan yang tersedia", |
| | | "SERVER_NOT_CONNECTED": "Tidak dapat terhubung ke layanan, coba lagi nanti", |
| | | "SERVER_TIMEOUT": "Waktu respons habis", |
| | | "SERVER_ERROR": "Pengiriman gagal, periksa jaringan", |
| | | "CONNECT_TO_HOTSPOT": "Hubungkan ponsel ke hotspot ", |
| | | "ACCESS_VIA_BROWSER": "ï¼akses melalui browser ", |
| | | "WIFI_CONFIG_MODE": "Mode konfigurasi jaringan", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Memasuki mode konfigurasi jaringan...", |
| | | "SCANNING_WIFI": "Memindai Wi-Fi...", |
| | | "NEW_VERSION": "Versi baru ", |
| | | "OTA_UPGRADE": "Pembaruan OTA", |
| | | "UPGRADING": "Memperbarui sistem...", |
| | | "UPGRADE_FAILED": "Pembaruan gagal", |
| | | "ACTIVATION": "Aktivasi perangkat", |
| | | "BATTERY_LOW": "Baterai lemah", |
| | | "BATTERY_CHARGING": "Mengisi", |
| | | "BATTERY_FULL": "Baterai penuh", |
| | | "BATTERY_NEED_CHARGE": "Baterai lemah, silakan isi", |
| | | "VOLUME": "Volume ", |
| | | "MUTED": "Bisu", |
| | | "MAX_VOLUME": "Volume maksimum", |
| | | "RTC_MODE_OFF": "AEC mati", |
| | | "RTC_MODE_ON": "AEC nyala", |
| | | "DOWNLOAD_ASSETS_FAILED": "Gagal mengunduh aset", |
| | | "LOADING_ASSETS": "Memuat aset...", |
| | | "PLEASE_WAIT": "Mohon tunggu...", |
| | | "FOUND_NEW_ASSETS": "Ditemukan aset baru: %s", |
| | | "HELLO_MY_FRIEND": "Halo, teman saya!", |
| | | "CONNECTION_SUCCESSFUL": "Koneksi berhasil", |
| | | "FLIGHT_MODE_OFF": "Mode pesawat nonaktif", |
| | | "FLIGHT_MODE_ON": "Mode pesawat aktif", |
| | | "MODEM_INIT_ERROR": "Gagal menginisialisasi modem" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "it-IT" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Avviso", |
| | | "INFO": "Informazione", |
| | | "ERROR": "Errore", |
| | | "VERSION": "Versione ", |
| | | "LOADING_PROTOCOL": "Connessione al server...", |
| | | "INITIALIZING": "Inizializzazione...", |
| | | "PIN_ERROR": "Inserire la scheda SIM", |
| | | "REG_ERROR": "Impossibile accedere alla rete, controllare lo stato della scheda dati", |
| | | "DETECTING_MODULE": "Rilevamento modulo...", |
| | | "REGISTERING_NETWORK": "In attesa della rete...", |
| | | "CHECKING_NEW_VERSION": "Controllo nuova versione...", |
| | | "CHECK_NEW_VERSION_FAILED": "Controllo nuova versione fallito, riprovo tra %d secondi: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Passaggio a Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Passaggio a 4G...", |
| | | "STANDBY": "In attesa", |
| | | "CONNECT_TO": "Connetti a ", |
| | | "CONNECTING": "Connessione...", |
| | | "CONNECTED_TO": "Connesso a ", |
| | | "LISTENING": "In ascolto...", |
| | | "SPEAKING": "Parlando...", |
| | | "SERVER_NOT_FOUND": "Ricerca servizio disponibile", |
| | | "SERVER_NOT_CONNECTED": "Impossibile connettersi al servizio, riprovare più tardi", |
| | | "SERVER_TIMEOUT": "Timeout risposta", |
| | | "SERVER_ERROR": "Invio fallito, controllare la rete", |
| | | "CONNECT_TO_HOTSPOT": "Connetti telefono al hotspot ", |
| | | "ACCESS_VIA_BROWSER": "ï¼accedi tramite browser ", |
| | | "WIFI_CONFIG_MODE": "Modalità configurazione rete", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Entrata in modalità configurazione rete...", |
| | | "SCANNING_WIFI": "Scansione Wi-Fi...", |
| | | "NEW_VERSION": "Nuova versione ", |
| | | "OTA_UPGRADE": "Aggiornamento OTA", |
| | | "UPGRADING": "Aggiornamento sistema...", |
| | | "UPGRADE_FAILED": "Aggiornamento fallito", |
| | | "ACTIVATION": "Attivazione dispositivo", |
| | | "BATTERY_LOW": "Batteria scarica", |
| | | "BATTERY_CHARGING": "In carica", |
| | | "BATTERY_FULL": "Batteria piena", |
| | | "BATTERY_NEED_CHARGE": "Batteria scarica, ricaricare", |
| | | "VOLUME": "Volume ", |
| | | "MUTED": "Silenziato", |
| | | "MAX_VOLUME": "Volume massimo", |
| | | "RTC_MODE_OFF": "AEC disattivato", |
| | | "RTC_MODE_ON": "AEC attivato", |
| | | "DOWNLOAD_ASSETS_FAILED": "Impossibile scaricare le risorse", |
| | | "LOADING_ASSETS": "Caricamento risorse...", |
| | | "PLEASE_WAIT": "Attendere prego...", |
| | | "FOUND_NEW_ASSETS": "Trovate nuove risorse: %s", |
| | | "HELLO_MY_FRIEND": "Ciao, amico mio!", |
| | | "CONNECTION_SUCCESSFUL": "Connessione riuscita", |
| | | "FLIGHT_MODE_OFF": "La modalità aereo è disattivata", |
| | | "FLIGHT_MODE_ON": "La modalità aereo è attiva", |
| | | "MODEM_INIT_ERROR": "Inizializzazione modem non riuscita" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "ja-JP" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "è¦å", |
| | | "INFO": "æ
å ±", |
| | | "ERROR": "ã¨ã©ã¼", |
| | | "VERSION": "ãã¼ã¸ã§ã³ ", |
| | | "LOADING_PROTOCOL": "ãµã¼ãã¼ã«ãã°ã¤ã³ä¸...", |
| | | "INITIALIZING": "åæåä¸...", |
| | | "PIN_ERROR": "SIMã«ã¼ããæ¿å
¥ãã¦ãã ãã", |
| | | "REG_ERROR": "ãããã¯ã¼ã¯ã«æ¥ç¶ã§ãã¾ããããããã¯ã¼ã¯ç¶æ
ã確èªãã¦ãã ãã", |
| | | "DETECTING_MODULE": "ã¢ã¸ã¥ã¼ã«ãæ¤åºä¸...", |
| | | "REGISTERING_NETWORK": "ãããã¯ã¼ã¯æ¥ç¶å¾
æ©ä¸...", |
| | | "CHECKING_NEW_VERSION": "æ°ãããã¼ã¸ã§ã³ã確èªä¸...", |
| | | "CHECK_NEW_VERSION_FAILED": "æ´æ°ç¢ºèªã«å¤±æãã¾ããã%d ç§å¾ã«å試è¡ãã¾ã: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Wi-Fiã«åãæ¿ãä¸...", |
| | | "SWITCH_TO_4G_NETWORK": "4Gã«åãæ¿ãä¸...", |
| | | "STANDBY": "å¾
æ©ä¸", |
| | | "CONNECT_TO": "æ¥ç¶å
", |
| | | "CONNECTING": "æ¥ç¶ä¸...", |
| | | "CONNECTED_TO": "æ¥ç¶å®äº ", |
| | | "LISTENING": "ãªã¹ãã³ã°ä¸...", |
| | | "SPEAKING": "話ãã¦ãã¾ã...", |
| | | "SERVER_NOT_FOUND": "å©ç¨å¯è½ãªãµã¼ãã¼ãæ¢ãã¦ãã¾ã", |
| | | "SERVER_NOT_CONNECTED": "ãµã¼ãã¼ã«æ¥ç¶ã§ãã¾ãããå¾ã§ããä¸åº¦ã試ããã ãã", |
| | | "SERVER_TIMEOUT": "å¿çå¾
æ©æéãçµäºãã¾ãã", |
| | | "SERVER_ERROR": "éä¿¡ã«å¤±æãã¾ããããããã¯ã¼ã¯ã確èªãã¦ãã ãã", |
| | | "CONNECT_TO_HOTSPOT": "ã¹ãã¼ããã©ã³ãWi-Fi ", |
| | | "ACCESS_VIA_BROWSER": " ã«æ¥ç¶ãããã©ã¦ã¶ã§ã¢ã¯ã»ã¹ãã¦ãã ãã ", |
| | | "WIFI_CONFIG_MODE": "ãããã¯ã¼ã¯è¨å®ã¢ã¼ã", |
| | | "ENTERING_WIFI_CONFIG_MODE": "ãããã¯ã¼ã¯è¨å®ä¸...", |
| | | "SCANNING_WIFI": "Wi-Fiãã¹ãã£ã³ä¸...", |
| | | "NEW_VERSION": "æ°ãããã¼ã¸ã§ã³ ", |
| | | "OTA_UPGRADE": "OTAã¢ããã°ã¬ã¼ã", |
| | | "UPGRADING": "ã·ã¹ãã ãã¢ããã°ã¬ã¼ãä¸...", |
| | | "UPGRADE_FAILED": "ã¢ããã°ã¬ã¼ã失æ", |
| | | "ACTIVATION": "ããã¤ã¹ãã¢ã¯ãã£ãã¼ã", |
| | | "BATTERY_LOW": "ããããªã¼ãå°ãªããªã£ã¦ãã¾ã", |
| | | "BATTERY_CHARGING": "å
é»ä¸", |
| | | "BATTERY_FULL": "ããããªã¼æºã¿ã³", |
| | | "BATTERY_NEED_CHARGE": "ããããªã¼ãä½ä¸ãã¦ãã¾ããå
é»ãã¦ãã ãã", |
| | | "VOLUME": "é³é ", |
| | | "MUTED": "ãã¥ã¼ãããã¦ãã¾ã", |
| | | "MAX_VOLUME": "æå¤§é³é", |
| | | "RTC_MODE_OFF": "AEC ç¡å¹", |
| | | "RTC_MODE_ON": "AEC æå¹", |
| | | "DOWNLOAD_ASSETS_FAILED": "ã¢ã»ããã®ãã¦ã³ãã¼ãã«å¤±æãã¾ãã", |
| | | "LOADING_ASSETS": "ã¢ã»ãããèªã¿è¾¼ã¿ä¸...", |
| | | "PLEASE_WAIT": "ãå¾
ã¡ãã ãã...", |
| | | "FOUND_NEW_ASSETS": "æ°ããã¢ã»ãããè¦ã¤ããã¾ãã: %s", |
| | | "HELLO_MY_FRIEND": "ããã«ã¡ã¯ãåéï¼", |
| | | "CONNECTION_SUCCESSFUL": "æ¥ç¶æå", |
| | | "FLIGHT_MODE_OFF": "æ©å
ã¢ã¼ãããªãã§ã", |
| | | "FLIGHT_MODE_ON": "æ©å
ã¢ã¼ãããªã³ã§ã", |
| | | "MODEM_INIT_ERROR": "ã¢ãã ã®åæåã«å¤±æãã¾ãã" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "ko-KR" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "ê²½ê³ ", |
| | | "INFO": "ì ë³´", |
| | | "ERROR": "ì¤ë¥", |
| | | "VERSION": "ë²ì ", |
| | | "LOADING_PROTOCOL": "ë¡ê·¸ì¸ ì¤...", |
| | | "INITIALIZING": "ì´ê¸°í ì¤...", |
| | | "PIN_ERROR": "SIM ì¹´ë를 ì½ì
íì¸ì", |
| | | "REG_ERROR": "ë¤í¸ìí¬ì ì ìí ì ììµëë¤. SIM ì¹´ë ìí를 íì¸íì¸ì", |
| | | "DETECTING_MODULE": "모ë ê°ì§ ì¤...", |
| | | "REGISTERING_NETWORK": "ë¤í¸ìí¬ ë기 ì¤...", |
| | | "CHECKING_NEW_VERSION": "ì ë²ì íì¸ ì¤...", |
| | | "CHECK_NEW_VERSION_FAILED": "ì ë²ì íì¸ì ì¤í¨íìµëë¤. %dì´ íì ë¤ì ìëí©ëë¤: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Wi-Fië¡ ì í ì¤...", |
| | | "SWITCH_TO_4G_NETWORK": "4Gë¡ ì í ì¤...", |
| | | "STANDBY": "ë기", |
| | | "CONNECT_TO": "ì°ê²° ëì: ", |
| | | "CONNECTING": "ì°ê²° ì¤...", |
| | | "CONNECTION_SUCCESSFUL": "ì°ê²° ì±ê³µ", |
| | | "CONNECTED_TO": "ì°ê²°ë¨: ", |
| | | "LISTENING": "ë£ë ì¤...", |
| | | "SPEAKING": "ë§íë ì¤...", |
| | | "SERVER_NOT_FOUND": "ì¬ì© ê°ë¥í ìë¹ì¤ë¥¼ ì°¾ë ì¤", |
| | | "SERVER_NOT_CONNECTED": "ìë¹ì¤ì ì°ê²°í ì ììµëë¤. ëì¤ì ë¤ì ìëíì¸ì", |
| | | "SERVER_TIMEOUT": "ìëµ ë기 ìê° ì´ê³¼", |
| | | "SERVER_ERROR": "ì ì¡ ì¤í¨, ë¤í¸ìí¬ë¥¼ íì¸íì¸ì", |
| | | "CONNECT_TO_HOTSPOT": "í«ì¤í: ", |
| | | "ACCESS_VIA_BROWSER": " ì¤ì URL: ", |
| | | "WIFI_CONFIG_MODE": "Wi-Fi ì¤ì 모ë", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Wi-Fi ì¤ì 모ë ì§ì
ì¤...", |
| | | "SCANNING_WIFI": "Wi-Fi ì¤ìº ì¤...", |
| | | "NEW_VERSION": "ì ë²ì ", |
| | | "OTA_UPGRADE": "OTA ì
ê·¸ë ì´ë", |
| | | "UPGRADING": "ìì¤í
ì
ê·¸ë ì´ë ì¤...", |
| | | "UPGRADE_FAILED": "ì
ê·¸ë ì´ë ì¤í¨", |
| | | "ACTIVATION": "íì±í", |
| | | "BATTERY_LOW": "ë°°í°ë¦¬ ë¶ì¡±", |
| | | "BATTERY_CHARGING": "ì¶©ì ì¤", |
| | | "BATTERY_FULL": "ë°°í°ë¦¬ ìì¶©", |
| | | "BATTERY_NEED_CHARGE": "ë°°í°ë¦¬ ë¶ì¡±, ì¶©ì íì¸ì", |
| | | "VOLUME": "볼륨 ", |
| | | "MUTED": "ììê±°", |
| | | "MAX_VOLUME": "ìµë 볼륨", |
| | | "RTC_MODE_OFF": "AEC ë기", |
| | | "RTC_MODE_ON": "AEC ì¼ê¸°", |
| | | "DOWNLOAD_ASSETS_FAILED": "ìì
ë¤ì´ë¡ë ì¤í¨", |
| | | "LOADING_ASSETS": "ìì
ë¡ë© ì¤...", |
| | | "PLEASE_WAIT": "ì ì 기ë¤ë ¤ 주ì¸ì...", |
| | | "FOUND_NEW_ASSETS": "ìë¡ì´ ìì
ì ë°ê²¬íìµëë¤: %s", |
| | | "HELLO_MY_FRIEND": "ìë
íì¸ì, ì¹êµ¬!", |
| | | "FLIGHT_MODE_OFF": "ë¹í기 모ëê° êº¼ì ¸ ììµëë¤", |
| | | "FLIGHT_MODE_ON": "ë¹í기 모ëê° ì¼ì ¸ ììµëë¤", |
| | | "MODEM_INIT_ERROR": "모ë ì´ê¸°í ì¤í¨" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "ms-MY" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Amaran", |
| | | "INFO": "Maklumat", |
| | | "ERROR": "Ralat", |
| | | "VERSION": "Versi ", |
| | | "LOADING_PROTOCOL": "Log masuk...", |
| | | "INITIALIZING": "Memulakan...", |
| | | "PIN_ERROR": "Sila masukkan kad SIM", |
| | | "REG_ERROR": "Tidak dapat mengakses rangkaian, sila semak status kad SIM", |
| | | "DETECTING_MODULE": "Mengesan modul...", |
| | | "REGISTERING_NETWORK": "Menunggu rangkaian...", |
| | | "CHECKING_NEW_VERSION": "Memeriksa versi baharu...", |
| | | "CHECK_NEW_VERSION_FAILED": "Pemeriksaan versi baharu gagal, akan cuba semula dalam %d saat: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Bertukar ke Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Bertukar ke 4G...", |
| | | "STANDBY": "Bersedia", |
| | | "CONNECT_TO": "Sambung ke ", |
| | | "CONNECTING": "Menyambung...", |
| | | "CONNECTION_SUCCESSFUL": "Sambungan berjaya", |
| | | "CONNECTED_TO": "Disambungkan ke ", |
| | | "LISTENING": "Mendengar...", |
| | | "SPEAKING": "Bercakap...", |
| | | "SERVER_NOT_FOUND": "Mencari perkhidmatan yang tersedia", |
| | | "SERVER_NOT_CONNECTED": "Tidak dapat menyambung ke perkhidmatan, sila cuba lagi kemudian", |
| | | "SERVER_TIMEOUT": "Masa tamat menunggu respons", |
| | | "SERVER_ERROR": "Penghantaran gagal, sila semak rangkaian", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " URL konfigurasi: ", |
| | | "WIFI_CONFIG_MODE": "Mod Konfigurasi Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Memasuki mod konfigurasi Wi-Fi...", |
| | | "SCANNING_WIFI": "Mengimbas Wi-Fi...", |
| | | "NEW_VERSION": "Versi baharu ", |
| | | "OTA_UPGRADE": "Peningkatan OTA", |
| | | "UPGRADING": "Sistem sedang dinaik taraf...", |
| | | "UPGRADE_FAILED": "Peningkatan gagal", |
| | | "ACTIVATION": "Pengaktifan", |
| | | "BATTERY_LOW": "Bateri lemah", |
| | | "BATTERY_CHARGING": "Mengecas", |
| | | "BATTERY_FULL": "Bateri penuh", |
| | | "BATTERY_NEED_CHARGE": "Bateri lemah, sila cas", |
| | | "VOLUME": "Kelantangan ", |
| | | "MUTED": "Dibisukan", |
| | | "MAX_VOLUME": "Kelantangan maksimum", |
| | | "RTC_MODE_OFF": "AEC Dimatikan", |
| | | "RTC_MODE_ON": "AEC Dihidupkan", |
| | | "PLEASE_WAIT": "Sila tunggu...", |
| | | "FOUND_NEW_ASSETS": "Menemui aset baharu: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Gagal memuat turun aset", |
| | | "LOADING_ASSETS": "Memuatkan aset...", |
| | | "HELLO_MY_FRIEND": "Hai, kawan saya!", |
| | | "FLIGHT_MODE_OFF": "Mod penerbangan dimatikan", |
| | | "FLIGHT_MODE_ON": "Mod penerbangan dihidupkan", |
| | | "MODEM_INIT_ERROR": "Modem gagal dimulakan" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "nb-NO" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Advarsel", |
| | | "INFO": "Informasjon", |
| | | "ERROR": "Feil", |
| | | "VERSION": "Versjon ", |
| | | "LOADING_PROTOCOL": "Logger inn...", |
| | | "INITIALIZING": "Initialiserer...", |
| | | "PIN_ERROR": "Vennligst sett inn SIM-kort", |
| | | "REG_ERROR": "Kan ikke få tilgang til nettverket, vennligst sjekk SIM-kortets status", |
| | | "DETECTING_MODULE": "Oppdager modul...", |
| | | "REGISTERING_NETWORK": "Venter på nettverk...", |
| | | "CHECKING_NEW_VERSION": "Sjekker for ny versjon...", |
| | | "CHECK_NEW_VERSION_FAILED": "Sjekk for ny versjon mislyktes, prøver på nytt om %d sekunder: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Bytter til Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Bytter til 4G...", |
| | | "STANDBY": "Standby", |
| | | "CONNECT_TO": "Koble til ", |
| | | "CONNECTING": "Kobler til...", |
| | | "CONNECTION_SUCCESSFUL": "Tilkobling vellykket", |
| | | "CONNECTED_TO": "Koblet til ", |
| | | "LISTENING": "Lytter...", |
| | | "SPEAKING": "Snakker...", |
| | | "SERVER_NOT_FOUND": "Søker etter tilgjengelig tjeneste", |
| | | "SERVER_NOT_CONNECTED": "Kan ikke koble til tjeneste, vennligst prøv igjen senere", |
| | | "SERVER_TIMEOUT": "Tidsavbrudd for å vente på svar", |
| | | "SERVER_ERROR": "Sending mislyktes, vennligst sjekk nettverket", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " Konfigurasjon-URL: ", |
| | | "WIFI_CONFIG_MODE": "Wi-Fi konfigurasjonsmodus", |
| | | "ENTERING_WIFI_CONFIG_MODE": "GÃ¥r inn i Wi-Fi konfigurasjonsmodus...", |
| | | "SCANNING_WIFI": "Skanner Wi-Fi...", |
| | | "NEW_VERSION": "Ny versjon ", |
| | | "OTA_UPGRADE": "OTA-oppgradering", |
| | | "UPGRADING": "Systemet oppgraderes...", |
| | | "UPGRADE_FAILED": "Oppgradering mislyktes", |
| | | "ACTIVATION": "Aktivering", |
| | | "BATTERY_LOW": "Lavt batteri", |
| | | "BATTERY_CHARGING": "Lader", |
| | | "BATTERY_FULL": "Batteriet er fullt", |
| | | "BATTERY_NEED_CHARGE": "Lavt batteri, vennligst lad", |
| | | "VOLUME": "Volum ", |
| | | "MUTED": "Dempet", |
| | | "MAX_VOLUME": "Maksimalt volum", |
| | | "RTC_MODE_OFF": "AEC av", |
| | | "RTC_MODE_ON": "AEC på", |
| | | "PLEASE_WAIT": "Vennligst vent...", |
| | | "FOUND_NEW_ASSETS": "Fant nye ressurser: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Nedlasting av ressurser mislyktes", |
| | | "LOADING_ASSETS": "Laster ressurser...", |
| | | "HELLO_MY_FRIEND": "Hei, min venn!", |
| | | "FLIGHT_MODE_OFF": "Flymodus er av", |
| | | "FLIGHT_MODE_ON": "Flymodus er på", |
| | | "MODEM_INIT_ERROR": "Modeminitialisering mislyktes" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "nl-NL" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Waarschuwing", |
| | | "INFO": "Informatie", |
| | | "ERROR": "Fout", |
| | | "VERSION": "Versie ", |
| | | "LOADING_PROTOCOL": "Inloggen...", |
| | | "INITIALIZING": "Initialiseren...", |
| | | "PIN_ERROR": "Plaats een SIM-kaart", |
| | | "REG_ERROR": "Geen toegang tot netwerk, controleer de SIM-kaartstatus", |
| | | "DETECTING_MODULE": "Module detecteren...", |
| | | "REGISTERING_NETWORK": "Wachten op netwerk...", |
| | | "CHECKING_NEW_VERSION": "Controleren op nieuwe versie...", |
| | | "CHECK_NEW_VERSION_FAILED": "Controle op nieuwe versie mislukt, opnieuw proberen over %d seconden: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Overschakelen naar Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Overschakelen naar 4G...", |
| | | "STANDBY": "Stand-by", |
| | | "CONNECT_TO": "Verbinden met ", |
| | | "CONNECTING": "Verbinden...", |
| | | "CONNECTION_SUCCESSFUL": "Verbinding geslaagd", |
| | | "CONNECTED_TO": "Verbonden met ", |
| | | "LISTENING": "Luisteren...", |
| | | "SPEAKING": "Spreken...", |
| | | "SERVER_NOT_FOUND": "Zoeken naar beschikbare service", |
| | | "SERVER_NOT_CONNECTED": "Kan niet verbinden met service, probeer later opnieuw", |
| | | "SERVER_TIMEOUT": "Timeout bij wachten op reactie", |
| | | "SERVER_ERROR": "Verzenden mislukt, controleer het netwerk", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " Configuratie-URL: ", |
| | | "WIFI_CONFIG_MODE": "Wi-Fi-configuratiemodus", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Wi-Fi-configuratiemodus openen...", |
| | | "SCANNING_WIFI": "Wi-Fi scannen...", |
| | | "NEW_VERSION": "Nieuwe versie ", |
| | | "OTA_UPGRADE": "OTA-upgrade", |
| | | "UPGRADING": "Systeem wordt geüpgraded...", |
| | | "UPGRADE_FAILED": "Upgrade mislukt", |
| | | "ACTIVATION": "Activering", |
| | | "BATTERY_LOW": "Batterij bijna leeg", |
| | | "BATTERY_CHARGING": "Opladen", |
| | | "BATTERY_FULL": "Batterij vol", |
| | | "BATTERY_NEED_CHARGE": "Batterij bijna leeg, laad op", |
| | | "VOLUME": "Volume ", |
| | | "MUTED": "Gedempt", |
| | | "MAX_VOLUME": "Maximaal volume", |
| | | "RTC_MODE_OFF": "AEC uit", |
| | | "RTC_MODE_ON": "AEC aan", |
| | | "PLEASE_WAIT": "Een ogenblik geduld...", |
| | | "FOUND_NEW_ASSETS": "Nieuwe bronnen gevonden: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Downloaden van bronnen mislukt", |
| | | "LOADING_ASSETS": "Bronnen laden...", |
| | | "HELLO_MY_FRIEND": "Hallo, mijn vriend!", |
| | | "FLIGHT_MODE_OFF": "Vliegtuigmodus is uitgeschakeld", |
| | | "FLIGHT_MODE_ON": "Vliegtuigmodus is ingeschakeld", |
| | | "MODEM_INIT_ERROR": "Modeminitialisatie mislukt" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "pl-PL" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Ostrzeżenie", |
| | | "INFO": "Informacja", |
| | | "ERROR": "BÅÄ
d", |
| | | "VERSION": "Wersja ", |
| | | "LOADING_PROTOCOL": "ÅÄ
czenie z serwerem...", |
| | | "INITIALIZING": "Inicjalizacja...", |
| | | "PIN_ERROR": "ProszÄ wÅożyÄ kartÄ SIM", |
| | | "REG_ERROR": "Nie można uzyskaÄ dostÄpu do sieci, sprawdź stan karty danych", |
| | | "DETECTING_MODULE": "Wykrywanie moduÅu...", |
| | | "REGISTERING_NETWORK": "Oczekiwanie na sieÄ...", |
| | | "CHECKING_NEW_VERSION": "Sprawdzanie nowej wersji...", |
| | | "CHECK_NEW_VERSION_FAILED": "Sprawdzanie nowej wersji nie powiodÅo siÄ, ponowna próba za %d sekund: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "PrzeÅÄ
czanie na Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "PrzeÅÄ
czanie na 4G...", |
| | | "STANDBY": "GotowoÅÄ", |
| | | "CONNECT_TO": "PoÅÄ
cz z ", |
| | | "CONNECTING": "ÅÄ
czenie...", |
| | | "CONNECTED_TO": "PoÅÄ
czono z ", |
| | | "LISTENING": "SÅuchanie...", |
| | | "SPEAKING": "Mówienie...", |
| | | "SERVER_NOT_FOUND": "Szukanie dostÄpnej usÅugi", |
| | | "SERVER_NOT_CONNECTED": "Nie można poÅÄ
czyÄ siÄ z usÅugÄ
, spróbuj ponownie później", |
| | | "SERVER_TIMEOUT": "Przekroczono czas oczekiwania na odpowiedź", |
| | | "SERVER_ERROR": "WysyÅanie nie powiodÅo siÄ, sprawdź sieÄ", |
| | | "CONNECT_TO_HOTSPOT": "PodÅÄ
cz telefon do hotspotu ", |
| | | "ACCESS_VIA_BROWSER": "ï¼dostÄp przez przeglÄ
darkÄ ", |
| | | "WIFI_CONFIG_MODE": "Tryb konfiguracji sieci", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Wchodzenie w tryb konfiguracji sieci...", |
| | | "SCANNING_WIFI": "Skanowanie Wi-Fi...", |
| | | "NEW_VERSION": "Nowa wersja ", |
| | | "OTA_UPGRADE": "Aktualizacja OTA", |
| | | "UPGRADING": "Aktualizacja systemu...", |
| | | "UPGRADE_FAILED": "Aktualizacja nie powiodÅa siÄ", |
| | | "ACTIVATION": "Aktywacja urzÄ
dzenia", |
| | | "BATTERY_LOW": "Niski poziom baterii", |
| | | "BATTERY_CHARGING": "Åadowanie", |
| | | "BATTERY_FULL": "Bateria peÅna", |
| | | "BATTERY_NEED_CHARGE": "Niski poziom baterii, proszÄ naÅadowaÄ", |
| | | "VOLUME": "GÅoÅnoÅÄ ", |
| | | "MUTED": "Wyciszony", |
| | | "MAX_VOLUME": "Maksymalna gÅoÅnoÅÄ", |
| | | "RTC_MODE_OFF": "AEC wyÅÄ
czony", |
| | | "RTC_MODE_ON": "AEC wÅÄ
czony", |
| | | "DOWNLOAD_ASSETS_FAILED": "Nie udaÅo siÄ pobraÄ zasobów", |
| | | "LOADING_ASSETS": "Åadowanie zasobów...", |
| | | "PLEASE_WAIT": "ProszÄ czekaÄ...", |
| | | "FOUND_NEW_ASSETS": "Znaleziono nowe zasoby: %s", |
| | | "HELLO_MY_FRIEND": "CzeÅÄ, mój przyjacielu!", |
| | | "CONNECTION_SUCCESSFUL": "PoÅÄ
czenie udane", |
| | | "FLIGHT_MODE_OFF": "Tryb samolotowy jest wyÅÄ
czony", |
| | | "FLIGHT_MODE_ON": "Tryb samolotowy jest wÅÄ
czony", |
| | | "MODEM_INIT_ERROR": "Inicjalizacja modemu nie powiodÅa siÄ" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "pt-PT" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Aviso", |
| | | "INFO": "Informação", |
| | | "ERROR": "Erro", |
| | | "VERSION": "Versão ", |
| | | "LOADING_PROTOCOL": "Ligando ao servidor...", |
| | | "INITIALIZING": "A inicializar...", |
| | | "PIN_ERROR": "Por favor insira o cartão SIM", |
| | | "REG_ERROR": "Não é possÃvel aceder à rede, verifique o estado do cartão de dados", |
| | | "DETECTING_MODULE": "A detectar módulo...", |
| | | "REGISTERING_NETWORK": "Ã espera da rede...", |
| | | "CHECKING_NEW_VERSION": "A verificar nova versão...", |
| | | "CHECK_NEW_VERSION_FAILED": "Falha na verificação de nova versão, nova tentativa em %d segundos: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "A mudar para Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "A mudar para 4G...", |
| | | "STANDBY": "Em espera", |
| | | "CONNECT_TO": "Ligar a ", |
| | | "CONNECTING": "A ligar...", |
| | | "CONNECTED_TO": "Ligado a ", |
| | | "LISTENING": "A escutar...", |
| | | "SPEAKING": "A falar...", |
| | | "SERVER_NOT_FOUND": "A procurar serviço disponÃvel", |
| | | "SERVER_NOT_CONNECTED": "Não é possÃvel ligar ao serviço, tente mais tarde", |
| | | "SERVER_TIMEOUT": "Tempo limite de resposta", |
| | | "SERVER_ERROR": "Falha no envio, verifique a rede", |
| | | "CONNECT_TO_HOTSPOT": "Ligue o telefone ao hotspot ", |
| | | "ACCESS_VIA_BROWSER": "ï¼aceder através do navegador ", |
| | | "WIFI_CONFIG_MODE": "Modo de configuração de rede", |
| | | "ENTERING_WIFI_CONFIG_MODE": "A entrar no modo de configuração de rede...", |
| | | "SCANNING_WIFI": "A procurar Wi-Fi...", |
| | | "NEW_VERSION": "Nova versão ", |
| | | "OTA_UPGRADE": "Atualização OTA", |
| | | "UPGRADING": "A atualizar sistema...", |
| | | "UPGRADE_FAILED": "Atualização falhada", |
| | | "ACTIVATION": "Ativação do dispositivo", |
| | | "BATTERY_LOW": "Bateria fraca", |
| | | "BATTERY_CHARGING": "A carregar", |
| | | "BATTERY_FULL": "Bateria cheia", |
| | | "BATTERY_NEED_CHARGE": "Bateria fraca, por favor carregue", |
| | | "VOLUME": "Volume ", |
| | | "MUTED": "Silenciado", |
| | | "MAX_VOLUME": "Volume máximo", |
| | | "RTC_MODE_OFF": "AEC desligado", |
| | | "RTC_MODE_ON": "AEC ligado", |
| | | "DOWNLOAD_ASSETS_FAILED": "Falha ao descarregar recursos", |
| | | "LOADING_ASSETS": "A carregar recursos...", |
| | | "PLEASE_WAIT": "Por favor aguarde...", |
| | | "FOUND_NEW_ASSETS": "Encontrados novos recursos: %s", |
| | | "HELLO_MY_FRIEND": "Olá, meu amigo!", |
| | | "CONNECTION_SUCCESSFUL": "Ligação bem-sucedida", |
| | | "FLIGHT_MODE_OFF": "O modo avião está desativado", |
| | | "FLIGHT_MODE_ON": "O modo avião está ativado", |
| | | "MODEM_INIT_ERROR": "Falha na inicialização do modem" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "ro-RO" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Avertisment", |
| | | "INFO": "InformaÈie", |
| | | "ERROR": "Eroare", |
| | | "VERSION": "Versiune ", |
| | | "LOADING_PROTOCOL": "Se conecteazÄ la server...", |
| | | "INITIALIZING": "Se iniÈializeazÄ...", |
| | | "PIN_ERROR": "VÄ rugÄm sÄ introduceÈi cardul SIM", |
| | | "REG_ERROR": "Nu se poate accesa reÈeaua, verificaÈi starea cardului de date", |
| | | "DETECTING_MODULE": "Se detecteazÄ modulul...", |
| | | "REGISTERING_NETWORK": "Se aÈteaptÄ reÈeaua...", |
| | | "CHECKING_NEW_VERSION": "Se verificÄ versiunea nouÄ...", |
| | | "CHECK_NEW_VERSION_FAILED": "Verificarea versiunii noi a eÈuat, se reîncearcÄ Ã®n %d secunde: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Se comutÄ la Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Se comutÄ la 4G...", |
| | | "STANDBY": "Ãn aÈteptare", |
| | | "CONNECT_TO": "Conectare la ", |
| | | "CONNECTING": "Se conecteazÄ...", |
| | | "CONNECTED_TO": "Conectat la ", |
| | | "LISTENING": "Se ascultÄ...", |
| | | "SPEAKING": "Se vorbeÈte...", |
| | | "SERVER_NOT_FOUND": "Se cautÄ serviciul disponibil", |
| | | "SERVER_NOT_CONNECTED": "Nu se poate conecta la serviciu, încercaÈi mai târziu", |
| | | "SERVER_TIMEOUT": "Timpul de rÄspuns a expirat", |
| | | "SERVER_ERROR": "Trimiterea a eÈuat, verificaÈi reÈeaua", |
| | | "CONNECT_TO_HOTSPOT": "ConectaÈi telefonul la hotspot ", |
| | | "ACCESS_VIA_BROWSER": "ï¼accesaÈi prin browser ", |
| | | "WIFI_CONFIG_MODE": "Modul de configurare reÈea", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Se intrÄ Ã®n modul de configurare reÈea...", |
| | | "SCANNING_WIFI": "Se scaneazÄ Wi-Fi...", |
| | | "NEW_VERSION": "Versiune nouÄ ", |
| | | "OTA_UPGRADE": "Actualizare OTA", |
| | | "UPGRADING": "Se actualizeazÄ sistemul...", |
| | | "UPGRADE_FAILED": "Actualizarea a eÈuat", |
| | | "ACTIVATION": "Activarea dispozitivului", |
| | | "BATTERY_LOW": "Baterie scÄzutÄ", |
| | | "BATTERY_CHARGING": "Se încarcÄ", |
| | | "BATTERY_FULL": "Baterie plinÄ", |
| | | "BATTERY_NEED_CHARGE": "Baterie scÄzutÄ, vÄ rugÄm sÄ Ã®ncÄrcaÈi", |
| | | "VOLUME": "Volum ", |
| | | "MUTED": "SilenÈios", |
| | | "MAX_VOLUME": "Volum maxim", |
| | | "RTC_MODE_OFF": "AEC oprit", |
| | | "RTC_MODE_ON": "AEC pornit", |
| | | "DOWNLOAD_ASSETS_FAILED": "EÈec la descÄrcarea resurselor", |
| | | "LOADING_ASSETS": "Se încarcÄ resursele...", |
| | | "PLEASE_WAIT": "VÄ rugÄm sÄ aÈteptaÈi...", |
| | | "FOUND_NEW_ASSETS": "S-au gÄsit resurse noi: %s", |
| | | "HELLO_MY_FRIEND": "Salut, prietenul meu!", |
| | | "CONNECTION_SUCCESSFUL": "Conexiune reuÈitÄ", |
| | | "FLIGHT_MODE_OFF": "Modul avion este dezactivat", |
| | | "FLIGHT_MODE_ON": "Modul avion este activat", |
| | | "MODEM_INIT_ERROR": "IniÈializarea modemului a eÈuat" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "ru-RU" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "ÐÑедÑпÑеждение", |
| | | "INFO": "ÐнÑоÑмаÑиÑ", |
| | | "ERROR": "ÐÑибка", |
| | | "VERSION": "ÐеÑÑÐ¸Ñ ", |
| | | "LOADING_PROTOCOL": "ÐодклÑÑение к ÑеÑвеÑÑ...", |
| | | "INITIALIZING": "ÐниÑиализаÑиÑ...", |
| | | "PIN_ERROR": "ÐожалÑйÑÑа, вÑÑавÑÑе SIM-каÑÑÑ", |
| | | "REG_ERROR": "Ðевозможно подклÑÑиÑÑÑÑ Ðº ÑеÑи, пÑовеÑÑÑе ÑоÑÑоÑние каÑÑÑ Ð´Ð°Ð½Ð½ÑÑ
", |
| | | "DETECTING_MODULE": "ÐбнаÑÑжение модÑлÑ...", |
| | | "REGISTERING_NETWORK": "Ðжидание ÑеÑи...", |
| | | "CHECKING_NEW_VERSION": "ÐÑовеÑка новой веÑÑии...", |
| | | "CHECK_NEW_VERSION_FAILED": "ÐÑибка пÑовеÑки новой веÑÑии, повÑÐ¾Ñ ÑеÑез %d ÑекÑнд: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "ÐеÑеклÑÑение на Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "ÐеÑеклÑÑение на 4G...", |
| | | "STANDBY": "Ðжидание", |
| | | "CONNECT_TO": "ÐодклÑÑение к ", |
| | | "CONNECTING": "ÐодклÑÑение...", |
| | | "CONNECTED_TO": "ÐодклÑÑено к ", |
| | | "LISTENING": "ÐÑоÑлÑÑивание...", |
| | | "SPEAKING": "ÐовоÑение...", |
| | | "SERVER_NOT_FOUND": "ÐоиÑк доÑÑÑпного ÑеÑвиÑа", |
| | | "SERVER_NOT_CONNECTED": "Ðевозможно подклÑÑиÑÑÑÑ Ðº ÑеÑвиÑÑ, попÑобÑйÑе позже", |
| | | "SERVER_TIMEOUT": "Тайм-аÑÑ Ð¾ÑвеÑа", |
| | | "SERVER_ERROR": "ÐÑибка оÑпÑавки, пÑовеÑÑÑе ÑеÑÑ", |
| | | "CONNECT_TO_HOTSPOT": "ÐодклÑÑиÑе ÑелеÑон к ÑоÑке доÑÑÑпа ", |
| | | "ACCESS_VIA_BROWSER": "ï¼Ð´Ð¾ÑÑÑп ÑеÑез бÑаÑÐ·ÐµÑ ", |
| | | "WIFI_CONFIG_MODE": "Режим наÑÑÑойки ÑеÑи", |
| | | "ENTERING_WIFI_CONFIG_MODE": "ÐÑ
од в Ñежим наÑÑÑойки ÑеÑи...", |
| | | "SCANNING_WIFI": "СканиÑование Wi-Fi...", |
| | | "NEW_VERSION": "ÐÐ¾Ð²Ð°Ñ Ð²ÐµÑÑÐ¸Ñ ", |
| | | "OTA_UPGRADE": "Ðбновление OTA", |
| | | "UPGRADING": "Ðбновление ÑиÑÑемÑ...", |
| | | "UPGRADE_FAILED": "Ðбновление не ÑдалоÑÑ", |
| | | "ACTIVATION": "ÐкÑиваÑÐ¸Ñ ÑÑÑÑойÑÑва", |
| | | "BATTERY_LOW": "Ðизкий заÑÑд баÑаÑеи", |
| | | "BATTERY_CHARGING": "ÐаÑÑдка", |
| | | "BATTERY_FULL": "ÐаÑаÑÐµÑ Ð¿Ð¾Ð»Ð½Ð°Ñ", |
| | | "BATTERY_NEED_CHARGE": "Ðизкий заÑÑд, пожалÑйÑÑа, заÑÑдиÑе", |
| | | "VOLUME": "ÐÑомкоÑÑÑ ", |
| | | "MUTED": "ÐвÑк оÑклÑÑен", |
| | | "MAX_VOLUME": "ÐакÑималÑÐ½Ð°Ñ Ð³ÑомкоÑÑÑ", |
| | | "RTC_MODE_OFF": "AEC вÑклÑÑен", |
| | | "RTC_MODE_ON": "AEC вклÑÑен", |
| | | "DOWNLOAD_ASSETS_FAILED": "Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ ÑеÑÑÑÑÑ", |
| | | "LOADING_ASSETS": "ÐагÑÑзка ÑеÑÑÑÑов...", |
| | | "PLEASE_WAIT": "ÐожалÑйÑÑа, подождиÑе...", |
| | | "FOUND_NEW_ASSETS": "ÐÐ°Ð¹Ð´ÐµÐ½Ñ Ð½Ð¾Ð²Ñе ÑеÑÑÑÑÑ: %s", |
| | | "HELLO_MY_FRIEND": "ÐÑивеÑ, мой дÑÑг!", |
| | | "CONNECTION_SUCCESSFUL": "ÐодклÑÑение ÑÑпеÑно", |
| | | "FLIGHT_MODE_OFF": "Режим полеÑа вÑклÑÑен", |
| | | "FLIGHT_MODE_ON": "Режим полеÑа вклÑÑен", |
| | | "MODEM_INIT_ERROR": "ÐÑибка иниÑиализаÑии модема" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "sk-SK" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Upozornenie", |
| | | "INFO": "Informácie", |
| | | "ERROR": "Chyba", |
| | | "VERSION": "Verzia ", |
| | | "LOADING_PROTOCOL": "Prihlasovanie...", |
| | | "INITIALIZING": "Inicializácia...", |
| | | "PIN_ERROR": "Vložte SIM kartu", |
| | | "REG_ERROR": "Nie je možné pristúpiť k sieti, skontrolujte stav SIM karty", |
| | | "DETECTING_MODULE": "Zisťovanie modulu...", |
| | | "REGISTERING_NETWORK": "Äakanie na sieÅ¥...", |
| | | "CHECKING_NEW_VERSION": "Kontrola novej verzie...", |
| | | "CHECK_NEW_VERSION_FAILED": "Kontrola novej verzie zlyhala, opakuje sa o %d sekúnd: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "PrepÃnanie na Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "PrepÃnanie na 4G...", |
| | | "STANDBY": "Pohotovostný režim", |
| | | "CONNECT_TO": "Pripojenie k ", |
| | | "CONNECTING": "Pripája sa...", |
| | | "CONNECTION_SUCCESSFUL": "Pripojenie úspešné", |
| | | "CONNECTED_TO": "Pripojené k ", |
| | | "LISTENING": "NaÄúvanie...", |
| | | "SPEAKING": "Hovorenie...", |
| | | "SERVER_NOT_FOUND": "Hľadanie dostupnej služby", |
| | | "SERVER_NOT_CONNECTED": "Nie je možné pripojiť sa k službe, skúste to neskôr", |
| | | "SERVER_TIMEOUT": "Äasový limit Äakania na odpoveÄ vyprÅ¡al", |
| | | "SERVER_ERROR": "Odosielanie zlyhalo, skontrolujte sieť", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " KonfiguraÄná URL: ", |
| | | "WIFI_CONFIG_MODE": "Režim konfigurácie Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Vstup do režimu konfigurácie Wi-Fi...", |
| | | "SCANNING_WIFI": "Skenovanie Wi-Fi...", |
| | | "NEW_VERSION": "Nová verzia ", |
| | | "OTA_UPGRADE": "OTA aktualizácia", |
| | | "UPGRADING": "Systém sa aktualizuje...", |
| | | "UPGRADE_FAILED": "Aktualizácia zlyhala", |
| | | "ACTIVATION": "Aktivácia", |
| | | "BATTERY_LOW": "Slabá batéria", |
| | | "BATTERY_CHARGING": "NabÃjanie", |
| | | "BATTERY_FULL": "Batéria plná", |
| | | "BATTERY_NEED_CHARGE": "Slabá batéria, nabite ju", |
| | | "VOLUME": "Hlasitosť ", |
| | | "MUTED": "StÃÅ¡ené", |
| | | "MAX_VOLUME": "Maximálna hlasitosť", |
| | | "RTC_MODE_OFF": "AEC vypnuté", |
| | | "RTC_MODE_ON": "AEC zapnuté", |
| | | "PLEASE_WAIT": "PoÄkajte prosÃm...", |
| | | "FOUND_NEW_ASSETS": "Nájdené nové zdroje: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "SÅ¥ahovanie zdrojov zlyhalo", |
| | | "LOADING_ASSETS": "NaÄÃtavanie zdrojov...", |
| | | "HELLO_MY_FRIEND": "Ahoj, môj priateľ!", |
| | | "FLIGHT_MODE_OFF": "Letecký režim je vypnutý", |
| | | "FLIGHT_MODE_ON": "Letecký režim je zapnutý", |
| | | "MODEM_INIT_ERROR": "Chyba inicializácie modemu" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "sl-SI" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Opozorilo", |
| | | "INFO": "Informacije", |
| | | "ERROR": "Napaka", |
| | | "VERSION": "RazliÄica ", |
| | | "LOADING_PROTOCOL": "Prijava...", |
| | | "INITIALIZING": "Inicializacija...", |
| | | "PIN_ERROR": "Vstavite SIM kartico", |
| | | "REG_ERROR": "Ni mogoÄe dostopati do omrežja, preverite stanje SIM kartice", |
| | | "DETECTING_MODULE": "Zaznavanje modula...", |
| | | "REGISTERING_NETWORK": "Äakanje na omrežje...", |
| | | "CHECKING_NEW_VERSION": "Preverjanje nove razliÄice...", |
| | | "CHECK_NEW_VERSION_FAILED": "Preverjanje nove razliÄice ni uspelo, ponovni poskus Äez %d sekund: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Preklop na Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Preklop na 4G...", |
| | | "STANDBY": "Pripravljenost", |
| | | "CONNECT_TO": "Poveži se z ", |
| | | "CONNECTING": "Povezovanje...", |
| | | "CONNECTION_SUCCESSFUL": "Povezava uspešna", |
| | | "CONNECTED_TO": "Povezano z ", |
| | | "LISTENING": "Poslušanje...", |
| | | "SPEAKING": "Govorjenje...", |
| | | "SERVER_NOT_FOUND": "Iskanje razpoložljive storitve", |
| | | "SERVER_NOT_CONNECTED": "Ni se mogoÄe povezati s storitvijo, poskusite kasneje", |
| | | "SERVER_TIMEOUT": "Äasovna omejitev Äakanja na odgovor", |
| | | "SERVER_ERROR": "Pošiljanje ni uspelo, preverite omrežje", |
| | | "CONNECT_TO_HOTSPOT": "Dostopna toÄka: ", |
| | | "ACCESS_VIA_BROWSER": " Konfiguracijski URL: ", |
| | | "WIFI_CONFIG_MODE": "NaÄin konfiguracije Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Vstop v naÄin konfiguracije Wi-Fi...", |
| | | "SCANNING_WIFI": "Skeniranje Wi-Fi...", |
| | | "NEW_VERSION": "Nova razliÄica ", |
| | | "OTA_UPGRADE": "OTA nadgradnja", |
| | | "UPGRADING": "Sistem se nadgrajuje...", |
| | | "UPGRADE_FAILED": "Nadgradnja ni uspela", |
| | | "ACTIVATION": "Aktivacija", |
| | | "BATTERY_LOW": "Nizka baterija", |
| | | "BATTERY_CHARGING": "Polnjenje", |
| | | "BATTERY_FULL": "Baterija polna", |
| | | "BATTERY_NEED_CHARGE": "Nizka baterija, napolnite", |
| | | "VOLUME": "Glasnost ", |
| | | "MUTED": "Utišano", |
| | | "MAX_VOLUME": "NajveÄja glasnost", |
| | | "RTC_MODE_OFF": "AEC izklopljen", |
| | | "RTC_MODE_ON": "AEC vklopljen", |
| | | "PLEASE_WAIT": "PoÄakajte...", |
| | | "FOUND_NEW_ASSETS": "Najdeni novi viri: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Prenos virov ni uspel", |
| | | "LOADING_ASSETS": "Nalaganje virov...", |
| | | "HELLO_MY_FRIEND": "Pozdravljeni, moj prijatelj!", |
| | | "FLIGHT_MODE_OFF": "NaÄin leta je izklopljen", |
| | | "FLIGHT_MODE_ON": "NaÄin leta je vklopljen", |
| | | "MODEM_INIT_ERROR": "Inicializacija modema ni uspela" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "sr-RS" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "УпозоÑеÑе", |
| | | "INFO": "ÐнÑоÑмаÑиÑе", |
| | | "ERROR": "ÐÑеÑка", |
| | | "VERSION": "ÐеÑзиÑа ", |
| | | "LOADING_PROTOCOL": "ÐÑиÑавÑиваÑе...", |
| | | "INITIALIZING": "ÐниÑиÑализаÑиÑа...", |
| | | "PIN_ERROR": "Ðолимо ÑбаÑиÑе SIM каÑÑиÑÑ", |
| | | "REG_ERROR": "ÐиÑе могÑÑе пÑиÑÑÑпиÑи мÑежи, пÑовеÑиÑе ÑÑаÑÑÑ SIM каÑÑиÑе", |
| | | "DETECTING_MODULE": "ÐÑкÑиваÑе модÑла...", |
| | | "REGISTERING_NETWORK": "ЧекаÑе мÑеже...", |
| | | "CHECKING_NEW_VERSION": "ÐÑовеÑа нове веÑзиÑе...", |
| | | "CHECK_NEW_VERSION_FAILED": "ÐÑовеÑа нове веÑзиÑе ниÑе ÑÑпела, покÑÑаÑе Ñе поново за %d ÑекÑнди: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "ÐÑебаÑиваÑе на Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "ÐÑебаÑиваÑе на 4G...", |
| | | "STANDBY": "СпÑемноÑÑ", |
| | | "CONNECT_TO": "Ðовежи Ñе Ñа ", |
| | | "CONNECTING": "ÐовезиваÑе...", |
| | | "CONNECTION_SUCCESSFUL": "УÑпеÑно повезиваÑе", |
| | | "CONNECTED_TO": "Ðовезано Ñа ", |
| | | "LISTENING": "СлÑÑаÑе...", |
| | | "SPEAKING": "ÐовоÑеÑе...", |
| | | "SERVER_NOT_FOUND": "ТÑажеÑе доÑÑÑпне ÑÑлÑге", |
| | | "SERVER_NOT_CONNECTED": "ÐиÑе могÑÑе повезаÑи Ñе Ñа ÑÑлÑгом, покÑÑаÑÑе каÑниÑе", |
| | | "SERVER_TIMEOUT": "ÐÑÑекло вÑеме ÑекаÑа одговоÑа", |
| | | "SERVER_ERROR": "СлаÑе ниÑе ÑÑпело, пÑовеÑиÑе мÑежÑ", |
| | | "CONNECT_TO_HOTSPOT": "ÐÑиÑÑÑпна ÑаÑка: ", |
| | | "ACCESS_VIA_BROWSER": " URL за конÑигÑÑаÑиÑÑ: ", |
| | | "WIFI_CONFIG_MODE": "Режим конÑигÑÑаÑиÑе Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Улазак Ñ Ñежим конÑигÑÑаÑиÑе Wi-Fi...", |
| | | "SCANNING_WIFI": "СкениÑаÑе Wi-Fi...", |
| | | "NEW_VERSION": "Ðова веÑзиÑа ", |
| | | "OTA_UPGRADE": "OTA надогÑадÑа", |
| | | "UPGRADING": "СиÑÑем Ñе надогÑаÑÑÑе...", |
| | | "UPGRADE_FAILED": "ÐадогÑадÑа ниÑе ÑÑпела", |
| | | "ACTIVATION": "ÐкÑиваÑиÑа", |
| | | "BATTERY_LOW": "Слаба баÑеÑиÑа", |
| | | "BATTERY_CHARGING": "ÐÑÑеÑе", |
| | | "BATTERY_FULL": "ÐаÑеÑиÑа пÑна", |
| | | "BATTERY_NEED_CHARGE": "Слаба баÑеÑиÑа, молимо напÑниÑе", |
| | | "VOLUME": "ÐаÑина звÑка ", |
| | | "MUTED": "УÑиÑано", |
| | | "MAX_VOLUME": "ÐакÑимална ÑаÑина звÑка", |
| | | "RTC_MODE_OFF": "AEC иÑкÑÑÑен", |
| | | "RTC_MODE_ON": "AEC ÑкÑÑÑен", |
| | | "PLEASE_WAIT": "Ðолимо ÑаÑекаÑÑе...", |
| | | "FOUND_NEW_ASSETS": "ÐÑонаÑени нови ÑеÑÑÑÑи: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "ÐÑеÑзимаÑе ÑеÑÑÑÑа ниÑе ÑÑпело", |
| | | "LOADING_ASSETS": "УÑиÑаваÑе ÑеÑÑÑÑа...", |
| | | "HELLO_MY_FRIEND": "ÐдÑаво, пÑиÑаÑеÑÑ!", |
| | | "FLIGHT_MODE_OFF": "Режим леÑа Ñе иÑкÑÑÑен", |
| | | "FLIGHT_MODE_ON": "Режим леÑа Ñе ÑкÑÑÑен", |
| | | "MODEM_INIT_ERROR": "ÐниÑиÑализаÑиÑа модема ниÑе ÑÑпела" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "sv-SE" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Varning", |
| | | "INFO": "Information", |
| | | "ERROR": "Fel", |
| | | "VERSION": "Version ", |
| | | "LOADING_PROTOCOL": "Loggar in...", |
| | | "INITIALIZING": "Initierar...", |
| | | "PIN_ERROR": "Vänligen sätt in SIM-kort", |
| | | "REG_ERROR": "Kan inte komma åt nätverket, kontrollera SIM-kortets status", |
| | | "DETECTING_MODULE": "Detekterar modul...", |
| | | "REGISTERING_NETWORK": "Väntar på nätverk...", |
| | | "CHECKING_NEW_VERSION": "Kontrollerar ny version...", |
| | | "CHECK_NEW_VERSION_FAILED": "Kontroll av ny version misslyckades, försöker igen om %d sekunder: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Byter till Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Byter till 4G...", |
| | | "STANDBY": "Standby", |
| | | "CONNECT_TO": "Anslut till ", |
| | | "CONNECTING": "Ansluter...", |
| | | "CONNECTION_SUCCESSFUL": "Anslutningen lyckades", |
| | | "CONNECTED_TO": "Ansluten till ", |
| | | "LISTENING": "Lyssnar...", |
| | | "SPEAKING": "Talar...", |
| | | "SERVER_NOT_FOUND": "Söker efter tillgänglig tjänst", |
| | | "SERVER_NOT_CONNECTED": "Kan inte ansluta till tjänsten, försök igen senare", |
| | | "SERVER_TIMEOUT": "Timeout vid väntan på svar", |
| | | "SERVER_ERROR": "Skickandet misslyckades, kontrollera nätverket", |
| | | "CONNECT_TO_HOTSPOT": "Hotspot: ", |
| | | "ACCESS_VIA_BROWSER": " Konfigurations-URL: ", |
| | | "WIFI_CONFIG_MODE": "Wi-Fi-konfigurationsläge", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Går in i Wi-Fi-konfigurationsläge...", |
| | | "SCANNING_WIFI": "Skannar Wi-Fi...", |
| | | "NEW_VERSION": "Ny version ", |
| | | "OTA_UPGRADE": "OTA-uppgradering", |
| | | "UPGRADING": "Systemet uppgraderas...", |
| | | "UPGRADE_FAILED": "Uppgraderingen misslyckades", |
| | | "ACTIVATION": "Aktivering", |
| | | "BATTERY_LOW": "LÃ¥gt batteri", |
| | | "BATTERY_CHARGING": "Laddar", |
| | | "BATTERY_FULL": "Batteriet fullt", |
| | | "BATTERY_NEED_CHARGE": "Lågt batteri, vänligen ladda", |
| | | "VOLUME": "Volym ", |
| | | "MUTED": "Tystad", |
| | | "MAX_VOLUME": "Maximal volym", |
| | | "RTC_MODE_OFF": "AEC av", |
| | | "RTC_MODE_ON": "AEC på", |
| | | "PLEASE_WAIT": "Vänligen vänta...", |
| | | "FOUND_NEW_ASSETS": "Hittade nya resurser: %s", |
| | | "DOWNLOAD_ASSETS_FAILED": "Nedladdning av resurser misslyckades", |
| | | "LOADING_ASSETS": "Laddar resurser...", |
| | | "HELLO_MY_FRIEND": "Hej, min vän!", |
| | | "FLIGHT_MODE_OFF": "Flygläge är av", |
| | | "FLIGHT_MODE_ON": "Flygläge är på", |
| | | "MODEM_INIT_ERROR": "Modeminitiering misslyckades" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "th-TH" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "à¸à¸³à¹à¸à¸·à¸à¸", |
| | | "INFO": "à¸à¹à¸à¸¡à¸¹à¸¥", |
| | | "ERROR": "à¸à¹à¸à¸à¸´à¸à¸à¸¥à¸²à¸", |
| | | "VERSION": "à¹à¸§à¸à¸£à¹à¸à¸±à¸ ", |
| | | "LOADING_PROTOCOL": "à¸à¸³à¸¥à¸±à¸à¹à¸à¹à¸²à¸ªà¸¹à¹à¸£à¸°à¸à¸...", |
| | | "INITIALIZING": "à¸à¸³à¸¥à¸±à¸à¹à¸£à¸´à¹à¸¡à¸à¹à¸à¸£à¸°à¸à¸...", |
| | | "PIN_ERROR": "à¸à¸£à¸¸à¸à¸²à¹à¸ªà¹à¸à¸´à¸¡à¸à¸²à¸£à¹à¸", |
| | | "REG_ERROR": "à¹à¸¡à¹à¸ªà¸²à¸¡à¸²à¸£à¸à¹à¸à¹à¸²à¸à¸¶à¸à¹à¸à¸£à¸·à¸à¸à¹à¸²à¸¢à¹à¸à¹ à¸à¸£à¸¸à¸à¸²à¸à¸£à¸§à¸à¸ªà¸à¸à¸ªà¸à¸²à¸à¸°à¸à¸´à¸¡à¸à¸²à¸£à¹à¸", |
| | | "DETECTING_MODULE": "à¸à¸³à¸¥à¸±à¸à¸à¸£à¸§à¸à¸à¸±à¸à¹à¸¡à¸à¸¹à¸¥...", |
| | | "REGISTERING_NETWORK": "à¸à¸³à¸¥à¸±à¸à¸£à¸à¹à¸à¸£à¸·à¸à¸à¹à¸²à¸¢...", |
| | | "CHECKING_NEW_VERSION": "à¸à¸³à¸¥à¸±à¸à¸à¸£à¸§à¸à¸ªà¸à¸à¹à¸§à¸à¸£à¹à¸à¸±à¸à¹à¸«à¸¡à¹...", |
| | | "CHECK_NEW_VERSION_FAILED": "à¸à¸²à¸£à¸à¸£à¸§à¸à¸ªà¸à¸à¹à¸§à¸à¸£à¹à¸à¸±à¸à¹à¸«à¸¡à¹à¸¥à¹à¸¡à¹à¸«à¸¥à¸§ à¸à¸°à¸¥à¸à¸à¹à¸«à¸¡à¹à¹à¸ %d วิà¸à¸²à¸à¸µ: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "à¸à¸³à¸¥à¸±à¸à¹à¸à¸¥à¸µà¹à¸¢à¸à¹à¸à¹à¸ Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "à¸à¸³à¸¥à¸±à¸à¹à¸à¸¥à¸µà¹à¸¢à¸à¹à¸à¹à¸ 4G...", |
| | | "STANDBY": "à¸à¸£à¹à¸à¸¡", |
| | | "CONNECT_TO": "à¹à¸à¸·à¹à¸à¸¡à¸à¹à¸à¸à¸±à¸ ", |
| | | "CONNECTING": "à¸à¸³à¸¥à¸±à¸à¹à¸à¸·à¹à¸à¸¡à¸à¹à¸...", |
| | | "CONNECTION_SUCCESSFUL": "à¹à¸à¸·à¹à¸à¸¡à¸à¹à¸à¸ªà¸³à¹à¸£à¹à¸", |
| | | "CONNECTED_TO": "à¹à¸à¸·à¹à¸à¸¡à¸à¹à¸à¸à¸±à¸ ", |
| | | "LISTENING": "à¸à¸³à¸¥à¸±à¸à¸à¸±à¸...", |
| | | "SPEAKING": "à¸à¸³à¸¥à¸±à¸à¸à¸¹à¸...", |
| | | "SERVER_NOT_FOUND": "à¸à¸³à¸¥à¸±à¸à¸à¹à¸à¸«à¸²à¸à¸£à¸´à¸à¸²à¸£à¸à¸µà¹à¹à¸à¹à¸à¸²à¸à¹à¸à¹", |
| | | "SERVER_NOT_CONNECTED": "à¹à¸¡à¹à¸ªà¸²à¸¡à¸²à¸£à¸à¹à¸à¸·à¹à¸à¸¡à¸à¹à¸à¸à¸±à¸à¸à¸£à¸´à¸à¸²à¸£à¹à¸à¹ à¸à¸£à¸¸à¸à¸²à¸¥à¸à¸à¹à¸«à¸¡à¹à¹à¸à¸ ายหลัà¸", |
| | | "SERVER_TIMEOUT": "หมà¸à¹à¸§à¸¥à¸²à¸£à¸à¸à¸²à¸£à¸à¸à¸à¸à¸¥à¸±à¸", |
| | | "SERVER_ERROR": "à¸à¸²à¸£à¸ªà¹à¸à¸à¹à¸à¸¡à¸¹à¸¥à¸¥à¹à¸¡à¹à¸«à¸¥à¸§ à¸à¸£à¸¸à¸à¸²à¸à¸£à¸§à¸à¸ªà¸à¸à¹à¸à¸£à¸·à¸à¸à¹à¸²à¸¢", |
| | | "CONNECT_TO_HOTSPOT": "ฮà¸à¸à¸ªà¸à¸à¸: ", |
| | | "ACCESS_VIA_BROWSER": " URL à¸à¸²à¸£à¸à¸±à¹à¸à¸à¹à¸²: ", |
| | | "WIFI_CONFIG_MODE": "à¹à¸«à¸¡à¸à¸à¸²à¸£à¸à¸±à¹à¸à¸à¹à¸² Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "à¸à¸³à¸¥à¸±à¸à¹à¸à¹à¸²à¸ªà¸¹à¹à¹à¸«à¸¡à¸à¸à¸²à¸£à¸à¸±à¹à¸à¸à¹à¸² Wi-Fi...", |
| | | "SCANNING_WIFI": "à¸à¸³à¸¥à¸±à¸à¸ªà¹à¸à¸ Wi-Fi...", |
| | | "NEW_VERSION": "à¹à¸§à¸à¸£à¹à¸à¸±à¸à¹à¸«à¸¡à¹ ", |
| | | "OTA_UPGRADE": "à¸à¸²à¸£à¸à¸±à¸à¹à¸à¸£à¸ OTA", |
| | | "UPGRADING": "ระà¸à¸à¸à¸³à¸¥à¸±à¸à¸à¸±à¸à¹à¸à¸£à¸...", |
| | | "UPGRADE_FAILED": "à¸à¸²à¸£à¸à¸±à¸à¹à¸à¸£à¸à¸¥à¹à¸¡à¹à¸«à¸¥à¸§", |
| | | "ACTIVATION": "à¸à¸²à¸£à¹à¸à¸´à¸à¹à¸à¹à¸à¸²à¸", |
| | | "BATTERY_LOW": "à¹à¸à¸à¹à¸à¸à¸£à¸µà¹à¸à¹à¸³", |
| | | "BATTERY_CHARGING": "à¸à¸³à¸¥à¸±à¸à¸à¸²à¸£à¹à¸", |
| | | "BATTERY_FULL": "à¹à¸à¸à¹à¸à¸à¸£à¸µà¹à¹à¸à¹à¸¡", |
| | | "BATTERY_NEED_CHARGE": "à¹à¸à¸à¹à¸à¸à¸£à¸µà¹à¸à¹à¸³ à¸à¸£à¸¸à¸à¸²à¸à¸²à¸£à¹à¸", |
| | | "VOLUME": "à¹à¸ªà¸µà¸¢à¸ ", |
| | | "MUTED": "à¸à¸´à¸à¹à¸ªà¸µà¸¢à¸", |
| | | "MAX_VOLUME": "à¹à¸ªà¸µà¸¢à¸à¸ªà¸¹à¸à¸ªà¸¸à¸", |
| | | "RTC_MODE_OFF": "à¸à¸´à¸ AEC", |
| | | "RTC_MODE_ON": "à¹à¸à¸´à¸ AEC", |
| | | "DOWNLOAD_ASSETS_FAILED": "à¸à¸²à¸§à¸à¹à¹à¸«à¸¥à¸à¸à¸£à¸±à¸à¸¢à¸²à¸à¸£à¸¥à¹à¸¡à¹à¸«à¸¥à¸§", |
| | | "LOADING_ASSETS": "à¸à¸³à¸¥à¸±à¸à¹à¸«à¸¥à¸à¸à¸£à¸±à¸à¸¢à¸²à¸à¸£...", |
| | | "PLEASE_WAIT": "à¸à¸£à¸¸à¸à¸²à¸£à¸à¸ªà¸±à¸à¸à¸£à¸¹à¹...", |
| | | "FOUND_NEW_ASSETS": "à¸à¸à¸à¸£à¸±à¸à¸¢à¸²à¸à¸£à¹à¸«à¸¡à¹: %s", |
| | | "HELLO_MY_FRIEND": "สวัสà¸à¸µ à¹à¸à¸·à¹à¸à¸à¸à¸à¸à¸à¸±à¸!", |
| | | "FLIGHT_MODE_OFF": "à¹à¸«à¸¡à¸à¹à¸à¸£à¸·à¹à¸à¸à¸à¸´à¸à¸à¸´à¸à¸à¸¢à¸¹à¹", |
| | | "FLIGHT_MODE_ON": "à¹à¸«à¸¡à¸à¹à¸à¸£à¸·à¹à¸à¸à¸à¸´à¸à¹à¸à¸´à¸à¸à¸¢à¸¹à¹", |
| | | "MODEM_INIT_ERROR": "à¸à¸²à¸£à¹à¸£à¸´à¹à¸¡à¸à¹à¸à¹à¸¡à¹à¸à¹à¸¡à¸¥à¹à¸¡à¹à¸«à¸¥à¸§" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "tr-TR" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Uyarı", |
| | | "INFO": "Bilgi", |
| | | "ERROR": "Hata", |
| | | "VERSION": "Sürüm ", |
| | | "LOADING_PROTOCOL": "Sunucuya baÄlanıyor...", |
| | | "INITIALIZING": "BaÅlatılıyor...", |
| | | "PIN_ERROR": "Lütfen SIM kartı takın", |
| | | "REG_ERROR": "AÄa eriÅilemiyor, veri kartı durumunu kontrol edin", |
| | | "DETECTING_MODULE": "Modül algılanıyor...", |
| | | "REGISTERING_NETWORK": "AÄ bekleniyor...", |
| | | "CHECKING_NEW_VERSION": "Yeni sürüm kontrol ediliyor...", |
| | | "CHECK_NEW_VERSION_FAILED": "Yeni sürüm kontrolü baÅarısız, %d saniye sonra tekrar denenecek: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Wi-Fi'ye geçiliyor...", |
| | | "SWITCH_TO_4G_NETWORK": "4G'ye geçiliyor...", |
| | | "STANDBY": "Bekleme", |
| | | "CONNECT_TO": "BaÄlan ", |
| | | "CONNECTING": "BaÄlanıyor...", |
| | | "CONNECTED_TO": "BaÄlandı ", |
| | | "LISTENING": "Dinleniyor...", |
| | | "SPEAKING": "KonuÅuluyor...", |
| | | "SERVER_NOT_FOUND": "Mevcut hizmet aranıyor", |
| | | "SERVER_NOT_CONNECTED": "Hizmete baÄlanılamıyor, lütfen daha sonra deneyin", |
| | | "SERVER_TIMEOUT": "Yanıt zaman aÅımı", |
| | | "SERVER_ERROR": "Gönderme baÅarısız, aÄı kontrol edin", |
| | | "CONNECT_TO_HOTSPOT": "Telefonu hotspot'a baÄlayın ", |
| | | "ACCESS_VIA_BROWSER": "ï¼tarayıcı üzerinden eriÅin ", |
| | | "WIFI_CONFIG_MODE": "AÄ yapılandırma modu", |
| | | "ENTERING_WIFI_CONFIG_MODE": "AÄ yapılandırma moduna giriliyor...", |
| | | "SCANNING_WIFI": "Wi-Fi taranıyor...", |
| | | "NEW_VERSION": "Yeni sürüm ", |
| | | "OTA_UPGRADE": "OTA güncelleme", |
| | | "UPGRADING": "Sistem güncelleniyor...", |
| | | "UPGRADE_FAILED": "Güncelleme baÅarısız", |
| | | "ACTIVATION": "Cihaz aktivasyonu", |
| | | "BATTERY_LOW": "Pil düÅük", |
| | | "BATTERY_CHARGING": "Åarj oluyor", |
| | | "BATTERY_FULL": "Pil dolu", |
| | | "BATTERY_NEED_CHARGE": "Pil düÅük, lütfen Åarj edin", |
| | | "VOLUME": "Ses ", |
| | | "MUTED": "Sessiz", |
| | | "MAX_VOLUME": "Maksimum ses", |
| | | "RTC_MODE_OFF": "AEC kapalı", |
| | | "RTC_MODE_ON": "AEC açık", |
| | | "DOWNLOAD_ASSETS_FAILED": "Varlıklar indirilemedi", |
| | | "LOADING_ASSETS": "Varlıklar yükleniyor...", |
| | | "PLEASE_WAIT": "Lütfen bekleyin...", |
| | | "FOUND_NEW_ASSETS": "Yeni varlıklar bulundu: %s", |
| | | "HELLO_MY_FRIEND": "Merhaba, arkadaÅım!", |
| | | "CONNECTION_SUCCESSFUL": "BaÄlantı baÅarılı", |
| | | "FLIGHT_MODE_OFF": "Uçak modu kapalı", |
| | | "FLIGHT_MODE_ON": "Uçak modu açık", |
| | | "MODEM_INIT_ERROR": "Modem baÅlatma hatası" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "uk-UA" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "ÐопеÑедженнÑ", |
| | | "INFO": "ÐнÑоÑмаÑÑÑ", |
| | | "ERROR": "Ðомилка", |
| | | "VERSION": "ÐеÑÑÑÑ ", |
| | | "LOADING_PROTOCOL": "ÐÑдклÑÑÐµÐ½Ð½Ñ Ð´Ð¾ ÑеÑвеÑа...", |
| | | "INITIALIZING": "ÐнÑÑÑалÑзаÑÑÑ...", |
| | | "PIN_ERROR": "ÐÑÐ´Ñ Ð»Ð°Ñка, вÑÑавÑе SIM-каÑÑÑ", |
| | | "REG_ERROR": "Ðеможливо оÑÑимаÑи доÑÑÑп до меÑежÑ, пеÑевÑÑÑе ÑÑан каÑÑи даниÑ
", |
| | | "DETECTING_MODULE": "ÐиÑÐ²Ð»ÐµÐ½Ð½Ñ Ð¼Ð¾Ð´ÑлÑ...", |
| | | "REGISTERING_NETWORK": "ÐÑÑкÑÐ²Ð°Ð½Ð½Ñ Ð¼ÐµÑежÑ...", |
| | | "CHECKING_NEW_VERSION": "ÐеÑевÑÑка Ð½Ð¾Ð²Ð¾Ñ Ð²ÐµÑÑÑÑ...", |
| | | "CHECK_NEW_VERSION_FAILED": "ÐеÑевÑÑка Ð½Ð¾Ð²Ð¾Ñ Ð²ÐµÑÑÑÑ Ð½Ðµ вдалаÑÑ, повÑÐ¾Ñ ÑеÑез %d ÑекÑнд: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "ÐеÑÐµÐ¼Ð¸ÐºÐ°Ð½Ð½Ñ Ð½Ð° Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "ÐеÑÐµÐ¼Ð¸ÐºÐ°Ð½Ð½Ñ Ð½Ð° 4G...", |
| | | "STANDBY": "ÐÑÑкÑваннÑ", |
| | | "CONNECT_TO": "ÐÑдклÑÑиÑиÑÑ Ð´Ð¾ ", |
| | | "CONNECTING": "ÐÑдклÑÑеннÑ...", |
| | | "CONNECTED_TO": "ÐÑдклÑÑено до ", |
| | | "LISTENING": "ÐÑоÑлÑÑ
овÑваннÑ...", |
| | | "SPEAKING": "ÐовоÑÑннÑ...", |
| | | "SERVER_NOT_FOUND": "ÐоÑÑк доÑÑÑпного ÑеÑвÑÑÑ", |
| | | "SERVER_NOT_CONNECTED": "Ðеможливо пÑдклÑÑиÑиÑÑ Ð´Ð¾ ÑеÑвÑÑÑ, ÑпÑобÑйÑе пÑзнÑÑе", |
| | | "SERVER_TIMEOUT": "Ð§Ð°Ñ Ð¾ÑÑкÑÐ²Ð°Ð½Ð½Ñ Ð²ÑдповÑдÑ", |
| | | "SERVER_ERROR": "Ðомилка вÑдпÑавки, пеÑевÑÑÑе меÑежÑ", |
| | | "CONNECT_TO_HOTSPOT": "ÐÑдклÑÑÑÑÑ ÑелеÑон до ÑоÑки доÑÑÑÐ¿Ñ ", |
| | | "ACCESS_VIA_BROWSER": "ï¼Ð´Ð¾ÑÑÑп ÑеÑез бÑаÑÐ·ÐµÑ ", |
| | | "WIFI_CONFIG_MODE": "Режим налаÑÑÑÐ²Ð°Ð½Ð½Ñ Ð¼ÐµÑежÑ", |
| | | "ENTERING_WIFI_CONFIG_MODE": "ÐÑ
Ñд Ñ Ñежим налаÑÑÑÐ²Ð°Ð½Ð½Ñ Ð¼ÐµÑежÑ...", |
| | | "SCANNING_WIFI": "СканÑÐ²Ð°Ð½Ð½Ñ Wi-Fi...", |
| | | "NEW_VERSION": "Ðова веÑÑÑÑ ", |
| | | "OTA_UPGRADE": "ÐÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ OTA", |
| | | "UPGRADING": "ÐÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÑиÑÑеми...", |
| | | "UPGRADE_FAILED": "ÐÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ðµ вдалоÑÑ", |
| | | "ACTIVATION": "ÐкÑиваÑÑÑ Ð¿ÑиÑÑÑоÑ", |
| | | "BATTERY_LOW": "ÐизÑкий заÑÑд баÑаÑеÑ", |
| | | "BATTERY_CHARGING": "ÐаÑÑдка", |
| | | "BATTERY_FULL": "ÐаÑаÑÐµÑ Ð¿Ð¾Ð²Ð½Ð°", |
| | | "BATTERY_NEED_CHARGE": "ÐизÑкий заÑÑд, бÑÐ´Ñ Ð»Ð°Ñка, заÑÑдÑÑÑ", |
| | | "VOLUME": "ÐÑÑнÑÑÑÑ ", |
| | | "MUTED": "ÐвÑк вимкнено", |
| | | "MAX_VOLUME": "ÐакÑималÑна гÑÑнÑÑÑÑ", |
| | | "RTC_MODE_OFF": "AEC вимкнено", |
| | | "RTC_MODE_ON": "AEC ÑвÑмкнено", |
| | | "DOWNLOAD_ASSETS_FAILED": "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½ÑажиÑи ÑеÑÑÑÑи", |
| | | "LOADING_ASSETS": "ÐаванÑÐ°Ð¶ÐµÐ½Ð½Ñ ÑеÑÑÑÑÑв...", |
| | | "PLEASE_WAIT": "ÐÑÐ´Ñ Ð»Ð°Ñка, заÑекайÑе...", |
| | | "FOUND_NEW_ASSETS": "Ðнайдено Ð½Ð¾Ð²Ñ ÑеÑÑÑÑи: %s", |
| | | "HELLO_MY_FRIEND": "ÐÑивÑÑ, мÑй дÑÑже!", |
| | | "CONNECTION_SUCCESSFUL": "ÐÑдклÑÑÐµÐ½Ð½Ñ ÑÑпÑÑне", |
| | | "FLIGHT_MODE_OFF": "Режим полÑоÑÑ Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð¾", |
| | | "FLIGHT_MODE_ON": "Режим полÑоÑÑ ÑвÑмкнено", |
| | | "MODEM_INIT_ERROR": "Ðомилка ÑнÑÑÑалÑзаÑÑÑ Ð¼Ð¾Ð´ÐµÐ¼Ñ" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "vi-VN" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "Cảnh báo", |
| | | "INFO": "Thông tin", |
| | | "ERROR": "Lá»i", |
| | | "VERSION": "Phiên bản ", |
| | | "LOADING_PROTOCOL": "Äang ÄÄng nháºp...", |
| | | "INITIALIZING": "Äang khá»i tạo...", |
| | | "PIN_ERROR": "Vui lòng cắm thẻ SIM", |
| | | "REG_ERROR": "Không thá» truy cáºp mạng, vui lòng kiá»m tra trạng thái thẻ SIM", |
| | | "DETECTING_MODULE": "Äang phát hiá»n module...", |
| | | "REGISTERING_NETWORK": "Äang chá» mạng...", |
| | | "CHECKING_NEW_VERSION": "Äang kiá»m tra phiên bản má»i...", |
| | | "CHECK_NEW_VERSION_FAILED": "Kiá»m tra phiên bản má»i thất bại, sẽ thá» lại sau %d giây: %s", |
| | | "SWITCH_TO_WIFI_NETWORK": "Äang chuyá»n sang Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "Äang chuyá»n sang 4G...", |
| | | "STANDBY": "Chá»", |
| | | "CONNECT_TO": "Kết ná»i Äến ", |
| | | "CONNECTING": "Äang kết ná»i...", |
| | | "CONNECTION_SUCCESSFUL": "Kết ná»i thà nh công", |
| | | "CONNECTED_TO": "Äã kết ná»i Äến ", |
| | | "LISTENING": "Äang lắng nghe...", |
| | | "SPEAKING": "Äang nói...", |
| | | "SERVER_NOT_FOUND": "Äang tìm dá»ch vụ khả dụng", |
| | | "SERVER_NOT_CONNECTED": "Không thá» kết ná»i Äến dá»ch vụ, vui lòng thá» lại sau", |
| | | "SERVER_TIMEOUT": "Hết thá»i gian chá» phản há»i", |
| | | "SERVER_ERROR": "Gá»i thất bại, vui lòng kiá»m tra mạng", |
| | | "CONNECT_TO_HOTSPOT": "Äiá»m phát sóng: ", |
| | | "ACCESS_VIA_BROWSER": " URL cấu hình: ", |
| | | "WIFI_CONFIG_MODE": "Chế Äá» cấu hình Wi-Fi", |
| | | "ENTERING_WIFI_CONFIG_MODE": "Äang và o chế Äá» cấu hình Wi-Fi...", |
| | | "SCANNING_WIFI": "Äang quét Wi-Fi...", |
| | | "NEW_VERSION": "Phiên bản má»i ", |
| | | "OTA_UPGRADE": "Nâng cấp OTA", |
| | | "UPGRADING": "Há» thá»ng Äang nâng cấp...", |
| | | "UPGRADE_FAILED": "Nâng cấp thất bại", |
| | | "ACTIVATION": "KÃch hoạt", |
| | | "BATTERY_LOW": "Pin yếu", |
| | | "BATTERY_CHARGING": "Äang sạc", |
| | | "BATTERY_FULL": "Pin Äầy", |
| | | "BATTERY_NEED_CHARGE": "Pin yếu, vui lòng sạc", |
| | | "VOLUME": "Ãm lượng ", |
| | | "MUTED": "Tắt tiếng", |
| | | "MAX_VOLUME": "Ãm lượng tá»i Äa", |
| | | "RTC_MODE_OFF": "Tắt AEC", |
| | | "RTC_MODE_ON": "Báºt AEC", |
| | | "DOWNLOAD_ASSETS_FAILED": "Tải xuá»ng tà i nguyên thất bại", |
| | | "LOADING_ASSETS": "Äang tải tà i nguyên...", |
| | | "PLEASE_WAIT": "Vui lòng Äợi...", |
| | | "FOUND_NEW_ASSETS": "Tìm thấy tà i nguyên má»i: %s", |
| | | "HELLO_MY_FRIEND": "Xin chà o, bạn của tôi!", |
| | | "FLIGHT_MODE_OFF": "Chế Äá» máy bay Äang tắt", |
| | | "FLIGHT_MODE_ON": "Chế Äá» máy bay Äang báºt", |
| | | "MODEM_INIT_ERROR": "Khá»i tạo modem thất bại" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "zh-CN" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "è¦å", |
| | | "INFO": "ä¿¡æ¯", |
| | | "ERROR": "é误", |
| | | "VERSION": "çæ¬ ", |
| | | "LOADING_PROTOCOL": "ç»å½æå¡å¨...", |
| | | "INITIALIZING": "æ£å¨åå§å...", |
| | | "PIN_ERROR": "请æå
¥ SIM å¡", |
| | | "REG_ERROR": "æ æ³æ¥å
¥ç½ç»ï¼è¯·æ£æ¥æµéå¡ç¶æ", |
| | | "MODEM_INIT_ERROR": "模ç»åå§å失败", |
| | | "DETECTING_MODULE": "æ£æµæ¨¡ç»...", |
| | | "REGISTERING_NETWORK": "çå¾
ç½ç»...", |
| | | "CHECKING_NEW_VERSION": "æ£æ¥æ°çæ¬...", |
| | | "CHECK_NEW_VERSION_FAILED": "æ£æ¥æ°çæ¬å¤±è´¥ï¼å°å¨ %d ç§åéè¯ï¼%s", |
| | | "SWITCH_TO_WIFI_NETWORK": "åæ¢å° Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "åæ¢å° 4G...", |
| | | "STANDBY": "å¾
å½", |
| | | "CONNECT_TO": "è¿æ¥ ", |
| | | "CONNECTING": "è¿æ¥ä¸...", |
| | | "CONNECTED_TO": "å·²è¿æ¥ ", |
| | | "LISTENING": "èå¬ä¸...", |
| | | "SPEAKING": "说è¯ä¸...", |
| | | "SERVER_NOT_FOUND": "æ£å¨å¯»æ¾å¯ç¨æå¡", |
| | | "SERVER_NOT_CONNECTED": "æ æ³è¿æ¥æå¡ï¼è¯·ç¨ååè¯", |
| | | "SERVER_TIMEOUT": "çå¾
ååºè¶
æ¶", |
| | | "SERVER_ERROR": "åé失败ï¼è¯·æ£æ¥ç½ç»", |
| | | "CONNECT_TO_HOTSPOT": "ææºè¿æ¥çç¹ ", |
| | | "ACCESS_VIA_BROWSER": "ï¼æµè§å¨è®¿é® ", |
| | | "WIFI_CONFIG_MODE": "é
ç½æ¨¡å¼", |
| | | "ENTERING_WIFI_CONFIG_MODE": "è¿å
¥é
ç½æ¨¡å¼...", |
| | | "SCANNING_WIFI": "æ«æ Wi-Fi...", |
| | | "NEW_VERSION": "æ°çæ¬ ", |
| | | "OTA_UPGRADE": "OTA å级", |
| | | "UPGRADING": "æ£å¨å级系ç»...", |
| | | "UPGRADE_FAILED": "å级失败", |
| | | "ACTIVATION": "æ¿æ´»è®¾å¤", |
| | | "BATTERY_LOW": "çµéä¸è¶³", |
| | | "BATTERY_CHARGING": "æ£å¨å
çµ", |
| | | "BATTERY_FULL": "çµé已满", |
| | | "BATTERY_NEED_CHARGE": "çµéä½ï¼è¯·å
çµ", |
| | | "VOLUME": "é³é ", |
| | | "MUTED": "å·²éé³", |
| | | "MAX_VOLUME": "æå¤§é³é", |
| | | "RTC_MODE_OFF": "AEC å
³é", |
| | | "RTC_MODE_ON": "AEC å¼å¯", |
| | | "DOWNLOAD_ASSETS_FAILED": "ä¸è½½èµæºå¤±è´¥", |
| | | "LOADING_ASSETS": "å è½½èµæº...", |
| | | "PLEASE_WAIT": "请ç¨å...", |
| | | "FOUND_NEW_ASSETS": "åç°æ°èµæº: %s", |
| | | "HELLO_MY_FRIEND": "ä½ å¥½ï¼æçæåï¼", |
| | | "CONNECTION_SUCCESSFUL": "è¿æ¥æå", |
| | | "FLIGHT_MODE_OFF": "é£è¡æ¨¡å¼å·²å
³é", |
| | | "FLIGHT_MODE_ON": "é£è¡æ¨¡å¼å·²å¼å¯" |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "language": { |
| | | "type": "zh-TW" |
| | | }, |
| | | "strings": { |
| | | "WARNING": "è¦å", |
| | | "INFO": "è³è¨", |
| | | "ERROR": "é¯èª¤", |
| | | "VERSION": "çæ¬ ", |
| | | "LOADING_PROTOCOL": "ç»å
¥ä¼ºæå¨...", |
| | | "INITIALIZING": "æ£å¨åå§å...", |
| | | "PIN_ERROR": "è«æå
¥ SIM å¡", |
| | | "REG_ERROR": "ç¡æ³æ¥å
¥ç¶²çµ¡ï¼è«æª¢æ¥ç¶²è·¯çæ
", |
| | | "DETECTING_MODULE": "檢測模çµ...", |
| | | "REGISTERING_NETWORK": "çå¾
網絡...", |
| | | "CHECKING_NEW_VERSION": "æª¢æ¥æ°çæ¬...", |
| | | "CHECK_NEW_VERSION_FAILED": "æª¢æ¥æ°çæ¬å¤±æï¼å°å¨ %d ç§å¾é試ï¼%s", |
| | | "SWITCH_TO_WIFI_NETWORK": "åæå° Wi-Fi...", |
| | | "SWITCH_TO_4G_NETWORK": "åæå° 4G...", |
| | | "STANDBY": "å¾
å½", |
| | | "CONNECT_TO": "飿¥ ", |
| | | "CONNECTING": "飿¥ä¸...", |
| | | "CONNECTED_TO": "已飿¥ ", |
| | | "LISTENING": "èè½ä¸...", |
| | | "SPEAKING": "說話ä¸...", |
| | | "SERVER_NOT_FOUND": "æ£å¨å°æ¾å¯ç¨æå", |
| | | "SERVER_NOT_CONNECTED": "ç¡æ³é£æ¥æåï¼è«ç¨å¾å試", |
| | | "SERVER_TIMEOUT": "çå¾
é¿æè¶
æ", |
| | | "SERVER_ERROR": "ç¼é失æï¼è«æª¢æ¥ç¶²çµ¡", |
| | | "CONNECT_TO_HOTSPOT": "ææ©é£æ¥WiFi ", |
| | | "ACCESS_VIA_BROWSER": "ï¼ç覽å¨è¨ªå ", |
| | | "WIFI_CONFIG_MODE": "網路è¨å®æ¨¡å¼", |
| | | "ENTERING_WIFI_CONFIG_MODE": "æ£å¨è¨å®ç¶²è·¯...", |
| | | "SCANNING_WIFI": "ææ Wi-Fi...", |
| | | "NEW_VERSION": "æ°çæ¬ ", |
| | | "OTA_UPGRADE": "OTA åç´", |
| | | "UPGRADING": "æ£å¨åç´ç³»çµ±...", |
| | | "UPGRADE_FAILED": "åç´å¤±æ", |
| | | "ACTIVATION": "åç¨è¨å", |
| | | "BATTERY_LOW": "é»éä¸è¶³", |
| | | "BATTERY_CHARGING": "æ£å¨å
é»", |
| | | "BATTERY_FULL": "é»é已滿", |
| | | "BATTERY_NEED_CHARGE": "é»éä½ï¼è«å
é»", |
| | | "VOLUME": "é³é ", |
| | | "MUTED": "å·²éé³", |
| | | "MAX_VOLUME": "æå¤§é³é", |
| | | "RTC_MODE_OFF": "AEC éé", |
| | | "RTC_MODE_ON": "AEC éå", |
| | | "DOWNLOAD_ASSETS_FAILED": "ä¸è¼è³æºå¤±æ", |
| | | "LOADING_ASSETS": "è¼å
¥è³æº...", |
| | | "PLEASE_WAIT": "è«ç¨å...", |
| | | "FOUND_NEW_ASSETS": "ç¼ç¾æ°è³æº: %s", |
| | | "HELLO_MY_FRIEND": "ä½ å¥½ï¼æçæåï¼", |
| | | "CONNECTION_SUCCESSFUL": "é£ç·æå", |
| | | "FLIGHT_MODE_OFF": "é£èªæ¨¡å¼å·²éé", |
| | | "FLIGHT_MODE_ON": "é£èªæ¨¡å¼å·²éå", |
| | | "MODEM_INIT_ERROR": "模çµåå§å失æ" |
| | | } |
| | | } |
| New file |
| | |
| | | # Audio Service Architecture |
| | | |
| | | The audio service is a core component responsible for managing all audio-related functionalities, including capturing audio from the microphone, processing it, encoding/decoding, and playing back audio through the speaker. It is designed to be modular and efficient, running its main operations in dedicated FreeRTOS tasks to ensure real-time performance. |
| | | |
| | | ## Key Components |
| | | |
| | | - **`AudioService`**: The central orchestrator. It initializes and manages all other audio components, tasks, and data queues. |
| | | - **`AudioCodec`**: A hardware abstraction layer (HAL) for the physical audio codec chip. It handles the raw I2S communication for audio input and output. |
| | | - **`AudioProcessor`**: Performs real-time audio processing on the microphone input stream. This typically includes Acoustic Echo Cancellation (AEC), noise suppression, and Voice Activity Detection (VAD). `AfeAudioProcessor` is the default implementation, utilizing the ESP-ADF Audio Front-End. |
| | | - **`WakeWord`**: Detects keywords (e.g., "ä½ å¥½ï¼å°æº", "Hi, ESP") from the audio stream. It runs independently from the main audio processor until a wake word is detected. |
| | | - **`OpusEncoderWrapper` / `OpusDecoderWrapper`**: Manages the encoding of PCM audio to the Opus format and decoding Opus packets back to PCM. Opus is used for its high compression and low latency, making it ideal for voice streaming. |
| | | - **`OpusResampler`**: A utility to convert audio streams between different sample rates (e.g., resampling from the codec's native sample rate to the required 16kHz for processing). |
| | | |
| | | ## Threading Model |
| | | |
| | | The service operates on three primary tasks to handle the different stages of the audio pipeline concurrently: |
| | | |
| | | 1. **`AudioInputTask`**: Solely responsible for reading raw PCM data from the `AudioCodec`. It then feeds this data to either the `WakeWord` engine or the `AudioProcessor` based on the current state. |
| | | 2. **`AudioOutputTask`**: Responsible for playing audio. It retrieves decoded PCM data from the `audio_playback_queue_` and sends it to the `AudioCodec` to be played on the speaker. |
| | | 3. **`OpusCodecTask`**: A worker task that handles both encoding and decoding. It fetches raw audio from `audio_encode_queue_`, encodes it into Opus packets, and places them in the `audio_send_queue_`. Concurrently, it fetches Opus packets from `audio_decode_queue_`, decodes them into PCM, and places the result in the `audio_playback_queue_`. |
| | | |
| | | ## Data Flow |
| | | |
| | | There are two primary data flows: audio input (uplink) and audio output (downlink). |
| | | |
| | | ### 1. Audio Input (Uplink) Flow |
| | | |
| | | This flow captures audio from the microphone, processes it, encodes it, and prepares it for sending to a server. |
| | | |
| | | ```mermaid |
| | | graph TD |
| | | subgraph Device |
| | | Mic[("Microphone")] -->|I2S| Codec(AudioCodec) |
| | | |
| | | subgraph AudioInputTask |
| | | Codec -->|Raw PCM| Read(ReadAudioData) |
| | | Read -->|16kHz PCM| Processor(AudioProcessor) |
| | | end |
| | | |
| | | subgraph OpusCodecTask |
| | | Processor -->|Clean PCM| EncodeQueue(audio_encode_queue_) |
| | | EncodeQueue --> Encoder(OpusEncoder) |
| | | Encoder -->|Opus Packet| SendQueue(audio_send_queue_) |
| | | end |
| | | |
| | | SendQueue --> |"PopPacketFromSendQueue()"| App(Application Layer) |
| | | end |
| | | |
| | | App -->|Network| Server((Cloud Server)) |
| | | ``` |
| | | |
| | | - The `AudioInputTask` continuously reads raw PCM data from the `AudioCodec`. |
| | | - This data is fed into an `AudioProcessor` for cleaning (AEC, VAD). |
| | | - The processed PCM data is pushed into the `audio_encode_queue_`. |
| | | - The `OpusCodecTask` picks up the PCM data, encodes it into Opus format, and pushes the resulting packet to the `audio_send_queue_`. |
| | | - The application can then retrieve these Opus packets and send them over the network. |
| | | |
| | | ### 2. Audio Output (Downlink) Flow |
| | | |
| | | This flow receives encoded audio data, decodes it, and plays it on the speaker. |
| | | |
| | | ```mermaid |
| | | graph TD |
| | | Server((Cloud Server)) -->|Network| App(Application Layer) |
| | | |
| | | subgraph Device |
| | | App -->|"PushPacketToDecodeQueue()"| DecodeQueue(audio_decode_queue_) |
| | | |
| | | subgraph OpusCodecTask |
| | | DecodeQueue -->|Opus Packet| Decoder(OpusDecoder) |
| | | Decoder -->|PCM| PlaybackQueue(audio_playback_queue_) |
| | | end |
| | | |
| | | subgraph AudioOutputTask |
| | | PlaybackQueue -->|PCM| Codec(AudioCodec) |
| | | end |
| | | |
| | | Codec -->|I2S| Speaker[("Speaker")] |
| | | end |
| | | ``` |
| | | |
| | | - The application receives Opus packets from the network and pushes them into the `audio_decode_queue_`. |
| | | - The `OpusCodecTask` retrieves these packets, decodes them back into PCM data, and pushes the data to the `audio_playback_queue_`. |
| | | - The `AudioOutputTask` takes the PCM data from the queue and sends it to the `AudioCodec` for playback. |
| | | |
| | | ## Power Management |
| | | |
| | | To conserve energy, the audio codec's input (ADC) and output (DAC) channels are automatically disabled after a period of inactivity (`AUDIO_POWER_TIMEOUT_MS`). A timer (`audio_power_timer_`) periodically checks for activity and manages the power state. The channels are automatically re-enabled when new audio needs to be captured or played. |
| New file |
| | |
| | | #include "audio_codec.h" |
| | | #include "board.h" |
| | | #include "settings.h" |
| | | |
| | | #include <esp_log.h> |
| | | #include <cstring> |
| | | #include <driver/i2s_common.h> |
| | | |
| | | #define TAG "AudioCodec" |
| | | |
| | | AudioCodec::AudioCodec() { |
| | | } |
| | | |
| | | AudioCodec::~AudioCodec() { |
| | | } |
| | | |
| | | void AudioCodec::OutputData(std::vector<int16_t>& data) { |
| | | Write(data.data(), data.size()); |
| | | } |
| | | |
| | | bool AudioCodec::InputData(std::vector<int16_t>& data) { |
| | | int samples = Read(data.data(), data.size()); |
| | | if (samples > 0) { |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | void AudioCodec::Start() { |
| | | Settings settings("audio", false); |
| | | output_volume_ = settings.GetInt("output_volume", output_volume_); |
| | | if (output_volume_ <= 0) { |
| | | ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_); |
| | | output_volume_ = 10; |
| | | } |
| | | |
| | | ESP_LOGI(TAG, "Audio codec started"); |
| | | } |
| | | |
| | | void AudioCodec::SetOutputVolume(int volume) { |
| | | output_volume_ = volume; |
| | | ESP_LOGI(TAG, "Set output volume to %d", output_volume_); |
| | | |
| | | Settings settings("audio", true); |
| | | settings.SetInt("output_volume", output_volume_); |
| | | } |
| | | |
| | | void AudioCodec::SetInputGain(float gain) { |
| | | input_gain_ = gain; |
| | | ESP_LOGI(TAG, "Set input gain to %.1f", input_gain_); |
| | | } |
| | | |
| | | void AudioCodec::EnableInput(bool enable) { |
| | | if (enable == input_enabled_) { |
| | | return; |
| | | } |
| | | input_enabled_ = enable; |
| | | ESP_LOGI(TAG, "Set input enable to %s", enable ? "true" : "false"); |
| | | } |
| | | |
| | | void AudioCodec::EnableOutput(bool enable) { |
| | | if (enable == output_enabled_) { |
| | | return; |
| | | } |
| | | output_enabled_ = enable; |
| | | ESP_LOGI(TAG, "Set output enable to %s", enable ? "true" : "false"); |
| | | } |
| New file |
| | |
| | | #ifndef _AUDIO_CODEC_H |
| | | #define _AUDIO_CODEC_H |
| | | |
| | | #include <freertos/FreeRTOS.h> |
| | | #include <freertos/event_groups.h> |
| | | #include <driver/i2s_std.h> |
| | | |
| | | #include <vector> |
| | | #include <string> |
| | | #include <functional> |
| | | |
| | | #include "board.h" |
| | | |
| | | #define AUDIO_CODEC_DMA_DESC_NUM 6 |
| | | #define AUDIO_CODEC_DMA_FRAME_NUM 240 |
| | | |
| | | class AudioCodec { |
| | | public: |
| | | AudioCodec(); |
| | | virtual ~AudioCodec(); |
| | | |
| | | virtual void SetOutputVolume(int volume); |
| | | virtual void SetInputGain(float gain); |
| | | virtual void EnableInput(bool enable); |
| | | virtual void EnableOutput(bool enable); |
| | | |
| | | virtual void OutputData(std::vector<int16_t>& data); |
| | | virtual bool InputData(std::vector<int16_t>& data); |
| | | virtual void Start(); |
| | | |
| | | inline bool duplex() const { return duplex_; } |
| | | inline bool input_reference() const { return input_reference_; } |
| | | inline int input_sample_rate() const { return input_sample_rate_; } |
| | | inline int output_sample_rate() const { return output_sample_rate_; } |
| | | inline int input_channels() const { return input_channels_; } |
| | | inline int output_channels() const { return output_channels_; } |
| | | inline int output_volume() const { return output_volume_; } |
| | | inline float input_gain() const { return input_gain_; } |
| | | inline bool input_enabled() const { return input_enabled_; } |
| | | inline bool output_enabled() const { return output_enabled_; } |
| | | |
| | | protected: |
| | | i2s_chan_handle_t tx_handle_ = nullptr; |
| | | i2s_chan_handle_t rx_handle_ = nullptr; |
| | | |
| | | bool duplex_ = false; |
| | | bool input_reference_ = false; |
| | | bool input_enabled_ = false; |
| | | bool output_enabled_ = false; |
| | | int input_sample_rate_ = 0; |
| | | int output_sample_rate_ = 0; |
| | | int input_channels_ = 1; |
| | | int output_channels_ = 1; |
| | | int output_volume_ = 70; |
| | | float input_gain_ = 0.0; |
| | | |
| | | virtual int Read(int16_t* dest, int samples) = 0; |
| | | virtual int Write(const int16_t* data, int samples) = 0; |
| | | }; |
| | | |
| | | #endif // _AUDIO_CODEC_H |
| New file |
| | |
| | | #ifndef AUDIO_PROCESSOR_H |
| | | #define AUDIO_PROCESSOR_H |
| | | |
| | | #include <string> |
| | | #include <vector> |
| | | #include <functional> |
| | | |
| | | #include <model_path.h> |
| | | #include "audio_codec.h" |
| | | |
| | | class AudioProcessor { |
| | | public: |
| | | virtual ~AudioProcessor() = default; |
| | | |
| | | virtual void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) = 0; |
| | | virtual void Feed(std::vector<int16_t>&& data) = 0; |
| | | virtual void Start() = 0; |
| | | virtual void Stop() = 0; |
| | | virtual bool IsRunning() = 0; |
| | | virtual void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) = 0; |
| | | virtual void OnVadStateChange(std::function<void(bool speaking)> callback) = 0; |
| | | virtual size_t GetFeedSize() = 0; |
| | | virtual void EnableDeviceAec(bool enable) = 0; |
| | | }; |
| | | |
| | | #endif |
| New file |
| | |
| | | #include "audio_service.h" |
| | | #include <esp_log.h> |
| | | #include <cstring> |
| | | |
| | | #define RATE_CVT_CFG(_src_rate, _dest_rate, _channel) \ |
| | | (esp_ae_rate_cvt_cfg_t) \ |
| | | { \ |
| | | .src_rate = (uint32_t)(_src_rate), \ |
| | | .dest_rate = (uint32_t)(_dest_rate), \ |
| | | .channel = (uint8_t)(_channel), \ |
| | | .bits_per_sample = ESP_AUDIO_BIT16, \ |
| | | .complexity = 2, \ |
| | | .perf_type = ESP_AE_RATE_CVT_PERF_TYPE_SPEED, \ |
| | | } |
| | | |
| | | #define OPUS_DEC_CFG(_sample_rate, _frame_duration_ms) \ |
| | | (esp_opus_dec_cfg_t) \ |
| | | { \ |
| | | .sample_rate = (uint32_t)(_sample_rate), \ |
| | | .channel = ESP_AUDIO_MONO, \ |
| | | .frame_duration = (esp_opus_dec_frame_duration_t)AS_OPUS_GET_FRAME_DRU_ENUM(_frame_duration_ms), \ |
| | | .self_delimited = false, \ |
| | | } |
| | | |
| | | #if CONFIG_USE_AUDIO_PROCESSOR |
| | | #include "processors/afe_audio_processor.h" |
| | | #else |
| | | #include "processors/no_audio_processor.h" |
| | | #endif |
| | | |
| | | #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 |
| | | #include "wake_words/afe_wake_word.h" |
| | | #include "wake_words/custom_wake_word.h" |
| | | #else |
| | | #include "wake_words/esp_wake_word.h" |
| | | #endif |
| | | |
| | | #define TAG "AudioService" |
| | | |
| | | AudioService::AudioService() { |
| | | event_group_ = xEventGroupCreate(); |
| | | } |
| | | |
| | | AudioService::~AudioService() { |
| | | if (event_group_ != nullptr) { |
| | | vEventGroupDelete(event_group_); |
| | | } |
| | | if (opus_encoder_ != nullptr) { |
| | | esp_opus_enc_close(opus_encoder_); |
| | | } |
| | | if (opus_decoder_ != nullptr) { |
| | | esp_opus_dec_close(opus_decoder_); |
| | | } |
| | | if (input_resampler_ != nullptr) { |
| | | esp_ae_rate_cvt_close(input_resampler_); |
| | | } |
| | | if (output_resampler_ != nullptr) { |
| | | esp_ae_rate_cvt_close(output_resampler_); |
| | | } |
| | | } |
| | | |
| | | void AudioService::Initialize(AudioCodec* codec) { |
| | | codec_ = codec; |
| | | codec_->Start(); |
| | | |
| | | esp_opus_dec_cfg_t opus_dec_cfg = OPUS_DEC_CFG(codec->output_sample_rate(), OPUS_FRAME_DURATION_MS); |
| | | auto ret = esp_opus_dec_open(&opus_dec_cfg, sizeof(esp_opus_dec_cfg_t), &opus_decoder_); |
| | | if (opus_decoder_ == nullptr) { |
| | | ESP_LOGE(TAG, "Failed to create audio decoder, error code: %d", ret); |
| | | } else { |
| | | decoder_sample_rate_ = codec->output_sample_rate(); |
| | | decoder_duration_ms_ = OPUS_FRAME_DURATION_MS; |
| | | decoder_frame_size_ = decoder_sample_rate_ / 1000 * OPUS_FRAME_DURATION_MS; |
| | | } |
| | | esp_opus_enc_config_t opus_enc_cfg = AS_OPUS_ENC_CONFIG(); |
| | | ret = esp_opus_enc_open(&opus_enc_cfg, sizeof(esp_opus_enc_config_t), &opus_encoder_); |
| | | if (opus_encoder_ == nullptr) { |
| | | ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", ret); |
| | | } else { |
| | | encoder_sample_rate_ = 16000; |
| | | encoder_duration_ms_ = OPUS_FRAME_DURATION_MS; |
| | | esp_opus_enc_get_frame_size(opus_encoder_, &encoder_frame_size_, &encoder_outbuf_size_); |
| | | encoder_frame_size_ = encoder_frame_size_ / sizeof(int16_t); |
| | | } |
| | | |
| | | if (codec->input_sample_rate() != 16000) { |
| | | esp_ae_rate_cvt_cfg_t input_resampler_cfg = RATE_CVT_CFG( |
| | | codec->input_sample_rate(), ESP_AUDIO_SAMPLE_RATE_16K, codec->input_channels()); |
| | | auto resampler_ret = esp_ae_rate_cvt_open(&input_resampler_cfg, &input_resampler_); |
| | | if (input_resampler_ == nullptr) { |
| | | ESP_LOGE(TAG, "Failed to create input resampler, error code: %d", resampler_ret); |
| | | } |
| | | } |
| | | |
| | | #if CONFIG_USE_AUDIO_PROCESSOR |
| | | audio_processor_ = std::make_unique<AfeAudioProcessor>(); |
| | | #else |
| | | audio_processor_ = std::make_unique<NoAudioProcessor>(); |
| | | #endif |
| | | |
| | | audio_processor_->OnOutput([this](std::vector<int16_t>&& data) { |
| | | PushTaskToEncodeQueue(kAudioTaskTypeEncodeToSendQueue, std::move(data)); |
| | | }); |
| | | |
| | | audio_processor_->OnVadStateChange([this](bool speaking) { |
| | | voice_detected_ = speaking; |
| | | if (callbacks_.on_vad_change) { |
| | | callbacks_.on_vad_change(speaking); |
| | | } |
| | | }); |
| | | |
| | | esp_timer_create_args_t audio_power_timer_args = { |
| | | .callback = [](void* arg) { |
| | | AudioService* audio_service = (AudioService*)arg; |
| | | audio_service->CheckAndUpdateAudioPowerState(); |
| | | }, |
| | | .arg = this, |
| | | .dispatch_method = ESP_TIMER_TASK, |
| | | .name = "audio_power_timer", |
| | | .skip_unhandled_events = true, |
| | | }; |
| | | esp_timer_create(&audio_power_timer_args, &audio_power_timer_); |
| | | } |
| | | |
| | | void AudioService::Start() { |
| | | service_stopped_ = false; |
| | | xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING | AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING); |
| | | |
| | | esp_timer_start_periodic(audio_power_timer_, 1000000); |
| | | |
| | | #if CONFIG_USE_AUDIO_PROCESSOR |
| | | /* Start the audio input task */ |
| | | xTaskCreatePinnedToCore([](void* arg) { |
| | | AudioService* audio_service = (AudioService*)arg; |
| | | audio_service->AudioInputTask(); |
| | | vTaskDelete(NULL); |
| | | }, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_, 0); |
| | | |
| | | /* Start the audio output task */ |
| | | xTaskCreate([](void* arg) { |
| | | AudioService* audio_service = (AudioService*)arg; |
| | | audio_service->AudioOutputTask(); |
| | | vTaskDelete(NULL); |
| | | }, "audio_output", 2048 * 2, this, 4, &audio_output_task_handle_); |
| | | #else |
| | | /* Start the audio input task */ |
| | | xTaskCreate([](void* arg) { |
| | | AudioService* audio_service = (AudioService*)arg; |
| | | audio_service->AudioInputTask(); |
| | | vTaskDelete(NULL); |
| | | }, "audio_input", 2048 * 2, this, 8, &audio_input_task_handle_); |
| | | |
| | | /* Start the audio output task */ |
| | | xTaskCreate([](void* arg) { |
| | | AudioService* audio_service = (AudioService*)arg; |
| | | audio_service->AudioOutputTask(); |
| | | vTaskDelete(NULL); |
| | | }, "audio_output", 2048, this, 4, &audio_output_task_handle_); |
| | | #endif |
| | | |
| | | /* Start the opus codec task */ |
| | | xTaskCreate([](void* arg) { |
| | | AudioService* audio_service = (AudioService*)arg; |
| | | audio_service->OpusCodecTask(); |
| | | vTaskDelete(NULL); |
| | | }, "opus_codec", 2048 * 12, this, 2, &opus_codec_task_handle_); |
| | | } |
| | | |
| | | void AudioService::Stop() { |
| | | esp_timer_stop(audio_power_timer_); |
| | | service_stopped_ = true; |
| | | xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING | |
| | | AS_EVENT_WAKE_WORD_RUNNING | |
| | | AS_EVENT_AUDIO_PROCESSOR_RUNNING); |
| | | |
| | | std::lock_guard<std::mutex> lock(audio_queue_mutex_); |
| | | audio_encode_queue_.clear(); |
| | | audio_decode_queue_.clear(); |
| | | audio_playback_queue_.clear(); |
| | | audio_testing_queue_.clear(); |
| | | audio_queue_cv_.notify_all(); |
| | | } |
| | | |
| | | bool AudioService::ReadAudioData(std::vector<int16_t>& data, int sample_rate, int samples) { |
| | | if (!codec_->input_enabled()) { |
| | | esp_timer_stop(audio_power_timer_); |
| | | esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000); |
| | | codec_->EnableInput(true); |
| | | } |
| | | |
| | | if (codec_->input_sample_rate() != sample_rate) { |
| | | data.resize(samples * codec_->input_sample_rate() / sample_rate * codec_->input_channels()); |
| | | if (!codec_->InputData(data)) { |
| | | return false; |
| | | } |
| | | if (input_resampler_ != nullptr) { |
| | | std::lock_guard<std::mutex> lock(input_resampler_mutex_); |
| | | uint32_t in_sample_num = data.size() / codec_->input_channels(); |
| | | uint32_t output_samples = 0; |
| | | esp_ae_rate_cvt_get_max_out_sample_num(input_resampler_, in_sample_num, &output_samples); |
| | | auto resampled = std::vector<int16_t>(output_samples * codec_->input_channels()); |
| | | uint32_t actual_output = output_samples; |
| | | esp_ae_rate_cvt_process(input_resampler_, (esp_ae_sample_t)data.data(), in_sample_num, |
| | | (esp_ae_sample_t)resampled.data(), &actual_output); |
| | | resampled.resize(actual_output * codec_->input_channels()); |
| | | data = std::move(resampled); |
| | | } |
| | | } else { |
| | | data.resize(samples * codec_->input_channels()); |
| | | if (!codec_->InputData(data)) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /* Update the last input time */ |
| | | last_input_time_ = std::chrono::steady_clock::now(); |
| | | debug_statistics_.input_count++; |
| | | |
| | | #if CONFIG_USE_AUDIO_DEBUGGER |
| | | // é³é¢è°è¯ï¼åéåå§é³é¢æ°æ® |
| | | if (audio_debugger_ == nullptr) { |
| | | audio_debugger_ = std::make_unique<AudioDebugger>(); |
| | | } |
| | | audio_debugger_->Feed(data); |
| | | #endif |
| | | |
| | | return true; |
| | | } |
| | | |
| | | void AudioService::AudioInputTask() { |
| | | while (true) { |
| | | EventBits_t bits = xEventGroupWaitBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING | |
| | | AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING, |
| | | pdFALSE, pdFALSE, portMAX_DELAY); |
| | | |
| | | if (service_stopped_) { |
| | | break; |
| | | } |
| | | if (audio_input_need_warmup_) { |
| | | audio_input_need_warmup_ = false; |
| | | vTaskDelay(pdMS_TO_TICKS(120)); |
| | | continue; |
| | | } |
| | | |
| | | /* Used for audio testing in NetworkConfiguring mode by clicking the BOOT button */ |
| | | if (bits & AS_EVENT_AUDIO_TESTING_RUNNING) { |
| | | if (audio_testing_queue_.size() >= AUDIO_TESTING_MAX_DURATION_MS / OPUS_FRAME_DURATION_MS) { |
| | | ESP_LOGW(TAG, "Audio testing queue is full, stopping audio testing"); |
| | | EnableAudioTesting(false); |
| | | continue; |
| | | } |
| | | std::vector<int16_t> data; |
| | | int samples = OPUS_FRAME_DURATION_MS * 16000 / 1000; |
| | | if (ReadAudioData(data, 16000, samples)) { |
| | | // If input channels is 2, we need to fetch the left channel data |
| | | if (codec_->input_channels() == 2) { |
| | | auto mono_data = std::vector<int16_t>(data.size() / 2); |
| | | for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) { |
| | | mono_data[i] = data[j]; |
| | | } |
| | | data = std::move(mono_data); |
| | | } |
| | | PushTaskToEncodeQueue(kAudioTaskTypeEncodeToTestingQueue, std::move(data)); |
| | | continue; |
| | | } |
| | | } |
| | | |
| | | /* Feed the wake word and/or audio processor */ |
| | | if (bits & (AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING)) { |
| | | int samples = 160; // 10ms |
| | | std::vector<int16_t> data; |
| | | if (ReadAudioData(data, 16000, samples)) { |
| | | if (bits & AS_EVENT_WAKE_WORD_RUNNING) { |
| | | wake_word_->Feed(data); |
| | | } |
| | | if (bits & AS_EVENT_AUDIO_PROCESSOR_RUNNING) { |
| | | audio_processor_->Feed(std::move(data)); |
| | | } |
| | | continue; |
| | | } |
| | | } |
| | | |
| | | // Read timeout/error should not terminate the input task. |
| | | vTaskDelay(pdMS_TO_TICKS(10)); |
| | | } |
| | | |
| | | ESP_LOGW(TAG, "Audio input task stopped"); |
| | | } |
| | | |
| | | void AudioService::AudioOutputTask() { |
| | | while (true) { |
| | | std::unique_lock<std::mutex> lock(audio_queue_mutex_); |
| | | audio_queue_cv_.wait(lock, [this]() { return !audio_playback_queue_.empty() || service_stopped_; }); |
| | | if (service_stopped_) { |
| | | break; |
| | | } |
| | | |
| | | auto task = std::move(audio_playback_queue_.front()); |
| | | audio_playback_queue_.pop_front(); |
| | | audio_queue_cv_.notify_all(); |
| | | lock.unlock(); |
| | | |
| | | if (!codec_->output_enabled()) { |
| | | esp_timer_stop(audio_power_timer_); |
| | | esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000); |
| | | codec_->EnableOutput(true); |
| | | } |
| | | |
| | | codec_->OutputData(task->pcm); |
| | | |
| | | /* Update the last output time */ |
| | | last_output_time_ = std::chrono::steady_clock::now(); |
| | | debug_statistics_.playback_count++; |
| | | |
| | | #if CONFIG_USE_SERVER_AEC |
| | | /* Record the timestamp for server AEC */ |
| | | if (task->timestamp > 0) { |
| | | lock.lock(); |
| | | timestamp_queue_.push_back(task->timestamp); |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | ESP_LOGW(TAG, "Audio output task stopped"); |
| | | } |
| | | |
| | | void AudioService::OpusCodecTask() { |
| | | while (true) { |
| | | std::unique_lock<std::mutex> lock(audio_queue_mutex_); |
| | | audio_queue_cv_.wait(lock, [this]() { |
| | | return service_stopped_ || |
| | | (!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) || |
| | | (!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE); |
| | | }); |
| | | if (service_stopped_) { |
| | | break; |
| | | } |
| | | |
| | | /* Decode the audio from decode queue */ |
| | | if (!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE) { |
| | | auto packet = std::move(audio_decode_queue_.front()); |
| | | audio_decode_queue_.pop_front(); |
| | | audio_queue_cv_.notify_all(); |
| | | lock.unlock(); |
| | | |
| | | auto task = std::make_unique<AudioTask>(); |
| | | task->type = kAudioTaskTypeDecodeToPlaybackQueue; |
| | | task->timestamp = packet->timestamp; |
| | | |
| | | SetDecodeSampleRate(packet->sample_rate, packet->frame_duration); |
| | | if (opus_decoder_ != nullptr) { |
| | | task->pcm.resize(decoder_frame_size_); |
| | | esp_audio_dec_in_raw_t raw = { |
| | | .buffer = (uint8_t *)(packet->payload.data()), |
| | | .len = (uint32_t)(packet->payload.size()), |
| | | .consumed = 0, |
| | | .frame_recover = ESP_AUDIO_DEC_RECOVERY_NONE, |
| | | }; |
| | | esp_audio_dec_out_frame_t out_frame = { |
| | | .buffer = (uint8_t *)(task->pcm.data()), |
| | | .len = (uint32_t)(task->pcm.size() * sizeof(int16_t)), |
| | | .decoded_size = 0, |
| | | }; |
| | | esp_audio_dec_info_t dec_info = {}; |
| | | std::unique_lock<std::mutex> decoder_lock(decoder_mutex_); |
| | | auto ret = esp_opus_dec_decode(opus_decoder_, &raw, &out_frame, &dec_info); |
| | | decoder_lock.unlock(); |
| | | if (ret == ESP_AUDIO_ERR_OK) { |
| | | task->pcm.resize(out_frame.decoded_size / sizeof(int16_t)); |
| | | if (decoder_sample_rate_ != codec_->output_sample_rate() && output_resampler_ != nullptr) { |
| | | uint32_t target_size = 0; |
| | | esp_ae_rate_cvt_get_max_out_sample_num(output_resampler_, task->pcm.size(), &target_size); |
| | | std::vector<int16_t> resampled(target_size); |
| | | uint32_t actual_output = target_size; |
| | | esp_ae_rate_cvt_process(output_resampler_, (esp_ae_sample_t)task->pcm.data(), task->pcm.size(), |
| | | (esp_ae_sample_t)resampled.data(), &actual_output); |
| | | resampled.resize(actual_output); |
| | | task->pcm = std::move(resampled); |
| | | } |
| | | lock.lock(); |
| | | audio_playback_queue_.push_back(std::move(task)); |
| | | audio_queue_cv_.notify_all(); |
| | | debug_statistics_.decode_count++; |
| | | } else { |
| | | ESP_LOGE(TAG, "Failed to decode audio after resize, error code: %d", ret); |
| | | lock.lock(); |
| | | } |
| | | } else { |
| | | ESP_LOGE(TAG, "Audio decoder is not configured"); |
| | | lock.lock(); |
| | | } |
| | | debug_statistics_.decode_count++; |
| | | } |
| | | /* Encode the audio to send queue */ |
| | | if (!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) { |
| | | auto task = std::move(audio_encode_queue_.front()); |
| | | audio_encode_queue_.pop_front(); |
| | | audio_queue_cv_.notify_all(); |
| | | lock.unlock(); |
| | | |
| | | auto packet = std::make_unique<AudioStreamPacket>(); |
| | | packet->frame_duration = OPUS_FRAME_DURATION_MS; |
| | | packet->sample_rate = 16000; |
| | | packet->timestamp = task->timestamp; |
| | | |
| | | if (opus_encoder_ != nullptr && task->pcm.size() == encoder_frame_size_) { |
| | | std::vector<uint8_t> buf(encoder_outbuf_size_); |
| | | esp_audio_enc_in_frame_t in = { |
| | | .buffer = (uint8_t *)(task->pcm.data()), |
| | | .len = (uint32_t)(encoder_frame_size_ * sizeof(int16_t)), |
| | | }; |
| | | esp_audio_enc_out_frame_t out = { |
| | | .buffer = buf.data(), |
| | | .len = (uint32_t)encoder_outbuf_size_, |
| | | .encoded_bytes = 0, |
| | | }; |
| | | auto ret = esp_opus_enc_process(opus_encoder_, &in, &out); |
| | | if (ret == ESP_AUDIO_ERR_OK) { |
| | | packet->payload.assign(buf.data(), buf.data() + out.encoded_bytes); |
| | | |
| | | if (task->type == kAudioTaskTypeEncodeToSendQueue) { |
| | | { |
| | | std::lock_guard<std::mutex> lock2(audio_queue_mutex_); |
| | | audio_send_queue_.push_back(std::move(packet)); |
| | | } |
| | | if (callbacks_.on_send_queue_available) { |
| | | callbacks_.on_send_queue_available(); |
| | | } |
| | | } else if (task->type == kAudioTaskTypeEncodeToTestingQueue) { |
| | | std::lock_guard<std::mutex> lock2(audio_queue_mutex_); |
| | | audio_testing_queue_.push_back(std::move(packet)); |
| | | } |
| | | debug_statistics_.encode_count++; |
| | | } else { |
| | | ESP_LOGE(TAG, "Failed to encode audio, error code: %d", ret); |
| | | } |
| | | } else { |
| | | ESP_LOGE(TAG, "Failed to encode audio: encoder not configured or invalid frame size (got %u, expected %u)", |
| | | task->pcm.size(), encoder_frame_size_); |
| | | } |
| | | lock.lock(); |
| | | } |
| | | } |
| | | |
| | | ESP_LOGW(TAG, "Opus codec task stopped"); |
| | | } |
| | | |
| | | void AudioService::SetDecodeSampleRate(int sample_rate, int frame_duration) { |
| | | if (decoder_sample_rate_ == sample_rate && decoder_duration_ms_ == frame_duration) { |
| | | return; |
| | | } |
| | | std::unique_lock<std::mutex> decoder_lock(decoder_mutex_); |
| | | if (opus_decoder_ != nullptr) { |
| | | esp_opus_dec_close(opus_decoder_); |
| | | opus_decoder_ = nullptr; |
| | | } |
| | | decoder_lock.unlock(); |
| | | esp_opus_dec_cfg_t opus_dec_cfg = OPUS_DEC_CFG(sample_rate, frame_duration); |
| | | auto ret = esp_opus_dec_open(&opus_dec_cfg, sizeof(esp_opus_dec_cfg_t), &opus_decoder_); |
| | | if (opus_decoder_ == nullptr) { |
| | | ESP_LOGE(TAG, "Failed to create audio decoder, error code: %d", ret); |
| | | return; |
| | | } |
| | | decoder_sample_rate_ = sample_rate; |
| | | decoder_duration_ms_ = frame_duration; |
| | | decoder_frame_size_ = decoder_sample_rate_ / 1000 * frame_duration; |
| | | |
| | | auto codec = Board::GetInstance().GetAudioCodec(); |
| | | if (decoder_sample_rate_ != codec->output_sample_rate()) { |
| | | ESP_LOGI(TAG, "Resampling audio from %d to %d", decoder_sample_rate_, codec->output_sample_rate()); |
| | | if (output_resampler_ != nullptr) { |
| | | esp_ae_rate_cvt_close(output_resampler_); |
| | | output_resampler_ = nullptr; |
| | | } |
| | | esp_ae_rate_cvt_cfg_t output_resampler_cfg = RATE_CVT_CFG( |
| | | decoder_sample_rate_, codec->output_sample_rate(), ESP_AUDIO_MONO); |
| | | auto resampler_ret = esp_ae_rate_cvt_open(&output_resampler_cfg, &output_resampler_); |
| | | if (output_resampler_ == nullptr) { |
| | | ESP_LOGE(TAG, "Failed to create output resampler, error code: %d", resampler_ret); |
| | | } |
| | | } |
| | | } |
| | | |
| | | void AudioService::PushTaskToEncodeQueue(AudioTaskType type, std::vector<int16_t>&& pcm) { |
| | | auto task = std::make_unique<AudioTask>(); |
| | | task->type = type; |
| | | task->pcm = std::move(pcm); |
| | | /* Push the task to the encode queue */ |
| | | std::unique_lock<std::mutex> lock(audio_queue_mutex_); |
| | | |
| | | /* If the task is to send queue, we need to set the timestamp */ |
| | | if (type == kAudioTaskTypeEncodeToSendQueue && !timestamp_queue_.empty()) { |
| | | if (timestamp_queue_.size() <= MAX_TIMESTAMPS_IN_QUEUE) { |
| | | task->timestamp = timestamp_queue_.front(); |
| | | } else { |
| | | ESP_LOGW(TAG, "Timestamp queue (%u) is full, dropping timestamp", timestamp_queue_.size()); |
| | | } |
| | | timestamp_queue_.pop_front(); |
| | | } |
| | | |
| | | audio_queue_cv_.wait(lock, [this]() { return audio_encode_queue_.size() < MAX_ENCODE_TASKS_IN_QUEUE; }); |
| | | audio_encode_queue_.push_back(std::move(task)); |
| | | audio_queue_cv_.notify_all(); |
| | | } |
| | | |
| | | bool AudioService::PushPacketToDecodeQueue(std::unique_ptr<AudioStreamPacket> packet, bool wait) { |
| | | std::unique_lock<std::mutex> lock(audio_queue_mutex_); |
| | | if (audio_decode_queue_.size() >= MAX_DECODE_PACKETS_IN_QUEUE) { |
| | | if (wait) { |
| | | audio_queue_cv_.wait(lock, [this]() { return audio_decode_queue_.size() < MAX_DECODE_PACKETS_IN_QUEUE; }); |
| | | } else { |
| | | return false; |
| | | } |
| | | } |
| | | audio_decode_queue_.push_back(std::move(packet)); |
| | | audio_queue_cv_.notify_all(); |
| | | return true; |
| | | } |
| | | |
| | | std::unique_ptr<AudioStreamPacket> AudioService::PopPacketFromSendQueue() { |
| | | std::lock_guard<std::mutex> lock(audio_queue_mutex_); |
| | | if (audio_send_queue_.empty()) { |
| | | return nullptr; |
| | | } |
| | | auto packet = std::move(audio_send_queue_.front()); |
| | | audio_send_queue_.pop_front(); |
| | | audio_queue_cv_.notify_all(); |
| | | return packet; |
| | | } |
| | | |
| | | void AudioService::EncodeWakeWord() { |
| | | if (wake_word_) { |
| | | wake_word_->EncodeWakeWordData(); |
| | | } |
| | | } |
| | | |
| | | const std::string& AudioService::GetLastWakeWord() const { |
| | | return wake_word_->GetLastDetectedWakeWord(); |
| | | } |
| | | |
| | | std::unique_ptr<AudioStreamPacket> AudioService::PopWakeWordPacket() { |
| | | auto packet = std::make_unique<AudioStreamPacket>(); |
| | | if (wake_word_->GetWakeWordOpus(packet->payload)) { |
| | | return packet; |
| | | } |
| | | return nullptr; |
| | | } |
| | | |
| | | void AudioService::EnableWakeWordDetection(bool enable) { |
| | | if (!wake_word_) { |
| | | return; |
| | | } |
| | | |
| | | ESP_LOGD(TAG, "%s wake word detection", enable ? "Enabling" : "Disabling"); |
| | | if (enable) { |
| | | if (!wake_word_initialized_) { |
| | | if (!wake_word_->Initialize(codec_, models_list_)) { |
| | | ESP_LOGE(TAG, "Failed to initialize wake word"); |
| | | return; |
| | | } |
| | | wake_word_initialized_ = true; |
| | | } |
| | | // Reset input resampler to clear cached data from previous mode (e.g. AudioProcessor) |
| | | // This prevents buffer overflow when switching between different feed sizes |
| | | { |
| | | std::lock_guard<std::mutex> lock(input_resampler_mutex_); |
| | | if (input_resampler_ != nullptr) { |
| | | esp_ae_rate_cvt_reset(input_resampler_); |
| | | } |
| | | } |
| | | wake_word_->Start(); |
| | | xEventGroupSetBits(event_group_, AS_EVENT_WAKE_WORD_RUNNING); |
| | | } else { |
| | | wake_word_->Stop(); |
| | | xEventGroupClearBits(event_group_, AS_EVENT_WAKE_WORD_RUNNING); |
| | | } |
| | | } |
| | | |
| | | void AudioService::EnableVoiceProcessing(bool enable) { |
| | | ESP_LOGD(TAG, "%s voice processing", enable ? "Enabling" : "Disabling"); |
| | | if (enable) { |
| | | if (!audio_processor_initialized_) { |
| | | audio_processor_->Initialize(codec_, OPUS_FRAME_DURATION_MS, models_list_); |
| | | audio_processor_initialized_ = true; |
| | | } |
| | | |
| | | /* We should make sure no audio is playing */ |
| | | ResetDecoder(); |
| | | audio_input_need_warmup_ = true; |
| | | // Reset input resampler to clear cached data from previous mode (e.g. WakeWord) |
| | | // This prevents buffer overflow when switching between different feed sizes |
| | | { |
| | | std::lock_guard<std::mutex> lock(input_resampler_mutex_); |
| | | if (input_resampler_ != nullptr) { |
| | | esp_ae_rate_cvt_reset(input_resampler_); |
| | | } |
| | | } |
| | | audio_processor_->Start(); |
| | | xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_PROCESSOR_RUNNING); |
| | | } else { |
| | | audio_processor_->Stop(); |
| | | xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_PROCESSOR_RUNNING); |
| | | } |
| | | } |
| | | |
| | | void AudioService::EnableAudioTesting(bool enable) { |
| | | ESP_LOGI(TAG, "%s audio testing", enable ? "Enabling" : "Disabling"); |
| | | if (enable) { |
| | | xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING); |
| | | } else { |
| | | xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING); |
| | | /* Copy audio_testing_queue_ to audio_decode_queue_ */ |
| | | std::lock_guard<std::mutex> lock(audio_queue_mutex_); |
| | | audio_decode_queue_ = std::move(audio_testing_queue_); |
| | | audio_queue_cv_.notify_all(); |
| | | } |
| | | } |
| | | |
| | | void AudioService::EnableDeviceAec(bool enable) { |
| | | ESP_LOGI(TAG, "%s device AEC", enable ? "Enabling" : "Disabling"); |
| | | if (!audio_processor_initialized_) { |
| | | audio_processor_->Initialize(codec_, OPUS_FRAME_DURATION_MS, models_list_); |
| | | audio_processor_initialized_ = true; |
| | | } |
| | | |
| | | audio_processor_->EnableDeviceAec(enable); |
| | | } |
| | | |
| | | void AudioService::SetCallbacks(AudioServiceCallbacks& callbacks) { |
| | | callbacks_ = callbacks; |
| | | } |
| | | |
| | | void AudioService::PlaySound(const std::string_view& ogg) { |
| | | if (!codec_->output_enabled()) { |
| | | esp_timer_stop(audio_power_timer_); |
| | | esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000); |
| | | codec_->EnableOutput(true); |
| | | } |
| | | |
| | | const auto* buf = reinterpret_cast<const uint8_t*>(ogg.data()); |
| | | size_t size = ogg.size(); |
| | | |
| | | auto demuxer = std::make_unique<OggDemuxer>(); |
| | | demuxer->OnDemuxerFinished([this](const uint8_t* data, int sample_rate, size_t size){ |
| | | auto packet = std::make_unique<AudioStreamPacket>(); |
| | | packet->sample_rate = sample_rate; |
| | | packet->frame_duration = 60; |
| | | packet->payload.resize(size); |
| | | std::memcpy(packet->payload.data(), data, size); |
| | | PushPacketToDecodeQueue(std::move(packet), true); |
| | | }); |
| | | demuxer->Reset(); |
| | | demuxer->Process(buf, size); |
| | | } |
| | | |
| | | bool AudioService::IsIdle() { |
| | | std::lock_guard<std::mutex> lock(audio_queue_mutex_); |
| | | return audio_encode_queue_.empty() && audio_decode_queue_.empty() && audio_playback_queue_.empty() && audio_testing_queue_.empty(); |
| | | } |
| | | |
| | | void AudioService::WaitForPlaybackQueueEmpty() { |
| | | std::unique_lock<std::mutex> lock(audio_queue_mutex_); |
| | | audio_queue_cv_.wait(lock, [this]() { |
| | | return service_stopped_ || (audio_decode_queue_.empty() && audio_playback_queue_.empty()); |
| | | }); |
| | | } |
| | | |
| | | void AudioService::ResetDecoder() { |
| | | std::lock_guard<std::mutex> lock(audio_queue_mutex_); |
| | | std::unique_lock<std::mutex> decoder_lock(decoder_mutex_); |
| | | if (opus_decoder_ != nullptr) { |
| | | esp_opus_dec_reset(opus_decoder_); |
| | | } |
| | | decoder_lock.unlock(); |
| | | timestamp_queue_.clear(); |
| | | audio_decode_queue_.clear(); |
| | | audio_playback_queue_.clear(); |
| | | audio_testing_queue_.clear(); |
| | | audio_queue_cv_.notify_all(); |
| | | } |
| | | |
| | | void AudioService::CheckAndUpdateAudioPowerState() { |
| | | auto now = std::chrono::steady_clock::now(); |
| | | auto input_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_input_time_).count(); |
| | | auto output_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_output_time_).count(); |
| | | if (input_elapsed > AUDIO_POWER_TIMEOUT_MS && codec_->input_enabled()) { |
| | | codec_->EnableInput(false); |
| | | } |
| | | if (output_elapsed > AUDIO_POWER_TIMEOUT_MS && codec_->output_enabled()) { |
| | | // Keep TX clock when duplex RX is active; otherwise RX may stall on some boards. |
| | | if (!(codec_->duplex() && codec_->input_enabled())) { |
| | | codec_->EnableOutput(false); |
| | | } |
| | | } |
| | | if (!codec_->input_enabled() && !codec_->output_enabled()) { |
| | | esp_timer_stop(audio_power_timer_); |
| | | } |
| | | } |
| | | |
| | | void AudioService::SetModelsList(srmodel_list_t* models_list) { |
| | | models_list_ = models_list; |
| | | |
| | | #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 |
| | | if (esp_srmodel_filter(models_list_, ESP_MN_PREFIX, NULL) != nullptr) { |
| | | wake_word_ = std::make_unique<CustomWakeWord>(); |
| | | } else if (esp_srmodel_filter(models_list_, ESP_WN_PREFIX, NULL) != nullptr) { |
| | | wake_word_ = std::make_unique<AfeWakeWord>(); |
| | | } else { |
| | | wake_word_ = nullptr; |
| | | } |
| | | #else |
| | | if (esp_srmodel_filter(models_list_, ESP_WN_PREFIX, NULL) != nullptr) { |
| | | wake_word_ = std::make_unique<EspWakeWord>(); |
| | | } else { |
| | | wake_word_ = nullptr; |
| | | } |
| | | #endif |
| | | |
| | | if (wake_word_) { |
| | | wake_word_->OnWakeWordDetected([this](const std::string& wake_word) { |
| | | if (callbacks_.on_wake_word_detected) { |
| | | callbacks_.on_wake_word_detected(wake_word); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | bool AudioService::IsAfeWakeWord() { |
| | | #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4 |
| | | return wake_word_ != nullptr && dynamic_cast<AfeWakeWord*>(wake_word_.get()) != nullptr; |
| | | #else |
| | | return false; |
| | | #endif |
| | | } |
| New file |
| | |
| | | #ifndef AUDIO_SERVICE_H |
| | | #define AUDIO_SERVICE_H |
| | | |
| | | #include <memory> |
| | | #include <deque> |
| | | #include <condition_variable> |
| | | #include <chrono> |
| | | #include <mutex> |
| | | |
| | | #include <freertos/FreeRTOS.h> |
| | | #include <freertos/task.h> |
| | | #include <freertos/event_groups.h> |
| | | #include <esp_timer.h> |
| | | #include <model_path.h> |
| | | #include "esp_audio_enc.h" |
| | | #include "esp_opus_enc.h" |
| | | #include "esp_opus_dec.h" |
| | | #include "esp_ae_rate_cvt.h" |
| | | #include "esp_audio_types.h" |
| | | |
| | | #include "audio_codec.h" |
| | | #include "audio_processor.h" |
| | | #include "processors/audio_debugger.h" |
| | | #include "wake_word.h" |
| | | #include "protocol.h" |
| | | #include "ogg_demuxer.h" |
| | | |
| | | /* |
| | | * There are two types of audio data flow: |
| | | * 1. (MIC) -> [Processors] -> {Encode Queue} -> [Opus Encoder] -> {Send Queue} -> (Server) |
| | | * 2. (Server) -> {Decode Queue} -> [Opus Decoder] -> {Playback Queue} -> (Speaker) |
| | | * |
| | | * We use one task for MIC / Speaker / Processors, and one task for Opus Encoder / Opus Decoder. |
| | | * |
| | | * Decode Queue and Send Queue are the main queues, because Opus packets are quite smaller than PCM packets. |
| | | * |
| | | */ |
| | | |
| | | #define OPUS_FRAME_DURATION_MS 60 |
| | | #define MAX_ENCODE_TASKS_IN_QUEUE 2 |
| | | #define MAX_PLAYBACK_TASKS_IN_QUEUE 2 |
| | | #define MAX_DECODE_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS) |
| | | #define MAX_SEND_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS) |
| | | #define AUDIO_TESTING_MAX_DURATION_MS 10000 |
| | | #define MAX_TIMESTAMPS_IN_QUEUE 3 |
| | | |
| | | #define AUDIO_POWER_TIMEOUT_MS 15000 |
| | | #define AUDIO_POWER_CHECK_INTERVAL_MS 1000 |
| | | |
| | | #define AS_EVENT_AUDIO_TESTING_RUNNING (1 << 0) |
| | | #define AS_EVENT_WAKE_WORD_RUNNING (1 << 1) |
| | | #define AS_EVENT_AUDIO_PROCESSOR_RUNNING (1 << 2) |
| | | #define AS_EVENT_PLAYBACK_NOT_EMPTY (1 << 3) |
| | | |
| | | #define AS_OPUS_GET_FRAME_DRU_ENUM(duration_ms) \ |
| | | ((duration_ms) == 5 ? ESP_OPUS_ENC_FRAME_DURATION_5_MS : \ |
| | | (duration_ms) == 10 ? ESP_OPUS_ENC_FRAME_DURATION_10_MS : \ |
| | | (duration_ms) == 20 ? ESP_OPUS_ENC_FRAME_DURATION_20_MS : \ |
| | | (duration_ms) == 40 ? ESP_OPUS_ENC_FRAME_DURATION_40_MS : \ |
| | | (duration_ms) == 60 ? ESP_OPUS_ENC_FRAME_DURATION_60_MS : \ |
| | | (duration_ms) == 80 ? ESP_OPUS_ENC_FRAME_DURATION_80_MS : \ |
| | | (duration_ms) == 100 ? ESP_OPUS_ENC_FRAME_DURATION_100_MS : \ |
| | | (duration_ms) == 120 ? ESP_OPUS_ENC_FRAME_DURATION_120_MS : -1) |
| | | |
| | | #define AS_OPUS_ENC_CONFIG() { \ |
| | | .sample_rate = ESP_AUDIO_SAMPLE_RATE_16K, \ |
| | | .channel = ESP_AUDIO_MONO, \ |
| | | .bits_per_sample = ESP_AUDIO_BIT16, \ |
| | | .bitrate = ESP_OPUS_BITRATE_AUTO, \ |
| | | .frame_duration = (esp_opus_enc_frame_duration_t)AS_OPUS_GET_FRAME_DRU_ENUM(OPUS_FRAME_DURATION_MS), \ |
| | | .application_mode = ESP_OPUS_ENC_APPLICATION_AUDIO, \ |
| | | .complexity = 0, \ |
| | | .enable_fec = false, \ |
| | | .enable_dtx = true, \ |
| | | .enable_vbr = true, \ |
| | | } |
| | | |
| | | struct AudioServiceCallbacks { |
| | | std::function<void(void)> on_send_queue_available; |
| | | std::function<void(const std::string&)> on_wake_word_detected; |
| | | std::function<void(bool)> on_vad_change; |
| | | std::function<void(void)> on_audio_testing_queue_full; |
| | | }; |
| | | |
| | | |
| | | enum AudioTaskType { |
| | | kAudioTaskTypeEncodeToSendQueue, |
| | | kAudioTaskTypeEncodeToTestingQueue, |
| | | kAudioTaskTypeDecodeToPlaybackQueue, |
| | | }; |
| | | |
| | | struct AudioTask { |
| | | AudioTaskType type; |
| | | std::vector<int16_t> pcm; |
| | | uint32_t timestamp; |
| | | }; |
| | | |
| | | struct DebugStatistics { |
| | | uint32_t input_count = 0; |
| | | uint32_t decode_count = 0; |
| | | uint32_t encode_count = 0; |
| | | uint32_t playback_count = 0; |
| | | }; |
| | | |
| | | class AudioService { |
| | | public: |
| | | AudioService(); |
| | | ~AudioService(); |
| | | |
| | | void Initialize(AudioCodec* codec); |
| | | void Start(); |
| | | void Stop(); |
| | | void EncodeWakeWord(); |
| | | std::unique_ptr<AudioStreamPacket> PopWakeWordPacket(); |
| | | const std::string& GetLastWakeWord() const; |
| | | bool IsVoiceDetected() const { return voice_detected_; } |
| | | bool IsIdle(); |
| | | void WaitForPlaybackQueueEmpty(); |
| | | bool IsWakeWordRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_WAKE_WORD_RUNNING; } |
| | | bool IsAudioProcessorRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_AUDIO_PROCESSOR_RUNNING; } |
| | | bool IsAfeWakeWord(); |
| | | |
| | | void EnableWakeWordDetection(bool enable); |
| | | void EnableVoiceProcessing(bool enable); |
| | | void EnableAudioTesting(bool enable); |
| | | void EnableDeviceAec(bool enable); |
| | | |
| | | void SetCallbacks(AudioServiceCallbacks& callbacks); |
| | | |
| | | bool PushPacketToDecodeQueue(std::unique_ptr<AudioStreamPacket> packet, bool wait = false); |
| | | std::unique_ptr<AudioStreamPacket> PopPacketFromSendQueue(); |
| | | void PlaySound(const std::string_view& sound); |
| | | bool ReadAudioData(std::vector<int16_t>& data, int sample_rate, int samples); |
| | | void ResetDecoder(); |
| | | void SetModelsList(srmodel_list_t* models_list); |
| | | |
| | | private: |
| | | AudioCodec* codec_ = nullptr; |
| | | AudioServiceCallbacks callbacks_; |
| | | std::unique_ptr<AudioProcessor> audio_processor_; |
| | | std::unique_ptr<WakeWord> wake_word_; |
| | | std::unique_ptr<AudioDebugger> audio_debugger_; |
| | | void* opus_encoder_ = nullptr; |
| | | void* opus_decoder_ = nullptr; |
| | | std::mutex decoder_mutex_; |
| | | std::mutex input_resampler_mutex_; |
| | | esp_ae_rate_cvt_handle_t input_resampler_ = nullptr; |
| | | esp_ae_rate_cvt_handle_t output_resampler_ = nullptr; |
| | | |
| | | // Encoder/Decoder state |
| | | int encoder_sample_rate_ = 16000; |
| | | int encoder_duration_ms_ = OPUS_FRAME_DURATION_MS; |
| | | int encoder_frame_size_ = 0; |
| | | int encoder_outbuf_size_ = 0; |
| | | int decoder_sample_rate_ = 0; |
| | | int decoder_duration_ms_ = OPUS_FRAME_DURATION_MS; |
| | | int decoder_frame_size_ = 0; |
| | | DebugStatistics debug_statistics_; |
| | | srmodel_list_t* models_list_ = nullptr; |
| | | |
| | | EventGroupHandle_t event_group_; |
| | | |
| | | // Audio encode / decode |
| | | TaskHandle_t audio_input_task_handle_ = nullptr; |
| | | TaskHandle_t audio_output_task_handle_ = nullptr; |
| | | TaskHandle_t opus_codec_task_handle_ = nullptr; |
| | | std::mutex audio_queue_mutex_; |
| | | std::condition_variable audio_queue_cv_; |
| | | std::deque<std::unique_ptr<AudioStreamPacket>> audio_decode_queue_; |
| | | std::deque<std::unique_ptr<AudioStreamPacket>> audio_send_queue_; |
| | | std::deque<std::unique_ptr<AudioStreamPacket>> audio_testing_queue_; |
| | | std::deque<std::unique_ptr<AudioTask>> audio_encode_queue_; |
| | | std::deque<std::unique_ptr<AudioTask>> audio_playback_queue_; |
| | | // For server AEC |
| | | std::deque<uint32_t> timestamp_queue_; |
| | | |
| | | bool wake_word_initialized_ = false; |
| | | bool audio_processor_initialized_ = false; |
| | | bool voice_detected_ = false; |
| | | bool service_stopped_ = true; |
| | | bool audio_input_need_warmup_ = false; |
| | | |
| | | esp_timer_handle_t audio_power_timer_ = nullptr; |
| | | std::chrono::steady_clock::time_point last_input_time_; |
| | | std::chrono::steady_clock::time_point last_output_time_; |
| | | |
| | | void AudioInputTask(); |
| | | void AudioOutputTask(); |
| | | void OpusCodecTask(); |
| | | void PushTaskToEncodeQueue(AudioTaskType type, std::vector<int16_t>&& pcm); |
| | | void SetDecodeSampleRate(int sample_rate, int frame_duration); |
| | | void CheckAndUpdateAudioPowerState(); |
| | | }; |
| | | |
| | | #endif |
| New file |
| | |
| | | #include "box_audio_codec.h" |
| | | |
| | | #include <esp_log.h> |
| | | #include <driver/i2c_master.h> |
| | | #include <driver/i2s_tdm.h> |
| | | |
| | | #define TAG "BoxAudioCodec" |
| | | |
| | | BoxAudioCodec::BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference) { |
| | | duplex_ = true; // æ¯å¦åå·¥ |
| | | input_reference_ = input_reference; // æ¯å¦ä½¿ç¨åèè¾å
¥ï¼å®ç°å声æ¶é¤ |
| | | input_channels_ = input_reference_ ? 2 : 1; // è¾å
¥ééæ° |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | input_gain_ = 30; |
| | | |
| | | CreateDuplexChannels(mclk, bclk, ws, dout, din); |
| | | |
| | | // Do initialize of related interface: data_if, ctrl_if and gpio_if |
| | | audio_codec_i2s_cfg_t i2s_cfg = { |
| | | .port = I2S_NUM_0, |
| | | .rx_handle = rx_handle_, |
| | | .tx_handle = tx_handle_, |
| | | }; |
| | | data_if_ = audio_codec_new_i2s_data(&i2s_cfg); |
| | | assert(data_if_ != NULL); |
| | | |
| | | // Output |
| | | audio_codec_i2c_cfg_t i2c_cfg = { |
| | | .port = (i2c_port_t)1, |
| | | .addr = es8311_addr, |
| | | .bus_handle = i2c_master_handle, |
| | | }; |
| | | out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); |
| | | assert(out_ctrl_if_ != NULL); |
| | | |
| | | gpio_if_ = audio_codec_new_gpio(); |
| | | assert(gpio_if_ != NULL); |
| | | |
| | | es8311_codec_cfg_t es8311_cfg = {}; |
| | | es8311_cfg.ctrl_if = out_ctrl_if_; |
| | | es8311_cfg.gpio_if = gpio_if_; |
| | | es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC; |
| | | es8311_cfg.pa_pin = pa_pin; |
| | | es8311_cfg.use_mclk = true; |
| | | es8311_cfg.hw_gain.pa_voltage = 5.0; |
| | | es8311_cfg.hw_gain.codec_dac_voltage = 3.3; |
| | | out_codec_if_ = es8311_codec_new(&es8311_cfg); |
| | | assert(out_codec_if_ != NULL); |
| | | |
| | | esp_codec_dev_cfg_t dev_cfg = { |
| | | .dev_type = ESP_CODEC_DEV_TYPE_OUT, |
| | | .codec_if = out_codec_if_, |
| | | .data_if = data_if_, |
| | | }; |
| | | output_dev_ = esp_codec_dev_new(&dev_cfg); |
| | | assert(output_dev_ != NULL); |
| | | |
| | | // Input |
| | | i2c_cfg.addr = es7210_addr; |
| | | in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); |
| | | assert(in_ctrl_if_ != NULL); |
| | | |
| | | es7210_codec_cfg_t es7210_cfg = {}; |
| | | es7210_cfg.ctrl_if = in_ctrl_if_; |
| | | es7210_cfg.mic_selected = ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4; |
| | | in_codec_if_ = es7210_codec_new(&es7210_cfg); |
| | | assert(in_codec_if_ != NULL); |
| | | |
| | | dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; |
| | | dev_cfg.codec_if = in_codec_if_; |
| | | input_dev_ = esp_codec_dev_new(&dev_cfg); |
| | | assert(input_dev_ != NULL); |
| | | |
| | | ESP_LOGI(TAG, "BoxAudioDevice initialized"); |
| | | } |
| | | |
| | | BoxAudioCodec::~BoxAudioCodec() { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); |
| | | esp_codec_dev_delete(output_dev_); |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); |
| | | esp_codec_dev_delete(input_dev_); |
| | | |
| | | audio_codec_delete_codec_if(in_codec_if_); |
| | | audio_codec_delete_ctrl_if(in_ctrl_if_); |
| | | audio_codec_delete_codec_if(out_codec_if_); |
| | | audio_codec_delete_ctrl_if(out_ctrl_if_); |
| | | audio_codec_delete_gpio_if(gpio_if_); |
| | | audio_codec_delete_data_if(data_if_); |
| | | } |
| | | |
| | | void BoxAudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { |
| | | assert(input_sample_rate_ == output_sample_rate_); |
| | | |
| | | i2s_chan_config_t chan_cfg = { |
| | | .id = I2S_NUM_0, |
| | | .role = I2S_ROLE_MASTER, |
| | | .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, |
| | | .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, |
| | | .auto_clear_after_cb = true, |
| | | .auto_clear_before_cb = false, |
| | | .intr_priority = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); |
| | | |
| | | i2s_std_config_t std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .ext_clk_freq_hz = 0, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256 |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_STEREO, |
| | | .slot_mask = I2S_STD_SLOT_BOTH, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = mclk, |
| | | .bclk = bclk, |
| | | .ws = ws, |
| | | .dout = dout, |
| | | .din = I2S_GPIO_UNUSED, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | |
| | | i2s_tdm_config_t tdm_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)input_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .ext_clk_freq_hz = 0, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256, |
| | | .bclk_div = 8, |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_STEREO, |
| | | .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3), |
| | | .ws_width = I2S_TDM_AUTO_WS_WIDTH, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | .left_align = false, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false, |
| | | .skip_mask = false, |
| | | .total_slot = I2S_TDM_AUTO_SLOT_NUM |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = mclk, |
| | | .bclk = bclk, |
| | | .ws = ws, |
| | | .dout = I2S_GPIO_UNUSED, |
| | | .din = din, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); |
| | | ESP_LOGI(TAG, "Duplex channels created"); |
| | | } |
| | | |
| | | void BoxAudioCodec::SetOutputVolume(int volume) { |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); |
| | | AudioCodec::SetOutputVolume(volume); |
| | | } |
| | | |
| | | void BoxAudioCodec::EnableInput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == input_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = 4, |
| | | .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), |
| | | .sample_rate = (uint32_t)output_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | if (input_reference_) { |
| | | fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); |
| | | } |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), input_gain_)); |
| | | } else { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); |
| | | } |
| | | AudioCodec::EnableInput(enable); |
| | | } |
| | | |
| | | void BoxAudioCodec::EnableOutput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == output_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | // Play 16bit 1 channel |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = 1, |
| | | .channel_mask = 0, |
| | | .sample_rate = (uint32_t)output_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); |
| | | } else { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); |
| | | } |
| | | AudioCodec::EnableOutput(enable); |
| | | } |
| | | |
| | | int BoxAudioCodec::Read(int16_t* dest, int samples) { |
| | | if (input_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| | | |
| | | int BoxAudioCodec::Write(const int16_t* data, int samples) { |
| | | if (output_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| New file |
| | |
| | | #ifndef _BOX_AUDIO_CODEC_H |
| | | #define _BOX_AUDIO_CODEC_H |
| | | |
| | | #include "audio_codec.h" |
| | | |
| | | #include <esp_codec_dev.h> |
| | | #include <esp_codec_dev_defaults.h> |
| | | #include <mutex> |
| | | |
| | | |
| | | class BoxAudioCodec : public AudioCodec { |
| | | private: |
| | | const audio_codec_data_if_t* data_if_ = nullptr; |
| | | const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; |
| | | const audio_codec_if_t* out_codec_if_ = nullptr; |
| | | const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; |
| | | const audio_codec_if_t* in_codec_if_ = nullptr; |
| | | const audio_codec_gpio_if_t* gpio_if_ = nullptr; |
| | | |
| | | esp_codec_dev_handle_t output_dev_ = nullptr; |
| | | esp_codec_dev_handle_t input_dev_ = nullptr; |
| | | std::mutex data_if_mutex_; |
| | | |
| | | void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); |
| | | |
| | | virtual int Read(int16_t* dest, int samples) override; |
| | | virtual int Write(const int16_t* data, int samples) override; |
| | | |
| | | public: |
| | | BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference); |
| | | virtual ~BoxAudioCodec(); |
| | | |
| | | virtual void SetOutputVolume(int volume) override; |
| | | virtual void EnableInput(bool enable) override; |
| | | virtual void EnableOutput(bool enable) override; |
| | | }; |
| | | |
| | | #endif // _BOX_AUDIO_CODEC_H |
| New file |
| | |
| | | #include "dummy_audio_codec.h" |
| | | |
| | | DummyAudioCodec::DummyAudioCodec(int input_sample_rate, int output_sample_rate) { |
| | | duplex_ = true; |
| | | input_reference_ = false; |
| | | input_channels_ = 1; |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | } |
| | | |
| | | DummyAudioCodec::~DummyAudioCodec() { |
| | | } |
| | | |
| | | int DummyAudioCodec::Read(int16_t* dest, int samples) { |
| | | return 0; |
| | | } |
| | | |
| | | int DummyAudioCodec::Write(const int16_t* data, int samples) { |
| | | return 0; |
| | | } |
| New file |
| | |
| | | #ifndef _DUMMY_AUDIO_CODEC_H |
| | | #define _DUMMY_AUDIO_CODEC_H |
| | | |
| | | #include "audio_codec.h" |
| | | |
| | | class DummyAudioCodec : public AudioCodec { |
| | | private: |
| | | virtual int Read(int16_t* dest, int samples) override; |
| | | virtual int Write(const int16_t* data, int samples) override; |
| | | |
| | | public: |
| | | DummyAudioCodec(int input_sample_rate, int output_sample_rate); |
| | | virtual ~DummyAudioCodec(); |
| | | }; |
| | | |
| | | #endif // _DUMMY_AUDIO_CODEC_H |
| New file |
| | |
| | | #include "es8311_audio_codec.h" |
| | | |
| | | #include <esp_log.h> |
| | | |
| | | #define TAG "Es8311AudioCodec" |
| | | |
| | | Es8311AudioCodec::Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk, bool pa_inverted) { |
| | | duplex_ = true; // æ¯å¦åå·¥ |
| | | input_reference_ = false; // æ¯å¦ä½¿ç¨åèè¾å
¥ï¼å®ç°å声æ¶é¤ |
| | | input_channels_ = 1; // è¾å
¥ééæ° |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | pa_pin_ = pa_pin; |
| | | pa_inverted_ = pa_inverted; |
| | | input_gain_ = 30; |
| | | |
| | | assert(input_sample_rate_ == output_sample_rate_); |
| | | CreateDuplexChannels(mclk, bclk, ws, dout, din); |
| | | |
| | | // Do initialize of related interface: data_if, ctrl_if and gpio_if |
| | | audio_codec_i2s_cfg_t i2s_cfg = { |
| | | .port = I2S_NUM_0, |
| | | .rx_handle = rx_handle_, |
| | | .tx_handle = tx_handle_, |
| | | }; |
| | | data_if_ = audio_codec_new_i2s_data(&i2s_cfg); |
| | | assert(data_if_ != NULL); |
| | | |
| | | // Output |
| | | audio_codec_i2c_cfg_t i2c_cfg = { |
| | | .port = i2c_port, |
| | | .addr = es8311_addr, |
| | | .bus_handle = i2c_master_handle, |
| | | }; |
| | | ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); |
| | | assert(ctrl_if_ != NULL); |
| | | |
| | | gpio_if_ = audio_codec_new_gpio(); |
| | | assert(gpio_if_ != NULL); |
| | | |
| | | es8311_codec_cfg_t es8311_cfg = {}; |
| | | es8311_cfg.ctrl_if = ctrl_if_; |
| | | es8311_cfg.gpio_if = gpio_if_; |
| | | es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; |
| | | es8311_cfg.pa_pin = pa_pin; |
| | | es8311_cfg.use_mclk = use_mclk; |
| | | es8311_cfg.hw_gain.pa_voltage = 5.0; |
| | | es8311_cfg.hw_gain.codec_dac_voltage = 3.3; |
| | | es8311_cfg.pa_reverted = pa_inverted_; |
| | | codec_if_ = es8311_codec_new(&es8311_cfg); |
| | | |
| | | if (codec_if_ == nullptr) { |
| | | ESP_LOGE(TAG, "Failed to create Es8311AudioCodec"); |
| | | } else { |
| | | ESP_LOGI(TAG, "Es8311AudioCodec initialized"); |
| | | } |
| | | } |
| | | |
| | | Es8311AudioCodec::~Es8311AudioCodec() { |
| | | esp_codec_dev_delete(dev_); |
| | | |
| | | audio_codec_delete_codec_if(codec_if_); |
| | | audio_codec_delete_ctrl_if(ctrl_if_); |
| | | audio_codec_delete_gpio_if(gpio_if_); |
| | | audio_codec_delete_data_if(data_if_); |
| | | } |
| | | |
| | | void Es8311AudioCodec::UpdateDeviceState() { |
| | | if ((input_enabled_ || output_enabled_) && dev_ == nullptr) { |
| | | esp_codec_dev_cfg_t dev_cfg = { |
| | | .dev_type = ESP_CODEC_DEV_TYPE_IN_OUT, |
| | | .codec_if = codec_if_, |
| | | .data_if = data_if_, |
| | | }; |
| | | dev_ = esp_codec_dev_new(&dev_cfg); |
| | | assert(dev_ != NULL); |
| | | |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = 1, |
| | | .channel_mask = 0, |
| | | .sample_rate = (uint32_t)input_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(dev_, &fs)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(dev_, input_gain_)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(dev_, output_volume_)); |
| | | } else if (!input_enabled_ && !output_enabled_ && dev_ != nullptr) { |
| | | esp_codec_dev_close(dev_); |
| | | dev_ = nullptr; |
| | | } |
| | | if (pa_pin_ != GPIO_NUM_NC) { |
| | | int level = output_enabled_ ? 1 : 0; |
| | | gpio_set_level(pa_pin_, pa_inverted_ ? !level : level); |
| | | } |
| | | } |
| | | |
| | | void Es8311AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { |
| | | assert(input_sample_rate_ == output_sample_rate_); |
| | | |
| | | i2s_chan_config_t chan_cfg = { |
| | | .id = I2S_NUM_0, |
| | | .role = I2S_ROLE_MASTER, |
| | | .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, |
| | | .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, |
| | | .auto_clear_after_cb = true, |
| | | .auto_clear_before_cb = false, |
| | | .intr_priority = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); |
| | | |
| | | i2s_std_config_t std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .ext_clk_freq_hz = 0, |
| | | #endif |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_STEREO, |
| | | .slot_mask = I2S_STD_SLOT_BOTH, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | #endif |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = mclk, |
| | | .bclk = bclk, |
| | | .ws = ws, |
| | | .dout = dout, |
| | | .din = din, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); |
| | | ESP_LOGI(TAG, "Duplex channels created"); |
| | | } |
| | | |
| | | void Es8311AudioCodec::SetOutputVolume(int volume) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (dev_ != nullptr) { |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(dev_, volume)); |
| | | } |
| | | AudioCodec::SetOutputVolume(volume); |
| | | } |
| | | |
| | | void Es8311AudioCodec::EnableInput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (codec_if_ == nullptr) { |
| | | return; |
| | | } |
| | | if (enable == input_enabled_) { |
| | | return; |
| | | } |
| | | AudioCodec::EnableInput(enable); |
| | | UpdateDeviceState(); |
| | | } |
| | | |
| | | void Es8311AudioCodec::EnableOutput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (codec_if_ == nullptr) { |
| | | return; |
| | | } |
| | | if (enable == output_enabled_) { |
| | | return; |
| | | } |
| | | AudioCodec::EnableOutput(enable); |
| | | UpdateDeviceState(); |
| | | } |
| | | |
| | | int Es8311AudioCodec::Read(int16_t* dest, int samples) { |
| | | if (input_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(dev_, (void*)dest, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| | | |
| | | int Es8311AudioCodec::Write(const int16_t* data, int samples) { |
| | | if (output_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(dev_, (void*)data, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| New file |
| | |
| | | #ifndef _ES8311_AUDIO_CODEC_H |
| | | #define _ES8311_AUDIO_CODEC_H |
| | | |
| | | #include "audio_codec.h" |
| | | |
| | | #include <driver/i2c_master.h> |
| | | #include <driver/gpio.h> |
| | | #include <esp_codec_dev.h> |
| | | #include <esp_codec_dev_defaults.h> |
| | | #include <mutex> |
| | | |
| | | |
| | | class Es8311AudioCodec : public AudioCodec { |
| | | private: |
| | | const audio_codec_data_if_t* data_if_ = nullptr; |
| | | const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; |
| | | const audio_codec_if_t* codec_if_ = nullptr; |
| | | const audio_codec_gpio_if_t* gpio_if_ = nullptr; |
| | | |
| | | esp_codec_dev_handle_t dev_ = nullptr; |
| | | gpio_num_t pa_pin_ = GPIO_NUM_NC; |
| | | bool pa_inverted_ = false; |
| | | std::mutex data_if_mutex_; |
| | | |
| | | void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); |
| | | void UpdateDeviceState(); |
| | | |
| | | virtual int Read(int16_t* dest, int samples) override; |
| | | virtual int Write(const int16_t* data, int samples) override; |
| | | |
| | | public: |
| | | Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk = true, bool pa_inverted = false); |
| | | virtual ~Es8311AudioCodec(); |
| | | |
| | | virtual void SetOutputVolume(int volume) override; |
| | | virtual void EnableInput(bool enable) override; |
| | | virtual void EnableOutput(bool enable) override; |
| | | }; |
| | | |
| | | #endif // _ES8311_AUDIO_CODEC_H |
| New file |
| | |
| | | #include "es8374_audio_codec.h" |
| | | |
| | | #include <esp_log.h> |
| | | |
| | | #define TAG "Es8374AudioCodec" |
| | | |
| | | Es8374AudioCodec::Es8374AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8374_addr, bool use_mclk) { |
| | | duplex_ = true; // æ¯å¦åå·¥ |
| | | input_reference_ = false; // æ¯å¦ä½¿ç¨åèè¾å
¥ï¼å®ç°å声æ¶é¤ |
| | | input_channels_ = 1; // è¾å
¥ééæ° |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | input_gain_ = 30; |
| | | |
| | | pa_pin_ = pa_pin; |
| | | CreateDuplexChannels(mclk, bclk, ws, dout, din); |
| | | |
| | | // Do initialize of related interface: data_if, ctrl_if and gpio_if |
| | | audio_codec_i2s_cfg_t i2s_cfg = { |
| | | .port = I2S_NUM_0, |
| | | .rx_handle = rx_handle_, |
| | | .tx_handle = tx_handle_, |
| | | }; |
| | | data_if_ = audio_codec_new_i2s_data(&i2s_cfg); |
| | | assert(data_if_ != NULL); |
| | | |
| | | // Output |
| | | audio_codec_i2c_cfg_t i2c_cfg = { |
| | | .port = i2c_port, |
| | | .addr = es8374_addr, |
| | | .bus_handle = i2c_master_handle, |
| | | }; |
| | | ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); |
| | | assert(ctrl_if_ != NULL); |
| | | |
| | | gpio_if_ = audio_codec_new_gpio(); |
| | | assert(gpio_if_ != NULL); |
| | | |
| | | es8374_codec_cfg_t es8374_cfg = {}; |
| | | es8374_cfg.ctrl_if = ctrl_if_; |
| | | es8374_cfg.gpio_if = gpio_if_; |
| | | es8374_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; |
| | | es8374_cfg.pa_pin = pa_pin; |
| | | codec_if_ = es8374_codec_new(&es8374_cfg); |
| | | assert(codec_if_ != NULL); |
| | | |
| | | esp_codec_dev_cfg_t dev_cfg = { |
| | | .dev_type = ESP_CODEC_DEV_TYPE_OUT, |
| | | .codec_if = codec_if_, |
| | | .data_if = data_if_, |
| | | }; |
| | | output_dev_ = esp_codec_dev_new(&dev_cfg); |
| | | assert(output_dev_ != NULL); |
| | | dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; |
| | | input_dev_ = esp_codec_dev_new(&dev_cfg); |
| | | assert(input_dev_ != NULL); |
| | | esp_codec_set_disable_when_closed(output_dev_, false); |
| | | esp_codec_set_disable_when_closed(input_dev_, false); |
| | | ESP_LOGI(TAG, "Es8374AudioCodec initialized"); |
| | | } |
| | | |
| | | Es8374AudioCodec::~Es8374AudioCodec() { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); |
| | | esp_codec_dev_delete(output_dev_); |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); |
| | | esp_codec_dev_delete(input_dev_); |
| | | |
| | | audio_codec_delete_codec_if(codec_if_); |
| | | audio_codec_delete_ctrl_if(ctrl_if_); |
| | | audio_codec_delete_gpio_if(gpio_if_); |
| | | audio_codec_delete_data_if(data_if_); |
| | | } |
| | | |
| | | void Es8374AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { |
| | | assert(input_sample_rate_ == output_sample_rate_); |
| | | |
| | | i2s_chan_config_t chan_cfg = { |
| | | .id = I2S_NUM_0, |
| | | .role = I2S_ROLE_MASTER, |
| | | .dma_desc_num = 6, |
| | | .dma_frame_num = 240, |
| | | .auto_clear_after_cb = true, |
| | | .auto_clear_before_cb = false, |
| | | .intr_priority = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); |
| | | |
| | | i2s_std_config_t std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .ext_clk_freq_hz = 0, |
| | | #endif |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_STEREO, |
| | | .slot_mask = I2S_STD_SLOT_BOTH, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | #endif |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = mclk, |
| | | .bclk = bclk, |
| | | .ws = ws, |
| | | .dout = dout, |
| | | .din = din, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); |
| | | ESP_LOGI(TAG, "Duplex channels created"); |
| | | } |
| | | |
| | | void Es8374AudioCodec::SetOutputVolume(int volume) { |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); |
| | | AudioCodec::SetOutputVolume(volume); |
| | | } |
| | | |
| | | void Es8374AudioCodec::EnableInput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == input_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = 1, |
| | | .channel_mask = 0, |
| | | .sample_rate = (uint32_t)input_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, input_gain_)); |
| | | } else { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); |
| | | } |
| | | AudioCodec::EnableInput(enable); |
| | | } |
| | | |
| | | void Es8374AudioCodec::EnableOutput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == output_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | // Play 16bit 1 channel |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = 1, |
| | | .channel_mask = 0, |
| | | .sample_rate = (uint32_t)output_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); |
| | | if (pa_pin_ != GPIO_NUM_NC) { |
| | | gpio_set_level(pa_pin_, 1); |
| | | } |
| | | } else { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); |
| | | if (pa_pin_ != GPIO_NUM_NC) { |
| | | gpio_set_level(pa_pin_, 0); |
| | | } |
| | | } |
| | | AudioCodec::EnableOutput(enable); |
| | | } |
| | | |
| | | int Es8374AudioCodec::Read(int16_t* dest, int samples) { |
| | | if (input_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| | | |
| | | int Es8374AudioCodec::Write(const int16_t* data, int samples) { |
| | | if (output_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| New file |
| | |
| | | #ifndef _ES8374_AUDIO_CODEC_H |
| | | #define _ES8374_AUDIO_CODEC_H |
| | | |
| | | #include "audio_codec.h" |
| | | |
| | | #include <driver/i2c.h> |
| | | #include <driver/gpio.h> |
| | | #include <esp_codec_dev.h> |
| | | #include <esp_codec_dev_defaults.h> |
| | | #include <mutex> |
| | | |
| | | |
| | | class Es8374AudioCodec : public AudioCodec { |
| | | private: |
| | | const audio_codec_data_if_t* data_if_ = nullptr; |
| | | const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; |
| | | const audio_codec_if_t* codec_if_ = nullptr; |
| | | const audio_codec_gpio_if_t* gpio_if_ = nullptr; |
| | | |
| | | esp_codec_dev_handle_t output_dev_ = nullptr; |
| | | esp_codec_dev_handle_t input_dev_ = nullptr; |
| | | gpio_num_t pa_pin_ = GPIO_NUM_NC; |
| | | std::mutex data_if_mutex_; |
| | | |
| | | void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); |
| | | |
| | | virtual int Read(int16_t* dest, int samples) override; |
| | | virtual int Write(const int16_t* data, int samples) override; |
| | | |
| | | public: |
| | | Es8374AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8374_addr, bool use_mclk = true); |
| | | virtual ~Es8374AudioCodec(); |
| | | |
| | | virtual void SetOutputVolume(int volume) override; |
| | | virtual void EnableInput(bool enable) override; |
| | | virtual void EnableOutput(bool enable) override; |
| | | }; |
| | | |
| | | #endif // _ES8374_AUDIO_CODEC_H |
| New file |
| | |
| | | #include "es8388_audio_codec.h" |
| | | |
| | | #include <esp_log.h> |
| | | |
| | | #define TAG "Es8388AudioCodec" |
| | | |
| | | Es8388AudioCodec::Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference) { |
| | | duplex_ = true; // æ¯å¦åå·¥ |
| | | input_reference_ = input_reference; // æ¯å¦ä½¿ç¨åèè¾å
¥ï¼å®ç°å声æ¶é¤ |
| | | input_channels_ = input_reference_ ? 2 : 1; // è¾å
¥ééæ° |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | input_gain_ = 24; |
| | | |
| | | pa_pin_ = pa_pin; |
| | | CreateDuplexChannels(mclk, bclk, ws, dout, din); |
| | | |
| | | // Do initialize of related interface: data_if, ctrl_if and gpio_if |
| | | audio_codec_i2s_cfg_t i2s_cfg = { |
| | | .port = I2S_NUM_0, |
| | | .rx_handle = rx_handle_, |
| | | .tx_handle = tx_handle_, |
| | | }; |
| | | data_if_ = audio_codec_new_i2s_data(&i2s_cfg); |
| | | assert(data_if_ != NULL); |
| | | |
| | | // Output |
| | | audio_codec_i2c_cfg_t i2c_cfg = { |
| | | .port = i2c_port, |
| | | .addr = es8388_addr, |
| | | .bus_handle = i2c_master_handle, |
| | | }; |
| | | ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); |
| | | assert(ctrl_if_ != NULL); |
| | | |
| | | gpio_if_ = audio_codec_new_gpio(); |
| | | assert(gpio_if_ != NULL); |
| | | |
| | | es8388_codec_cfg_t es8388_cfg = {}; |
| | | es8388_cfg.ctrl_if = ctrl_if_; |
| | | es8388_cfg.gpio_if = gpio_if_; |
| | | es8388_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; |
| | | es8388_cfg.master_mode = true; |
| | | es8388_cfg.pa_pin = pa_pin; |
| | | es8388_cfg.pa_reverted = false; |
| | | es8388_cfg.hw_gain.pa_voltage = 5.0; |
| | | es8388_cfg.hw_gain.codec_dac_voltage = 3.3; |
| | | codec_if_ = es8388_codec_new(&es8388_cfg); |
| | | assert(codec_if_ != NULL); |
| | | |
| | | esp_codec_dev_cfg_t outdev_cfg = { |
| | | .dev_type = ESP_CODEC_DEV_TYPE_OUT, |
| | | .codec_if = codec_if_, |
| | | .data_if = data_if_, |
| | | }; |
| | | output_dev_ = esp_codec_dev_new(&outdev_cfg); |
| | | assert(output_dev_ != NULL); |
| | | |
| | | esp_codec_dev_cfg_t indev_cfg = { |
| | | .dev_type = ESP_CODEC_DEV_TYPE_IN, |
| | | .codec_if = codec_if_, |
| | | .data_if = data_if_, |
| | | }; |
| | | input_dev_ = esp_codec_dev_new(&indev_cfg); |
| | | assert(input_dev_ != NULL); |
| | | esp_codec_set_disable_when_closed(output_dev_, false); |
| | | esp_codec_set_disable_when_closed(input_dev_, false); |
| | | ESP_LOGI(TAG, "Es8388AudioCodec initialized"); |
| | | } |
| | | |
| | | Es8388AudioCodec::~Es8388AudioCodec() { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); |
| | | esp_codec_dev_delete(output_dev_); |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); |
| | | esp_codec_dev_delete(input_dev_); |
| | | |
| | | audio_codec_delete_codec_if(codec_if_); |
| | | audio_codec_delete_ctrl_if(ctrl_if_); |
| | | audio_codec_delete_gpio_if(gpio_if_); |
| | | audio_codec_delete_data_if(data_if_); |
| | | } |
| | | |
| | | void Es8388AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din){ |
| | | assert(input_sample_rate_ == output_sample_rate_); |
| | | |
| | | i2s_chan_config_t chan_cfg = { |
| | | .id = I2S_NUM_0, |
| | | .role = I2S_ROLE_MASTER, |
| | | .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, |
| | | .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, |
| | | .auto_clear_after_cb = true, |
| | | .auto_clear_before_cb = false, |
| | | .intr_priority = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); |
| | | |
| | | i2s_std_config_t std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .ext_clk_freq_hz = 0, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256 |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_STEREO, |
| | | .slot_mask = I2S_STD_SLOT_BOTH, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = mclk, |
| | | .bclk = bclk, |
| | | .ws = ws, |
| | | .dout = dout, |
| | | .din = din, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); |
| | | ESP_LOGI(TAG, "Duplex channels created"); |
| | | } |
| | | |
| | | void Es8388AudioCodec::SetOutputVolume(int volume) { |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); |
| | | AudioCodec::SetOutputVolume(volume); |
| | | } |
| | | |
| | | void Es8388AudioCodec::EnableInput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == input_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = (uint8_t) input_channels_, |
| | | .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), |
| | | .sample_rate = (uint32_t)input_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | if (input_reference_) { |
| | | fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); |
| | | } |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); |
| | | if (input_reference_) { |
| | | uint8_t gain = (11 << 4) + 0; |
| | | ctrl_if_->write_reg(ctrl_if_, 0x09, 1, &gain, 1); |
| | | }else{ |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, input_gain_)); |
| | | } |
| | | } else { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); |
| | | } |
| | | AudioCodec::EnableInput(enable); |
| | | } |
| | | |
| | | void Es8388AudioCodec::EnableOutput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == output_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = 1, |
| | | .channel_mask = 0, |
| | | .sample_rate = (uint32_t)output_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); |
| | | |
| | | // Set analog output volume to 0dB, default is -45dB |
| | | uint8_t reg_val = 30; // 0dB |
| | | uint8_t regs[] = { 46, 47, 48, 49 }; // HP_LVOL, HP_RVOL, SPK_LVOL, SPK_RVOL |
| | | for (uint8_t reg : regs) { |
| | | ctrl_if_->write_reg(ctrl_if_, reg, 1, ®_val, 1); |
| | | } |
| | | |
| | | if (pa_pin_ != GPIO_NUM_NC) { |
| | | gpio_set_level(pa_pin_, 1); |
| | | } |
| | | } else { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); |
| | | if (pa_pin_ != GPIO_NUM_NC) { |
| | | gpio_set_level(pa_pin_, 0); |
| | | } |
| | | } |
| | | AudioCodec::EnableOutput(enable); |
| | | } |
| | | |
| | | int Es8388AudioCodec::Read(int16_t* dest, int samples) { |
| | | if (input_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| | | |
| | | int Es8388AudioCodec::Write(const int16_t* data, int samples) { |
| | | if (output_enabled_ && output_dev_ && data != nullptr) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| New file |
| | |
| | | #ifndef _ES8388_AUDIO_CODEC_H |
| | | #define _ES8388_AUDIO_CODEC_H |
| | | |
| | | #include "audio_codec.h" |
| | | |
| | | #include <driver/i2c_master.h> |
| | | #include <esp_codec_dev.h> |
| | | #include <esp_codec_dev_defaults.h> |
| | | #include <mutex> |
| | | |
| | | |
| | | class Es8388AudioCodec : public AudioCodec { |
| | | private: |
| | | const audio_codec_data_if_t* data_if_ = nullptr; |
| | | const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; |
| | | const audio_codec_if_t* codec_if_ = nullptr; |
| | | const audio_codec_gpio_if_t* gpio_if_ = nullptr; |
| | | |
| | | esp_codec_dev_handle_t output_dev_ = nullptr; |
| | | esp_codec_dev_handle_t input_dev_ = nullptr; |
| | | gpio_num_t pa_pin_ = GPIO_NUM_NC; |
| | | std::mutex data_if_mutex_; |
| | | |
| | | void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); |
| | | |
| | | virtual int Read(int16_t* dest, int samples) override; |
| | | virtual int Write(const int16_t* data, int samples) override; |
| | | |
| | | public: |
| | | Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference = false); |
| | | virtual ~Es8388AudioCodec(); |
| | | |
| | | virtual void SetOutputVolume(int volume) override; |
| | | virtual void EnableInput(bool enable) override; |
| | | virtual void EnableOutput(bool enable) override; |
| | | }; |
| | | |
| | | #endif // _ES8388_AUDIO_CODEC_H |
| New file |
| | |
| | | #include "es8389_audio_codec.h" |
| | | |
| | | #include <esp_log.h> |
| | | |
| | | static const char TAG[] = "Es8389AudioCodec"; |
| | | |
| | | Es8389AudioCodec::Es8389AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8389_addr, bool use_mclk) { |
| | | duplex_ = true; // æ¯å¦åå·¥ |
| | | input_reference_ = false; // æ¯å¦ä½¿ç¨åèè¾å
¥ï¼å®ç°å声æ¶é¤ |
| | | input_channels_ = 1; // è¾å
¥ééæ° |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | input_gain_ = 40; |
| | | pa_pin_ = pa_pin; |
| | | CreateDuplexChannels(mclk, bclk, ws, dout, din); |
| | | |
| | | // Do initialize of related interface: data_if, ctrl_if and gpio_if |
| | | audio_codec_i2s_cfg_t i2s_cfg = { |
| | | .port = I2S_NUM_0, |
| | | .rx_handle = rx_handle_, |
| | | .tx_handle = tx_handle_, |
| | | }; |
| | | data_if_ = audio_codec_new_i2s_data(&i2s_cfg); |
| | | assert(data_if_ != NULL); |
| | | |
| | | // Output |
| | | audio_codec_i2c_cfg_t i2c_cfg = { |
| | | .port = i2c_port, |
| | | .addr = es8389_addr, |
| | | .bus_handle = i2c_master_handle, |
| | | }; |
| | | ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); |
| | | assert(ctrl_if_ != NULL); |
| | | |
| | | gpio_if_ = audio_codec_new_gpio(); |
| | | assert(gpio_if_ != NULL); |
| | | |
| | | es8389_codec_cfg_t es8389_cfg = {}; |
| | | es8389_cfg.ctrl_if = ctrl_if_; |
| | | es8389_cfg.gpio_if = gpio_if_; |
| | | es8389_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; |
| | | es8389_cfg.pa_pin = pa_pin; |
| | | es8389_cfg.use_mclk = use_mclk; |
| | | es8389_cfg.hw_gain.pa_voltage = 5.0; |
| | | es8389_cfg.hw_gain.codec_dac_voltage = 3.3; |
| | | codec_if_ = es8389_codec_new(&es8389_cfg); |
| | | |
| | | assert(codec_if_ != NULL); |
| | | |
| | | esp_codec_dev_cfg_t outdev_cfg = { |
| | | .dev_type = ESP_CODEC_DEV_TYPE_OUT, |
| | | .codec_if = codec_if_, |
| | | .data_if = data_if_, |
| | | }; |
| | | output_dev_ = esp_codec_dev_new(&outdev_cfg); |
| | | assert(output_dev_ != NULL); |
| | | |
| | | esp_codec_dev_cfg_t indev_cfg = { |
| | | .dev_type = ESP_CODEC_DEV_TYPE_IN, |
| | | .codec_if = codec_if_, |
| | | .data_if = data_if_, |
| | | }; |
| | | input_dev_ = esp_codec_dev_new(&indev_cfg); |
| | | assert(input_dev_ != NULL); |
| | | esp_codec_set_disable_when_closed(output_dev_, false); |
| | | esp_codec_set_disable_when_closed(input_dev_, false); |
| | | ESP_LOGI(TAG, "Es8389AudioCodec initialized"); |
| | | } |
| | | |
| | | Es8389AudioCodec::~Es8389AudioCodec() { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); |
| | | esp_codec_dev_delete(output_dev_); |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); |
| | | esp_codec_dev_delete(input_dev_); |
| | | |
| | | audio_codec_delete_codec_if(codec_if_); |
| | | audio_codec_delete_ctrl_if(ctrl_if_); |
| | | audio_codec_delete_gpio_if(gpio_if_); |
| | | audio_codec_delete_data_if(data_if_); |
| | | } |
| | | |
| | | void Es8389AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { |
| | | assert(input_sample_rate_ == output_sample_rate_); |
| | | |
| | | i2s_chan_config_t chan_cfg = { |
| | | .id = I2S_NUM_0, |
| | | .role = I2S_ROLE_MASTER, |
| | | .dma_desc_num = 6, |
| | | .dma_frame_num = 240, |
| | | .auto_clear_after_cb = true, |
| | | .auto_clear_before_cb = false, |
| | | .intr_priority = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); |
| | | |
| | | i2s_std_config_t std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .ext_clk_freq_hz = 0, |
| | | #endif |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_STEREO, |
| | | .slot_mask = I2S_STD_SLOT_BOTH, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_16BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = mclk, |
| | | .bclk = bclk, |
| | | .ws = ws, |
| | | .dout = dout, |
| | | .din = din, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); |
| | | ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); |
| | | ESP_LOGI(TAG, "Duplex channels created"); |
| | | } |
| | | |
| | | void Es8389AudioCodec::SetOutputVolume(int volume) { |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); |
| | | AudioCodec::SetOutputVolume(volume); |
| | | } |
| | | |
| | | void Es8389AudioCodec::EnableInput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == input_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = 1, |
| | | .channel_mask = 0, |
| | | .sample_rate = (uint32_t)input_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, input_gain_)); |
| | | } else { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); |
| | | } |
| | | AudioCodec::EnableInput(enable); |
| | | } |
| | | |
| | | void Es8389AudioCodec::EnableOutput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == output_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | // Play 16bit 1 channel |
| | | esp_codec_dev_sample_info_t fs = { |
| | | .bits_per_sample = 16, |
| | | .channel = 1, |
| | | .channel_mask = 0, |
| | | .sample_rate = (uint32_t)output_sample_rate_, |
| | | .mclk_multiple = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); |
| | | ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); |
| | | if (pa_pin_ != GPIO_NUM_NC) { |
| | | gpio_set_level(pa_pin_, 1); |
| | | } |
| | | } else { |
| | | ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); |
| | | if (pa_pin_ != GPIO_NUM_NC) { |
| | | gpio_set_level(pa_pin_, 0); |
| | | } |
| | | } |
| | | AudioCodec::EnableOutput(enable); |
| | | } |
| | | |
| | | int Es8389AudioCodec::Read(int16_t* dest, int samples) { |
| | | if (input_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| | | |
| | | int Es8389AudioCodec::Write(const int16_t* data, int samples) { |
| | | if (output_enabled_) { |
| | | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); |
| | | } |
| | | return samples; |
| | | } |
| New file |
| | |
| | | #ifndef _ES8389_AUDIO_CODEC_H |
| | | #define _ES8389_AUDIO_CODEC_H |
| | | |
| | | #include "audio_codec.h" |
| | | |
| | | #include <driver/i2c.h> |
| | | #include <driver/gpio.h> |
| | | #include <esp_codec_dev.h> |
| | | #include <esp_codec_dev_defaults.h> |
| | | #include <mutex> |
| | | |
| | | class Es8389AudioCodec : public AudioCodec { |
| | | private: |
| | | const audio_codec_data_if_t* data_if_ = nullptr; |
| | | const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; |
| | | const audio_codec_if_t* codec_if_ = nullptr; |
| | | const audio_codec_gpio_if_t* gpio_if_ = nullptr; |
| | | |
| | | esp_codec_dev_handle_t output_dev_ = nullptr; |
| | | esp_codec_dev_handle_t input_dev_ = nullptr; |
| | | gpio_num_t pa_pin_ = GPIO_NUM_NC; |
| | | std::mutex data_if_mutex_; |
| | | |
| | | void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); |
| | | |
| | | virtual int Read(int16_t* dest, int samples) override; |
| | | virtual int Write(const int16_t* data, int samples) override; |
| | | |
| | | public: |
| | | Es8389AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, |
| | | gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din, |
| | | gpio_num_t pa_pin, uint8_t es8389_addr, bool use_mclk = true); |
| | | virtual ~Es8389AudioCodec(); |
| | | |
| | | virtual void SetOutputVolume(int volume) override; |
| | | virtual void EnableInput(bool enable) override; |
| | | virtual void EnableOutput(bool enable) override; |
| | | }; |
| | | |
| | | #endif // _ES8389_AUDIO_CODEC_H |
| New file |
| | |
| | | #include "no_audio_codec.h" |
| | | |
| | | #include <esp_log.h> |
| | | #include <cmath> |
| | | #include <cstring> |
| | | |
| | | #define TAG "NoAudioCodec" |
| | | |
| | | NoAudioCodec::~NoAudioCodec() { |
| | | if (rx_handle_ != nullptr) { |
| | | ESP_ERROR_CHECK(i2s_channel_disable(rx_handle_)); |
| | | } |
| | | if (tx_handle_ != nullptr) { |
| | | ESP_ERROR_CHECK(i2s_channel_disable(tx_handle_)); |
| | | } |
| | | } |
| | | |
| | | NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { |
| | | duplex_ = true; |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | |
| | | i2s_chan_config_t chan_cfg = { |
| | | .id = I2S_NUM_0, |
| | | .role = I2S_ROLE_MASTER, |
| | | .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, |
| | | .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, |
| | | .auto_clear_after_cb = true, |
| | | .auto_clear_before_cb = false, |
| | | .intr_priority = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); |
| | | |
| | | i2s_std_config_t std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .ext_clk_freq_hz = 0, |
| | | #endif |
| | | |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_MONO, |
| | | .slot_mask = I2S_STD_SLOT_LEFT, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_32BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | #endif |
| | | |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = I2S_GPIO_UNUSED, |
| | | .bclk = bclk, |
| | | .ws = ws, |
| | | .dout = dout, |
| | | .din = din, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); |
| | | ESP_LOGI(TAG, "Duplex channels created"); |
| | | } |
| | | |
| | | |
| | | NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din) { |
| | | duplex_ = false; |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | |
| | | // Create a new channel for speaker |
| | | i2s_chan_config_t chan_cfg = { |
| | | .id = (i2s_port_t)0, |
| | | .role = I2S_ROLE_MASTER, |
| | | .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, |
| | | .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, |
| | | .auto_clear_after_cb = true, |
| | | .auto_clear_before_cb = false, |
| | | .intr_priority = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr)); |
| | | |
| | | i2s_std_config_t std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .ext_clk_freq_hz = 0, |
| | | #endif |
| | | |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_MONO, |
| | | .slot_mask = I2S_STD_SLOT_LEFT, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_32BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | #endif |
| | | |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = I2S_GPIO_UNUSED, |
| | | .bclk = spk_bclk, |
| | | .ws = spk_ws, |
| | | .dout = spk_dout, |
| | | .din = I2S_GPIO_UNUSED, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); |
| | | |
| | | // Create a new channel for MIC |
| | | chan_cfg.id = (i2s_port_t)1; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_)); |
| | | std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_; |
| | | std_cfg.gpio_cfg.bclk = mic_sck; |
| | | std_cfg.gpio_cfg.ws = mic_ws; |
| | | std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; |
| | | std_cfg.gpio_cfg.din = mic_din; |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); |
| | | ESP_LOGI(TAG, "Simplex channels created"); |
| | | } |
| | | |
| | | NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din, i2s_std_slot_mask_t mic_slot_mask){ |
| | | duplex_ = false; |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | |
| | | // Create a new channel for speaker |
| | | i2s_chan_config_t chan_cfg = { |
| | | .id = (i2s_port_t)0, |
| | | .role = I2S_ROLE_MASTER, |
| | | .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, |
| | | .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, |
| | | .auto_clear_after_cb = true, |
| | | .auto_clear_before_cb = false, |
| | | .intr_priority = 0, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr)); |
| | | |
| | | i2s_std_config_t std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .ext_clk_freq_hz = 0, |
| | | #endif |
| | | |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_MONO, |
| | | .slot_mask = spk_slot_mask, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_32BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | #endif |
| | | |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = I2S_GPIO_UNUSED, |
| | | .bclk = spk_bclk, |
| | | .ws = spk_ws, |
| | | .dout = spk_dout, |
| | | .din = I2S_GPIO_UNUSED, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false |
| | | } |
| | | } |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); |
| | | |
| | | // Create a new channel for MIC |
| | | chan_cfg.id = (i2s_port_t)1; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_)); |
| | | std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_; |
| | | std_cfg.slot_cfg.slot_mask = mic_slot_mask; |
| | | std_cfg.gpio_cfg.bclk = mic_sck; |
| | | std_cfg.gpio_cfg.ws = mic_ws; |
| | | std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; |
| | | std_cfg.gpio_cfg.din = mic_din; |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); |
| | | ESP_LOGI(TAG, "Simplex channels created"); |
| | | } |
| | | |
| | | int NoAudioCodec::Write(const int16_t* data, int samples) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | std::vector<int32_t> buffer(samples); |
| | | |
| | | // output_volume_: 0-100 |
| | | // volume_factor_: 0-65536 |
| | | int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536; |
| | | for (int i = 0; i < samples; i++) { |
| | | int64_t temp = int64_t(data[i]) * volume_factor; // ä½¿ç¨ int64_t è¿è¡ä¹æ³è¿ç® |
| | | if (temp > INT32_MAX) { |
| | | buffer[i] = INT32_MAX; |
| | | } else if (temp < INT32_MIN) { |
| | | buffer[i] = INT32_MIN; |
| | | } else { |
| | | buffer[i] = static_cast<int32_t>(temp); |
| | | } |
| | | } |
| | | |
| | | size_t bytes_written; |
| | | ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY)); |
| | | return bytes_written / sizeof(int32_t); |
| | | } |
| | | |
| | | int NoAudioCodec::Read(int16_t* dest, int samples) { |
| | | size_t bytes_read; |
| | | constexpr TickType_t kReadTimeoutTicks = pdMS_TO_TICKS(200); |
| | | |
| | | std::vector<int32_t> bit32_buffer(samples); |
| | | if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, kReadTimeoutTicks) != ESP_OK) { |
| | | return 0; |
| | | } |
| | | |
| | | samples = bytes_read / sizeof(int32_t); |
| | | for (int i = 0; i < samples; i++) { |
| | | int32_t value = bit32_buffer[i] >> 12; |
| | | dest[i] = (value > INT16_MAX) ? INT16_MAX : (value < -INT16_MAX) ? -INT16_MAX : (int16_t)value; |
| | | } |
| | | return samples; |
| | | } |
| | | |
| | | void NoAudioCodec::EnableInput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == input_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); |
| | | } else { |
| | | ESP_ERROR_CHECK(i2s_channel_disable(rx_handle_)); |
| | | } |
| | | AudioCodec::EnableInput(enable); |
| | | } |
| | | |
| | | void NoAudioCodec::EnableOutput(bool enable) { |
| | | std::lock_guard<std::mutex> lock(data_if_mutex_); |
| | | if (enable == output_enabled_) { |
| | | return; |
| | | } |
| | | if (enable) { |
| | | ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); |
| | | } else { |
| | | ESP_ERROR_CHECK(i2s_channel_disable(tx_handle_)); |
| | | } |
| | | AudioCodec::EnableOutput(enable); |
| | | } |
| | | |
| | | // Delegating constructor: calls the main constructor with default slot mask |
| | | NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din) |
| | | : NoAudioCodecSimplexPdm(input_sample_rate, output_sample_rate, spk_bclk, spk_ws, spk_dout, I2S_STD_SLOT_LEFT, mic_sck, mic_din) { |
| | | // All initialization is handled by the delegated constructor |
| | | } |
| | | |
| | | NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_din) { |
| | | duplex_ = false; |
| | | input_sample_rate_ = input_sample_rate; |
| | | output_sample_rate_ = output_sample_rate; |
| | | |
| | | // Create a new channel for speaker |
| | | i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)1, I2S_ROLE_MASTER); |
| | | tx_chan_cfg.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM; |
| | | tx_chan_cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM; |
| | | tx_chan_cfg.auto_clear_after_cb = true; |
| | | tx_chan_cfg.auto_clear_before_cb = false; |
| | | tx_chan_cfg.intr_priority = 0; |
| | | ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle_, NULL)); |
| | | |
| | | |
| | | i2s_std_config_t tx_std_cfg = { |
| | | .clk_cfg = { |
| | | .sample_rate_hz = (uint32_t)output_sample_rate_, |
| | | .clk_src = I2S_CLK_SRC_DEFAULT, |
| | | .mclk_multiple = I2S_MCLK_MULTIPLE_256, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .ext_clk_freq_hz = 0, |
| | | #endif |
| | | |
| | | }, |
| | | .slot_cfg = { |
| | | .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, |
| | | .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, |
| | | .slot_mode = I2S_SLOT_MODE_MONO, |
| | | .slot_mask = spk_slot_mask, |
| | | .ws_width = I2S_DATA_BIT_WIDTH_32BIT, |
| | | .ws_pol = false, |
| | | .bit_shift = true, |
| | | #ifdef I2S_HW_VERSION_2 |
| | | .left_align = true, |
| | | .big_endian = false, |
| | | .bit_order_lsb = false |
| | | #endif |
| | | |
| | | }, |
| | | .gpio_cfg = { |
| | | .mclk = I2S_GPIO_UNUSED, |
| | | .bclk = spk_bclk, |
| | | .ws = spk_ws, |
| | | .dout = spk_dout, |
| | | .din = I2S_GPIO_UNUSED, |
| | | .invert_flags = { |
| | | .mclk_inv = false, |
| | | .bclk_inv = false, |
| | | .ws_inv = false, |
| | | }, |
| | | }, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &tx_std_cfg)); |
| | | #if SOC_I2S_SUPPORTS_PDM_RX |
| | | // Create a new channel for MIC in PDM mode |
| | | i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER); |
| | | ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_)); |
| | | i2s_pdm_rx_config_t pdm_rx_cfg = { |
| | | .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_), |
| | | /* The data bit-width of PDM mode is fixed to 16 */ |
| | | .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), |
| | | .gpio_cfg = { |
| | | .clk = mic_sck, |
| | | .din = mic_din, |
| | | |
| | | .invert_flags = { |
| | | .clk_inv = false, |
| | | }, |
| | | }, |
| | | }; |
| | | ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle_, &pdm_rx_cfg)); |
| | | #else |
| | | ESP_LOGE(TAG, "PDM is not supported"); |
| | | #endif |
| | | ESP_LOGI(TAG, "Simplex channels created"); |
| | | } |
| | | |
| | | int NoAudioCodecSimplexPdm::Read(int16_t* dest, int samples) { |
| | | size_t bytes_read; |
| | | |
| | | // PDM è§£è°åçæ°æ®ä½å®½ä¸º 16 ä½ï¼ç´æ¥è¯»åå°ç®æ ç¼å²åº |
| | | if (i2s_channel_read(rx_handle_, dest, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY) != ESP_OK) { |
| | | ESP_LOGE(TAG, "Read Failed!"); |
| | | return 0; |
| | | } |
| | | |
| | | samples = bytes_read / sizeof(int16_t); |
| | | if (input_gain_ > 0) { |
| | | int gain_factor = (int)input_gain_; |
| | | for (int i = 0; i < samples; i++) { |
| | | int32_t amplified = dest[i] * gain_factor; |
| | | dest[i] = (amplified > INT16_MAX) ? INT16_MAX : (amplified < -INT16_MAX) ? -INT16_MAX : (int16_t)amplified; |
| | | } |
| | | } |
| | | return samples; |
| | | } |
| New file |
| | |
| | | #ifndef _NO_AUDIO_CODEC_H |
| | | #define _NO_AUDIO_CODEC_H |
| | | |
| | | #include "audio_codec.h" |
| | | |
| | | #include <driver/gpio.h> |
| | | #include <driver/i2s_pdm.h> |
| | | #include <mutex> |
| | | |
| | | class NoAudioCodec : public AudioCodec { |
| | | protected: |
| | | std::mutex data_if_mutex_; |
| | | |
| | | virtual int Write(const int16_t* data, int samples) override; |
| | | virtual int Read(int16_t* dest, int samples) override; |
| | | virtual void EnableInput(bool enable) override; |
| | | virtual void EnableOutput(bool enable) override; |
| | | |
| | | public: |
| | | virtual ~NoAudioCodec(); |
| | | }; |
| | | |
| | | class NoAudioCodecDuplex : public NoAudioCodec { |
| | | public: |
| | | NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din); |
| | | }; |
| | | |
| | | class NoAudioCodecSimplex : public NoAudioCodec { |
| | | public: |
| | | NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din); |
| | | NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din, i2s_std_slot_mask_t mic_slot_mask); |
| | | }; |
| | | |
| | | class NoAudioCodecSimplexPdm : public NoAudioCodec { |
| | | public: |
| | | NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din); |
| | | NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_din); |
| | | int Read(int16_t* dest, int samples); |
| | | }; |
| | | |
| | | #endif // _NO_AUDIO_CODEC_H |
| New file |
| | |
| | | #include "ogg_demuxer.h" |
| | | #include "esp_log.h" |
| | | |
| | | #define TAG "OggDemuxer" |
| | | |
| | | /// @brief é置解å°å¨ |
| | | void OggDemuxer::Reset() |
| | | { |
| | | opus_info_ = { |
| | | .head_seen = false, |
| | | .tags_seen = false, |
| | | .sample_rate = 48000 |
| | | }; |
| | | |
| | | state_ = ParseState::FIND_PAGE; |
| | | ctx_.packet_len = 0; |
| | | ctx_.seg_count = 0; |
| | | ctx_.seg_index = 0; |
| | | ctx_.data_offset = 0; |
| | | ctx_.bytes_needed = 4; // éè¦4åè"OggS" |
| | | ctx_.seg_remaining = 0; |
| | | ctx_.body_size = 0; |
| | | ctx_.body_offset = 0; |
| | | ctx_.packet_continued = false; |
| | | |
| | | // æ¸
空ç¼å²åºæ°æ® |
| | | memset(ctx_.header, 0, sizeof(ctx_.header)); |
| | | memset(ctx_.seg_table, 0, sizeof(ctx_.seg_table)); |
| | | memset(ctx_.packet_buf, 0, sizeof(ctx_.packet_buf)); |
| | | } |
| | | |
| | | /// @brief å¤çæ°æ®å |
| | | /// @param data è¾å
¥æ°æ® |
| | | /// @param size è¾å
¥æ°æ®å¤§å° |
| | | /// @return å·²å¤ççåèæ° |
| | | size_t OggDemuxer::Process(const uint8_t* data, size_t size) |
| | | { |
| | | size_t processed = 0; // å·²å¤ççåèæ° |
| | | |
| | | while (processed < size) { |
| | | switch (state_) { |
| | | case ParseState::FIND_PAGE: { |
| | | // 寻æ¾é¡µå¤´"OggS" |
| | | if (ctx_.bytes_needed < 4) { |
| | | // å¤çä¸å®æ´ç"OggS"å¹é
ï¼è·¨æ°æ®åï¼ |
| | | size_t to_copy = std::min(size - processed, ctx_.bytes_needed); |
| | | memcpy(ctx_.header + (4 - ctx_.bytes_needed), data + processed, to_copy); |
| | | |
| | | processed += to_copy; |
| | | ctx_.bytes_needed -= to_copy; |
| | | |
| | | if (ctx_.bytes_needed == 0) { |
| | | // æ£æ¥æ¯å¦å¹é
"OggS" |
| | | if (memcmp(ctx_.header, "OggS", 4) == 0) { |
| | | state_ = ParseState::PARSE_HEADER; |
| | | ctx_.data_offset = 4; |
| | | ctx_.bytes_needed = 27 - 4; // è¿éè¦23åè宿页头 |
| | | } else { |
| | | // å¹é
å¤±è´¥ï¼æ»å¨1åèç»§ç»å¹é
|
| | | memmove(ctx_.header, ctx_.header + 1, 3); |
| | | ctx_.bytes_needed = 1; |
| | | } |
| | | } else { |
| | | // æ°æ®ä¸è¶³ï¼çå¾
æ´å¤æ°æ® |
| | | return processed; |
| | | } |
| | | } else if (ctx_.bytes_needed == 4) { |
| | | // 卿°æ®å䏿¥æ¾å®æ´ç"OggS" |
| | | bool found = false; |
| | | size_t i = 0; |
| | | size_t remaining = size - processed; |
| | | |
| | | // æç´¢"OggS" |
| | | for (; i + 4 <= remaining; i++) { |
| | | if (memcmp(data + processed + i, "OggS", 4) == 0) { |
| | | found = true; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (found) { |
| | | // æ¾å°"OggS"ï¼è·³è¿å·²æç´¢çåè |
| | | processed += i; |
| | | |
| | | // ä¸è®°å½æ¾å°ç"OggS"ï¼æ å¿
è¦ |
| | | // memcpy(ctx_.header, data + processed, 4); |
| | | processed += 4; |
| | | |
| | | state_ = ParseState::PARSE_HEADER; |
| | | ctx_.data_offset = 4; |
| | | ctx_.bytes_needed = 27 - 4; // è¿éè¦23åè |
| | | } else { |
| | | // æ²¡ææ¾å°å®æ´"OggS"ï¼ä¿åå¯è½çé¨åå¹é
|
| | | size_t partial_len = remaining - i; |
| | | if (partial_len > 0) { |
| | | memcpy(ctx_.header, data + processed + i, partial_len); |
| | | ctx_.bytes_needed = 4 - partial_len; |
| | | processed += i + partial_len; |
| | | } else { |
| | | processed += i; // å·²æç´¢ææåè |
| | | } |
| | | return processed; // è¿åå·²å¤ççåèæ° |
| | | } |
| | | } else { |
| | | ESP_LOGE(TAG, "OggDemuxer run in error state: bytes_needed=%zu", ctx_.bytes_needed); |
| | | Reset(); |
| | | return processed; |
| | | } |
| | | break; |
| | | } |
| | | |
| | | case ParseState::PARSE_HEADER: { |
| | | size_t available = size - processed; |
| | | |
| | | if (available < ctx_.bytes_needed) { |
| | | // æ°æ®ä¸è¶³ï¼å¤å¶å¯ç¨çé¨å |
| | | memcpy(ctx_.header + ctx_.data_offset, |
| | | data + processed, available); |
| | | |
| | | ctx_.data_offset += available; |
| | | ctx_.bytes_needed -= available; |
| | | processed += available; |
| | | return processed; // çå¾
æ´å¤æ°æ® |
| | | } else { |
| | | // æè¶³å¤çæ°æ®å®æé¡µå¤´ |
| | | size_t to_copy = ctx_.bytes_needed; |
| | | memcpy(ctx_.header + ctx_.data_offset, |
| | | data + processed, to_copy); |
| | | |
| | | processed += to_copy; |
| | | ctx_.data_offset += to_copy; |
| | | ctx_.bytes_needed = 0; |
| | | |
| | | // éªè¯é¡µå¤´ |
| | | if (ctx_.header[4] != 0) { |
| | | ESP_LOGE(TAG, "æ æçOggçæ¬: %d", ctx_.header[4]); |
| | | state_ = ParseState::FIND_PAGE; |
| | | ctx_.bytes_needed = 4; |
| | | ctx_.data_offset = 0; |
| | | break; |
| | | } |
| | | |
| | | ctx_.seg_count = ctx_.header[26]; |
| | | if (ctx_.seg_count > 0 && ctx_.seg_count <= 255) { |
| | | state_ = ParseState::PARSE_SEGMENTS; |
| | | ctx_.bytes_needed = ctx_.seg_count; |
| | | ctx_.data_offset = 0; |
| | | } else if (ctx_.seg_count == 0) { |
| | | // æ²¡ææ®µï¼ç´æ¥è·³å°ä¸ä¸ä¸ªé¡µé¢ |
| | | state_ = ParseState::FIND_PAGE; |
| | | ctx_.bytes_needed = 4; |
| | | ctx_.data_offset = 0; |
| | | } else { |
| | | ESP_LOGE(TAG, "æ æçæ®µæ°: %u", ctx_.seg_count); |
| | | state_ = ParseState::FIND_PAGE; |
| | | ctx_.bytes_needed = 4; |
| | | ctx_.data_offset = 0; |
| | | } |
| | | } |
| | | break; |
| | | } |
| | | |
| | | case ParseState::PARSE_SEGMENTS: { |
| | | size_t available = size - processed; |
| | | |
| | | if (available < ctx_.bytes_needed) { |
| | | memcpy(ctx_.seg_table + ctx_.data_offset, |
| | | data + processed, available); |
| | | |
| | | ctx_.data_offset += available; |
| | | ctx_.bytes_needed -= available; |
| | | processed += available; |
| | | return processed; // çå¾
æ´å¤æ°æ® |
| | | } else { |
| | | size_t to_copy = ctx_.bytes_needed; |
| | | memcpy(ctx_.seg_table + ctx_.data_offset, |
| | | data + processed, to_copy); |
| | | |
| | | processed += to_copy; |
| | | ctx_.data_offset += to_copy; |
| | | ctx_.bytes_needed = 0; |
| | | |
| | | state_ = ParseState::PARSE_DATA; |
| | | ctx_.seg_index = 0; |
| | | ctx_.data_offset = 0; |
| | | |
| | | // è®¡ç®æ°æ®ä½æ»å¤§å° |
| | | ctx_.body_size = 0; |
| | | for (size_t i = 0; i < ctx_.seg_count; ++i) { |
| | | ctx_.body_size += ctx_.seg_table[i]; |
| | | } |
| | | ctx_.body_offset = 0; |
| | | ctx_.seg_remaining = 0; |
| | | } |
| | | break; |
| | | } |
| | | |
| | | case ParseState::PARSE_DATA: { |
| | | while (ctx_.seg_index < ctx_.seg_count && processed < size) { |
| | | uint8_t seg_len = ctx_.seg_table[ctx_.seg_index]; |
| | | |
| | | // æ£æ¥æ®µæ°æ®æ¯å¦å·²ç»é¨å读å |
| | | if (ctx_.seg_remaining > 0) { |
| | | seg_len = ctx_.seg_remaining; |
| | | } else { |
| | | ctx_.seg_remaining = seg_len; |
| | | } |
| | | |
| | | // æ£æ¥ç¼å²åºæ¯å¦è¶³å¤ |
| | | if (ctx_.packet_len + seg_len > sizeof(ctx_.packet_buf)) { |
| | | ESP_LOGE(TAG, "å
ç¼å²åºæº¢åº: %zu + %u > %zu", ctx_.packet_len, seg_len, sizeof(ctx_.packet_buf)); |
| | | state_ = ParseState::FIND_PAGE; |
| | | ctx_.packet_len = 0; |
| | | ctx_.packet_continued = false; |
| | | ctx_.seg_remaining = 0; |
| | | ctx_.bytes_needed = 4; |
| | | return processed; |
| | | } |
| | | |
| | | // å¤å¶æ°æ® |
| | | size_t to_copy = std::min(size - processed, (size_t)seg_len); |
| | | memcpy(ctx_.packet_buf + ctx_.packet_len, data + processed, to_copy); |
| | | |
| | | processed += to_copy; |
| | | ctx_.packet_len += to_copy; |
| | | ctx_.body_offset += to_copy; |
| | | ctx_.seg_remaining -= to_copy; |
| | | |
| | | // æ£æ¥æ®µæ¯å¦å®æ´ |
| | | if (ctx_.seg_remaining > 0) { |
| | | // 段ä¸å®æ´ï¼çå¾
æ´å¤æ°æ® |
| | | return processed; |
| | | } |
| | | |
| | | // æ®µå®æ´ |
| | | bool seg_continued = (ctx_.seg_table[ctx_.seg_index] == 255); |
| | | |
| | | if (!seg_continued) { |
| | | // å
ç»æ |
| | | if (ctx_.packet_len) { |
| | | if (!opus_info_.head_seen) { |
| | | if (ctx_.packet_len >=8 && memcmp(ctx_.packet_buf, "OpusHead", 8) == 0) { |
| | | opus_info_.head_seen = true; |
| | | if (ctx_.packet_len >= 19) { |
| | | opus_info_.sample_rate = ctx_.packet_buf[12] | |
| | | (ctx_.packet_buf[13] << 8) | |
| | | (ctx_.packet_buf[14] << 16) | |
| | | (ctx_.packet_buf[15] << 24); |
| | | ESP_LOGD(TAG, "OpusHead found, sample_rate=%d", opus_info_.sample_rate); |
| | | } |
| | | ctx_.packet_len = 0; |
| | | ctx_.packet_continued = false; |
| | | ctx_.seg_index++; |
| | | ctx_.seg_remaining = 0; |
| | | continue; |
| | | } |
| | | } |
| | | if (!opus_info_.tags_seen) { |
| | | if (ctx_.packet_len >= 8 && memcmp(ctx_.packet_buf, "OpusTags", 8) == 0) { |
| | | opus_info_.tags_seen = true; |
| | | ESP_LOGD(TAG, "OpusTags found."); |
| | | ctx_.packet_len = 0; |
| | | ctx_.packet_continued = false; |
| | | ctx_.seg_index++; |
| | | ctx_.seg_remaining = 0; |
| | | continue; |
| | | } |
| | | } |
| | | if (opus_info_.head_seen && opus_info_.tags_seen) { |
| | | if (on_demuxer_finished_) { |
| | | on_demuxer_finished_(ctx_.packet_buf, opus_info_.sample_rate, ctx_.packet_len); |
| | | } |
| | | } else { |
| | | ESP_LOGW(TAG, "å½åOgg容卿ªè§£æå°OpusHead/OpusTagsï¼ä¸¢å¼"); |
| | | } |
| | | } |
| | | ctx_.packet_len = 0; |
| | | ctx_.packet_continued = false; |
| | | } else { |
| | | ctx_.packet_continued = true; |
| | | } |
| | | |
| | | ctx_.seg_index++; |
| | | ctx_.seg_remaining = 0; |
| | | } |
| | | |
| | | if (ctx_.seg_index == ctx_.seg_count) { |
| | | // æ£æ¥æ¯å¦æææ°æ®ä½é½å·²è¯»å |
| | | if (ctx_.body_offset < ctx_.body_size) { |
| | | ESP_LOGW(TAG, "æ°æ®ä½ä¸å®æ´: %zu/%zu", |
| | | ctx_.body_offset, ctx_.body_size); |
| | | } |
| | | |
| | | // 妿å
跨页ï¼ä¿æpacket_lenåpacket_continued |
| | | if (!ctx_.packet_continued) { |
| | | ctx_.packet_len = 0; |
| | | } |
| | | |
| | | // è¿å
¥ä¸ä¸é¡µé¢ |
| | | state_ = ParseState::FIND_PAGE; |
| | | ctx_.bytes_needed = 4; |
| | | ctx_.data_offset = 0; |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return processed; |
| | | } |
| | | |
| xiaozhi-esp32-2.2.6/main/audio/demuxer/ogg_demuxer.h
xiaozhi-esp32-2.2.6/main/audio/processors/afe_audio_processor.cc
xiaozhi-esp32-2.2.6/main/audio/processors/afe_audio_processor.h
xiaozhi-esp32-2.2.6/main/audio/processors/audio_debugger.cc
xiaozhi-esp32-2.2.6/main/audio/processors/audio_debugger.h
xiaozhi-esp32-2.2.6/main/audio/processors/no_audio_processor.cc
xiaozhi-esp32-2.2.6/main/audio/processors/no_audio_processor.h
xiaozhi-esp32-2.2.6/main/audio/wake_word.h
xiaozhi-esp32-2.2.6/main/audio/wake_words/afe_wake_word.cc
xiaozhi-esp32-2.2.6/main/audio/wake_words/afe_wake_word.h
xiaozhi-esp32-2.2.6/main/audio/wake_words/custom_wake_word.cc
xiaozhi-esp32-2.2.6/main/audio/wake_words/custom_wake_word.h
xiaozhi-esp32-2.2.6/main/audio/wake_words/esp_wake_word.cc
xiaozhi-esp32-2.2.6/main/audio/wake_words/esp_wake_word.h
xiaozhi-esp32-2.2.6/main/boards/aipi-lite/README.md
xiaozhi-esp32-2.2.6/main/boards/aipi-lite/README_en.md
xiaozhi-esp32-2.2.6/main/boards/aipi-lite/aipi-lite.cc
xiaozhi-esp32-2.2.6/main/boards/aipi-lite/config.h
xiaozhi-esp32-2.2.6/main/boards/aipi-lite/config.json
xiaozhi-esp32-2.2.6/main/boards/aipi-lite/config_en.json
xiaozhi-esp32-2.2.6/main/boards/aipi-lite/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box/config.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box/config.json
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box0/atk_dnesp32s3_box0.cc
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box0/config.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box0/config.json
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box0/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box2-4g/atk_dnesp32s3_box2.cc
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box2-4g/config.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box2-4g/config.json
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box2-4g/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box2-wifi/atk_dnesp32s3_box2.cc
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box2-wifi/config.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box2-wifi/config.json
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box2-wifi/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box3/atk_dnesp32s3_box3.cc
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box3/config.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box3/config.json
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3-box3/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3/atk_dnesp32s3.cc
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3/config.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3/config.json
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3m-4g/atk_dnesp32s3m.cc
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3m-4g/config.h
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3m-wifi/atk_dnesp32s3m.cc
xiaozhi-esp32-2.2.6/main/boards/atk-dnesp32s3m-wifi/config.h
xiaozhi-esp32-2.2.6/main/boards/atom-echos3r/README.md
xiaozhi-esp32-2.2.6/main/boards/atom-echos3r/atom_echos3r.cc
xiaozhi-esp32-2.2.6/main/boards/atom-echos3r/config.h
xiaozhi-esp32-2.2.6/main/boards/atom-echos3r/config.json
xiaozhi-esp32-2.2.6/main/boards/atommatrix-echo-base/README.md
xiaozhi-esp32-2.2.6/main/boards/atommatrix-echo-base/atommatrix_echo_base.cc
xiaozhi-esp32-2.2.6/main/boards/atommatrix-echo-base/config.h
xiaozhi-esp32-2.2.6/main/boards/atommatrix-echo-base/config.json
xiaozhi-esp32-2.2.6/main/boards/atoms3-echo-base/README.md
xiaozhi-esp32-2.2.6/main/boards/atoms3-echo-base/atoms3_echo_base.cc
xiaozhi-esp32-2.2.6/main/boards/atoms3-echo-base/config.h
xiaozhi-esp32-2.2.6/main/boards/atoms3-echo-base/config.json
xiaozhi-esp32-2.2.6/main/boards/atoms3r-cam-m12-echo-base/README.md
xiaozhi-esp32-2.2.6/main/boards/atoms3r-cam-m12-echo-base/atoms3r_cam_m12_echo_base.cc
xiaozhi-esp32-2.2.6/main/boards/atoms3r-cam-m12-echo-base/config.h
xiaozhi-esp32-2.2.6/main/boards/atoms3r-cam-m12-echo-base/config.json
xiaozhi-esp32-2.2.6/main/boards/atoms3r-echo-base/README.md
xiaozhi-esp32-2.2.6/main/boards/atoms3r-echo-base/atoms3r_echo_base.cc
xiaozhi-esp32-2.2.6/main/boards/atoms3r-echo-base/config.h
xiaozhi-esp32-2.2.6/main/boards/atoms3r-echo-base/config.json
xiaozhi-esp32-2.2.6/main/boards/bread-compact-esp32-lcd/config.h
xiaozhi-esp32-2.2.6/main/boards/bread-compact-esp32-lcd/config.json
xiaozhi-esp32-2.2.6/main/boards/bread-compact-esp32-lcd/esp32_bread_board_lcd.cc
xiaozhi-esp32-2.2.6/main/boards/bread-compact-esp32/README.md
xiaozhi-esp32-2.2.6/main/boards/bread-compact-esp32/config.h
xiaozhi-esp32-2.2.6/main/boards/bread-compact-esp32/config.json
xiaozhi-esp32-2.2.6/main/boards/bread-compact-esp32/esp32_bread_board.cc
xiaozhi-esp32-2.2.6/main/boards/bread-compact-ml307/compact_ml307_board.cc
xiaozhi-esp32-2.2.6/main/boards/bread-compact-ml307/config.h
xiaozhi-esp32-2.2.6/main/boards/bread-compact-ml307/config.json
xiaozhi-esp32-2.2.6/main/boards/bread-compact-nt26/compact_nt26_board.cc
xiaozhi-esp32-2.2.6/main/boards/bread-compact-nt26/config.h
xiaozhi-esp32-2.2.6/main/boards/bread-compact-nt26/config.json
xiaozhi-esp32-2.2.6/main/boards/bread-compact-wifi-lcd/compact_wifi_board_lcd.cc
xiaozhi-esp32-2.2.6/main/boards/bread-compact-wifi-lcd/config.h
xiaozhi-esp32-2.2.6/main/boards/bread-compact-wifi-s3cam/README.md
xiaozhi-esp32-2.2.6/main/boards/bread-compact-wifi-s3cam/compact_wifi_board_s3cam.cc
xiaozhi-esp32-2.2.6/main/boards/bread-compact-wifi-s3cam/config.h
xiaozhi-esp32-2.2.6/main/boards/bread-compact-wifi/compact_wifi_board.cc
xiaozhi-esp32-2.2.6/main/boards/bread-compact-wifi/config.h
xiaozhi-esp32-2.2.6/main/boards/bread-compact-wifi/config.json
xiaozhi-esp32-2.2.6/main/boards/common/adc_battery_monitor.cc
xiaozhi-esp32-2.2.6/main/boards/common/adc_battery_monitor.h
xiaozhi-esp32-2.2.6/main/boards/common/afsk_demod.cc
xiaozhi-esp32-2.2.6/main/boards/common/afsk_demod.h
xiaozhi-esp32-2.2.6/main/boards/common/axp2101.cc
xiaozhi-esp32-2.2.6/main/boards/common/axp2101.h
xiaozhi-esp32-2.2.6/main/boards/common/backlight.cc
xiaozhi-esp32-2.2.6/main/boards/common/backlight.h
xiaozhi-esp32-2.2.6/main/boards/common/blufi.cpp
xiaozhi-esp32-2.2.6/main/boards/common/blufi.h
xiaozhi-esp32-2.2.6/main/boards/common/board.cc
xiaozhi-esp32-2.2.6/main/boards/common/board.h
xiaozhi-esp32-2.2.6/main/boards/common/button.cc
xiaozhi-esp32-2.2.6/main/boards/common/button.h
xiaozhi-esp32-2.2.6/main/boards/common/camera.h
xiaozhi-esp32-2.2.6/main/boards/common/dual_network_board.cc
xiaozhi-esp32-2.2.6/main/boards/common/dual_network_board.h
xiaozhi-esp32-2.2.6/main/boards/common/esp32_camera.cc
xiaozhi-esp32-2.2.6/main/boards/common/esp32_camera.h
xiaozhi-esp32-2.2.6/main/boards/common/esp_video.cc
xiaozhi-esp32-2.2.6/main/boards/common/esp_video.h
xiaozhi-esp32-2.2.6/main/boards/common/i2c_device.cc
xiaozhi-esp32-2.2.6/main/boards/common/i2c_device.h
xiaozhi-esp32-2.2.6/main/boards/common/knob.cc
xiaozhi-esp32-2.2.6/main/boards/common/knob.h
xiaozhi-esp32-2.2.6/main/boards/common/lamp_controller.h
xiaozhi-esp32-2.2.6/main/boards/common/ml307_board.cc
xiaozhi-esp32-2.2.6/main/boards/common/ml307_board.h
xiaozhi-esp32-2.2.6/main/boards/common/nt26_board.cc
xiaozhi-esp32-2.2.6/main/boards/common/nt26_board.h
xiaozhi-esp32-2.2.6/main/boards/common/power_save_timer.cc
xiaozhi-esp32-2.2.6/main/boards/common/power_save_timer.h
xiaozhi-esp32-2.2.6/main/boards/common/press_to_talk_mcp_tool.cc
xiaozhi-esp32-2.2.6/main/boards/common/press_to_talk_mcp_tool.h
xiaozhi-esp32-2.2.6/main/boards/common/rndis_board.cc
xiaozhi-esp32-2.2.6/main/boards/common/rndis_board.h
xiaozhi-esp32-2.2.6/main/boards/common/sleep_timer.cc
xiaozhi-esp32-2.2.6/main/boards/common/sleep_timer.h
xiaozhi-esp32-2.2.6/main/boards/common/sy6970.cc
xiaozhi-esp32-2.2.6/main/boards/common/sy6970.h
xiaozhi-esp32-2.2.6/main/boards/common/system_reset.cc
xiaozhi-esp32-2.2.6/main/boards/common/system_reset.h
xiaozhi-esp32-2.2.6/main/boards/common/wifi_board.cc
xiaozhi-esp32-2.2.6/main/boards/common/wifi_board.h
xiaozhi-esp32-2.2.6/main/boards/df-k10/README.md
xiaozhi-esp32-2.2.6/main/boards/df-k10/config.h
xiaozhi-esp32-2.2.6/main/boards/df-k10/config.json
xiaozhi-esp32-2.2.6/main/boards/df-k10/df_k10_board.cc
xiaozhi-esp32-2.2.6/main/boards/df-k10/k10_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/df-k10/k10_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/df-k10/led_control.cc
xiaozhi-esp32-2.2.6/main/boards/df-k10/led_control.h
xiaozhi-esp32-2.2.6/main/boards/df-s3-ai-cam/README.md
xiaozhi-esp32-2.2.6/main/boards/df-s3-ai-cam/config.h
xiaozhi-esp32-2.2.6/main/boards/df-s3-ai-cam/config.json
xiaozhi-esp32-2.2.6/main/boards/df-s3-ai-cam/df_s3_ai_cam.cc
xiaozhi-esp32-2.2.6/main/boards/doit-s3-aibox/README.md
xiaozhi-esp32-2.2.6/main/boards/doit-s3-aibox/config.h
xiaozhi-esp32-2.2.6/main/boards/doit-s3-aibox/config.json
xiaozhi-esp32-2.2.6/main/boards/doit-s3-aibox/doit_s3_aibox.cc
xiaozhi-esp32-2.2.6/main/boards/du-chatx/config.h
xiaozhi-esp32-2.2.6/main/boards/du-chatx/config.json
xiaozhi-esp32-2.2.6/main/boards/du-chatx/du-chatx-wifi.cc
xiaozhi-esp32-2.2.6/main/boards/du-chatx/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/electron-bot/README.md
xiaozhi-esp32-2.2.6/main/boards/electron-bot/config.h
xiaozhi-esp32-2.2.6/main/boards/electron-bot/config.json
xiaozhi-esp32-2.2.6/main/boards/electron-bot/electron_bot.cc
xiaozhi-esp32-2.2.6/main/boards/electron-bot/electron_bot_controller.cc
xiaozhi-esp32-2.2.6/main/boards/electron-bot/electron_emoji_display.cc
xiaozhi-esp32-2.2.6/main/boards/electron-bot/electron_emoji_display.h
xiaozhi-esp32-2.2.6/main/boards/electron-bot/movements.cc
xiaozhi-esp32-2.2.6/main/boards/electron-bot/movements.h
xiaozhi-esp32-2.2.6/main/boards/electron-bot/oscillator.cc
xiaozhi-esp32-2.2.6/main/boards/electron-bot/oscillator.h
xiaozhi-esp32-2.2.6/main/boards/electron-bot/otto_icon_font.c
xiaozhi-esp32-2.2.6/main/boards/electron-bot/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/esp-box-3/README.md
xiaozhi-esp32-2.2.6/main/boards/esp-box-3/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-box-3/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-box-3/esp_box3_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp-box-lite/box_audio_codec_lite.cc
xiaozhi-esp32-2.2.6/main/boards/esp-box-lite/box_audio_codec_lite.h
xiaozhi-esp32-2.2.6/main/boards/esp-box-lite/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-box-lite/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-box-lite/esp_box_lite_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp-box/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-box/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-box/esp_box_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp-hi/README.md
xiaozhi-esp32-2.2.6/main/boards/esp-hi/adc_pdm_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/esp-hi/adc_pdm_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/esp-hi/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-hi/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-hi/emoji_display.cc
xiaozhi-esp32-2.2.6/main/boards/esp-hi/emoji_display.h
xiaozhi-esp32-2.2.6/main/boards/esp-hi/esp_hi.cc
xiaozhi-esp32-2.2.6/main/boards/esp-p4-function-ev-board/README.md
xiaozhi-esp32-2.2.6/main/boards/esp-p4-function-ev-board/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-p4-function-ev-board/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-p4-function-ev-board/esp-p4-function-ev-board.cc
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board-2/README.md
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board-2/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board-2/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board-2/esp-s3-lcd-ev-board-2.cc
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board-2/esp_lcd_gc9503.c
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board-2/esp_lcd_gc9503.h
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board-2/pin_config.h
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/README.md
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/esp-s3-lcd-ev-board.cc
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/esp_io_expander_tca9554.c
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/esp_io_expander_tca9554.h
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/esp_lcd_gc9503.c
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/esp_lcd_gc9503.h
xiaozhi-esp32-2.2.6/main/boards/esp-s3-lcd-ev-board/pin_config.h
xiaozhi-esp32-2.2.6/main/boards/esp-sensairshuttle/README.md
xiaozhi-esp32-2.2.6/main/boards/esp-sensairshuttle/adc_pdm_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/esp-sensairshuttle/adc_pdm_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/esp-sensairshuttle/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-sensairshuttle/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-sensairshuttle/esp-sensairshuttle.cc
xiaozhi-esp32-2.2.6/main/boards/esp-sparkbot/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-sparkbot/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-sparkbot/esp_sparkbot_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp-spot/README.md
xiaozhi-esp32-2.2.6/main/boards/esp-spot/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-spot/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-spot/esp_spot_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp-vocat/README.md
xiaozhi-esp32-2.2.6/main/boards/esp-vocat/config.h
xiaozhi-esp32-2.2.6/main/boards/esp-vocat/config.json
xiaozhi-esp32-2.2.6/main/boards/esp-vocat/esp_vocat.cc
xiaozhi-esp32-2.2.6/main/boards/esp-vocat/touch.h
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc-144/README.md
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc-144/config.h
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc-144/config.json
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc-144/esp32_cgc_144_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc-144/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc-144/power_manager_lite.h
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc/README.md
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc/config.h
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc/config.json
xiaozhi-esp32-2.2.6/main/boards/esp32-cgc/esp32_cgc_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp32s3-korvo2-v3-rndis/config.h
xiaozhi-esp32-2.2.6/main/boards/esp32s3-korvo2-v3-rndis/config.json
xiaozhi-esp32-2.2.6/main/boards/esp32s3-korvo2-v3-rndis/esp32s3_korvo2_v3_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp32s3-korvo2-v3-rndis/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/esp32s3-korvo2-v3/config.h
xiaozhi-esp32-2.2.6/main/boards/esp32s3-korvo2-v3/config.json
xiaozhi-esp32-2.2.6/main/boards/esp32s3-korvo2-v3/esp32s3_korvo2_v3_board.cc
xiaozhi-esp32-2.2.6/main/boards/esp32s3-korvo2-v3/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/freenove-esp32s3-display-2.8-lcd/ReadMe.md
xiaozhi-esp32-2.2.6/main/boards/freenove-esp32s3-display-2.8-lcd/config.h
xiaozhi-esp32-2.2.6/main/boards/freenove-esp32s3-display-2.8-lcd/config.json
xiaozhi-esp32-2.2.6/main/boards/freenove-esp32s3-display-2.8-lcd/freenove-esp32s3-display-2.8-lcd.cc
xiaozhi-esp32-2.2.6/main/boards/genjutech-s3-1.54tft/config.h
xiaozhi-esp32-2.2.6/main/boards/genjutech-s3-1.54tft/config.json
xiaozhi-esp32-2.2.6/main/boards/genjutech-s3-1.54tft/genjutech-s3-1.54tft.cc
xiaozhi-esp32-2.2.6/main/boards/genjutech-s3-1.54tft/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/hu-087/README.md
xiaozhi-esp32-2.2.6/main/boards/hu-087/config.h
xiaozhi-esp32-2.2.6/main/boards/hu-087/config.json
xiaozhi-esp32-2.2.6/main/boards/hu-087/hu_087_board.cc
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/README.md
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/config.h
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/config.json
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/esp_lcd_panel_gc9301.c
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/esp_lcd_panel_gc9301.h
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/gpio_manager.h
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/jiuchuan_dev_board.cc
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/power_controller.h
xiaozhi-esp32-2.2.6/main/boards/jiuchuan-s3/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/kevin-box-2/config.h
xiaozhi-esp32-2.2.6/main/boards/kevin-box-2/config.json
xiaozhi-esp32-2.2.6/main/boards/kevin-box-2/kevin_box_board.cc
xiaozhi-esp32-2.2.6/main/boards/kevin-c3/config.h
xiaozhi-esp32-2.2.6/main/boards/kevin-c3/config.json
xiaozhi-esp32-2.2.6/main/boards/kevin-c3/kevin_c3_board.cc
xiaozhi-esp32-2.2.6/main/boards/kevin-c3/led_strip_control.cc
xiaozhi-esp32-2.2.6/main/boards/kevin-c3/led_strip_control.h
xiaozhi-esp32-2.2.6/main/boards/kevin-sp-v3-dev/config.h
xiaozhi-esp32-2.2.6/main/boards/kevin-sp-v3-dev/kevin-sp-v3_board.cc
xiaozhi-esp32-2.2.6/main/boards/kevin-sp-v4-dev/config.h
xiaozhi-esp32-2.2.6/main/boards/kevin-sp-v4-dev/config.json
xiaozhi-esp32-2.2.6/main/boards/kevin-sp-v4-dev/kevin-sp-v4_board.cc
xiaozhi-esp32-2.2.6/main/boards/kevin-yuying-313lcd/config.h
xiaozhi-esp32-2.2.6/main/boards/kevin-yuying-313lcd/config.json
xiaozhi-esp32-2.2.6/main/boards/kevin-yuying-313lcd/esp_lcd_gc9503.c
xiaozhi-esp32-2.2.6/main/boards/kevin-yuying-313lcd/esp_lcd_gc9503.h
xiaozhi-esp32-2.2.6/main/boards/kevin-yuying-313lcd/kevin_yuying_313lcd.cc
xiaozhi-esp32-2.2.6/main/boards/kevin-yuying-313lcd/pin_config.h
xiaozhi-esp32-2.2.6/main/boards/labplus-ledong-v2/README.md
xiaozhi-esp32-2.2.6/main/boards/labplus-ledong-v2/config.h
xiaozhi-esp32-2.2.6/main/boards/labplus-ledong-v2/config.json
xiaozhi-esp32-2.2.6/main/boards/labplus-ledong-v2/labplus_ledong_v2.cc
xiaozhi-esp32-2.2.6/main/boards/labplus-mpython-v3/README.md
xiaozhi-esp32-2.2.6/main/boards/labplus-mpython-v3/config.h
xiaozhi-esp32-2.2.6/main/boards/labplus-mpython-v3/config.json
xiaozhi-esp32-2.2.6/main/boards/labplus-mpython-v3/mpython_pro.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/README.md
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/config.h
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/config.json
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/eda_dog_controller.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/eda_dog_movements.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/eda_dog_movements.h
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/eda_robot_pro.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/oscillator.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-robot-pro/oscillator.h
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/README.md
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/config.h
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/config.json
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/eda_super_bear.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/eda_super_bear_controller.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/eda_super_bear_movements.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/eda_super_bear_movements.h
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/oscillator.cc
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-super-bear/oscillator.h
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-tv-pro/README.md
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-tv-pro/config.h
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-tv-pro/config.json
xiaozhi-esp32-2.2.6/main/boards/lceda-course-examples/eda-tv-pro/eda-tv-pro.cc
xiaozhi-esp32-2.2.6/main/boards/lichuang-c3-dev/README.md
xiaozhi-esp32-2.2.6/main/boards/lichuang-c3-dev/config.h
xiaozhi-esp32-2.2.6/main/boards/lichuang-c3-dev/config.json
xiaozhi-esp32-2.2.6/main/boards/lichuang-c3-dev/lichuang_c3_dev_board.cc
xiaozhi-esp32-2.2.6/main/boards/lichuang-dev/config.h
xiaozhi-esp32-2.2.6/main/boards/lichuang-dev/config.json
xiaozhi-esp32-2.2.6/main/boards/lichuang-dev/lichuang_dev_board.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-cameraplus-s3/README.md
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-cameraplus-s3/config.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-cameraplus-s3/config.json
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-cameraplus-s3/ir_filter_controller.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-cameraplus-s3/lilygo-t-cameraplus-s3.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-cameraplus-s3/pin_config.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-cameraplus-s3/tcamerapluss3_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-cameraplus-s3/tcamerapluss3_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/README.md
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/config.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/config.json
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/lilygo-t-circle-s3.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/pin_config.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/tcircles3_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-circle-s3/tcircles3_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-p4/README.md
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-p4/config.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-p4/hi8561_driver.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-p4/hi8561_driver.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-p4/lilygo-t-display-p4.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-p4/rm69a10_driver.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-p4/rm69a10_driver.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-p4/t_display_p4_config.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-s3-pro-mvsrlora/README.md
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-s3-pro-mvsrlora/config.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-s3-pro-mvsrlora/config.json
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-s3-pro-mvsrlora/lilygo-t-display-s3-pro-mvsrlora.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-s3-pro-mvsrlora/pin_config.h
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-s3-pro-mvsrlora/tdisplays3promvsrlora_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/lilygo-t-display-s3-pro-mvsrlora/tdisplays3promvsrlora_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/m5stack-cardputer-adv/README.md
xiaozhi-esp32-2.2.6/main/boards/m5stack-cardputer-adv/config.h
xiaozhi-esp32-2.2.6/main/boards/m5stack-cardputer-adv/config.json
xiaozhi-esp32-2.2.6/main/boards/m5stack-cardputer-adv/m5stack_cardputer_adv.cc
xiaozhi-esp32-2.2.6/main/boards/m5stack-cardputer-adv/tca8418_keyboard.cc
xiaozhi-esp32-2.2.6/main/boards/m5stack-cardputer-adv/tca8418_keyboard.h
xiaozhi-esp32-2.2.6/main/boards/m5stack-cardputer-adv/wifi_config_ui.cc
xiaozhi-esp32-2.2.6/main/boards/m5stack-cardputer-adv/wifi_config_ui.h
xiaozhi-esp32-2.2.6/main/boards/m5stack-core-s3/README.md
xiaozhi-esp32-2.2.6/main/boards/m5stack-core-s3/config.h
xiaozhi-esp32-2.2.6/main/boards/m5stack-core-s3/config.json
xiaozhi-esp32-2.2.6/main/boards/m5stack-core-s3/cores3_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/m5stack-core-s3/cores3_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/m5stack-core-s3/m5stack_core_s3.cc
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/README.md
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/config.h
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/config.json
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/esp_lcd_st7123.c
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/esp_lcd_st7123.h
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/m5stack_tab5.cc
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/sdkconfig.tab5
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/tab5_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/m5stack-tab5/tab5_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/magiclick-2p4/config.h
xiaozhi-esp32-2.2.6/main/boards/magiclick-2p4/config.json
xiaozhi-esp32-2.2.6/main/boards/magiclick-2p4/magiclick_2p4_board.cc
xiaozhi-esp32-2.2.6/main/boards/magiclick-2p5/config.h
xiaozhi-esp32-2.2.6/main/boards/magiclick-2p5/config.json
xiaozhi-esp32-2.2.6/main/boards/magiclick-2p5/magiclick_2p5_board.cc
xiaozhi-esp32-2.2.6/main/boards/magiclick-2p5/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/magiclick-c3-v2/config.h
xiaozhi-esp32-2.2.6/main/boards/magiclick-c3-v2/config.json
xiaozhi-esp32-2.2.6/main/boards/magiclick-c3-v2/magiclick_c3_v2_board.cc
xiaozhi-esp32-2.2.6/main/boards/magiclick-c3/config.h
xiaozhi-esp32-2.2.6/main/boards/magiclick-c3/config.json
xiaozhi-esp32-2.2.6/main/boards/magiclick-c3/magiclick_c3_board.cc
xiaozhi-esp32-2.2.6/main/boards/minsi-k08-dual/README.md
xiaozhi-esp32-2.2.6/main/boards/minsi-k08-dual/config.h
xiaozhi-esp32-2.2.6/main/boards/minsi-k08-dual/config.json
xiaozhi-esp32-2.2.6/main/boards/minsi-k08-dual/minsi_k08_dual.cc
xiaozhi-esp32-2.2.6/main/boards/minsi-k08-dual/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/mixgo-nova/README.md
xiaozhi-esp32-2.2.6/main/boards/mixgo-nova/config.h
xiaozhi-esp32-2.2.6/main/boards/mixgo-nova/config.json
xiaozhi-esp32-2.2.6/main/boards/mixgo-nova/mixgo-nova.cc
xiaozhi-esp32-2.2.6/main/boards/movecall-cuican-esp32s3/README.md
xiaozhi-esp32-2.2.6/main/boards/movecall-cuican-esp32s3/config.h
xiaozhi-esp32-2.2.6/main/boards/movecall-cuican-esp32s3/config.json
xiaozhi-esp32-2.2.6/main/boards/movecall-cuican-esp32s3/movecall_cuican_esp32s3.cc
xiaozhi-esp32-2.2.6/main/boards/movecall-moji-esp32s3/README.md
xiaozhi-esp32-2.2.6/main/boards/movecall-moji-esp32s3/config.h
xiaozhi-esp32-2.2.6/main/boards/movecall-moji-esp32s3/config.json
xiaozhi-esp32-2.2.6/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc
xiaozhi-esp32-2.2.6/main/boards/movecall-moji2-esp32c5/README.md
xiaozhi-esp32-2.2.6/main/boards/movecall-moji2-esp32c5/README_zh.md
xiaozhi-esp32-2.2.6/main/boards/movecall-moji2-esp32c5/config.h
xiaozhi-esp32-2.2.6/main/boards/movecall-moji2-esp32c5/config.json
xiaozhi-esp32-2.2.6/main/boards/movecall-moji2-esp32c5/movecall_moji2_esp32s3.cc
xiaozhi-esp32-2.2.6/main/boards/nulllab-ai-vox-v3/README.md
xiaozhi-esp32-2.2.6/main/boards/nulllab-ai-vox-v3/ai_vox3_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/nulllab-ai-vox-v3/ai_vox3_board.cc
xiaozhi-esp32-2.2.6/main/boards/nulllab-ai-vox-v3/config.h
xiaozhi-esp32-2.2.6/main/boards/nulllab-ai-vox-v3/config.json
xiaozhi-esp32-2.2.6/main/boards/nulllab-ai-vox-v3/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/otto-robot/README.md
xiaozhi-esp32-2.2.6/main/boards/otto-robot/config.h
xiaozhi-esp32-2.2.6/main/boards/otto-robot/config.json
xiaozhi-esp32-2.2.6/main/boards/otto-robot/oscillator.cc
xiaozhi-esp32-2.2.6/main/boards/otto-robot/oscillator.h
xiaozhi-esp32-2.2.6/main/boards/otto-robot/otto_controller.cc
xiaozhi-esp32-2.2.6/main/boards/otto-robot/otto_emoji_display.cc
xiaozhi-esp32-2.2.6/main/boards/otto-robot/otto_emoji_display.h
xiaozhi-esp32-2.2.6/main/boards/otto-robot/otto_icon_font.c
xiaozhi-esp32-2.2.6/main/boards/otto-robot/otto_movements.cc
xiaozhi-esp32-2.2.6/main/boards/otto-robot/otto_movements.h
xiaozhi-esp32-2.2.6/main/boards/otto-robot/otto_robot.cc
xiaozhi-esp32-2.2.6/main/boards/otto-robot/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/otto-robot/websocket_control_server.cc
xiaozhi-esp32-2.2.6/main/boards/otto-robot/websocket_control_server.h
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/README.md
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/README_en.md
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/config.h
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/config.json
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/config_en.json
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/sensecap_audio_codec.cc
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/sensecap_audio_codec.h
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/sensecap_watcher.cc
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/sscma_camera.cc
xiaozhi-esp32-2.2.6/main/boards/sensecap-watcher/sscma_camera.h
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.28-box/README.md
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.28-box/config.h
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.28-box/config.json
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.28-box/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.54-muma/README.md
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.54-muma/config.h
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.54-muma/config.json
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.54-muma/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/sp-esp32-s3-1.54-muma/sp-esp32-s3-1.54-muma.cc
xiaozhi-esp32-2.2.6/main/boards/surfer-c3-1.14tft/README.md
xiaozhi-esp32-2.2.6/main/boards/surfer-c3-1.14tft/config.h
xiaozhi-esp32-2.2.6/main/boards/surfer-c3-1.14tft/config.json
xiaozhi-esp32-2.2.6/main/boards/surfer-c3-1.14tft/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/surfer-c3-1.14tft/surfer-c3-1.14tft.cc
xiaozhi-esp32-2.2.6/main/boards/taiji-pi-s3/README.md
xiaozhi-esp32-2.2.6/main/boards/taiji-pi-s3/config.h
xiaozhi-esp32-2.2.6/main/boards/taiji-pi-s3/config.json
xiaozhi-esp32-2.2.6/main/boards/taiji-pi-s3/taiji_pi_s3.cc
xiaozhi-esp32-2.2.6/main/boards/tudouzi/config.h
xiaozhi-esp32-2.2.6/main/boards/tudouzi/config.json
xiaozhi-esp32-2.2.6/main/boards/tudouzi/kevin_box_board.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-lcd-1.69/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-lcd-1.69/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-lcd-1.69/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-lcd-1.69/esp32-c6-lcd-1.69.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-lcd-1.69/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.32/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.32/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.32/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.32/esp32-c6-touch-amoled-1.32.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.43/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.43/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.43/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.43/esp32-c6-touch-amoled-1.43.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.8/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.8/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.8/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-1.8/esp32-c6-touch-amoled-1.8.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-2.06/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-2.06/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-2.06/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-2.06/esp32-c6-touch-amoled-2.06.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-2.16/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-2.16/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-2.16/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-amoled-2.16/esp32-c6-touch-amoled-2.16.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-lcd-1.83/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-lcd-1.83/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-lcd-1.83/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-c6-touch-lcd-1.83/esp32-c6-touch-lcd-1.83.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-nano/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-nano/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-nano/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-nano/esp32-p4-nano.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-nano/lcd_init_cmds.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd-3.5/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd-3.5/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd-3.5/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd-3.5/esp32-p4-wifi6-touch-lcd-3.5.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd/esp32-p4-wifi6-touch-lcd.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-p4-wifi6-touch-lcd/lcd_init_cmds.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-audio-board/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-audio-board/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-audio-board/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-audio-board/esp32-s3-audio_board.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-audio-board/esp_lcd_jd9853.c
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-audio-board/esp_lcd_jd9853.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-cam/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-cam/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-cam/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-cam/esp32-s3-cam-xxxx.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-1.54/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-1.54/board_power_bsp.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-1.54/board_power_bsp.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-1.54/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-1.54/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-1.54/custom_lcd_display.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-1.54/custom_lcd_display.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-1.54/waveshare-s3-epaper-1.54.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-3.97/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-3.97/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-3.97/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-3.97/custom_lcd_display.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-3.97/custom_lcd_display.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-epaper-3.97/waveshare-s3-epaper-3.97.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-lcd-0.85/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-lcd-0.85/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-lcd-0.85/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-lcd-0.85/esp32-s3-lcd-0.85.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-lcd-0.85/led_strip_control.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-lcd-0.85/led_strip_control.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-lcd-0.85/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-rlcd-4.2/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-rlcd-4.2/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-rlcd-4.2/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-rlcd-4.2/custom_lcd_display.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-rlcd-4.2/custom_lcd_display.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-rlcd-4.2/waveshare-s3-rlcd-4.2.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.32/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.32/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.32/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.32/esp32-s3-touch-amoled-1.32.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.43c/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.43c/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.43c/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.43c/esp32-s3-touch-amoled-1.43c.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.75/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.75/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.75/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.75/esp32-s3-touch-amoled-1.75.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.8/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.8/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-1.8/esp32-s3-touch-amoled-1.8.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-2.06/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-2.06/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-2.06/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-amoled-2.06/esp32-s3-touch-amoled-2.06.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.46/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.46/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.46/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.46/esp32-s3-touch-lcd-1.46.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.54/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.54/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.54/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.54/esp32-s3-touch-lcd-1.54.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.54/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.83/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.83/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.83/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.83/esp32-s3-touch-lcd-1.83.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.85/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.85/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.85/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.85/esp32-s3-touch-lcd-1.85.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.85c/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.85c/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.85c/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-1.85c/esp32-s3-touch-lcd-1.85c.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.49/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.49/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.49/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.49/custom_lcd_display.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.49/custom_lcd_display.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.49/waveshare-s3-touch-lcd-3.49.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5/esp32-s3-touch-lcd-3.5.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5b/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5b/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5b/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5b/custom_lcd_display.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5b/custom_lcd_display.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-3.5b/waveshare-s3-touch-lcd-3.5b.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4.3c/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4.3c/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4.3c/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4.3c/esp32_s3_touch_lcd_4.3c.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4.3c/sdkconfig.4_3c
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4b/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4b/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4b/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-s3-touch-lcd-4b/esp32-s3-touch-lcd-4b.cc
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-touch-lcd-3.5/README.md
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-touch-lcd-3.5/config.h
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-touch-lcd-3.5/config.json
xiaozhi-esp32-2.2.6/main/boards/waveshare/esp32-touch-lcd-3.5/esp32-touch-lcd-3.5.cc
xiaozhi-esp32-2.2.6/main/boards/wireless-tag-wtp4c5mp07s/README.md
xiaozhi-esp32-2.2.6/main/boards/wireless-tag-wtp4c5mp07s/config.h
xiaozhi-esp32-2.2.6/main/boards/wireless-tag-wtp4c5mp07s/config.json
xiaozhi-esp32-2.2.6/main/boards/wireless-tag-wtp4c5mp07s/wireless-tag-wtp4c5mp07s.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-abs-2.0/README.md
xiaozhi-esp32-2.2.6/main/boards/xingzhi-abs-2.0/config.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-abs-2.0/config.json
xiaozhi-esp32-2.2.6/main/boards/xingzhi-abs-2.0/customlcddisplay.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-abs-2.0/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-abs-2.0/xingzhi-abs-2.0.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.85tft-ml307/config.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.85tft-ml307/config.json
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.85tft-ml307/xingzhi-cube-0.85tft-ml307.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.85tft-wifi/config.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.85tft-wifi/config.json
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.85tft-wifi/xingzhi-cube-0.85tft-wifi.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.96oled-ml307/config.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.96oled-ml307/config.json
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.96oled-ml307/xingzhi-cube-0.96oled-ml307.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.96oled-wifi/config.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.96oled-wifi/config.json
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-0.96oled-wifi/xingzhi-cube-0.96oled-wifi.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-1.54tft-ml307/config.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-1.54tft-ml307/config.json
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-1.54tft-ml307/xingzhi-cube-1.54tft-ml307.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-1.54tft-wifi/config.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-1.54tft-wifi/config.json
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-1.54tft-wifi/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-cube-1.54tft-wifi/xingzhi-cube-1.54tft-wifi.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-metal-1.54-wifi/README.md
xiaozhi-esp32-2.2.6/main/boards/xingzhi-metal-1.54-wifi/config.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-metal-1.54-wifi/config.json
xiaozhi-esp32-2.2.6/main/boards/xingzhi-metal-1.54-wifi/cst816x.cc
xiaozhi-esp32-2.2.6/main/boards/xingzhi-metal-1.54-wifi/cst816x.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-metal-1.54-wifi/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/xingzhi-metal-1.54-wifi/xingzhi-metal-1.54-wifi.cc
xiaozhi-esp32-2.2.6/main/boards/xmini-c3-4g/README.md
xiaozhi-esp32-2.2.6/main/boards/xmini-c3-4g/config.h
xiaozhi-esp32-2.2.6/main/boards/xmini-c3-4g/config.json
xiaozhi-esp32-2.2.6/main/boards/xmini-c3-4g/xmini_c3_4g_board.cc
xiaozhi-esp32-2.2.6/main/boards/xmini-c3-v3/README.md
xiaozhi-esp32-2.2.6/main/boards/xmini-c3-v3/config.h
xiaozhi-esp32-2.2.6/main/boards/xmini-c3-v3/config.json
xiaozhi-esp32-2.2.6/main/boards/xmini-c3-v3/xmini_c3_board.cc
xiaozhi-esp32-2.2.6/main/boards/xmini-c3/config.h
xiaozhi-esp32-2.2.6/main/boards/xmini-c3/config.json
xiaozhi-esp32-2.2.6/main/boards/xmini-c3/xmini_c3_board.cc
xiaozhi-esp32-2.2.6/main/boards/yunliao-s3/README.md
xiaozhi-esp32-2.2.6/main/boards/yunliao-s3/config.h
xiaozhi-esp32-2.2.6/main/boards/yunliao-s3/config.json
xiaozhi-esp32-2.2.6/main/boards/yunliao-s3/power_manager.cc
xiaozhi-esp32-2.2.6/main/boards/yunliao-s3/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/yunliao-s3/yunliao_s3.cc
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-ml307/README.md
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-ml307/config.h
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-ml307/config.json
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-ml307/zhengchen-1.54tft-ml307.cc
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-wifi/README.md
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-wifi/config.h
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-wifi/config.json
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-wifi/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-wifi/zhengchen-1.54tft-wifi.cc
xiaozhi-esp32-2.2.6/main/boards/zhengchen-1.54tft-wifi/zhengchen_lcd_display.h
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam-ml307/README.md
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam-ml307/config.h
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam-ml307/config.json
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam-ml307/mcp_controller.cc
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam-ml307/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam-ml307/zhengchen_cam_board_ml307.cc
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam/README.md
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam/config.h
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam/config.json
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam/mcp_controller.cc
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam/power_manager.h
xiaozhi-esp32-2.2.6/main/boards/zhengchen-cam/zhengchen_cam_board.cc
xiaozhi-esp32-2.2.6/main/device_state.h
xiaozhi-esp32-2.2.6/main/device_state_machine.cc
xiaozhi-esp32-2.2.6/main/device_state_machine.h
xiaozhi-esp32-2.2.6/main/display/display.cc
xiaozhi-esp32-2.2.6/main/display/display.h
xiaozhi-esp32-2.2.6/main/display/emote_display.cc
xiaozhi-esp32-2.2.6/main/display/emote_display.h
xiaozhi-esp32-2.2.6/main/display/lcd_display.cc
xiaozhi-esp32-2.2.6/main/display/lcd_display.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/emoji_collection.cc
xiaozhi-esp32-2.2.6/main/display/lvgl_display/emoji_collection.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/gif/LICENSE.txt
xiaozhi-esp32-2.2.6/main/display/lvgl_display/gif/README.md
xiaozhi-esp32-2.2.6/main/display/lvgl_display/gif/gifdec.c
xiaozhi-esp32-2.2.6/main/display/lvgl_display/gif/gifdec.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/gif/gifdec_mve.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/gif/lvgl_gif.cc
xiaozhi-esp32-2.2.6/main/display/lvgl_display/gif/lvgl_gif.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/jpg/image_to_jpeg.cpp
xiaozhi-esp32-2.2.6/main/display/lvgl_display/jpg/image_to_jpeg.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/jpg/jpeg_to_image.c
xiaozhi-esp32-2.2.6/main/display/lvgl_display/jpg/jpeg_to_image.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/lvgl_display.cc
xiaozhi-esp32-2.2.6/main/display/lvgl_display/lvgl_display.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/lvgl_font.cc
xiaozhi-esp32-2.2.6/main/display/lvgl_display/lvgl_font.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/lvgl_image.cc
xiaozhi-esp32-2.2.6/main/display/lvgl_display/lvgl_image.h
xiaozhi-esp32-2.2.6/main/display/lvgl_display/lvgl_theme.cc
xiaozhi-esp32-2.2.6/main/display/lvgl_display/lvgl_theme.h
xiaozhi-esp32-2.2.6/main/display/oled_display.cc
xiaozhi-esp32-2.2.6/main/display/oled_display.h
xiaozhi-esp32-2.2.6/main/idf_component.yml
xiaozhi-esp32-2.2.6/main/led/circular_strip.cc
xiaozhi-esp32-2.2.6/main/led/circular_strip.h
xiaozhi-esp32-2.2.6/main/led/gpio_led.cc
xiaozhi-esp32-2.2.6/main/led/gpio_led.h
xiaozhi-esp32-2.2.6/main/led/led.h
xiaozhi-esp32-2.2.6/main/led/single_led.cc
xiaozhi-esp32-2.2.6/main/led/single_led.h
xiaozhi-esp32-2.2.6/main/main.cc
xiaozhi-esp32-2.2.6/main/mcp_server.cc
xiaozhi-esp32-2.2.6/main/mcp_server.h
xiaozhi-esp32-2.2.6/main/ota.cc
xiaozhi-esp32-2.2.6/main/ota.h
xiaozhi-esp32-2.2.6/main/protocols/mqtt_protocol.cc
xiaozhi-esp32-2.2.6/main/protocols/mqtt_protocol.h
xiaozhi-esp32-2.2.6/main/protocols/protocol.cc
xiaozhi-esp32-2.2.6/main/protocols/protocol.h
xiaozhi-esp32-2.2.6/main/protocols/websocket_protocol.cc
xiaozhi-esp32-2.2.6/main/protocols/websocket_protocol.h
xiaozhi-esp32-2.2.6/main/settings.cc
xiaozhi-esp32-2.2.6/main/settings.h
xiaozhi-esp32-2.2.6/main/system_info.cc
xiaozhi-esp32-2.2.6/main/system_info.h
xiaozhi-esp32-2.2.6/partitions/v1/16m.csv
xiaozhi-esp32-2.2.6/partitions/v1/16m_custom_wakeword.csv
xiaozhi-esp32-2.2.6/partitions/v1/16m_echoear.csv
xiaozhi-esp32-2.2.6/partitions/v1/32m.csv
xiaozhi-esp32-2.2.6/partitions/v1/4m.csv
xiaozhi-esp32-2.2.6/partitions/v1/4m_esp-hi.csv
xiaozhi-esp32-2.2.6/partitions/v1/8m.csv
xiaozhi-esp32-2.2.6/partitions/v2/16m.csv
xiaozhi-esp32-2.2.6/partitions/v2/16m_c3.csv
xiaozhi-esp32-2.2.6/partitions/v2/32m.csv
xiaozhi-esp32-2.2.6/partitions/v2/4m.csv
xiaozhi-esp32-2.2.6/partitions/v2/8m.csv
xiaozhi-esp32-2.2.6/partitions/v2/README.md
xiaozhi-esp32-2.2.6/scripts/Image_Converter/LVGLImage.py
xiaozhi-esp32-2.2.6/scripts/Image_Converter/README.md
xiaozhi-esp32-2.2.6/scripts/Image_Converter/lvgl_tools_gui.py
xiaozhi-esp32-2.2.6/scripts/Image_Converter/requirements.txt
xiaozhi-esp32-2.2.6/scripts/acoustic_check/demod.py
xiaozhi-esp32-2.2.6/scripts/acoustic_check/graphic.py
xiaozhi-esp32-2.2.6/scripts/acoustic_check/main.py
xiaozhi-esp32-2.2.6/scripts/acoustic_check/readme.md
xiaozhi-esp32-2.2.6/scripts/acoustic_check/requirements.txt
xiaozhi-esp32-2.2.6/scripts/audio_debug_server.py
xiaozhi-esp32-2.2.6/scripts/build_default_assets.py
xiaozhi-esp32-2.2.6/scripts/download_github_runs.py
xiaozhi-esp32-2.2.6/scripts/gen_lang.py
xiaozhi-esp32-2.2.6/scripts/mp3_to_ogg.sh
xiaozhi-esp32-2.2.6/scripts/ogg_converter/README.md
xiaozhi-esp32-2.2.6/scripts/ogg_converter/xiaozhi_ogg_converter.py
xiaozhi-esp32-2.2.6/scripts/p3_tools/README.md
xiaozhi-esp32-2.2.6/scripts/p3_tools/batch_convert_gui.py
xiaozhi-esp32-2.2.6/scripts/p3_tools/convert_audio_to_p3.py
xiaozhi-esp32-2.2.6/scripts/p3_tools/convert_p3_to_audio.py
xiaozhi-esp32-2.2.6/scripts/p3_tools/img/img.png
xiaozhi-esp32-2.2.6/scripts/p3_tools/p3_gui_player.py
xiaozhi-esp32-2.2.6/scripts/p3_tools/play_p3.py
xiaozhi-esp32-2.2.6/scripts/p3_tools/requirements.txt
xiaozhi-esp32-2.2.6/scripts/release.py
xiaozhi-esp32-2.2.6/scripts/sonic_wifi_config.html
xiaozhi-esp32-2.2.6/scripts/spiffs_assets/README.md
xiaozhi-esp32-2.2.6/scripts/spiffs_assets/build.py
xiaozhi-esp32-2.2.6/scripts/spiffs_assets/build_all.py
xiaozhi-esp32-2.2.6/scripts/spiffs_assets/pack_model.py
xiaozhi-esp32-2.2.6/scripts/spiffs_assets/spiffs_assets_gen.py
xiaozhi-esp32-2.2.6/scripts/versions.py
xiaozhi-esp32-2.2.6/sdkconfig.defaults
xiaozhi-esp32-2.2.6/sdkconfig.defaults.esp32
xiaozhi-esp32-2.2.6/sdkconfig.defaults.esp32c3
xiaozhi-esp32-2.2.6/sdkconfig.defaults.esp32c5
xiaozhi-esp32-2.2.6/sdkconfig.defaults.esp32c6
xiaozhi-esp32-2.2.6/sdkconfig.defaults.esp32p4
xiaozhi-esp32-2.2.6/sdkconfig.defaults.esp32s3 |