创建第一个版本的工程,基本逻辑和显示界面已经完成
This commit is contained in:
272
remo_disp_viewer.py
Normal file
272
remo_disp_viewer.py
Normal file
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
远程显示查看器 - 带 GUI 的实时显存查看与按键模拟
|
||||
|
||||
用法: python remo_disp_viewer.py <装置IP>
|
||||
"""
|
||||
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# 协议常量
|
||||
TAG_CLIENT = 0xAA
|
||||
TAG_DEVICE = 0xBB
|
||||
PORT = 7003
|
||||
CMD_KEEPLIVE = 0
|
||||
CMD_INIT = 1
|
||||
CMD_KEY = 2
|
||||
CMD_LCDMEM = 3
|
||||
|
||||
KEY_U, KEY_D, KEY_L, KEY_R = 0x02, 0x40, 0x10, 0x08
|
||||
KEY_ENT, KEY_ESC, KEY_F1, KEY_F2 = 0x20, 0x01, 0x04, 0x80
|
||||
|
||||
|
||||
def calc_crc(data: bytes) -> int:
|
||||
crc = 0
|
||||
for b in data:
|
||||
crc ^= b
|
||||
return crc & 0xFF
|
||||
|
||||
|
||||
def build_frame(cmd: int, data: bytes) -> bytes:
|
||||
length = len(data)
|
||||
header = bytes([TAG_CLIENT, cmd & 0xFF, (length >> 8) & 0xFF, length & 0xFF])
|
||||
return header + data + bytes([calc_crc(data)])
|
||||
|
||||
|
||||
def parse_frame(raw: bytes) -> Optional[Tuple[int, bytes]]:
|
||||
if len(raw) < 6 or raw[0] != TAG_DEVICE:
|
||||
return None
|
||||
length = (raw[2] << 8) | raw[3]
|
||||
if len(raw) < 5 + length + 1:
|
||||
return None
|
||||
data = raw[4:4 + length]
|
||||
if calc_crc(data) != raw[4 + length]:
|
||||
return None
|
||||
return (raw[1], data)
|
||||
|
||||
|
||||
class RemoDispViewer:
|
||||
def __init__(self, host: str, port: int = PORT):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self._sock: Optional[socket.socket] = None
|
||||
self._init_info: Optional[dict] = None
|
||||
self._running = False
|
||||
self._refresh_thread: Optional[threading.Thread] = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
try:
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._sock.settimeout(3.0)
|
||||
self._sock.connect((self.host, self.port))
|
||||
self._sock.settimeout(0.5)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
self._running = False
|
||||
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
|
||||
try:
|
||||
self._sock.sendall(build_frame(cmd, data))
|
||||
return True
|
||||
except Exception:
|
||||
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
|
||||
return parse_frame(header + rest)
|
||||
except (socket.timeout, BlockingIOError):
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def request_init(self) -> Optional[dict]:
|
||||
if not self._send(CMD_INIT):
|
||||
return None
|
||||
for _ in range(20):
|
||||
result = self._recv()
|
||||
if result and result[0] == CMD_INIT:
|
||||
data = result[1]
|
||||
if len(data) >= 29:
|
||||
self._init_info = {
|
||||
"width": (data[0] << 8) | data[1],
|
||||
"height": (data[2] << 8) | data[3],
|
||||
"mem_size": (data[5] << 24) | (data[6] << 16) | (data[7] << 8) | data[8],
|
||||
}
|
||||
return self._init_info
|
||||
time.sleep(0.05)
|
||||
return None
|
||||
|
||||
def send_key(self, key: int) -> bool:
|
||||
return self._send(CMD_KEY, bytes([key]))
|
||||
|
||||
def fetch_screen(self) -> Optional[bytes]:
|
||||
info = self._init_info
|
||||
if not info:
|
||||
info = self.request_init()
|
||||
if not info:
|
||||
return None
|
||||
mem_size = info["mem_size"]
|
||||
buffer = bytearray(mem_size)
|
||||
pos = 0
|
||||
while pos < mem_size:
|
||||
if not self._send(CMD_LCDMEM, struct.pack(">I", pos)):
|
||||
return None
|
||||
for _ in range(30):
|
||||
result = self._recv()
|
||||
if result and result[0] == CMD_LCDMEM:
|
||||
payload = result[1]
|
||||
if len(payload) >= 4:
|
||||
start = struct.unpack(">I", payload[:4])[0]
|
||||
chunk = payload[4:]
|
||||
buffer[start : start + len(chunk)] = chunk
|
||||
pos = start + len(chunk)
|
||||
break
|
||||
time.sleep(0.02)
|
||||
else:
|
||||
return None
|
||||
return bytes(buffer)
|
||||
|
||||
|
||||
class ViewerApp:
|
||||
def __init__(self, host: str):
|
||||
self.host = host
|
||||
self.client = RemoDispViewer(host)
|
||||
self.root = tk.Tk()
|
||||
self.root.title(f"远程显示 - {host}")
|
||||
self.root.geometry("400x320")
|
||||
self._canvas = None
|
||||
self._photo = None
|
||||
self._scale = 2
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self):
|
||||
# 连接区
|
||||
frm = ttk.Frame(self.root, padding=5)
|
||||
frm.pack(fill=tk.X)
|
||||
ttk.Label(frm, text=f"目标: {self.host}:7003").pack(side=tk.LEFT)
|
||||
self._status = ttk.Label(frm, text="未连接")
|
||||
self._status.pack(side=tk.RIGHT)
|
||||
|
||||
# 画布
|
||||
self._canvas = tk.Canvas(self.root, width=320, height=320, bg="black")
|
||||
self._canvas.pack(padx=5, pady=5)
|
||||
|
||||
# 按键区
|
||||
btn_frm = ttk.Frame(self.root, padding=5)
|
||||
btn_frm.pack(fill=tk.X)
|
||||
keys = [
|
||||
("↑", KEY_U), ("↓", KEY_D), ("←", KEY_L), ("→", KEY_R),
|
||||
("确认", KEY_ENT), ("取消", KEY_ESC), ("F1", KEY_F1), ("F2", KEY_F2),
|
||||
]
|
||||
for label, key in keys:
|
||||
ttk.Button(btn_frm, text=label, width=6,
|
||||
command=lambda k=key: self._on_key(k)).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# 刷新
|
||||
ttk.Button(btn_frm, text="刷新", command=self._refresh).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
def _connect(self) -> bool:
|
||||
if self.client.connect():
|
||||
if self.client.request_init():
|
||||
self._status.config(text="已连接")
|
||||
return True
|
||||
self.client.disconnect()
|
||||
self._status.config(text="连接失败")
|
||||
return False
|
||||
|
||||
def _on_key(self, key: int):
|
||||
if not self.client._sock:
|
||||
if not self._connect():
|
||||
messagebox.showerror("错误", "请先连接装置")
|
||||
return
|
||||
self.client.send_key(key)
|
||||
|
||||
def _refresh(self):
|
||||
if not self.client._sock:
|
||||
if not self._connect():
|
||||
messagebox.showerror("错误", "连接失败")
|
||||
return
|
||||
data = self.client.fetch_screen()
|
||||
if not data:
|
||||
messagebox.showerror("错误", "拉取显存失败")
|
||||
return
|
||||
info = self.client._init_info
|
||||
w, h = info["width"], info["height"]
|
||||
# 单色转图像
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
img = Image.new("1", (w, h))
|
||||
pix = img.load()
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
bi = (y * w + x) // 8
|
||||
bit = 7 - (x % 8)
|
||||
pix[x, y] = 255 if (bi < len(data) and (data[bi] >> bit) & 1) else 0
|
||||
img = img.resize((w * self._scale, h * self._scale), Image.NEAREST)
|
||||
self._photo = ImageTk.PhotoImage(img)
|
||||
self._canvas.delete("all")
|
||||
self._canvas.create_image(0, 0, anchor=tk.NW, image=self._photo)
|
||||
except ImportError:
|
||||
# 无 PIL:用简单矩形模拟
|
||||
self._canvas.delete("all")
|
||||
step = max(1, 320 // w)
|
||||
for y in range(min(h, 160)):
|
||||
for x in range(min(w, 160)):
|
||||
bi = (y * w + x) // 8
|
||||
bit = 7 - (x % 8)
|
||||
if bi < len(data) and (data[bi] >> bit) & 1:
|
||||
self._canvas.create_rectangle(
|
||||
x * step, y * step, (x + 1) * step, (y + 1) * step,
|
||||
fill="white", outline=""
|
||||
)
|
||||
|
||||
def _on_close(self):
|
||||
self.client.disconnect()
|
||||
self.root.destroy()
|
||||
|
||||
def run(self):
|
||||
self._connect()
|
||||
self._refresh()
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python remo_disp_viewer.py <装置IP>")
|
||||
sys.exit(1)
|
||||
app = ViewerApp(sys.argv[1])
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user