创建第一个版本的工程,基本逻辑和显示界面已经完成

This commit is contained in:
2026-03-02 15:47:47 +08:00
commit ba4431c01f
6 changed files with 1251 additions and 0 deletions

216
remo_disp_server.py Normal file
View File

@@ -0,0 +1,216 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
远程显示 Web 服务 - Flask 实现
用法: python remo_disp_server.py [装置IP]
启动后访问 http://localhost:8181
依赖: pip install flask
"""
import socket
import struct
import time
import sys
import os
import io
from typing import Optional, Tuple
from flask import Flask, request, jsonify, send_file, Response
# 协议常量
TAG_CLIENT, TAG_DEVICE = 0xAA, 0xBB
PORT = 7003
CMD_KEEPLIVE, CMD_INIT, CMD_KEY, CMD_LCDMEM = 0, 1, 2, 3
# 全局连接
_sock: Optional[socket.socket] = None
_init_info: Optional[dict] = None
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)
def connect(host: str, port: int = PORT) -> bool:
global _sock, _init_info
try:
if _sock:
_sock.close()
_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_sock.settimeout(5.0)
_sock.connect((host, port))
_sock.settimeout(2.0)
_init_info = _request_init()
return True
except Exception:
return False
def _send(cmd: int, data: bytes = b"") -> bool:
if not _sock:
return False
try:
_sock.sendall(build_frame(cmd, data))
return True
except Exception:
return False
def _recv() -> Optional[Tuple[int, bytes]]:
if not _sock:
return None
try:
header = _sock.recv(5)
if len(header) < 5:
return None
length = (header[2] << 8) | header[3]
rest = _sock.recv(length + 1)
if len(rest) < length + 1:
return None
return parse_frame(header + rest)
except Exception:
return None
def _request_init() -> Optional[dict]:
if not _send(CMD_INIT):
return None
for _ in range(30):
result = _recv()
if result and result[0] == CMD_INIT:
data = result[1]
if len(data) >= 29:
return {
"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],
}
time.sleep(0.05)
return None
def send_key(code: int) -> bool:
return _send(CMD_KEY, bytes([code & 0xFF]))
def fetch_screen() -> Optional[bytes]:
global _init_info
info = _init_info or _request_init()
if not info:
return None
mem_size = info["mem_size"]
buffer = bytearray(mem_size)
pos = 0
while pos < mem_size:
if not _send(CMD_LCDMEM, struct.pack(">I", pos)):
return None
for _ in range(40):
result = _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)
def mono_to_png(data: bytes, width: int, height: int) -> Optional[bytes]:
try:
from PIL import Image
img = Image.new("1", (width, height))
pix = img.load()
for y in range(height):
for x in range(width):
bi = (y * width + x) // 8
bit = 7 - (x % 8)
pix[x, y] = 255 if (bi < len(data) and (data[bi] >> bit) & 1) else 0
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
except ImportError:
return None
app = Flask(__name__)
@app.route("/")
@app.route("/index.html")
def index():
return send_file(
os.path.join(os.path.dirname(__file__), "remo_disp_ui.html"),
mimetype="text/html; charset=utf-8",
)
@app.route("/connect")
def api_connect():
host = request.args.get("host", "").strip()
port = int(request.args.get("port", PORT))
ok = connect(host, port)
return jsonify(success=ok, error=None if ok else "connect failed")
@app.route("/key")
def api_key():
code = int(request.args.get("code", 0))
send_key(code)
return jsonify(ok=True)
@app.route("/screen")
def api_screen():
data = fetch_screen()
if not data:
return "", 500
info = _init_info or {}
w, h = info.get("width", 160), info.get("height", 160)
png = mono_to_png(data, w, h)
if png:
return Response(png, mimetype="image/png")
resp = Response(data, mimetype="application/octet-stream")
resp.headers["X-Width"] = str(w)
resp.headers["X-Height"] = str(h)
return resp
def main():
port = 8181
print(f"远程显示服务: http://localhost:{port}")
print("在浏览器中打开上述地址,点击「连接」输入装置 IP")
if len(sys.argv) >= 2:
connect(sys.argv[1])
print(f"已预连接: {sys.argv[1]}")
app.run(host="0.0.0.0", port=port, debug=False, threaded=True)
if __name__ == "__main__":
main()