diff --git a/.gitignore b/.gitignore index 3776a10..d0835e6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ !.vscode/launch.json !.vscode/extensions.json *.code-workspace - +*.log # Local History for Visual Studio Code .history/ *.app diff --git a/.vscode/settings.json b/.vscode/settings.json index a8c2003..4b5a294 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { "python-envs.defaultEnvManager": "ms-python.python:conda", - "python-envs.defaultPackageManager": "ms-python.python:conda", - "python-envs.pythonProjects": [] + "python-envs.defaultPackageManager": "ms-python.python:conda" } \ No newline at end of file diff --git a/__pycache__/remo_disp_server.cpython-312.pyc b/__pycache__/remo_disp_server.cpython-312.pyc new file mode 100644 index 0000000..4539401 Binary files /dev/null and b/__pycache__/remo_disp_server.cpython-312.pyc differ diff --git a/_proto_test.png b/_proto_test.png new file mode 100644 index 0000000..735da16 Binary files /dev/null and b/_proto_test.png differ diff --git a/remo_disp_server.py b/remo_disp_server.py index 4617a46..e41f612 100644 --- a/remo_disp_server.py +++ b/remo_disp_server.py @@ -267,18 +267,39 @@ def mono_to_png(data: bytes, width: int, height: int) -> Optional[bytes]: height=height, len_=len(data), ) - img = Image.new("1", (width, height)) # 创建 1bpp 黑白图像 - pix = img.load() # 获取像素访问对象 - ''' - for y in range(height): - for x in range(width): - bi = (y * width + x) // 8 # 字节索引:每 8 像素一字节 - bit = 7 - (x % 8) # 位索引:高位在前 - pix[x, y] = 0 if (bi < len(data) and (data[bi] >> bit) & 1) else 255 # 1→白 0→黑 - ''' - for y in range(height): - for x in range(width): - pix[x, y] = data[y * width + x] + expected_bits = (width * height + 7) // 8 # 1bpp 打包长度 + expected_bytes = width * height # 每像素 1 字节(0/1或0-255)长度 + + if len(data) == expected_bits: + # 1bpp:每字节 8 像素,高位在前 + logger.debug("[mono_to_png] 使用 1bpp 打包解码") + img = Image.new("1", (width, height)) + pix = img.load() + for y in range(height): + row_base = y * width + for x in range(width): + byte_i = (row_base + x) // 8 + bit_i = 7 - (x % 8) + bit = (data[byte_i] >> bit_i) & 1 + pix[x, y] = 255 if bit else 0 + elif len(data) == expected_bytes: + # 8bit:每字节直接对应一个像素,非零当作白 + logger.debug("[mono_to_png] 使用每像素 1 字节解码") + img = Image.new("1", (width, height)) + pix = img.load() + for y in range(height): + row_base = y * width + for x in range(width): + pix[x, y] = 255 if data[row_base + x] else 0 + else: + logger.warning( + "[mono_to_png] payload 长度不匹配,丢弃该帧: data_len={len_}, expected_bits={bits}, expected_bytes={bytes}", + len_=len(data), + bits=expected_bits, + bytes=expected_bytes, + ) + return None + buf = io.BytesIO() # 内存缓冲区 img.save(buf, format="PNG") # 保存为 PNG 格式 png_bytes = buf.getvalue() @@ -329,7 +350,7 @@ def _screen_refresh_loop(): # 当前协议未返回宽高,这里固定为 160x160,可根据实际情况调整 w, h = 160, 160 png = mono_to_png(data, w, h) # 转为 PNG - if png: + if png and isinstance(png, (bytes, bytearray)) and png.startswith(b"\x89PNG\r\n\x1a\n"): b64 = base64.b64encode(png).decode("ascii") # base64 编码便于 JSON 传输 for sid in list(_screen_clients): # 复制列表避免迭代时修改 try: @@ -344,6 +365,11 @@ def _screen_refresh_loop(): sid=sid, err=e, ) + elif png: + logger.warning( + "[_screen_refresh_loop] 生成的 PNG 无效或签名错误,丢弃该帧 png_len={len_}", + len_=len(png), + ) else: # 拉取失败,累计连续失败次数 consecutive_failures += 1 diff --git a/remo_disp_ui.html b/remo_disp_ui.html index fc253f8..2358cb4 100644 --- a/remo_disp_ui.html +++ b/remo_disp_ui.html @@ -392,8 +392,12 @@ } }); // 服务端推送屏幕:data.png 为 base64 编码的 PNG,解码后绘制到 canvas + const scale = 2; // 放大倍数(160x160 -> 320x320) + let canvasInitialized = false; + let latestFrameId = 0; // 用于丢弃旧帧,避免异步解码乱序导致闪烁 socket.on('screen', (data) => { if (!data.png) return; + const frameId = ++latestFrameId; // 无 base64 数据则跳过 const binary = atob(data.png); // base64 解码为二进制字符串 @@ -406,21 +410,38 @@ // 创建临时 URL const img = new Image(); img.onload = () => { - // 图片加载完成后,放大一倍显示(160x160 -> 320x320) - const scale = 2; - canvas.width = img.width * scale; - canvas.height = img.height * scale; + // 图片加载完成后:若该帧不是最新帧则直接丢弃 + if (frameId !== latestFrameId) { + URL.revokeObjectURL(url); + return; + } + + // 首帧初始化 canvas 尺寸(只做一次,避免反复重设导致清空闪烁) + const targetW = img.width * scale; + const targetH = img.height * scale; + if (!canvasInitialized) { + canvas.width = targetW; + canvas.height = targetH; + canvasInitialized = true; + canvas.style.display = 'block'; + placeholder.style.display = 'none'; + } + const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; - // 关闭平滑,像素清晰放大 + // 关闭平滑:像素清晰放大 ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height); - // 绘制到 canvas(2 倍尺寸) - canvas.style.display = 'block'; - placeholder.style.display = 'none'; - // 显示 canvas,隐藏占位符 + URL.revokeObjectURL(url); // 释放临时 URL }; + img.onerror = () => { + // 发生错误时同样释放临时 URL + if (frameId === latestFrameId) { + // 不弹窗,避免影响观察;必要时可在这里加日志 + } + URL.revokeObjectURL(url); + }; img.src = url; // 触发加载 }); @@ -465,6 +486,8 @@ placeholder.style.display = ''; placeholder.textContent = '点击「连接」连接装置'; // 隐藏 canvas,显示占位符 + canvasInitialized = false; + latestFrameId = 0; } document.querySelectorAll('.dpad .btn, .action-btn').forEach(btn => {