screen.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. import datetime
  2. import queue
  3. import socket
  4. import threading
  5. import time
  6. from typing import List, Tuple, Optional, Dict, Any
  7. import codecs
  8. from bx import BxResp, NewBxDataPackCmd, NewBxAreaProgram, BxCmdFactory, NewBxFile
  9. # 核心常量定义
  10. class Color:
  11. DEFAULT = 0
  12. RED = 1
  13. GREEN = 2
  14. YELLOW = 3
  15. BLUE = 4
  16. LIGHT_BLUE = 5
  17. LIGHT_PURPLE = 6
  18. WHITE = 7
  19. class RunMode:
  20. LOOP = 0 # 循环
  21. LOOP_AND_STAY_AT_END = 1 # 循环直到最后,停留在最后一个动态区
  22. LOOP_AND_TIMEOUT_OFF = 2 # 循环直到超时,超时后未更新不在显示
  23. LOOP_AND_STAY_AT_LOGO = 3 # 循环完后,停留显示LOGO
  24. LOOP_AND_OFF = 4 # 循环完后不在显示
  25. LOOP_AND_COUNT_OFF = 5 # 循环设定次数后不在显示
  26. DEFAULT = LOOP # 默认
  27. class DisplayMode:
  28. STATIC = 1 # 静止显示
  29. QUICK_PUNCH = 2 # 快速打出
  30. MOVE_LEFT = 3 # 向左移动
  31. MOVE_RIGHT = 4 # 向右移动
  32. MOVE_UP = 5 # 向上移动
  33. MOVE_DOWN = 6 # 向下移动
  34. FLICKER = 7 # 闪烁
  35. DEFAULT = STATIC # 默认
  36. class FlashFile:
  37. def __init__(self):
  38. self.msg = ""
  39. self.sound_data = ""
  40. self.color = Color.DEFAULT
  41. self.run_mode = RunMode.DEFAULT
  42. self.disp_mode = DisplayMode.DEFAULT
  43. self.origin_x = 0
  44. self.origin_y = 0
  45. self.width = 0
  46. self.height = 0
  47. def set_msg(self, msg: str, color: int):
  48. self.msg = msg
  49. self.color = color
  50. def set_mode(self, run_mode: int, disp_mode: int):
  51. self.run_mode = run_mode
  52. self.disp_mode = disp_mode
  53. def set_origin(self, x: int, x_is_pixel: bool, y: int):
  54. self.origin_y = y
  55. if x_is_pixel:
  56. self.origin_x = 0x8000 | x
  57. else:
  58. self.origin_x = x
  59. def set_area(self, w: int, y_is_pixel: bool, h: int):
  60. self.height = h
  61. if y_is_pixel:
  62. self.width = 0x8000 | w
  63. else:
  64. self.width = w
  65. class CommandQueue:
  66. def __init__(self, max_size: int = 100):
  67. self.queue = queue.Queue(maxsize=max_size)
  68. self.stop_flag = False
  69. self.thread: Optional[threading.Thread] = None
  70. def start(self, handle_func):
  71. """启动队列消费线程"""
  72. self.stop_flag = False
  73. self.thread = threading.Thread(target=self._consumer, args=(handle_func,), daemon=True)
  74. self.thread.start()
  75. def stop(self):
  76. """停止队列消费"""
  77. self.stop_flag = True
  78. if self.thread:
  79. self.thread.join(timeout=2)
  80. def add_task(self, data: bytes):
  81. """添加发送任务到队列"""
  82. try:
  83. self.queue.put(data, block=False)
  84. except queue.Full:
  85. print("发送队列已满,丢弃任务")
  86. def _consumer(self, handle_func):
  87. """队列消费逻辑"""
  88. while not self.stop_flag:
  89. try:
  90. data = self.queue.get(timeout=1)
  91. handle_func(data)
  92. self.queue.task_done()
  93. except queue.Empty:
  94. continue
  95. class Screen:
  96. def __init__(self, name: str, ip: str, port: str):
  97. self.name = name
  98. self.addr = f"{ip}:{port}"
  99. self.conn: Optional[socket.socket] = None
  100. self.live_state = False
  101. self.state_info = dict() # 替代bx.StateInfo
  102. self.params = dict() # 替代bx.Params
  103. self.send_queue = CommandQueue(max_size=100)
  104. # 新增:重连锁,防止多线程同时重连
  105. self.reconnect_lock = threading.Lock()
  106. # 新增:退出标记,用于停止监控线程
  107. self.stop_flag = False
  108. # 启动发送队列
  109. self.send_queue.start(self.handle_send)
  110. # 启动连接监控线程
  111. self.monitor_thread = threading.Thread(target=self._monitor_connection, daemon=True)
  112. self.monitor_thread.start()
  113. # 初始重连(非阻塞式,在后台尝试连接)
  114. self._initial_connect()
  115. def _initial_connect(self):
  116. """初始连接(非阻塞,快速尝试一次)"""
  117. try:
  118. ip, port = self.addr.split(":")
  119. conn = socket.create_connection((ip, int(port)), timeout=10)
  120. # 新增:设置Socket超时,避免recv无限阻塞
  121. conn.settimeout(10.0)
  122. print(f"[{self.name}] 初始连接成功: {self.addr}")
  123. self.set_conn(conn)
  124. except Exception as e:
  125. print(f"[{self.name}] 初始连接失败: {self.addr}, 将在后台重试...")
  126. # 连接失败不阻塞,监控线程会自动重连
  127. def handle_send(self, data: bytes):
  128. """实际执行TCP发送(由队列线程调用)"""
  129. if not self.get_live_state() or self.conn is None:
  130. return
  131. try:
  132. self.conn.sendall(data)
  133. except Exception as e:
  134. print(f"[{self.name}] TCP发送失败: {e}")
  135. # 优化:仅在发送失败时标记为断开,加锁保护
  136. with self.reconnect_lock:
  137. self.live_state = False
  138. # ========== 新增:直接发送方法(跳过队列,用于雷达屏幕) ==========
  139. def send_direct(self, data: bytes):
  140. """
  141. 直接发送数据(跳过队列)
  142. 用于强实时性场景(如雷达测速)
  143. """
  144. if not self.get_live_state() or self.conn is None:
  145. return False
  146. try:
  147. self.conn.sendall(data)
  148. return True
  149. except Exception as e:
  150. print(f"[{self.name}] 直接发送失败: {e}")
  151. # 发送失败时标记为断开
  152. with self.reconnect_lock:
  153. self.live_state = False
  154. return False
  155. def send(self, data: bytes):
  156. """添加发送任务到队列"""
  157. if not self.get_live_state():
  158. return
  159. self.send_queue.add_task(data)
  160. def close(self):
  161. """关闭连接和队列"""
  162. # 新增:设置退出标记,停止监控线程
  163. self.stop_flag = True
  164. self.send_queue.stop()
  165. # 加锁保护,防止并发修改
  166. with self.reconnect_lock:
  167. if self.conn:
  168. try:
  169. self.conn.close()
  170. except Exception as e:
  171. print(f"[{self.name}] 关闭连接失败: {e}")
  172. self.conn = None
  173. self.live_state = False
  174. # 等待监控线程退出
  175. if self.monitor_thread:
  176. self.monitor_thread.join(timeout=3)
  177. def get_live_state(self) -> bool:
  178. """获取在线状态(加锁保护)"""
  179. with self.reconnect_lock:
  180. return self.live_state
  181. def set_conn(self, conn: socket.socket):
  182. """设置连接并更新在线状态(加锁保护)"""
  183. with self.reconnect_lock:
  184. self.conn = conn
  185. # 新增:设置Socket超时
  186. self.conn.settimeout(10.0)
  187. self.live_state = True
  188. def reconnect(self):
  189. """重连屏幕TCP连接(加锁防止并发)"""
  190. # 加锁:同一时间只能有一个重连操作
  191. with self.reconnect_lock:
  192. if self.live_state:
  193. return
  194. # 关闭旧连接
  195. if self.conn:
  196. try:
  197. self.conn.close()
  198. except Exception:
  199. pass
  200. self.conn = None
  201. # 新建连接
  202. try:
  203. ip, port = self.addr.split(":")
  204. conn = socket.create_connection((ip, int(port)), timeout=10)
  205. conn.settimeout(10.0)
  206. print(f"[{self.name}] 连接成功: {self.addr}")
  207. self.set_conn(conn)
  208. # 优化:延迟读取状态/参数,给屏幕缓冲时间
  209. time.sleep(0.5)
  210. # 读取屏状态和参数(非阻塞,失败不影响连接状态)
  211. try:
  212. state_resp = self.state()
  213. if state_resp:
  214. self.state_info = self._parse_state(state_resp.Data)
  215. params_resp = self.param()
  216. if params_resp:
  217. self.params = self._parse_params(params_resp.Data)
  218. except Exception as e:
  219. print(f"[{self.name}] 读取状态/参数失败(不影响连接): {e}")
  220. except Exception as e:
  221. print(f"[{self.name}] 重连失败: {self.addr}, error: {e}")
  222. self.live_state = False
  223. def force_reconnect(self, max_retries: int = 10):
  224. """强制重连屏幕TCP连接(带最大重试次数限制)"""
  225. retry_count = 0
  226. while not self.get_live_state() and retry_count < max_retries and not self.stop_flag:
  227. retry_count += 1
  228. print(f"[{self.name}] 强制重连中... (第{retry_count}/{max_retries}次尝试)")
  229. self.reconnect()
  230. if not self.get_live_state():
  231. print(f"[{self.name}] 重连失败,5秒后重试...")
  232. time.sleep(5)
  233. if not self.get_live_state() and not self.stop_flag:
  234. print(f"[{self.name}] 达到最大重试次数,停止重连,监控线程将继续尝试")
  235. def _monitor_connection(self):
  236. """连接监控线程,定期检查连接状态并自动重连(新增退出机制)"""
  237. while not self.stop_flag:
  238. try:
  239. # 每5秒检查一次连接状态
  240. time.sleep(5)
  241. # 检查是否需要重连
  242. if not self.get_live_state() and not self.stop_flag:
  243. print(f"[{self.name}] 连接断开,开始重连...")
  244. self.force_reconnect()
  245. except Exception as e:
  246. print(f"[{self.name}] 监控线程错误: {e}")
  247. time.sleep(5)
  248. def _parse_state(self, data: bytes) -> Dict[str, Any]:
  249. """解析屏幕状态(需根据实际协议实现)"""
  250. return {}
  251. def _parse_params(self, data: bytes) -> Dict[str, Any]:
  252. """解析屏幕参数(需根据实际协议实现)"""
  253. return {}
  254. def display(self, color: int, run_mode: int, display_mode: int, is_program: int, speed: int, text: str):
  255. """核心显示方法"""
  256. if not self.get_live_state():
  257. return
  258. self.send_ram(color, run_mode, display_mode, is_program, speed, text)
  259. def correct(self):
  260. """校正屏幕时间"""
  261. if not self.get_live_state():
  262. return
  263. now = datetime.datetime.now()
  264. cmd = BxCmdFactory.NewBxCmdSystemClockCorrect(now)
  265. pack = NewBxDataPackCmd(cmd,0x0001)
  266. self.send(pack.Pack())
  267. def format_speed(self, speed: int) -> str:
  268. """格式化速度参数"""
  269. if speed < 0 or speed > 99:
  270. return "00"
  271. return f"{speed:02d}"
  272. def send_ram(self, color: int, run_mode: int, display_mode: int, is_program: int, speed: int, text: str):
  273. """发送动态区节目"""
  274. # if is_program == 0:
  275. # ff = FlashFile()
  276. # ff.set_msg(self.format_speed(speed), color)
  277. # ff.set_mode(run_mode, display_mode)
  278. # ff.set_origin(0, True, 32)
  279. # ff.set_area(128, True, 96)
  280. # self.text_ram(ff, False)
  281. # elif is_program == 2:
  282. # ff = FlashFile()
  283. # ff.set_msg(text, color)
  284. # ff.set_mode(run_mode, display_mode)
  285. # ff.set_origin(0, True, 0)
  286. # ff.set_area(128, True, 32)
  287. # self.text_ram(ff, True)
  288. return
  289. def text_ram(self, ff: FlashFile, is_number: bool):
  290. """发送动态区节目实现(新增:车牌长度校验+绿牌FE编码区分)"""
  291. if not self.get_live_state():
  292. return
  293. areas = []
  294. # 编码转换
  295. encoder = codecs.getencoder("gb2312")
  296. bytes_data = b""
  297. id = 0
  298. alignment = 0x00
  299. disp_mode = ff.disp_mode
  300. dst_addr = 0x0003
  301. if ff.color == Color.DEFAULT and not is_number:
  302. bytes_data, _ = encoder(ff.msg)
  303. elif is_number:
  304. # 完整车牌(如湘F65354)
  305. full_plate = ff.msg.strip()
  306. print(f"车牌号: {full_plate}")
  307. # ========== 核心新增:车牌合法性校验(按长度) ==========
  308. # 校验规则:
  309. # - 普通燃油车牌:7位(如湘F65354)→ FE000
  310. # - 新能源绿色车牌:8位(如湘F653548)→ FE001
  311. # - 非7/8位直接判定为非法,跳过处理
  312. full_plate_len = len(full_plate)
  313. if full_plate_len not in [7, 8]:
  314. print(f"【校验失败】车牌{full_plate}长度{full_plate_len}位,非法(仅支持7/8位),跳过")
  315. return
  316. # 解析省份+号码(复用原有方法)
  317. province, number = self.parse_plate_utf8(full_plate)
  318. # 校验解析后的格式(省份非空 + 号码长度6/7位 + 号码首字符是字母)
  319. if not province or len(number) not in [6, 7] or not number[:1].isalpha() or not number[1:].isalnum():
  320. print(f"【校验失败】车牌{full_plate}格式错误(解析:省份{province},号码{number}),跳过")
  321. return
  322. # ========== 原有逻辑:判断显示模式 ==========
  323. if len(number) == 6:
  324. disp_mode = DisplayMode.QUICK_PUNCH
  325. # ========== 核心修改:按长度判断绿牌,设置FE编码 ==========
  326. # 8位车牌(新能源绿牌)→ FE001,7位车牌(普通牌)→ FE000
  327. fe_code = "001" if full_plate_len == 8 else "000"
  328. # 构建显示指令(替换FE编码,ff.color为屏幕颜色,保持不变)
  329. msg = f"\\FO000\\C{ff.color}{province}\\FE{fe_code}\\C{Color.GREEN}{number}"
  330. bytes_data, _ = encoder(msg)
  331. id = 1
  332. dst_addr = 0x0001
  333. elif not is_number:
  334. # 优化:使用与数字显示相同的格式,避免设备拒绝写入
  335. msg = f"\\FO000\\C{ff.color}{ff.msg}"
  336. # print(ff.msg)
  337. bytes_data, _ = encoder(msg)
  338. alignment = 0x00
  339. dst_addr = 0x0003
  340. # 构建动态区
  341. area = NewBxAreaProgram(
  342. id, ff.run_mode, disp_mode, alignment,
  343. ff.origin_x, ff.origin_y, ff.width, ff.height,
  344. bytes_data, False
  345. )
  346. areas.append(area)
  347. # 打包指令并发送
  348. cmd = BxCmdFactory.NewBxCmdSendDynamicArea(areas)
  349. pack = NewBxDataPackCmd(cmd, dst_addr)
  350. pack.SetDisplayType(1)
  351. data = pack.Pack()
  352. # print(f"打包后数据(十六进制): {data.hex()} {is_number}")
  353. self.send(data)
  354. if is_number:
  355. # 主屏幕:走队列
  356. self.send(data)
  357. else:
  358. # 雷达屏幕:直接发送
  359. send_success = self.send_direct(data)
  360. if not send_success:
  361. return
  362. # 读取响应
  363. resp = self.read_resp()
  364. # resp_time_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] # 获取当前时间 毫秒级
  365. # print(f"[响应时间 {resp_time_str}]")
  366. if not resp.IsAck():
  367. remote_ip, remote_port = self.conn.getpeername()
  368. error_msg = resp.Error() or f"错误码: {resp.Err}"
  369. print(
  370. f"[{self.name}] 设备拒绝写文件: {resp.Error().ErrorCode} 错误:{remote_ip} {remote_port} {data.hex()}")
  371. return
  372. self.state_info["dyna_area_num"] = self.state_info.get("dyna_area_num", 0) + 1
  373. def parse_plate_utf8(self, raw_plate: str) -> Tuple[str, str]:
  374. """解析车牌号(省份+号码)"""
  375. if not raw_plate:
  376. return "", ""
  377. province = raw_plate[0]
  378. number = raw_plate[1:]
  379. return province, number
  380. def text_flash(self, ft_list: List[FlashFile], name: str, is_number: bool, is_logo: bool):
  381. """发送静态文件节目(掉电保存)"""
  382. if not self.get_live_state():
  383. return
  384. if is_logo:
  385. name = "LOGO"
  386. encoder = codecs.getencoder("gb18030")
  387. areas = []
  388. for ff in ft_list:
  389. bytes_data = b""
  390. if ff.color == Color.DEFAULT and not is_number:
  391. bytes_data, _ = encoder(ff.msg)
  392. elif is_number:
  393. province, number = self.parse_plate_utf8(ff.msg)
  394. print(f"车牌号: {ff.msg}, 省份: {province}, 号码: {number}")
  395. msg = f"\\FO000\\C{ff.color}{province}\\FE001\\C{Color.GREEN}{number}"
  396. bytes_data, _ = encoder(msg)
  397. elif not is_number:
  398. msg = f"\\FO000\\C{ff.color}{ff.msg}"
  399. bytes_data, _ = encoder(msg)
  400. area = NewBxAreaProgram(
  401. 0xff, ff.run_mode, ff.disp_mode, 0x00,
  402. ff.origin_x, ff.origin_y, ff.width, ff.height,
  403. bytes_data, False
  404. )
  405. areas.append(area)
  406. # 构建文件并发送
  407. file = NewBxFile(name, "", areas)
  408. cmd = file.NewCmdWriteFile()
  409. pack = NewBxDataPackCmd(cmd,0x0001)
  410. data = pack.Pack()
  411. self.send(data)
  412. resp = self.read_resp()
  413. if not resp.IsAck():
  414. print(f"[{self.name}] 设备拒绝写文件: {resp.error}")
  415. return
  416. def read_resp(self) -> BxResp:
  417. """读取屏幕响应(优化:打印完整错误信息)"""
  418. resp = BxResp()
  419. if not self.get_live_state() or self.conn is None:
  420. return resp
  421. try:
  422. recv_data = self.conn.recv(1024)
  423. if not recv_data:
  424. print(f"[{self.name}] 未收到响应")
  425. return resp
  426. resp = resp.Parse(recv_data, len(recv_data))
  427. return resp
  428. except socket.timeout:
  429. print(f"[{self.name}] 读取响应超时")
  430. return resp
  431. except Exception as e:
  432. print(f"[{self.name}] 读取响应失败: {e}")
  433. import traceback
  434. traceback.print_exc()
  435. return resp
  436. def state(self) -> Optional[BxResp]:
  437. """获取屏幕状态"""
  438. if not self.get_live_state():
  439. return None
  440. cmd = BxCmdFactory.NewCmdState()
  441. pack = NewBxDataPackCmd(cmd,0x0001)
  442. self.send(pack.Pack())
  443. return self.read_resp()
  444. def param(self) -> Optional[BxResp]:
  445. """获取屏幕参数"""
  446. if not self.get_live_state():
  447. return None
  448. cmd = BxCmdFactory.NewCmdReadParams()
  449. pack = NewBxDataPackCmd(cmd,0x0001)
  450. self.send(pack.Pack())
  451. return self.read_resp()
  452. def turn_on_off(self, on_off: bool):
  453. """开关屏"""
  454. if not self.get_live_state():
  455. return
  456. cmd = BxCmdFactory.NewBxCmdTurnOnOff(on_off)
  457. pack = NewBxDataPackCmd(cmd,0x0001)
  458. self.send(pack.Pack())
  459. def brightness(self, brightness_type: int, current_brightness: int, brightness_value: bytes):
  460. """设置亮度"""
  461. if not self.get_live_state():
  462. return
  463. cmd = BxCmdFactory.NewCmdBrightness(brightness_type, current_brightness, brightness_value)
  464. pack = NewBxDataPackCmd(cmd,0x0001)
  465. print(f"亮度设置指令: {codecs.encode(pack.Pack(), 'hex')}")
  466. self.send(pack.Pack())
  467. resp = self.read_resp()
  468. if not resp.IsAck():
  469. print(f"[{self.name}] 设备拒绝设置亮度: {resp.error}")
  470. def timing_switch(self, on_off_set: List[Tuple[int, int]]):
  471. """设置定时开关屏"""
  472. if not self.get_live_state():
  473. return
  474. cmd = BxCmdFactory.NewCmdTimingSwitch(on_off_set)
  475. pack = NewBxDataPackCmd(cmd,0x0001)
  476. self.send(pack.Pack())
  477. def cancel_timing_switch(self):
  478. """取消定时开关屏"""
  479. if not self.get_live_state():
  480. return
  481. cmd = BxCmdFactory.NewCmdCancelTimingSwitch()
  482. pack = NewBxDataPackCmd(cmd,0x0001)
  483. self.send(pack.Pack())
  484. def del_file(self, del_files: List[str]):
  485. """删除静态文件"""
  486. if not self.get_live_state():
  487. return
  488. cmd = BxCmdFactory.NewCmdDeleteFile(del_files)
  489. pack = NewBxDataPackCmd(cmd,0x0001)
  490. self.send(pack.Pack())
  491. self.read_resp()
  492. if not del_files:
  493. self.state_info["program_num"] = 0
  494. else:
  495. self.state_info["program_num"] = self.state_info.get("program_num", 0) - 1
  496. def del_ram_text(self, *numbers: int):
  497. """
  498. 对应 Go 的 DelRamText 方法:删除 RAM 文本动态区域
  499. :param numbers: 可变参数,字节类型的动态区域编号
  500. """
  501. # 1. 检查屏幕是否处于活跃状态
  502. if not self.get_live_state():
  503. return
  504. # 2. 创建删除动态区域的指令(对应 Go 的 bx.NewCmdDelDynamicArea)
  505. cmd = BxCmdFactory.NewCmdDelDynamicArea(numbers)
  506. # 3. 打包指令(对应 Go 的 bx.NewBxDataPackCmd)
  507. pack = NewBxDataPackCmd(cmd,0x0003)
  508. # 4. 发送打包后的指令
  509. data = pack.Pack()
  510. # print(f"打包后数据(十六进制): {data.hex()}")
  511. self.send(data)
  512. # 5. 读取响应
  513. self.read_resp()
  514. # 6. 更新动态区域数量状态
  515. if len(numbers) == 0:
  516. self.state_info["dyna_area_num"] = 0
  517. else:
  518. self.state_info["dyna_area_num"] -= 1