nextion_display.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """Nextion 串口屏协议封装。"""
  4. import logging
  5. import threading
  6. import time
  7. from typing import Tuple, List, Dict, Any
  8. import serial
  9. logger = logging.getLogger('nextion_display')
  10. EVENT_HEADER = 0x65
  11. TERMINATOR = b'\xFF\xFF\xFF'
  12. def parse_nextion_events(data: bytes) -> Tuple[List[Dict[str, Any]], bytes]:
  13. """从字节流中解析 Nextion 触控事件(0x65 事件头,7 字节一包)。
  14. 返回 (events, remaining_bytes),其中 remaining_bytes 是未消费的字节,
  15. 可能是一个不完整事件包的开头,需要保留给下一次读取继续解析。
  16. """
  17. events = []
  18. i = 0
  19. remaining = b''
  20. while i < len(data):
  21. idx = data.find(EVENT_HEADER, i)
  22. if idx < 0:
  23. break
  24. if idx + 7 > len(data):
  25. remaining = data[idx:]
  26. break
  27. if data[idx + 4:idx + 7] == TERMINATOR:
  28. events.append({
  29. 'type': 'button',
  30. 'page': data[idx + 1],
  31. 'comp': data[idx + 2],
  32. 'event': data[idx + 3],
  33. })
  34. i = idx + 7
  35. else:
  36. i = idx + 1
  37. return events, remaining
  38. class NextionDisplay:
  39. """Nextion 串口屏管理类。"""
  40. def __init__(self):
  41. self.ser = None
  42. self.lock = threading.RLock()
  43. self.read_thread = None
  44. self.stop_event = threading.Event()
  45. self.button_callback = None
  46. self.port = None
  47. self.baudrate = None
  48. self.connected = False
  49. def connect(self, port: str, baudrate: int = 9600, timeout: float = 0.1):
  50. """连接 Nextion 屏幕串口。
  51. 如果已经连接,会先断开当前连接。
  52. Args:
  53. port: 串口设备路径。
  54. baudrate: 波特率,默认 9600。
  55. timeout: 串口读取超时时间,默认 0.1 秒。
  56. Returns:
  57. tuple[bool, str]: 成功时返回 (True, 描述信息),失败时返回 (False, 错误信息)。
  58. """
  59. with self.lock:
  60. if self.connected:
  61. # 释放锁后再调用 disconnect,避免在同一把锁上产生死锁。
  62. self.lock.release()
  63. try:
  64. self.disconnect()
  65. finally:
  66. self.lock.acquire()
  67. try:
  68. logger.info(f"连接 Nextion 屏幕串口: {port} @ {baudrate}")
  69. ser = serial.Serial(port, baudrate, timeout=timeout)
  70. except Exception as e:
  71. with self.lock:
  72. self.connected = False
  73. self.ser = None
  74. msg = f"连接 Nextion 屏幕串口失败: {e}"
  75. logger.error(msg)
  76. return False, msg
  77. with self.lock:
  78. self.ser = ser
  79. self.port = port
  80. self.baudrate = baudrate
  81. self.connected = True
  82. self.stop_event.clear()
  83. self.read_thread = threading.Thread(target=self._read_loop, daemon=True)
  84. self.read_thread.start()
  85. logger.info(f"Nextion 屏幕串口已连接: {port}")
  86. return True, f"已连接 {port} @ {baudrate}"
  87. def disconnect(self):
  88. try:
  89. with self.lock:
  90. was_connected = self.connected
  91. self.stop_event.set()
  92. ser = self.ser
  93. self.ser = None
  94. self.connected = False
  95. self.port = None
  96. self.baudrate = None
  97. if self.read_thread and self.read_thread.is_alive():
  98. self.read_thread.join(timeout=2.0)
  99. if ser and ser.is_open:
  100. try:
  101. ser.close()
  102. except Exception as e:
  103. logger.error(f"关闭 Nextion 串口失败: {e}")
  104. if was_connected:
  105. logger.info("Nextion 屏幕串口已断开")
  106. return True, "已断开"
  107. except Exception as e:
  108. logger.error(f"断开 Nextion 屏幕串口失败: {e}")
  109. return False, str(e)
  110. def _read_loop(self):
  111. logger.info("启动 Nextion 事件监听线程")
  112. buf = bytearray()
  113. while not self.stop_event.is_set():
  114. try:
  115. with self.lock:
  116. ser = self.ser
  117. if ser and ser.is_open and ser.in_waiting > 0:
  118. buf.extend(ser.read(ser.in_waiting))
  119. events, remaining = parse_nextion_events(bytes(buf))
  120. buf.clear()
  121. buf.extend(remaining)
  122. for evt in events:
  123. logger.debug(f"Nextion 事件: {evt}")
  124. with self.lock:
  125. callback = self.button_callback
  126. if callback:
  127. try:
  128. callback(evt['page'], evt['comp'], evt['event'])
  129. except Exception as e:
  130. logger.error(f"按钮回调异常: {e}")
  131. else:
  132. time.sleep(0.01)
  133. except Exception as e:
  134. logger.error(f"Nextion 读取线程异常: {e}")
  135. time.sleep(0.5)
  136. logger.info("Nextion 事件监听线程结束")
  137. def _encode_cmd(self, cmd: str) -> bytes:
  138. return cmd.encode('gb2312', errors='replace') + TERMINATOR
  139. def send_cmd(self, cmd: str, delay_ms: float = 0) -> bool:
  140. try:
  141. with self.lock:
  142. if not self.connected or not self.ser or not self.ser.is_open:
  143. logger.warning(f"Nextion 未连接,无法发送: {cmd}")
  144. return False
  145. data = self._encode_cmd(cmd)
  146. self.ser.write(data)
  147. self.ser.flush()
  148. logger.debug(f"[Nextion TX] {cmd}")
  149. if delay_ms > 0:
  150. time.sleep(delay_ms / 1000.0)
  151. return True
  152. except Exception as e:
  153. logger.error(f"发送 Nextion 指令失败 '{cmd}': {e}")
  154. with self.lock:
  155. self.connected = False
  156. return False
  157. def send_raw(self, data: bytes) -> bool:
  158. try:
  159. with self.lock:
  160. if not self.connected or not self.ser or not self.ser.is_open:
  161. logger.warning("Nextion 未连接,无法发送原始数据")
  162. return False
  163. self.ser.write(data)
  164. self.ser.flush()
  165. logger.debug(f"[Nextion TX RAW] {data.hex()}")
  166. return True
  167. except Exception as e:
  168. logger.error(f"发送 Nextion 原始数据失败: {e}")
  169. with self.lock:
  170. self.connected = False
  171. return False
  172. def set_button_callback(self, callback):
  173. with self.lock:
  174. self.button_callback = callback
  175. def get_status(self):
  176. with self.lock:
  177. return {
  178. 'connected': self.connected and self.ser is not None and self.ser.is_open,
  179. 'port': self.port,
  180. 'baudrate': self.baudrate,
  181. }
  182. if __name__ == '__main__':
  183. logging.basicConfig(level=logging.DEBUG)
  184. disp = NextionDisplay()
  185. ok, msg = disp.connect('/dev/ttyS9', 9600)
  186. print(ok, msg)
  187. if ok:
  188. disp.send_cmd('t2.txt="测试"')
  189. time.sleep(1)
  190. disp.disconnect()