创建第一个版本的工程,基本逻辑和显示界面已经完成
This commit is contained in:
216
remo_disp_server.py
Normal file
216
remo_disp_server.py
Normal 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()
|
||||
Reference in New Issue
Block a user