#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 远程显示通信工具 - 与 RemoDispBus 协议兼容的 Python 客户端 协议说明(与 HMI/RemoeDisp/RemoDispBus.c 一致): - 工具 → 装置: [0xAA][功能码][长度高][长度低][数据...][CRC] - 装置 → 工具: [0xBB][功能码][长度高][长度低][数据...][CRC] - CRC: 数据区异或校验(不含 Tag、功能码、长度) - 端口: 7003 """ import socket import struct import threading import time import argparse from typing import Optional, Tuple, Callable # 协议常量(与 RemoDispBus.h 一致) TAG_CLIENT = 0xAA # 工具侧发送 TAG_DEVICE = 0xBB # 装置侧发送 PORT = 7003 # 功能码 CMD_KEEPLIVE = 0 CMD_INIT = 1 CMD_KEY = 2 CMD_LCDMEM = 3 CMD_ACK = 4 CMD_NCK = 5 CMD_IDLE = 0xFF # 按键值(与 RemoDispBus.h 一致) KEY_U = 0x02 # 上 KEY_D = 0x40 # 下 KEY_L = 0x10 # 左 KEY_R = 0x08 # 右 KEY_ENT = 0x20 # 确认 KEY_ESC = 0x01 # 取消 KEY_F1 = 0x04 KEY_F2 = 0x80 KEY_NONE = 0 def calc_crc(data: bytes) -> int: """异或校验,与 RDisp_CalCrc 一致""" crc = 0 for b in data: crc ^= b return crc & 0xFF def build_frame(cmd: int, data: bytes) -> bytes: """组帧:工具→装置 [0xAA][cmd][len_hi][len_lo][data][crc]""" length = len(data) header = bytes([TAG_CLIENT, cmd & 0xFF, (length >> 8) & 0xFF, length & 0xFF]) crc = calc_crc(data) return header + data + bytes([crc]) def parse_frame(raw: bytes) -> Optional[Tuple[int, bytes]]: """解析装置回复:[0xBB][cmd][len_hi][len_lo][data][crc]""" if len(raw) < 6: return None if raw[0] != TAG_DEVICE: return None cmd = raw[1] length = (raw[2] << 8) | raw[3] if len(raw) < 5 + length + 1: return None data = raw[4:4 + length] crc_recv = raw[4 + length] if calc_crc(data) != crc_recv: return None return (cmd, data) class RemoDispClient: """远程显示 TCP 客户端""" def __init__(self, host: str, port: int = PORT, timeout: float = 5.0): self.host = host self.port = port self.timeout = timeout self._sock: Optional[socket.socket] = None self._init_info: Optional[dict] = None def connect(self) -> bool: """连接装置""" try: self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.settimeout(self.timeout) self._sock.connect((self.host, self.port)) return True except Exception as e: print(f"连接失败: {e}") return False def disconnect(self): """断开连接""" if self._sock: try: self._sock.close() except Exception: pass self._sock = None def _send(self, cmd: int, data: bytes = b"") -> bool: """发送报文""" if not self._sock: return False frame = build_frame(cmd, data) try: self._sock.sendall(frame) return True except Exception as e: print(f"发送失败: {e}") return False def _recv(self) -> Optional[Tuple[int, bytes]]: """接收并解析一帧""" if not self._sock: return None try: header = self._sock.recv(5) if len(header) < 5: return None length = (header[2] << 8) | header[3] rest = self._sock.recv(length + 1) if len(rest) < length + 1: return None raw = header + rest return parse_frame(raw) except socket.timeout: return None except Exception as e: print(f"接收失败: {e}") return None def request_init(self) -> Optional[dict]: """召唤初始化,解析 LCD 尺寸、显存大小、按键映射""" if not self._send(CMD_INIT): return None result = self._recv() if not result or result[0] != CMD_INIT: return None data = result[1] if len(data) < 29: return None # 与 RDisp_Form_InitInf 格式一致 width = (data[0] << 8) | data[1] height = (data[2] << 8) | data[3] lcd_type = data[4] mem_size = (data[5] << 24) | (data[6] << 16) | (data[7] << 8) | data[8] rgb_red = struct.unpack(">I", data[9:13])[0] rgb_green = struct.unpack(">I", data[13:17])[0] rgb_blue = struct.unpack(">I", data[17:21])[0] keys = list(data[21:29]) self._init_info = { "width": width, "height": height, "lcd_type": lcd_type, "mem_size": mem_size, "rgb_red": rgb_red, "rgb_green": rgb_green, "rgb_blue": rgb_blue, "keys": keys, } return self._init_info def send_key(self, key_value: int) -> bool: """下发按键""" return self._send(CMD_KEY, bytes([key_value & 0xFF])) def request_lcdmem(self, start_pos: int = 0) -> Optional[Tuple[int, bytes]]: """召唤显存,从 start_pos 起。返回 (下一位置, 显存数据)""" data = struct.pack(">I", start_pos) if not self._send(CMD_LCDMEM, data): return None result = self._recv() if not result or result[0] != CMD_LCDMEM: return None payload = result[1] if len(payload) < 4: return None pos = struct.unpack(">I", payload[:4])[0] mem_data = payload[4:] return (pos + len(mem_data), mem_data) def fetch_full_screen(self, on_progress: Optional[Callable[[int, int], None]] = None) -> Optional[bytes]: """拉取完整显存,支持断点续传""" info = self._init_info or self.request_init() if not info: return None mem_size = info["mem_size"] buffer = bytearray(mem_size) pos = 0 while pos < mem_size: result = self.request_lcdmem(pos) if not result: return None next_pos, chunk = result buffer[pos : pos + len(chunk)] = chunk pos = next_pos if on_progress: on_progress(pos, mem_size) return bytes(buffer) def send_keepalive(self) -> bool: """发送链路保持(装置端 KEEPLIVE 不回复,仅刷新计时器)""" return self._send(CMD_KEEPLIVE) def mono_to_image(data: bytes, width: int, height: int) -> "Optional[object]": """单色显存转 PIL Image(1bit -> 黑白图)""" try: from PIL import Image except ImportError: return None img = Image.new("1", (width, height)) pix = img.load() for y in range(height): for x in range(width): byte_idx = (y * width + x) // 8 bit_idx = 7 - (x % 8) if byte_idx < len(data): pix[x, y] = 1 if (data[byte_idx] >> bit_idx) & 1 else 0 else: pix[x, y] = 0 return img def main(): parser = argparse.ArgumentParser(description="远程显示通信工具") parser.add_argument("host", help="装置 IP 地址") parser.add_argument("-p", "--port", type=int, default=PORT, help=f"端口 (默认 {PORT})") parser.add_argument("--init", action="store_true", help="仅获取初始化信息") parser.add_argument("--screen", action="store_true", help="拉取显存并保存为 screen.png") parser.add_argument("--key", type=str, help="发送按键: U/D/L/R/ENT/ESC/F1/F2") parser.add_argument("--keepalive", action="store_true", help="发送一次 KEEPLIVE") args = parser.parse_args() key_map = { "U": KEY_U, "D": KEY_D, "L": KEY_L, "R": KEY_R, "ENT": KEY_ENT, "ESC": KEY_ESC, "F1": KEY_F1, "F2": KEY_F2, } client = RemoDispClient(args.host, args.port) if not client.connect(): return 1 try: if args.init: info = client.request_init() if info: print("初始化信息:") print(f" LCD: {info['width']}x{info['height']}, 类型={info['lcd_type']}") print(f" 显存大小: {info['mem_size']} 字节") print(f" 按键映射: {info['keys']}") else: print("获取初始化失败") return 1 elif args.key: k = key_map.get(args.key.upper(), KEY_NONE) if k == KEY_NONE: print(f"未知按键: {args.key}") return 1 if client.send_key(k): print(f"已发送按键: {args.key}") else: print("发送按键失败") return 1 elif args.keepalive: if client.send_keepalive(): print("已发送 KEEPLIVE") else: print("发送失败") return 1 elif args.screen: def progress(cur, total): print(f"\r拉取显存: {cur}/{total} ({100*cur//total}%)", end="") data = client.fetch_full_screen(on_progress=progress) if not data: print("\n拉取显存失败") return 1 info = client._init_info or {} w = info.get("width", 160) h = info.get("height", 160) img = mono_to_image(data, w, h) if img: img.save("screen.png") print(f"\n已保存 screen.png ({w}x{h})") else: with open("screen.raw", "wb") as f: f.write(data) print(f"\n已保存 screen.raw ({len(data)} 字节,需 PIL 才能转 PNG)") else: # 默认:获取初始化并拉取一帧显存 info = client.request_init() if info: print(f"LCD {info['width']}x{info['height']}, 显存 {info['mem_size']} 字节") result = client.request_lcdmem(0) if result: next_pos, chunk = result print(f"收到显存 {len(chunk)} 字节,下一位置 {next_pos}") else: print("召唤显存失败") finally: client.disconnect() return 0 if __name__ == "__main__": exit(main())