serial_port.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import serial
  2. import serial.tools.list_ports
  3. import threading
  4. import time
  5. import logging
  6. import platform
  7. import glob
  8. from dataclasses import dataclass
  9. # 配置日志
  10. logging.basicConfig(
  11. level=logging.INFO,
  12. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  13. )
  14. logger = logging.getLogger('serial_port')
  15. @dataclass
  16. class SerialConfig:
  17. """串口配置数据类"""
  18. port: str
  19. baudrate: int = 115200
  20. bytesize: int = serial.EIGHTBITS
  21. parity: str = serial.PARITY_NONE
  22. stopbits: int = serial.STOPBITS_ONE
  23. timeout: float = 1.0
  24. xonxoff: bool = False
  25. rtscts: bool = False
  26. dsrdtr: bool = False
  27. class SerialPort:
  28. """串口通信类,提供串口连接、读写和状态管理功能"""
  29. def __init__(self):
  30. self.ser = None
  31. self.is_connected = False
  32. self.lock = threading.RLock() # 使用可重入锁
  33. self.read_thread = None
  34. self.stop_event = threading.Event()
  35. self.data_callback = None
  36. self.send_callback = None
  37. self.status_callback = None
  38. self.error_callback = None
  39. self.current_config = None
  40. self.reconnect_attempts = 0
  41. self.max_reconnect_attempts = 3
  42. self.raw_response_buffer = []
  43. def list_ports(self):
  44. """列出系统中可用的串口"""
  45. ports = []
  46. try:
  47. # 首先尝试使用serial.tools.list_ports
  48. try:
  49. detected_ports = [port.device for port in serial.tools.list_ports.comports()]
  50. ports.extend(detected_ports)
  51. except Exception as e:
  52. logger.warning(f"使用serial.tools.list_ports失败: {str(e)}")
  53. # 根据不同平台进行补充查找
  54. system = platform.system()
  55. if system == 'Windows':
  56. try:
  57. import winreg
  58. # 在Windows系统中读取注册表
  59. key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
  60. r'HARDWARE\DEVICEMAP\SERIALCOMM')
  61. i = 0
  62. while True:
  63. try:
  64. port, value, _ = winreg.EnumValue(key, i)
  65. if value not in ports:
  66. ports.append(value)
  67. i += 1
  68. except OSError:
  69. break
  70. except Exception as e:
  71. logger.error(f"读取Windows串口注册表失败: {str(e)}")
  72. elif system == 'Darwin': # macOS
  73. # 使用glob查找/dev/tty.*设备
  74. darwin_ports = glob.glob('/dev/tty.*')
  75. # 过滤掉不需要的端口
  76. for port in darwin_ports:
  77. if not ('Bluetooth' in port or 'debug' in port or 'com.apple' in port) and port not in ports:
  78. ports.append(port)
  79. elif system == 'Linux':
  80. # 使用glob查找Linux系统中的串口
  81. linux_ports = glob.glob('/dev/ttyS*') + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')
  82. for port in linux_ports:
  83. if port not in ports:
  84. ports.append(port)
  85. logger.info(f"找到 {len(ports)} 个可用串口: {ports}")
  86. except Exception as e:
  87. logger.error(f"列出串口时出错: {str(e)}")
  88. return sorted(ports) # 排序返回
  89. def connect(self, port, baudrate=9600, timeout=1, **kwargs):
  90. """连接到串口"""
  91. try:
  92. # 构建配置
  93. config = SerialConfig(
  94. port=port,
  95. baudrate=baudrate,
  96. timeout=timeout,
  97. **kwargs
  98. )
  99. with self.lock:
  100. if self.is_connected:
  101. self.disconnect()
  102. logger.info(f"尝试连接串口: {port}, 波特率: {baudrate}")
  103. self.ser = serial.Serial(
  104. port=config.port,
  105. baudrate=config.baudrate,
  106. bytesize=config.bytesize,
  107. parity=config.parity,
  108. stopbits=config.stopbits,
  109. timeout=config.timeout,
  110. xonxoff=config.xonxoff,
  111. rtscts=config.rtscts,
  112. dsrdtr=config.dsrdtr
  113. )
  114. # 检查连接是否成功
  115. if not self.ser.is_open:
  116. raise Exception("串口打开失败")
  117. self.is_connected = True
  118. self.stop_event.clear()
  119. self.current_config = config
  120. self.reconnect_attempts = 0
  121. # 启动读取线程
  122. self.read_thread = threading.Thread(target=self._read_loop, daemon=True)
  123. self.read_thread.start()
  124. if self.status_callback:
  125. self.status_callback(True)
  126. logger.info(f"已连接到 {port},波特率 {baudrate}")
  127. return True, f"已连接到 {port},波特率 {baudrate}"
  128. except Exception as e:
  129. error_msg = f"连接失败: {str(e)}"
  130. logger.error(error_msg)
  131. if self.status_callback:
  132. self.status_callback(False)
  133. if self.error_callback:
  134. self.error_callback(error_msg)
  135. return False, error_msg
  136. def disconnect(self):
  137. """断开串口连接"""
  138. try:
  139. with self.lock:
  140. logger.info("断开串口连接")
  141. self.stop_event.set()
  142. if self.read_thread and self.read_thread.is_alive():
  143. self.read_thread.join(timeout=2.0)
  144. if self.read_thread.is_alive():
  145. logger.warning("读取线程未能正常终止")
  146. if self.ser and self.ser.is_open:
  147. try:
  148. self.ser.close()
  149. except Exception as e:
  150. logger.error(f"关闭串口时出错: {str(e)}")
  151. finally:
  152. self.ser = None
  153. self.is_connected = False
  154. self.current_config = None
  155. if self.status_callback:
  156. self.status_callback(False)
  157. return True, "已断开连接"
  158. except Exception as e:
  159. error_msg = f"断开连接失败: {str(e)}"
  160. logger.error(error_msg)
  161. if self.error_callback:
  162. self.error_callback(error_msg)
  163. return False, error_msg
  164. def _read_loop(self):
  165. """读取串口数据的循环"""
  166. logger.info("启动串口读取线程")
  167. while not self.stop_event.is_set():
  168. try:
  169. if self.ser and self.ser.is_open:
  170. # 使用in_waiting提高效率
  171. if self.ser.in_waiting > 0:
  172. data = self.ser.read(self.ser.in_waiting)
  173. # 默认将原始数据以十六进制存入缓冲区
  174. hex_data = data.hex()
  175. with self.lock:
  176. self.raw_response_buffer.append(hex_data)
  177. if self.data_callback:
  178. self.data_callback(hex_data)
  179. # 如果能解码为文本,也通知回调
  180. try:
  181. decoded_data = data.decode('utf-8').strip()
  182. if decoded_data and self.data_callback:
  183. self.data_callback(decoded_data)
  184. except:
  185. pass
  186. time.sleep(0.001)
  187. except Exception as e:
  188. error_msg = f"读取串口数据错误: {str(e)}"
  189. logger.error(error_msg)
  190. if self.error_callback:
  191. self.error_callback(error_msg)
  192. # 尝试重连
  193. if self._should_reconnect():
  194. logger.warning(f"尝试重连串口... (第{self.reconnect_attempts}次)")
  195. if self.current_config:
  196. # 等待一段时间后重连
  197. time.sleep(2)
  198. self.connect(
  199. port=self.current_config.port,
  200. baudrate=self.current_config.baudrate,
  201. timeout=self.current_config.timeout,
  202. bytesize=self.current_config.bytesize,
  203. parity=self.current_config.parity,
  204. stopbits=self.current_config.stopbits,
  205. xonxoff=self.current_config.xonxoff,
  206. rtscts=self.current_config.rtscts,
  207. dsrdtr=self.current_config.dsrdtr
  208. )
  209. break
  210. # 线程结束时清理资源
  211. logger.info("串口读取线程结束")
  212. # 只有错误退出才断开连接,stop_event触发的暂停不重置
  213. if not self.stop_event.is_set():
  214. with self.lock:
  215. self.is_connected = False
  216. if self.ser:
  217. try:
  218. self.ser.close()
  219. except:
  220. pass
  221. self.ser = None
  222. if self.status_callback:
  223. self.status_callback(False)
  224. def send_data(self, data, encoding='utf-8'):
  225. """发送数据到串口"""
  226. try:
  227. with self.lock:
  228. if not self.is_connected or not self.ser or not self.ser.is_open:
  229. return False, "串口未连接"
  230. # 确保数据以换行符结束
  231. if isinstance(data, str):
  232. if not data.endswith('\n'):
  233. data += '\n'
  234. bytes_data = data.encode(encoding)
  235. elif isinstance(data, bytes):
  236. if not data.endswith(b'\n'):
  237. bytes_data = data + b'\n'
  238. else:
  239. bytes_data = data
  240. else:
  241. raise TypeError("数据必须是字符串或字节类型")
  242. bytes_sent = self.ser.write(bytes_data)
  243. self.ser.flush() # 确保数据被发送
  244. logger.debug(f"发送数据到串口: {bytes_data.hex()[:50]}... (共{bytes_sent}字节)")
  245. return True, "发送成功"
  246. except Exception as e:
  247. error_msg = f"发送失败: {str(e)}"
  248. logger.error(error_msg)
  249. if self.error_callback:
  250. self.error_callback(error_msg)
  251. return False, error_msg
  252. def set_data_callback(self, callback):
  253. """设置数据接收回调函数"""
  254. self.data_callback = callback
  255. def set_send_callback(self, callback):
  256. """设置数据发送回调函数"""
  257. self.send_callback = callback
  258. def set_status_callback(self, callback):
  259. """设置状态变化回调函数"""
  260. self.status_callback = callback
  261. def set_error_callback(self, callback):
  262. """设置错误回调函数"""
  263. self.error_callback = callback
  264. def get_status(self):
  265. """获取当前连接状态"""
  266. with self.lock:
  267. return {
  268. 'connected': self.is_connected,
  269. 'config': self.current_config,
  270. 'has_error': self.reconnect_attempts > 0
  271. }
  272. def _should_reconnect(self):
  273. """判断是否应该尝试重连"""
  274. self.reconnect_attempts += 1
  275. return self.reconnect_attempts <= self.max_reconnect_attempts
  276. def send_raw(self, data: bytes):
  277. """send raw binary data without adding newline"""
  278. try:
  279. with self.lock:
  280. if not self.is_connected or not self.ser or not self.ser.is_open:
  281. return False, "serial port not connected"
  282. bytes_sent = self.ser.write(data)
  283. self.ser.flush()
  284. if self.send_callback:
  285. self.send_callback(data.hex())
  286. return True, "send ok"
  287. except Exception as e:
  288. error_msg = "send raw failed: " + str(e)
  289. logger.error(error_msg)
  290. if self.error_callback:
  291. self.error_callback(error_msg)
  292. return False, error_msg
  293. def flush_input(self):
  294. """清空输入缓冲区"""
  295. try:
  296. with self.lock:
  297. if self.ser and self.ser.is_open:
  298. self.ser.reset_input_buffer()
  299. return True, "输入缓冲区已清空"
  300. return False, "串口未连接"
  301. except Exception as e:
  302. error_msg = f"清空缓冲区失败: {str(e)}"
  303. logger.error(error_msg)
  304. return False, error_msg
  305. def flush_output(self):
  306. """清空输出缓冲区"""
  307. try:
  308. with self.lock:
  309. if self.ser and self.ser.is_open:
  310. self.ser.reset_output_buffer()
  311. return True, "输出缓冲区已清空"
  312. return False, "串口未连接"
  313. except Exception as e:
  314. error_msg = f"清空缓冲区失败: {str(e)}"
  315. logger.error(error_msg)
  316. return False, error_msg
  317. def send_and_wait(self, data: bytes, timeout: float = 2.0, min_response_bytes: int = 1) -> bytes:
  318. """sync send and wait (read from buffer, thread continues running)"""
  319. import time
  320. with self.lock:
  321. if not self.ser or not self.ser.is_open:
  322. return b''
  323. self.raw_response_buffer.clear()
  324. self.ser.reset_input_buffer()
  325. self.ser.write(data)
  326. self.ser.flush()
  327. if self.send_callback:
  328. self.send_callback(data.hex())
  329. response = b''
  330. start = time.time()
  331. while time.time() - start < timeout:
  332. while self.raw_response_buffer:
  333. hex_str = self.raw_response_buffer.pop(0)
  334. try:
  335. chunk = bytes.fromhex(hex_str)
  336. response += chunk
  337. if len(response) >= min_response_bytes:
  338. time.sleep(0.3)
  339. while self.raw_response_buffer:
  340. try:
  341. response += bytes.fromhex(self.raw_response_buffer.pop(0))
  342. except:
  343. pass
  344. return response
  345. except:
  346. pass
  347. time.sleep(0.01)
  348. return response
  349. # 创建全局串口实例
  350. global_serial = SerialPort()