speaker.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import serial
  2. import threading
  3. import time
  4. import struct
  5. import io
  6. import codecs
  7. import sys
  8. from typing import Optional, Dict, Any
  9. # ========== 基础数据结构定义(对应Go的DeviceData) ==========
  10. class DeviceData:
  11. """语音参数结构体(对应Go的DeviceData)"""
  12. def __init__(self, speaker: int = 3, volume: int = 5, tone: int = 5, speed: int = 15):
  13. self.Speaker: int = speaker # 发音人
  14. self.Volume: int = volume # 音量(0-10)
  15. self.Tone: int = tone # 语调(0-10)
  16. self.Speed: int = speed # 语速(0-30)
  17. # ========== 串口语音播报核心类(对应Go的IpCast) ==========
  18. class IpCast:
  19. def __init__(self, port: str):
  20. self.Port: str = port # 串口端口
  21. self.Baud: int = 115200 # 固定波特率115200
  22. self.serialPort: Optional[serial.Serial] = None # 串口实例
  23. self.mu: threading.Lock = threading.Lock() # 全局互斥锁(并发安全)
  24. self.isClosed: bool = False # 串口是否关闭
  25. self.isPlaying: bool = False # 播放状态:True=播放中,False=空闲
  26. # 初始化串口
  27. try:
  28. self.Reconnect()
  29. except Exception as e:
  30. print(f"串口初始化失败: {e}")
  31. # 启动后台监听串口响应的线程(守护线程)
  32. self.listen_thread = threading.Thread(target=self.listenSerialResponse, daemon=True)
  33. self.listen_thread.start()
  34. def Reconnect(self) -> None:
  35. """重连串口(加锁保护,更新串口状态)"""
  36. with self.mu: # 等价于Go的mu.Lock() + defer mu.Unlock()
  37. # 标记关闭并清理旧串口
  38. self.isClosed = True
  39. if self.serialPort is not None and self.serialPort.is_open:
  40. try:
  41. self.serialPort.close()
  42. except Exception:
  43. pass
  44. self.serialPort = None
  45. # 配置串口参数
  46. try:
  47. self.serialPort = serial.Serial(
  48. port=self.Port,
  49. baudrate=self.Baud,
  50. bytesize=serial.EIGHTBITS,
  51. parity=serial.PARITY_NONE,
  52. stopbits=serial.STOPBITS_ONE,
  53. timeout=0.2, # 对应Go的ReadTimeout=200ms
  54. write_timeout=1.0
  55. )
  56. self.isClosed = False
  57. print("串口重连成功")
  58. except Exception as e:
  59. raise Exception(f"打开串口失败: {e}")
  60. def listenSerialResponse(self) -> None:
  61. """后台监听串口返回(核心逻辑:解析0x41/0x4F,更新播放状态)"""
  62. buf = bytearray(64) # 读取缓冲区
  63. while True:
  64. # 先获取当前串口状态(加锁读取)
  65. with self.mu:
  66. isClosed = self.isClosed
  67. serialPort = self.serialPort
  68. # 串口关闭/未初始化,等待1秒重试
  69. if isClosed or serialPort is None:
  70. time.sleep(1)
  71. continue
  72. # 读取串口数据(加锁读取,避免并发读写冲突)
  73. try:
  74. with self.mu:
  75. n = serialPort.readinto(buf) # 读取数据到缓冲区
  76. if n == 0:
  77. continue
  78. # 解析收到的字节
  79. recv_bytes = buf[:n]
  80. for b in recv_bytes:
  81. if b == 0x41:
  82. print("<---- 41 指令接收成功,芯片开始播放")
  83. elif b == 0x4F:
  84. # print("<---- 4F 语音播放完成 ✅,释放播放权限")
  85. # 加锁更新播放状态
  86. with self.mu:
  87. self.isPlaying = False
  88. except Exception as err:
  89. # 处理读取异常
  90. err_msg = str(err)
  91. if "timed out" in err_msg or err == EOFError:
  92. continue
  93. else:
  94. print(f"串口读取异常: {err}")
  95. with self.mu:
  96. self.isClosed = True
  97. continue
  98. def convertToGBK(self, s: str) -> bytes:
  99. """文本转GBK编码(对应Go的convertToGBK)"""
  100. if not s:
  101. raise ValueError("文本不能为空")
  102. try:
  103. # Python直接转GBK编码(等价于Go的simplifiedchinese.GBK.NewEncoder)
  104. gbk_bytes = s.encode('gbk')
  105. except Exception as e:
  106. raise Exception(f"编码转换失败: {e}")
  107. # 关键修改:用bytes内置的hex()方法替代错误的hex_module.hexlify
  108. # print(f"文本「{s}」的GBK编码: {gbk_bytes.hex()}")
  109. if len(gbk_bytes) == 0:
  110. raise ValueError("GBK转换结果为空")
  111. return gbk_bytes
  112. def Speak(self, txt: str) -> None:
  113. """发送语音指令(核心改版,极简逻辑)"""
  114. # 第一步:加锁判断播放状态,播放中直接丢弃
  115. with self.mu:
  116. if self.isPlaying:
  117. # print(f"[丢弃雷达信号] 语音播放中,拒绝执行:{txt}")
  118. return
  119. # 空闲状态,标记为播放中
  120. self.isPlaying = True
  121. # 异常兜底:任何失败都重置播放状态
  122. def reset_playing_state():
  123. with self.mu:
  124. if self.isClosed:
  125. self.isPlaying = False
  126. print("[异常] 串口关闭,重置播放状态为空闲")
  127. try:
  128. # 校验串口状态
  129. with self.mu:
  130. isClosed = self.isClosed
  131. serialPort = self.serialPort
  132. if isClosed or serialPort is None:
  133. print("串口未连接,尝试重连...")
  134. try:
  135. self.Reconnect()
  136. except Exception as e:
  137. print(f"串口重连失败,无法发送指令: {e}")
  138. with self.mu:
  139. self.isPlaying = False
  140. return
  141. # 重连后重新获取串口实例
  142. with self.mu:
  143. serialPort = self.serialPort
  144. # 文本转GBK编码
  145. try:
  146. gbk_bytes = self.convertToGBK(txt)
  147. except Exception as e:
  148. print(f"文本转GBK失败: {e}")
  149. with self.mu:
  150. self.isPlaying = False
  151. return
  152. # 构造语音帧数据(对应Go的binary.Write + bytes.Buffer)
  153. data_area_len = 1 + 1 + len(gbk_bytes) # 0x01 + 0x01 + 文本GBK长度
  154. frame_buf = io.BytesIO()
  155. # 写入帧头0xFD
  156. frame_buf.write(bytes([0xFD]))
  157. # 写入数据区长度(大端序uint16),对应Go的binary.Write(BigEndian)
  158. frame_buf.write(struct.pack('>H', data_area_len))
  159. # 写入指令类型0x01 + 编码类型0x01
  160. frame_buf.write(bytes([0x01, 0x01]))
  161. # 写入GBK编码文本
  162. frame_buf.write(gbk_bytes)
  163. # 获取最终帧数据
  164. frame_data = frame_buf.getvalue()
  165. # 500ms延迟(保留原有逻辑)
  166. time.sleep(0.5)
  167. # 发送语音指令(加锁发送)
  168. with self.mu:
  169. if serialPort is None or not serialPort.is_open:
  170. raise Exception("串口未就绪")
  171. serialPort.write(frame_data)
  172. # 发送成功,直接退出(无等待/轮询/超时)
  173. except Exception as e:
  174. print(f"串口发送失败: {e}")
  175. # 标记串口关闭并尝试重连
  176. with self.mu:
  177. self.isClosed = True
  178. self.isPlaying = False
  179. try:
  180. self.Reconnect()
  181. except:
  182. pass
  183. finally:
  184. # 执行状态重置兜底逻辑
  185. reset_playing_state()
  186. def CorrectTime(self) -> None:
  187. """保留原有方法(空实现)"""
  188. pass
  189. def validateVoiceParams(self, params: DeviceData) -> None:
  190. """参数校验(对应Go的validateVoiceParams)"""
  191. valid_speakers = {3, 51, 52, 53, 54, 55, 56, 57}
  192. if params.Speaker not in valid_speakers:
  193. raise ValueError(f"发音人无效: {params.Speaker}")
  194. if not (0 <= params.Volume <= 10):
  195. raise ValueError(f"音量无效: {params.Volume}")
  196. if not (0 <= params.Tone <= 10):
  197. raise ValueError(f"语调无效: {params.Tone}")
  198. if not (0 <= params.Speed <= 30):
  199. raise ValueError(f"语速无效: {params.Speed}")
  200. def SetVoiceParams(self, params: DeviceData) -> None:
  201. """独立设置语音参数(播放中禁止修改)"""
  202. # 播放中禁止修改
  203. with self.mu:
  204. if self.isPlaying:
  205. raise Exception("语音播放中,禁止修改参数")
  206. # 参数校验
  207. try:
  208. self.validateVoiceParams(params)
  209. except ValueError as e:
  210. raise Exception(f"参数校验失败: {e}")
  211. # 加锁操作串口
  212. with self.mu:
  213. # 校验串口状态,未连接则重连
  214. if self.isClosed or self.serialPort is None:
  215. print("串口未连接,尝试重连...")
  216. try:
  217. self.Reconnect()
  218. except Exception as e:
  219. raise Exception(f"串口重连失败: {e}")
  220. serialPort = self.serialPort
  221. # 构造参数字符串
  222. param_str = f"[m{params.Speaker}][v{params.Volume}][t{params.Tone}][s{params.Speed}]"
  223. print(f"设置语音参数:{param_str}")
  224. # 参数字符串转GBK
  225. try:
  226. gbk_bytes = self.convertToGBK(param_str)
  227. except Exception as e:
  228. raise Exception(f"参数字符串转GBK失败: {e}")
  229. # 构造参数帧数据
  230. data_len = 1 + 1 + len(gbk_bytes)
  231. frame_buf = io.BytesIO()
  232. frame_buf.write(bytes([0xFD]))
  233. frame_buf.write(struct.pack('>H', data_len))
  234. frame_buf.write(bytes([0x06, 0x01])) # 指令类型0x06 + 编码类型0x01
  235. frame_buf.write(gbk_bytes)
  236. frame_data = frame_buf.getvalue()
  237. # 发送参数帧
  238. try:
  239. with self.mu:
  240. serialPort.write(frame_data)
  241. except Exception as e:
  242. with self.mu:
  243. self.isClosed = True
  244. try:
  245. self.Reconnect()
  246. except:
  247. pass
  248. raise Exception(f"发送参数失败: {e}")
  249. # 等待参数设置响应(3秒超时)
  250. resp_buf = bytearray(16)
  251. timeout = time.time() + 3 # 3秒超时
  252. while time.time() < timeout:
  253. try:
  254. n = serialPort.readinto(resp_buf)
  255. if n > 0 and 0x41 in resp_buf[:n]:
  256. print(
  257. f"参数设置成功: Speaker={params.Speaker}, Volume={params.Volume}, Tone={params.Tone}, Speed={params.Speed}")
  258. return
  259. except Exception as e:
  260. if "timed out" not in str(e):
  261. raise Exception(f"读取参数响应失败: {e}")
  262. time.sleep(0.1)
  263. raise Exception("参数设置超时")
  264. def Close(self) -> None:
  265. """手动关闭串口(清理资源)"""
  266. with self.mu:
  267. self.isClosed = True
  268. self.isPlaying = False
  269. if self.serialPort is not None and self.serialPort.is_open:
  270. try:
  271. self.serialPort.close()
  272. except Exception:
  273. pass
  274. self.serialPort = None
  275. print("串口已关闭,播放状态已重置")