rfid_client.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import os
  2. import socket
  3. import subprocess
  4. import threading
  5. import time
  6. import logging
  7. from typing import Callable, Optional
  8. logger = logging.getLogger(__name__)
  9. _MIN_BACKOFF = 1
  10. _MAX_BACKOFF = 60
  11. class RFIDClient:
  12. def __init__(self, config, on_tag_callback: Callable[[Optional[str]], None],
  13. on_state_changed=None, tag_offset: int = -6, tag_length: int = 6, hex_output: bool = True,
  14. valid_codes: Optional[set] = None):
  15. self.config = config
  16. self.on_tag_callback = on_tag_callback
  17. self.tag_offset = tag_offset
  18. self.tag_length = tag_length
  19. self.hex_output = hex_output
  20. self.valid_codes = valid_codes # 允许的卡号集合,None 表示不过滤
  21. self.sock: Optional[socket.socket] = None
  22. self.running = False
  23. self._online: Optional[bool] = None
  24. self._last_tag: Optional[str] = None
  25. self._stable_count = 0
  26. self._last_time = time.time()
  27. self._last_receive_time = time.time()
  28. self._backoff = _MIN_BACKOFF
  29. self._last_connect_attempt = 0
  30. self.thread = None
  31. self.buffer = b""
  32. self.on_state_changed = on_state_changed
  33. self._ping_interval = 5 # 周期 ping 间隔(秒)
  34. self._ping_threshold = 3 # 连续 ping 失败多少次判定离线
  35. self._ping_failures = 0
  36. self._ping_running = False
  37. self._ping_thread = None
  38. self._start_ping_monitor()
  39. def _start_ping_monitor(self):
  40. """启动后台周期 ping,用于检测读卡器设备是否在线(与 TCP 数据连接无关)"""
  41. if self._ping_thread and self._ping_thread.is_alive():
  42. return
  43. self._ping_running = True
  44. self._ping_thread = threading.Thread(target=self._ping_loop, daemon=True)
  45. self._ping_thread.start()
  46. def _ping_once(self) -> bool:
  47. try:
  48. if os.name == 'nt':
  49. cmd = ['ping', '-n', '1', '-w', '1000', self.config.ip]
  50. flags = 0x08000000 # CREATE_NO_WINDOW,避免弹控制台窗口
  51. else:
  52. cmd = ['ping', '-c', '1', '-W', '1', self.config.ip]
  53. flags = 0
  54. r = subprocess.run(cmd, capture_output=True, timeout=3, creationflags=flags)
  55. return r.returncode == 0
  56. except Exception:
  57. return False
  58. def _ping_loop(self):
  59. while self._ping_running:
  60. if self._ping_once():
  61. self._ping_failures = 0
  62. self._notify_state(True)
  63. else:
  64. self._ping_failures += 1
  65. if self._ping_failures >= self._ping_threshold:
  66. self._notify_state(False)
  67. time.sleep(self._ping_interval)
  68. def _notify_state(self, online: bool):
  69. """仅在上线/离线状态发生切换时通知上层,避免重试期间重复报警"""
  70. if online == self._online:
  71. return
  72. self._online = online
  73. if self.on_state_changed:
  74. self.on_state_changed(online)
  75. def connect(self) -> bool:
  76. """尝试建立连接,若已连接则直接返回 True,否则尝试重连(有间隔限制)"""
  77. if self.sock:
  78. return True
  79. # 避免频繁重连
  80. if time.time() - self._last_connect_attempt < self._backoff:
  81. return False
  82. self._last_connect_attempt = time.time()
  83. try:
  84. self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  85. self.sock.settimeout(3) # 设置接收超时,避免阻塞
  86. self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
  87. if hasattr(socket, 'TCP_KEEPIDLE'):
  88. self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 5)
  89. self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
  90. self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
  91. self.sock.connect((self.config.ip, self.config.port))
  92. self.running = True
  93. self.thread = threading.Thread(target=self._read_loop, daemon=True)
  94. self.thread.start()
  95. self._last_receive_time = time.time()
  96. self._backoff = _MIN_BACKOFF
  97. logger.info(f"RFID连接成功 {self.config.ip}:{self.config.port}")
  98. self._notify_state(True)
  99. return True
  100. except Exception as e:
  101. self._backoff = min(self._backoff * 2, _MAX_BACKOFF)
  102. logger.error(f"RFID连接失败,下次{self._backoff}s后重试: {e}")
  103. self.sock = None
  104. self._notify_state(False)
  105. return False
  106. def disconnect(self):
  107. """主动断开连接"""
  108. self.running = False
  109. self._ping_running = False
  110. sock = self.sock
  111. self.sock = None
  112. if sock:
  113. try:
  114. sock.close()
  115. except OSError:
  116. pass
  117. logger.info("RFID已断开")
  118. self._notify_state(False)
  119. self.on_tag_callback(None) # 通知上层
  120. def is_connected(self) -> bool:
  121. """检查连接是否正常"""
  122. return self.sock is not None and self.running
  123. def _read_loop(self):
  124. while self.running:
  125. sock = self.sock
  126. if sock is None:
  127. break
  128. try:
  129. data = sock.recv(1024)
  130. if not data:
  131. break
  132. self._last_receive_time = time.time() # 收到任何数据都更新时间
  133. self.buffer += data
  134. if len(self.buffer) > 65536:
  135. self.buffer = self.buffer[-32768:]
  136. while True:
  137. start = self.buffer.find(b"CT")
  138. if start == -1:
  139. break
  140. next_start = self.buffer.find(b"CT", start + 2)
  141. if next_start == -1:
  142. break
  143. packet = self.buffer[start:next_start]
  144. self.buffer = self.buffer[next_start:]
  145. self._process_packet(packet)
  146. except socket.timeout:
  147. continue # 超时不影响循环
  148. except OSError:
  149. break
  150. except Exception as e:
  151. logger.error(f"RFID读取错误: {e}")
  152. break
  153. # 退出循环(读卡器主动断开或网络异常),静默清理并快速重连。
  154. # 该读卡器会周期性主动断开连接,属正常行为,在线状态由周期 ping 判定。
  155. logger.info("RFID读取循环结束,将触发重连")
  156. self.running = False
  157. # 先重置重连时间戳,避免 check_idle 在清理完成前提前重连(旧线程误报)
  158. self._last_connect_attempt = time.time()
  159. self._backoff = _MIN_BACKOFF
  160. sock = self.sock
  161. if sock is not None:
  162. self.sock = None
  163. try:
  164. sock.close()
  165. except OSError:
  166. pass
  167. # 不通知上层:读卡器周期主动断开会话属正常行为,卡号仍有效,
  168. # 卡离开只由 check_idle 空闲超时判定
  169. def _process_packet(self, packet):
  170. hex_str = packet.hex()
  171. logger.debug(f"RFID包 长度 {len(packet)} 十六进制: {hex_str}")
  172. offset = self.tag_offset
  173. length = self.tag_length
  174. if offset < 0:
  175. offset = len(packet) + offset
  176. if offset < 0 or offset + length > len(packet):
  177. logger.debug("提取范围越界,跳过")
  178. return
  179. tag_bytes = packet[offset:offset + length]
  180. if self.hex_output:
  181. tag = tag_bytes.hex().upper()
  182. else:
  183. try:
  184. tag = tag_bytes.decode('ascii')
  185. except UnicodeDecodeError:
  186. tag = tag_bytes.hex().upper()
  187. if len(tag) < 4 or tag.strip('0') == '':
  188. logger.debug("无效卡号(空或全0),忽略")
  189. return
  190. # 过滤不在配置中的卡号(读卡器同时读到多张卡时,只保留配置的工位卡)
  191. if self.valid_codes and tag not in self.valid_codes:
  192. logger.debug(f"卡号 {tag} 不在配置列表中,过滤")
  193. return
  194. logger.debug(f"提取卡号: {tag}")
  195. self._process_tag(tag)
  196. def _process_tag(self, tag):
  197. now = time.time()
  198. if tag == self._last_tag:
  199. self._stable_count += 1
  200. else:
  201. self._last_tag = tag
  202. self._stable_count = 1
  203. self._last_time = now
  204. logger.debug(f"稳定: 标签 {tag}, 计数 {self._stable_count}/{self.config.stability_count}")
  205. if self._stable_count >= self.config.stability_count:
  206. logger.info(f"触发稳定标签: {tag}")
  207. self.on_tag_callback(tag)
  208. self._stable_count = 0
  209. def check_idle(self):
  210. """
  211. 外部定时(每秒)调用的维护方法:
  212. 1. 检查空闲超时(卡离开)
  213. 2. 检查连接状态,若断开或长时间无数据则自动重连
  214. """
  215. # 空闲超时:仅在连接正常时才触发,避免断开期间误判
  216. if self.sock is not None and self._last_tag and (time.time() - self._last_time) > self.config.stability_time:
  217. logger.info("空闲超时,清空状态")
  218. self._last_tag = None
  219. self._stable_count = 0
  220. self.on_tag_callback(None)
  221. # 连接状态维护
  222. if self.sock is None:
  223. # 如果 socket 已断开,尝试重连
  224. if time.time() - self._last_connect_attempt > self._backoff:
  225. logger.debug("RFID socket为None,尝试重连")
  226. self.connect()
  227. else:
  228. # socket 存在,检查是否长时间无数据(可能连接已假死)
  229. idle_time = time.time() - self._last_receive_time
  230. if idle_time > self.config.stability_time * 2:
  231. # 超过两倍空闲时间无数据,主动断开重连
  232. logger.warning(f"RFID连接假死({idle_time:.1f}s无数据),主动重连")
  233. self.running = False
  234. sock = self.sock
  235. self.sock = None
  236. if sock:
  237. try:
  238. sock.close()
  239. except OSError:
  240. pass