nextion_display.py 7.2 KB

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