nextion_display.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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. def parse_nextion_events(data: bytes):
  10. """从字节流中解析 Nextion 触控事件(0x65 事件头,7 字节一包)。"""
  11. events = []
  12. buf = bytearray(data)
  13. while len(buf) >= 7:
  14. idx = -1
  15. for i in range(len(buf)):
  16. if buf[i] == 0x65:
  17. idx = i
  18. break
  19. if idx < 0:
  20. break
  21. if idx > 0:
  22. del buf[:idx]
  23. if len(buf) < 7:
  24. break
  25. if buf[4] == 0xFF and buf[5] == 0xFF and buf[6] == 0xFF:
  26. events.append({
  27. 'type': 'button',
  28. 'page': buf[1],
  29. 'comp': buf[2],
  30. 'event': buf[3],
  31. })
  32. del buf[:7]
  33. else:
  34. del buf[:1]
  35. return events
  36. class NextionDisplay:
  37. """Nextion 串口屏管理类。"""
  38. def __init__(self):
  39. self.ser = None
  40. self.lock = threading.Lock()
  41. self.read_thread = None
  42. self.stop_event = threading.Event()
  43. self.button_callback = None
  44. self.port = None
  45. self.baudrate = None
  46. self.connected = False
  47. def connect(self, port: str, baudrate: int = 9600, timeout: float = 0.1):
  48. try:
  49. with self.lock:
  50. if self.connected:
  51. self.disconnect()
  52. logger.info(f"连接 Nextion 屏幕串口: {port} @ {baudrate}")
  53. self.ser = serial.Serial(port, baudrate, timeout=timeout)
  54. self.port = port
  55. self.baudrate = baudrate
  56. self.connected = True
  57. self.stop_event.clear()
  58. self.read_thread = threading.Thread(target=self._read_loop, daemon=True)
  59. self.read_thread.start()
  60. logger.info(f"Nextion 屏幕串口已连接: {port}")
  61. return True, f"已连接 {port} @ {baudrate}"
  62. except Exception as e:
  63. self.connected = False
  64. self.ser = None
  65. msg = f"连接 Nextion 屏幕串口失败: {e}"
  66. logger.error(msg)
  67. return False, msg
  68. def disconnect(self):
  69. try:
  70. with self.lock:
  71. self.stop_event.set()
  72. if self.read_thread and self.read_thread.is_alive():
  73. self.read_thread.join(timeout=2.0)
  74. if self.ser and self.ser.is_open:
  75. try:
  76. self.ser.close()
  77. except Exception as e:
  78. logger.error(f"关闭 Nextion 串口失败: {e}")
  79. self.ser = None
  80. self.connected = False
  81. self.port = None
  82. self.baudrate = None
  83. logger.info("Nextion 屏幕串口已断开")
  84. return True, "已断开"
  85. except Exception as e:
  86. logger.error(f"断开 Nextion 屏幕串口失败: {e}")
  87. return False, str(e)
  88. def _read_loop(self):
  89. logger.info("启动 Nextion 事件监听线程")
  90. buf = bytearray()
  91. while not self.stop_event.is_set():
  92. try:
  93. if self.ser and self.ser.is_open and self.ser.in_waiting > 0:
  94. buf.extend(self.ser.read(self.ser.in_waiting))
  95. events = parse_nextion_events(bytes(buf))
  96. if events:
  97. buf.clear()
  98. for evt in events:
  99. logger.debug(f"Nextion 事件: {evt}")
  100. if self.button_callback:
  101. try:
  102. self.button_callback(evt['page'], evt['comp'], evt['event'])
  103. except Exception as e:
  104. logger.error(f"按钮回调异常: {e}")
  105. else:
  106. time.sleep(0.01)
  107. except Exception as e:
  108. logger.error(f"Nextion 读取线程异常: {e}")
  109. time.sleep(0.5)
  110. logger.info("Nextion 事件监听线程结束")
  111. def _encode_cmd(self, cmd: str) -> bytes:
  112. return cmd.encode('gb2312', errors='replace') + b'\xFF\xFF\xFF'
  113. def send_cmd(self, cmd: str, delay_ms: float = 0) -> bool:
  114. try:
  115. with self.lock:
  116. if not self.connected or not self.ser or not self.ser.is_open:
  117. logger.warning(f"Nextion 未连接,无法发送: {cmd}")
  118. return False
  119. data = self._encode_cmd(cmd)
  120. self.ser.write(data)
  121. self.ser.flush()
  122. logger.debug(f"[Nextion TX] {cmd}")
  123. if delay_ms > 0:
  124. time.sleep(delay_ms / 1000.0)
  125. return True
  126. except Exception as e:
  127. logger.error(f"发送 Nextion 指令失败 '{cmd}': {e}")
  128. self.connected = False
  129. return False
  130. def send_raw(self, data: bytes) -> bool:
  131. try:
  132. with self.lock:
  133. if not self.connected or not self.ser or not self.ser.is_open:
  134. logger.warning("Nextion 未连接,无法发送原始数据")
  135. return False
  136. self.ser.write(data)
  137. self.ser.flush()
  138. logger.debug(f"[Nextion TX RAW] {data.hex()}")
  139. return True
  140. except Exception as e:
  141. logger.error(f"发送 Nextion 原始数据失败: {e}")
  142. self.connected = False
  143. return False
  144. def set_button_callback(self, callback):
  145. self.button_callback = callback
  146. def get_status(self):
  147. return {
  148. 'connected': self.connected and self.ser is not None and self.ser.is_open,
  149. 'port': self.port,
  150. 'baudrate': self.baudrate,
  151. }
  152. if __name__ == '__main__':
  153. logging.basicConfig(level=logging.DEBUG)
  154. disp = NextionDisplay()
  155. ok, msg = disp.connect('/dev/ttyS9', 9600)
  156. print(ok, msg)
  157. if ok:
  158. disp.send_cmd('t2.txt="测试"')
  159. time.sleep(1)
  160. disp.disconnect()