xiaozhi-sdk 0.0.4__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xiaozhi-sdk might be problematic. Click here for more details.
- {xiaozhi_sdk-0.0.4/xiaozhi_sdk.egg-info → xiaozhi_sdk-0.0.6}/PKG-INFO +9 -19
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/README.md +5 -17
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/pyproject.toml +4 -1
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/tests/test_iot.py +14 -9
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/tests/test_xiaozhi.py +12 -4
- xiaozhi_sdk-0.0.6/xiaozhi_sdk/__init__.py +3 -0
- xiaozhi_sdk-0.0.6/xiaozhi_sdk/__main__.py +11 -0
- xiaozhi_sdk-0.0.4/xiaozhi_sdk/__main__.py → xiaozhi_sdk-0.0.6/xiaozhi_sdk/cli.py +38 -24
- xiaozhi_sdk-0.0.6/xiaozhi_sdk/config.py +3 -0
- xiaozhi_sdk-0.0.4/xiaozhi_sdk/__init__.py → xiaozhi_sdk-0.0.6/xiaozhi_sdk/core.py +67 -32
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/iot.py +8 -2
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/mcp.py +2 -2
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/opus.py +1 -1
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6/xiaozhi_sdk.egg-info}/PKG-INFO +9 -19
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk.egg-info/SOURCES.txt +2 -1
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk.egg-info/requires.txt +3 -1
- xiaozhi_sdk-0.0.4/tests/test_opus.py +0 -9
- xiaozhi_sdk-0.0.4/xiaozhi_sdk/config.py +0 -3
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/LICENSE +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/MANIFEST.in +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/audio/greet.wav +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/audio/say_hello.wav +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/audio/take_photo.wav +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/image/leijun.jpg +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/opus/linux-arm64-libopus.so +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/opus/linux-x64-libopus.so +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/opus/macos-arm64-libopus.dylib +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/opus/macos-x64-libopus.dylib +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/file/opus/windows-x86_64-opus.dll +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/setup.cfg +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/tests/test_pic.py +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/data.py +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/utils.py +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk.egg-info/dependency_links.txt +0 -0
- {xiaozhi_sdk-0.0.4 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xiaozhi-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
|
|
5
5
|
Author-email: dairoot <623815825@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -12,13 +12,15 @@ Requires-Python: >=3.8.1
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy
|
|
15
|
-
Requires-Dist: websockets
|
|
15
|
+
Requires-Dist: websockets>=15.0.1
|
|
16
16
|
Requires-Dist: aiohttp
|
|
17
17
|
Requires-Dist: av
|
|
18
18
|
Requires-Dist: opuslib
|
|
19
19
|
Requires-Dist: requests
|
|
20
20
|
Requires-Dist: sounddevice
|
|
21
21
|
Requires-Dist: python-socks
|
|
22
|
+
Requires-Dist: click
|
|
23
|
+
Requires-Dist: colorlog
|
|
22
24
|
Dynamic: license-file
|
|
23
25
|
|
|
24
26
|
# 小智SDK (XiaoZhi SDK)
|
|
@@ -53,21 +55,7 @@ pip install xiaozhi-sdk
|
|
|
53
55
|
#### 查看帮助信息
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
|
-
python -m xiaozhi_sdk
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
输出示例:
|
|
60
|
-
```text
|
|
61
|
-
positional arguments:
|
|
62
|
-
device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
63
|
-
|
|
64
|
-
options:
|
|
65
|
-
-h, --help show this help message and exit
|
|
66
|
-
--url URL 服务端websocket地址
|
|
67
|
-
--ota_url OTA_URL OTA地址
|
|
68
|
-
--serial_number SERIAL_NUMBER 设备的序列号
|
|
69
|
-
--license_key LICENSE_KEY 设备的授权密钥
|
|
70
|
-
|
|
58
|
+
python -m xiaozhi_sdk --help
|
|
71
59
|
```
|
|
72
60
|
|
|
73
61
|
#### 连接设备(需要提供 MAC 地址)
|
|
@@ -76,11 +64,13 @@ options:
|
|
|
76
64
|
python -m xiaozhi_sdk 00:22:44:66:88:00
|
|
77
65
|
```
|
|
78
66
|
|
|
79
|
-
### 2. 编程使用
|
|
67
|
+
### 2. 编程使用 (高阶用法)
|
|
80
68
|
参考 [examples](examples/) 文件中的示例代码,可以快速开始使用 SDK。
|
|
81
69
|
|
|
82
70
|
|
|
83
|
-
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ✅ 运行测试
|
|
84
74
|
|
|
85
75
|
```bash
|
|
86
76
|
pytest tests/
|
|
@@ -30,21 +30,7 @@ pip install xiaozhi-sdk
|
|
|
30
30
|
#### 查看帮助信息
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
python -m xiaozhi_sdk
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
输出示例:
|
|
37
|
-
```text
|
|
38
|
-
positional arguments:
|
|
39
|
-
device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
40
|
-
|
|
41
|
-
options:
|
|
42
|
-
-h, --help show this help message and exit
|
|
43
|
-
--url URL 服务端websocket地址
|
|
44
|
-
--ota_url OTA_URL OTA地址
|
|
45
|
-
--serial_number SERIAL_NUMBER 设备的序列号
|
|
46
|
-
--license_key LICENSE_KEY 设备的授权密钥
|
|
47
|
-
|
|
33
|
+
python -m xiaozhi_sdk --help
|
|
48
34
|
```
|
|
49
35
|
|
|
50
36
|
#### 连接设备(需要提供 MAC 地址)
|
|
@@ -53,11 +39,13 @@ options:
|
|
|
53
39
|
python -m xiaozhi_sdk 00:22:44:66:88:00
|
|
54
40
|
```
|
|
55
41
|
|
|
56
|
-
### 2. 编程使用
|
|
42
|
+
### 2. 编程使用 (高阶用法)
|
|
57
43
|
参考 [examples](examples/) 文件中的示例代码,可以快速开始使用 SDK。
|
|
58
44
|
|
|
59
45
|
|
|
60
|
-
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ✅ 运行测试
|
|
61
49
|
|
|
62
50
|
```bash
|
|
63
51
|
pytest tests/
|
|
@@ -12,13 +12,15 @@ license = {text = "MIT"}
|
|
|
12
12
|
requires-python = ">=3.8.1"
|
|
13
13
|
dependencies = [
|
|
14
14
|
"numpy",
|
|
15
|
-
"websockets",
|
|
15
|
+
"websockets>=15.0.1",
|
|
16
16
|
"aiohttp",
|
|
17
17
|
"av",
|
|
18
18
|
"opuslib",
|
|
19
19
|
"requests",
|
|
20
20
|
"sounddevice",
|
|
21
21
|
"python-socks",
|
|
22
|
+
"click",
|
|
23
|
+
"colorlog",
|
|
22
24
|
]
|
|
23
25
|
classifiers = [
|
|
24
26
|
"Programming Language :: Python :: 3",
|
|
@@ -44,6 +46,7 @@ xiaozhi_sdk = ["../file/**/*"]
|
|
|
44
46
|
[tool.coverage.run]
|
|
45
47
|
omit = [
|
|
46
48
|
"xiaozhi_sdk/__main__.py",
|
|
49
|
+
"xiaozhi_sdk/cli.py",
|
|
47
50
|
"tests/*",
|
|
48
51
|
]
|
|
49
52
|
|
|
@@ -1,23 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
1
3
|
import os
|
|
2
4
|
import sys
|
|
3
5
|
import uuid
|
|
4
6
|
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
7
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
8
8
|
|
|
9
9
|
from xiaozhi_sdk.iot import OtaDevice
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
async def test_main():
|
|
12
|
+
async def iot_main():
|
|
14
13
|
serial_number = ""
|
|
15
14
|
license_key = ""
|
|
16
|
-
mac_address = "00:22:44:66:88:
|
|
15
|
+
mac_address = "00:22:44:66:88:14"
|
|
17
16
|
ota_url = "http://localhost:3080/api/ota"
|
|
18
|
-
|
|
17
|
+
ota_url = None
|
|
19
18
|
ota = OtaDevice(mac_addr=mac_address, client_id=str(uuid.uuid4()), serial_number=serial_number, ota_url=ota_url)
|
|
20
19
|
res = await ota.activate_device()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
print(json.dumps(res["mqtt"]))
|
|
21
|
+
|
|
22
|
+
# if not res.get("activation"):
|
|
23
|
+
# return
|
|
24
|
+
# await ota.check_activate(res["activation"]["challenge"], license_key)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
asyncio.run(iot_main())
|
|
@@ -13,11 +13,11 @@ from xiaozhi_sdk import XiaoZhiWebsocket
|
|
|
13
13
|
from xiaozhi_sdk.utils import read_audio_file
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
async def assistant_audio_play(audio_queue, wait_time=
|
|
16
|
+
async def assistant_audio_play(audio_queue, wait_time=5):
|
|
17
17
|
# 创建一个持续播放的流
|
|
18
18
|
stream = sd.OutputStream(samplerate=16000, channels=1, dtype=np.int16)
|
|
19
19
|
stream.start()
|
|
20
|
-
last_time =
|
|
20
|
+
last_time = int(time.time())
|
|
21
21
|
while True:
|
|
22
22
|
if not audio_queue:
|
|
23
23
|
await asyncio.sleep(0.01)
|
|
@@ -62,13 +62,21 @@ async def message_handler_callback(message):
|
|
|
62
62
|
pass
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
MAC_ADDR = "
|
|
65
|
+
MAC_ADDR = "64:e8:13:18:21:1c"
|
|
66
|
+
|
|
67
|
+
ota_url = "http://localhost:3080/api/ota"
|
|
68
|
+
URL = "ws://120.79.156.134:8380"
|
|
69
|
+
|
|
70
|
+
ota_url = None
|
|
66
71
|
URL = None
|
|
67
72
|
|
|
68
73
|
|
|
74
|
+
# URL = None
|
|
75
|
+
|
|
76
|
+
|
|
69
77
|
@pytest.mark.asyncio
|
|
70
78
|
async def test_main():
|
|
71
|
-
xiaozhi = XiaoZhiWebsocket(message_handler_callback, url=URL)
|
|
79
|
+
xiaozhi = XiaoZhiWebsocket(message_handler_callback, url=URL, ota_url=ota_url)
|
|
72
80
|
await xiaozhi.set_mcp_tool_callback(mcp_tool_func())
|
|
73
81
|
await xiaozhi.init_connection(MAC_ADDR)
|
|
74
82
|
|
|
@@ -1,21 +1,36 @@
|
|
|
1
|
-
import argparse
|
|
2
1
|
import asyncio
|
|
3
2
|
import logging
|
|
4
3
|
import time
|
|
5
4
|
from collections import deque
|
|
6
5
|
from typing import Optional
|
|
7
6
|
|
|
7
|
+
import click
|
|
8
|
+
import colorlog
|
|
8
9
|
import numpy as np
|
|
9
10
|
import sounddevice as sd
|
|
10
11
|
|
|
11
12
|
from xiaozhi_sdk import XiaoZhiWebsocket
|
|
12
13
|
from xiaozhi_sdk.config import INPUT_SERVER_AUDIO_SAMPLE_RATE
|
|
13
14
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# 配置彩色logging
|
|
16
|
+
handler = colorlog.StreamHandler()
|
|
17
|
+
handler.setFormatter(
|
|
18
|
+
colorlog.ColoredFormatter(
|
|
19
|
+
"%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
20
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
21
|
+
log_colors={
|
|
22
|
+
"DEBUG": "green",
|
|
23
|
+
"INFO": "white",
|
|
24
|
+
"WARNING": "yellow",
|
|
25
|
+
"ERROR": "red",
|
|
26
|
+
"CRITICAL": "red,bg_white",
|
|
27
|
+
},
|
|
28
|
+
)
|
|
17
29
|
)
|
|
30
|
+
|
|
18
31
|
logger = logging.getLogger("xiaozhi_sdk")
|
|
32
|
+
logger.addHandler(handler)
|
|
33
|
+
logger.setLevel(logging.DEBUG)
|
|
19
34
|
|
|
20
35
|
# 全局状态
|
|
21
36
|
input_audio_buffer: deque[bytes] = deque()
|
|
@@ -66,11 +81,12 @@ class XiaoZhiClient:
|
|
|
66
81
|
self.xiaozhi: Optional[XiaoZhiWebsocket] = None
|
|
67
82
|
self.url = url
|
|
68
83
|
self.ota_url = ota_url
|
|
84
|
+
self.mac_address = ""
|
|
69
85
|
|
|
70
86
|
async def start(self, mac_address: str, serial_number: str = "", license_key: str = ""):
|
|
71
87
|
"""启动客户端连接"""
|
|
72
88
|
self.mac_address = mac_address
|
|
73
|
-
self.xiaozhi = XiaoZhiWebsocket(handle_message, url=self.url, ota_url=self.ota_url)
|
|
89
|
+
self.xiaozhi = XiaoZhiWebsocket(handle_message, url=self.url, ota_url=self.ota_url, send_wake=True)
|
|
74
90
|
await self.xiaozhi.init_connection(
|
|
75
91
|
self.mac_address, aec=False, serial_number=serial_number, license_key=license_key
|
|
76
92
|
)
|
|
@@ -97,27 +113,25 @@ class XiaoZhiClient:
|
|
|
97
113
|
await self.xiaozhi.send_audio(pcm_data)
|
|
98
114
|
|
|
99
115
|
|
|
100
|
-
async def
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
parser.add_argument("--ota_url", help="OTA地址")
|
|
106
|
-
|
|
107
|
-
parser.add_argument("--serial_number", default="", help="设备的序列号")
|
|
108
|
-
parser.add_argument("--license_key", default="", help="设备的授权密钥")
|
|
109
|
-
|
|
110
|
-
args = parser.parse_args()
|
|
111
|
-
logger.info("Recording... Press Ctrl+C to stop.")
|
|
112
|
-
client = XiaoZhiClient(args.url, args.ota_url)
|
|
113
|
-
await client.start(args.device, args.serial_number, args.license_key)
|
|
116
|
+
async def run_client(mac_address: str, url: str, ota_url: str, serial_number: str, license_key: str):
|
|
117
|
+
"""运行客户端的异步函数"""
|
|
118
|
+
logger.debug("Recording... Press Ctrl+C to stop.")
|
|
119
|
+
client = XiaoZhiClient(url, ota_url)
|
|
120
|
+
await client.start(mac_address, serial_number, license_key)
|
|
114
121
|
|
|
115
122
|
with sd.InputStream(callback=client.audio_callback, channels=1, samplerate=16000, blocksize=960):
|
|
116
123
|
await client.process_audio_input()
|
|
117
124
|
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
@click.command()
|
|
127
|
+
@click.argument("mac_address")
|
|
128
|
+
@click.option("--url", help="服务端websocket地址")
|
|
129
|
+
@click.option("--ota_url", help="OTA地址")
|
|
130
|
+
@click.option("--serial_number", default="", help="设备的序列号")
|
|
131
|
+
@click.option("--license_key", default="", help="设备的授权密钥")
|
|
132
|
+
def main(mac_address: str, url: str, ota_url: str, serial_number: str, license_key: str):
|
|
133
|
+
"""小智SDK客户端
|
|
134
|
+
|
|
135
|
+
MAC_ADDRESS: 设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
136
|
+
"""
|
|
137
|
+
asyncio.run(run_client(mac_address, url, ota_url, serial_number, license_key))
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
__version__ = "0.0.4"
|
|
2
|
-
|
|
3
1
|
import asyncio
|
|
4
2
|
import json
|
|
5
3
|
import logging
|
|
@@ -31,16 +29,20 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
31
29
|
ota_url: Optional[str] = None,
|
|
32
30
|
audio_sample_rate: int = 16000,
|
|
33
31
|
audio_channels: int = 1,
|
|
32
|
+
send_wake: bool = False,
|
|
34
33
|
):
|
|
35
34
|
super().__init__()
|
|
36
35
|
self.url = url
|
|
37
36
|
self.ota_url = ota_url
|
|
37
|
+
self.send_wake = send_wake
|
|
38
38
|
self.audio_channels = audio_channels
|
|
39
39
|
self.audio_opus = AudioOpus(audio_sample_rate, audio_channels)
|
|
40
40
|
|
|
41
41
|
# 客户端标识
|
|
42
42
|
self.client_id = str(uuid.uuid4())
|
|
43
43
|
self.mac_addr: Optional[str] = None
|
|
44
|
+
self.aec = False
|
|
45
|
+
self.websocket_token = ""
|
|
44
46
|
|
|
45
47
|
# 回调函数
|
|
46
48
|
self.message_handler_callback = message_handler_callback
|
|
@@ -56,17 +58,19 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
56
58
|
|
|
57
59
|
# OTA设备
|
|
58
60
|
self.ota: Optional[OtaDevice] = None
|
|
61
|
+
self.iot_task: Optional[asyncio.Task] = None
|
|
62
|
+
self.wait_device_activated: bool = False
|
|
59
63
|
|
|
60
64
|
async def _send_hello(self, aec: bool) -> None:
|
|
61
65
|
"""发送hello消息"""
|
|
62
66
|
hello_message = {
|
|
63
67
|
"type": "hello",
|
|
64
68
|
"version": 1,
|
|
65
|
-
"features": {"
|
|
69
|
+
"features": {"mcp": True, "aec": aec},
|
|
66
70
|
"transport": "websocket",
|
|
67
71
|
"audio_params": {
|
|
68
72
|
"format": "opus",
|
|
69
|
-
"sample_rate":
|
|
73
|
+
"sample_rate": 16000,
|
|
70
74
|
"channels": 1,
|
|
71
75
|
"frame_duration": 60,
|
|
72
76
|
},
|
|
@@ -76,24 +80,27 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
76
80
|
|
|
77
81
|
async def _start_listen(self) -> None:
|
|
78
82
|
"""开始监听"""
|
|
79
|
-
|
|
80
83
|
listen_message = {"session_id": self.session_id, "type": "listen", "state": "start", "mode": "realtime"}
|
|
81
84
|
await self.websocket.send(json.dumps(listen_message))
|
|
82
85
|
|
|
86
|
+
async def is_activate(self, ota_info):
|
|
87
|
+
"""是否激活"""
|
|
88
|
+
if ota_info.get("activation"):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
return True
|
|
92
|
+
|
|
83
93
|
async def _activate_iot_device(self, license_key: str, ota_info: Dict[str, Any]) -> None:
|
|
84
94
|
"""激活IoT设备"""
|
|
85
|
-
if not ota_info.get("activation"):
|
|
86
|
-
return
|
|
87
|
-
|
|
88
95
|
if not self.ota:
|
|
89
96
|
return
|
|
90
97
|
|
|
91
|
-
await self._send_demo_audio()
|
|
92
98
|
challenge = ota_info["activation"]["challenge"]
|
|
93
99
|
await asyncio.sleep(3)
|
|
94
|
-
|
|
100
|
+
self.wait_device_activated = True
|
|
95
101
|
for _ in range(10):
|
|
96
102
|
if await self.ota.check_activate(challenge, license_key):
|
|
103
|
+
self.wait_device_activated = False
|
|
97
104
|
break
|
|
98
105
|
await asyncio.sleep(3)
|
|
99
106
|
|
|
@@ -109,6 +116,12 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
109
116
|
await self.websocket.send(opus_data)
|
|
110
117
|
await self.send_silence_audio()
|
|
111
118
|
|
|
119
|
+
async def send_wake_word(self, wake_word: str = "你好,小智") -> None:
|
|
120
|
+
"""发送唤醒词"""
|
|
121
|
+
await self.websocket.send(
|
|
122
|
+
json.dumps({"session_id": self.session_id, "type": "listen", "state": "detect", "text": wake_word})
|
|
123
|
+
)
|
|
124
|
+
|
|
112
125
|
async def send_silence_audio(self, duration_seconds: float = 1.2) -> None:
|
|
113
126
|
"""发送静音音频"""
|
|
114
127
|
frames_count = int(duration_seconds * 1000 / 60)
|
|
@@ -147,48 +160,64 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
147
160
|
await self.message_handler_callback(
|
|
148
161
|
{"type": "websocket", "state": "close", "source": "sdk.message_handler"}
|
|
149
162
|
)
|
|
150
|
-
logger.
|
|
163
|
+
logger.debug("[websocket] close")
|
|
151
164
|
|
|
152
165
|
async def set_mcp_tool_callback(self, tool_func: Dict[str, Callable[..., Any]]) -> None:
|
|
153
166
|
"""设置MCP工具回调函数"""
|
|
154
167
|
self.tool_func = tool_func
|
|
155
168
|
|
|
169
|
+
async def connect_websocket(self, websocket_token):
|
|
170
|
+
"""连接websocket"""
|
|
171
|
+
headers = {
|
|
172
|
+
"Authorization": "Bearer {}".format(websocket_token),
|
|
173
|
+
"Protocol-Version": "1",
|
|
174
|
+
"Device-Id": self.mac_addr,
|
|
175
|
+
"Client-Id": self.client_id,
|
|
176
|
+
}
|
|
177
|
+
try:
|
|
178
|
+
self.websocket = await websockets.connect(uri=self.url, additional_headers=headers)
|
|
179
|
+
except websockets.exceptions.InvalidMessage as e:
|
|
180
|
+
logger.error("[websocket] 连接失败,请检查网络连接或设备状态。当前链接地址: %s, 错误信息:%s", self.url, e)
|
|
181
|
+
return
|
|
182
|
+
self.message_handler_task = asyncio.create_task(self._message_handler())
|
|
183
|
+
|
|
184
|
+
await self._send_hello(self.aec)
|
|
185
|
+
await self._start_listen()
|
|
186
|
+
logger.debug("[websocket] Connection successful")
|
|
187
|
+
await asyncio.sleep(0.5)
|
|
188
|
+
|
|
156
189
|
async def init_connection(
|
|
157
190
|
self, mac_addr: str, aec: bool = False, serial_number: str = "", license_key: str = ""
|
|
158
191
|
) -> None:
|
|
159
192
|
"""初始化连接"""
|
|
160
|
-
# 校验MAC地址格式 XX:XX:XX:XX:XX:XX
|
|
161
193
|
mac_pattern = r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"
|
|
162
194
|
if not re.match(mac_pattern, mac_addr):
|
|
163
195
|
raise ValueError(f"无效的MAC地址格式: {mac_addr}。正确格式应为 XX:XX:XX:XX:XX:XX")
|
|
164
196
|
|
|
165
197
|
self.mac_addr = mac_addr.lower()
|
|
198
|
+
self.aec = aec
|
|
166
199
|
|
|
167
200
|
self.ota = OtaDevice(self.mac_addr, self.client_id, self.ota_url, serial_number)
|
|
168
201
|
ota_info = await self.ota.activate_device()
|
|
169
|
-
ws_url = ota_info
|
|
202
|
+
ws_url = ota_info.get("websocket", {}).get("url")
|
|
170
203
|
self.url = self.url or ws_url
|
|
171
204
|
|
|
205
|
+
if not self.url:
|
|
206
|
+
logger.warning("[websocket] 未找到websocket链接地址")
|
|
207
|
+
return
|
|
208
|
+
|
|
172
209
|
if "tenclass.net" not in self.url and "xiaozhi.me" not in self.url:
|
|
173
|
-
logger.warning("[websocket]
|
|
210
|
+
logger.warning("[websocket] 检测到非官方服务器,当前链接地址: %s", self.url)
|
|
174
211
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"Protocol-Version": "1",
|
|
178
|
-
"Device-Id": self.mac_addr,
|
|
179
|
-
"Client-Id": self.client_id,
|
|
180
|
-
}
|
|
181
|
-
try:
|
|
182
|
-
self.websocket = await websockets.connect(uri=self.url, additional_headers=headers)
|
|
183
|
-
except websockets.exceptions.InvalidMessage as e:
|
|
184
|
-
logger.error("[websocket] 连接失败,请检查网络连接或设备状态。当前链接地址: %s, 错误信息:%s", self.url, e)
|
|
185
|
-
return
|
|
186
|
-
self.message_handler_task = asyncio.create_task(self._message_handler())
|
|
212
|
+
self.websocket_token = ota_info["websocket"]["token"]
|
|
213
|
+
await self.connect_websocket(self.websocket_token)
|
|
187
214
|
|
|
188
|
-
await self.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
215
|
+
if not await self.is_activate(ota_info):
|
|
216
|
+
self.iot_task = asyncio.create_task(self._activate_iot_device(license_key, ota_info))
|
|
217
|
+
logger.debug("[IOT] 设备未激活")
|
|
218
|
+
|
|
219
|
+
if self.send_wake:
|
|
220
|
+
await self.send_wake_word()
|
|
192
221
|
|
|
193
222
|
async def send_audio(self, pcm: bytes) -> None:
|
|
194
223
|
"""发送音频数据"""
|
|
@@ -200,10 +229,13 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
200
229
|
opus_data = await self.audio_opus.pcm_to_opus(pcm)
|
|
201
230
|
await self.websocket.send(opus_data)
|
|
202
231
|
elif state in [websockets.protocol.State.CLOSED, websockets.protocol.State.CLOSING]:
|
|
203
|
-
if self.
|
|
232
|
+
if self.wait_device_activated:
|
|
233
|
+
logger.debug("[websocket] Server actively disconnected, reconnecting...")
|
|
234
|
+
await self.connect_websocket(self.websocket_token)
|
|
235
|
+
elif self.message_handler_callback:
|
|
204
236
|
await self.message_handler_callback({"type": "websocket", "state": "close", "source": "sdk.send_audio"})
|
|
205
237
|
self.websocket = None
|
|
206
|
-
logger.
|
|
238
|
+
logger.debug("[websocket] Server actively disconnected")
|
|
207
239
|
|
|
208
240
|
await asyncio.sleep(0.5)
|
|
209
241
|
else:
|
|
@@ -218,5 +250,8 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
218
250
|
except asyncio.CancelledError:
|
|
219
251
|
pass
|
|
220
252
|
|
|
253
|
+
if self.iot_task:
|
|
254
|
+
self.iot_task.cancel()
|
|
255
|
+
|
|
221
256
|
if self.websocket:
|
|
222
257
|
await self.websocket.close()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import hmac
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
from typing import Any, Dict, Optional
|
|
5
6
|
|
|
6
7
|
import aiohttp
|
|
@@ -13,6 +14,8 @@ BOARD_TYPE = "xiaozhi-sdk-box"
|
|
|
13
14
|
USER_AGENT = "xiaozhi-sdk/{}".format(__version__)
|
|
14
15
|
BOARD_NAME = "xiaozhi-sdk-{}".format(__version__)
|
|
15
16
|
|
|
17
|
+
logger = logging.getLogger("xiaozhi_sdk")
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
class OtaDevice:
|
|
18
21
|
"""
|
|
@@ -56,7 +59,7 @@ class OtaDevice:
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
async with aiohttp.ClientSession() as session:
|
|
59
|
-
async with session.post(self.ota_url, headers=headers, data=json.dumps(payload)) as response:
|
|
62
|
+
async with session.post(self.ota_url + "/", headers=headers, data=json.dumps(payload)) as response:
|
|
60
63
|
response.raise_for_status()
|
|
61
64
|
return await response.json()
|
|
62
65
|
|
|
@@ -72,4 +75,7 @@ class OtaDevice:
|
|
|
72
75
|
|
|
73
76
|
async with aiohttp.ClientSession() as session:
|
|
74
77
|
async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
|
|
75
|
-
|
|
78
|
+
is_ok = response.status == 200
|
|
79
|
+
if not is_ok:
|
|
80
|
+
logger.debug("[IOT] wait for activate device...")
|
|
81
|
+
return is_ok
|
|
@@ -87,7 +87,7 @@ class McpTool(object):
|
|
|
87
87
|
mcp_tool_conf[name]["name"] = name
|
|
88
88
|
mcp_tools_payload["result"]["tools"].append(mcp_tool_conf[name])
|
|
89
89
|
await self.websocket.send(self.get_mcp_json(mcp_tools_payload))
|
|
90
|
-
logger.
|
|
90
|
+
logger.debug("[MCP] 加载成功,当前可用工具列表为:%s", tool_list)
|
|
91
91
|
|
|
92
92
|
elif method == "tools/call":
|
|
93
93
|
tool_name = payload["params"]["name"]
|
|
@@ -97,6 +97,6 @@ class McpTool(object):
|
|
|
97
97
|
|
|
98
98
|
mcp_res = await self.mcp_tool_call(payload)
|
|
99
99
|
await self.websocket.send(mcp_res)
|
|
100
|
-
logger.
|
|
100
|
+
logger.debug("[MCP] Tool %s called", tool_name)
|
|
101
101
|
else:
|
|
102
102
|
logger.warning("[MCP] unknown method %s: %s", method, payload)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xiaozhi-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
|
|
5
5
|
Author-email: dairoot <623815825@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -12,13 +12,15 @@ Requires-Python: >=3.8.1
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy
|
|
15
|
-
Requires-Dist: websockets
|
|
15
|
+
Requires-Dist: websockets>=15.0.1
|
|
16
16
|
Requires-Dist: aiohttp
|
|
17
17
|
Requires-Dist: av
|
|
18
18
|
Requires-Dist: opuslib
|
|
19
19
|
Requires-Dist: requests
|
|
20
20
|
Requires-Dist: sounddevice
|
|
21
21
|
Requires-Dist: python-socks
|
|
22
|
+
Requires-Dist: click
|
|
23
|
+
Requires-Dist: colorlog
|
|
22
24
|
Dynamic: license-file
|
|
23
25
|
|
|
24
26
|
# 小智SDK (XiaoZhi SDK)
|
|
@@ -53,21 +55,7 @@ pip install xiaozhi-sdk
|
|
|
53
55
|
#### 查看帮助信息
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
|
-
python -m xiaozhi_sdk
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
输出示例:
|
|
60
|
-
```text
|
|
61
|
-
positional arguments:
|
|
62
|
-
device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
63
|
-
|
|
64
|
-
options:
|
|
65
|
-
-h, --help show this help message and exit
|
|
66
|
-
--url URL 服务端websocket地址
|
|
67
|
-
--ota_url OTA_URL OTA地址
|
|
68
|
-
--serial_number SERIAL_NUMBER 设备的序列号
|
|
69
|
-
--license_key LICENSE_KEY 设备的授权密钥
|
|
70
|
-
|
|
58
|
+
python -m xiaozhi_sdk --help
|
|
71
59
|
```
|
|
72
60
|
|
|
73
61
|
#### 连接设备(需要提供 MAC 地址)
|
|
@@ -76,11 +64,13 @@ options:
|
|
|
76
64
|
python -m xiaozhi_sdk 00:22:44:66:88:00
|
|
77
65
|
```
|
|
78
66
|
|
|
79
|
-
### 2. 编程使用
|
|
67
|
+
### 2. 编程使用 (高阶用法)
|
|
80
68
|
参考 [examples](examples/) 文件中的示例代码,可以快速开始使用 SDK。
|
|
81
69
|
|
|
82
70
|
|
|
83
|
-
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ✅ 运行测试
|
|
84
74
|
|
|
85
75
|
```bash
|
|
86
76
|
pytest tests/
|
|
@@ -12,12 +12,13 @@ file/opus/macos-arm64-libopus.dylib
|
|
|
12
12
|
file/opus/macos-x64-libopus.dylib
|
|
13
13
|
file/opus/windows-x86_64-opus.dll
|
|
14
14
|
tests/test_iot.py
|
|
15
|
-
tests/test_opus.py
|
|
16
15
|
tests/test_pic.py
|
|
17
16
|
tests/test_xiaozhi.py
|
|
18
17
|
xiaozhi_sdk/__init__.py
|
|
19
18
|
xiaozhi_sdk/__main__.py
|
|
19
|
+
xiaozhi_sdk/cli.py
|
|
20
20
|
xiaozhi_sdk/config.py
|
|
21
|
+
xiaozhi_sdk/core.py
|
|
21
22
|
xiaozhi_sdk/data.py
|
|
22
23
|
xiaozhi_sdk/iot.py
|
|
23
24
|
xiaozhi_sdk/mcp.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|