Files
DTU-RemtoeLCD/remo_disp_viewer.py

273 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()