xiaozhi-sdk 0.0.5__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.

Files changed (34) hide show
  1. {xiaozhi_sdk-0.0.5/xiaozhi_sdk.egg-info → xiaozhi_sdk-0.0.6}/PKG-INFO +8 -18
  2. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/README.md +5 -17
  3. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/pyproject.toml +3 -0
  4. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/tests/test_iot.py +13 -8
  5. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/tests/test_xiaozhi.py +12 -4
  6. xiaozhi_sdk-0.0.6/xiaozhi_sdk/__init__.py +3 -0
  7. xiaozhi_sdk-0.0.6/xiaozhi_sdk/__main__.py +11 -0
  8. xiaozhi_sdk-0.0.5/xiaozhi_sdk/__main__.py → xiaozhi_sdk-0.0.6/xiaozhi_sdk/cli.py +38 -24
  9. xiaozhi_sdk-0.0.5/xiaozhi_sdk/__init__.py → xiaozhi_sdk-0.0.6/xiaozhi_sdk/core.py +61 -30
  10. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/iot.py +7 -1
  11. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/mcp.py +2 -2
  12. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/opus.py +1 -1
  13. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6/xiaozhi_sdk.egg-info}/PKG-INFO +8 -18
  14. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk.egg-info/SOURCES.txt +2 -1
  15. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk.egg-info/requires.txt +2 -0
  16. xiaozhi_sdk-0.0.5/tests/test_opus.py +0 -9
  17. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/LICENSE +0 -0
  18. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/MANIFEST.in +0 -0
  19. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/audio/greet.wav +0 -0
  20. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/audio/say_hello.wav +0 -0
  21. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/audio/take_photo.wav +0 -0
  22. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/image/leijun.jpg +0 -0
  23. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/opus/linux-arm64-libopus.so +0 -0
  24. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/opus/linux-x64-libopus.so +0 -0
  25. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/opus/macos-arm64-libopus.dylib +0 -0
  26. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/opus/macos-x64-libopus.dylib +0 -0
  27. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/file/opus/windows-x86_64-opus.dll +0 -0
  28. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/setup.cfg +0 -0
  29. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/tests/test_pic.py +0 -0
  30. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/config.py +0 -0
  31. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/data.py +0 -0
  32. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk/utils.py +0 -0
  33. {xiaozhi_sdk-0.0.5 → xiaozhi_sdk-0.0.6}/xiaozhi_sdk.egg-info/dependency_links.txt +0 -0
  34. {xiaozhi_sdk-0.0.5 → 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.5
3
+ Version: 0.0.6
4
4
  Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
5
5
  Author-email: dairoot <623815825@qq.com>
6
6
  License: MIT
@@ -19,6 +19,8 @@ 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 -h
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 -h
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/
@@ -19,6 +19,8 @@ dependencies = [
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
- @pytest.mark.asyncio
13
- async def test_main():
12
+ async def iot_main():
14
13
  serial_number = ""
15
14
  license_key = ""
16
- mac_address = ""
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
- if not res.get("activation"):
22
- return
23
- await ota.check_activate(res["activation"]["challenge"], license_key)
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=1):
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 = None
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 = "00:22:44:66:88:00"
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
 
@@ -0,0 +1,3 @@
1
+ __version__ = "0.0.6"
2
+
3
+ from xiaozhi_sdk.core import XiaoZhiWebsocket # noqa
@@ -0,0 +1,11 @@
1
+ import logging
2
+
3
+ from xiaozhi_sdk.cli import main
4
+
5
+ logger = logging.getLogger("xiaozhi_sdk")
6
+
7
+ if __name__ == "__main__":
8
+ try:
9
+ main()
10
+ except KeyboardInterrupt:
11
+ logger.debug("Stopping...")
@@ -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
- # 配置logging
15
- logging.basicConfig(
16
- level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
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 main():
101
- """主函数"""
102
- parser = argparse.ArgumentParser(description="小智SDK客户端")
103
- parser.add_argument("device", help="设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)")
104
- parser.add_argument("--url", help="服务端websocket地址")
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
- if __name__ == "__main__":
120
- try:
121
- asyncio.run(main())
122
- except KeyboardInterrupt:
123
- logger.info("Stopping...")
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.5"
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": {"aec": aec, "mcp": True},
69
+ "features": {"mcp": True, "aec": aec},
66
70
  "transport": "websocket",
67
71
  "audio_params": {
68
72
  "format": "opus",
69
- "sample_rate": INPUT_SERVER_AUDIO_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,22 +160,42 @@ class XiaoZhiWebsocket(McpTool):
147
160
  await self.message_handler_callback(
148
161
  {"type": "websocket", "state": "close", "source": "sdk.message_handler"}
149
162
  )
150
- logger.info("[websocket] close")
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()
@@ -176,23 +209,15 @@ class XiaoZhiWebsocket(McpTool):
176
209
  if "tenclass.net" not in self.url and "xiaozhi.me" not in self.url:
177
210
  logger.warning("[websocket] 检测到非官方服务器,当前链接地址: %s", self.url)
178
211
 
179
- headers = {
180
- "Authorization": "Bearer {}".format(ota_info["websocket"]["token"]),
181
- "Protocol-Version": "1",
182
- "Device-Id": self.mac_addr,
183
- "Client-Id": self.client_id,
184
- }
185
- try:
186
- self.websocket = await websockets.connect(uri=self.url, additional_headers=headers)
187
- except websockets.exceptions.InvalidMessage as e:
188
- logger.error("[websocket] 连接失败,请检查网络连接或设备状态。当前链接地址: %s, 错误信息:%s", self.url, e)
189
- return
190
- 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)
191
214
 
192
- await self._send_hello(aec)
193
- await self._start_listen()
194
- asyncio.create_task(self._activate_iot_device(license_key, ota_info))
195
- await asyncio.sleep(0.5)
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()
196
221
 
197
222
  async def send_audio(self, pcm: bytes) -> None:
198
223
  """发送音频数据"""
@@ -204,10 +229,13 @@ class XiaoZhiWebsocket(McpTool):
204
229
  opus_data = await self.audio_opus.pcm_to_opus(pcm)
205
230
  await self.websocket.send(opus_data)
206
231
  elif state in [websockets.protocol.State.CLOSED, websockets.protocol.State.CLOSING]:
207
- if self.message_handler_callback:
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:
208
236
  await self.message_handler_callback({"type": "websocket", "state": "close", "source": "sdk.send_audio"})
209
237
  self.websocket = None
210
- logger.info("[websocket] close")
238
+ logger.debug("[websocket] Server actively disconnected")
211
239
 
212
240
  await asyncio.sleep(0.5)
213
241
  else:
@@ -222,5 +250,8 @@ class XiaoZhiWebsocket(McpTool):
222
250
  except asyncio.CancelledError:
223
251
  pass
224
252
 
253
+ if self.iot_task:
254
+ self.iot_task.cancel()
255
+
225
256
  if self.websocket:
226
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
  """
@@ -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
- return response.status == 200
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.info("[MCP] 加载成功,当前可用工具列表为:%s", tool_list)
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.info("[MCP] Tool %s called", tool_name)
100
+ logger.debug("[MCP] Tool %s called", tool_name)
101
101
  else:
102
102
  logger.warning("[MCP] unknown method %s: %s", method, payload)
@@ -2,7 +2,7 @@ import av
2
2
  import numpy as np
3
3
  import opuslib
4
4
 
5
- from xiaozhi_sdk import INPUT_SERVER_AUDIO_SAMPLE_RATE
5
+ from xiaozhi_sdk.config import INPUT_SERVER_AUDIO_SAMPLE_RATE
6
6
 
7
7
 
8
8
  class AudioOpus:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xiaozhi-sdk
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
5
5
  Author-email: dairoot <623815825@qq.com>
6
6
  License: MIT
@@ -19,6 +19,8 @@ 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 -h
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
@@ -6,3 +6,5 @@ opuslib
6
6
  requests
7
7
  sounddevice
8
8
  python-socks
9
+ click
10
+ colorlog
@@ -1,9 +0,0 @@
1
- import os
2
- import sys
3
-
4
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
5
-
6
- from xiaozhi_sdk.utils import setup_opus
7
-
8
- setup_opus()
9
- import opuslib
File without changes
File without changes
File without changes