可以使用的版本

This commit is contained in:
2026-09-20 00:29:42 +08:00
parent f7898adc5d
commit d6969177ed
6 changed files with 73 additions and 25 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
*.code-workspace *.code-workspace
*.log
# Local History for Visual Studio Code # Local History for Visual Studio Code
.history/ .history/
*.app *.app
+1 -2
View File
@@ -1,5 +1,4 @@
{ {
"python-envs.defaultEnvManager": "ms-python.python:conda", "python-envs.defaultEnvManager": "ms-python.python:conda",
"python-envs.defaultPackageManager": "ms-python.python:conda", "python-envs.defaultPackageManager": "ms-python.python:conda"
"python-envs.pythonProjects": []
} }
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+39 -13
View File
@@ -267,18 +267,39 @@ def mono_to_png(data: bytes, width: int, height: int) -> Optional[bytes]:
height=height, height=height,
len_=len(data), len_=len(data),
) )
img = Image.new("1", (width, height)) # 创建 1bpp 黑白图像 expected_bits = (width * height + 7) // 8 # 1bpp 打包长度
pix = img.load() # 获取像素访问对象 expected_bytes = width * height # 每像素 1 字节(0/1或0-255)长度
'''
for y in range(height): if len(data) == expected_bits:
for x in range(width): # 1bpp:每字节 8 像素,高位在前
bi = (y * width + x) // 8 # 字节索引:每 8 像素一字节 logger.debug("[mono_to_png] 使用 1bpp 打包解码")
bit = 7 - (x % 8) # 位索引:高位在前 img = Image.new("1", (width, height))
pix[x, y] = 0 if (bi < len(data) and (data[bi] >> bit) & 1) else 255 # 1→白 0→黑 pix = img.load()
''' for y in range(height):
for y in range(height): row_base = y * width
for x in range(width): for x in range(width):
pix[x, y] = data[y * width + x] 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() # 内存缓冲区 buf = io.BytesIO() # 内存缓冲区
img.save(buf, format="PNG") # 保存为 PNG 格式 img.save(buf, format="PNG") # 保存为 PNG 格式
png_bytes = buf.getvalue() png_bytes = buf.getvalue()
@@ -329,7 +350,7 @@ def _screen_refresh_loop():
# 当前协议未返回宽高,这里固定为 160x160,可根据实际情况调整 # 当前协议未返回宽高,这里固定为 160x160,可根据实际情况调整
w, h = 160, 160 w, h = 160, 160
png = mono_to_png(data, w, h) # 转为 PNG 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 传输 b64 = base64.b64encode(png).decode("ascii") # base64 编码便于 JSON 传输
for sid in list(_screen_clients): # 复制列表避免迭代时修改 for sid in list(_screen_clients): # 复制列表避免迭代时修改
try: try:
@@ -344,6 +365,11 @@ def _screen_refresh_loop():
sid=sid, sid=sid,
err=e, err=e,
) )
elif png:
logger.warning(
"[_screen_refresh_loop] 生成的 PNG 无效或签名错误,丢弃该帧 png_len={len_}",
len_=len(png),
)
else: else:
# 拉取失败,累计连续失败次数 # 拉取失败,累计连续失败次数
consecutive_failures += 1 consecutive_failures += 1
+32 -9
View File
@@ -392,8 +392,12 @@
} }
}); });
// 服务端推送屏幕:data.png 为 base64 编码的 PNG,解码后绘制到 canvas // 服务端推送屏幕:data.png 为 base64 编码的 PNG,解码后绘制到 canvas
const scale = 2; // 放大倍数(160x160 -> 320x320)
let canvasInitialized = false;
let latestFrameId = 0; // 用于丢弃旧帧,避免异步解码乱序导致闪烁
socket.on('screen', (data) => { socket.on('screen', (data) => {
if (!data.png) return; if (!data.png) return;
const frameId = ++latestFrameId;
// 无 base64 数据则跳过 // 无 base64 数据则跳过
const binary = atob(data.png); const binary = atob(data.png);
// base64 解码为二进制字符串 // base64 解码为二进制字符串
@@ -406,21 +410,38 @@
// 创建临时 URL // 创建临时 URL
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
// 图片加载完成后,放大一倍显示(160x160 -> 320x320) // 图片加载完成后:若该帧不是最新帧则直接丢弃
const scale = 2; if (frameId !== latestFrameId) {
canvas.width = img.width * scale; URL.revokeObjectURL(url);
canvas.height = img.height * scale; 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'); const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
// 关闭平滑,像素清晰放大 // 关闭平滑:像素清晰放大
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height); 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.revokeObjectURL(url);
// 释放临时 URL // 释放临时 URL
}; };
img.onerror = () => {
// 发生错误时同样释放临时 URL
if (frameId === latestFrameId) {
// 不弹窗,避免影响观察;必要时可在这里加日志
}
URL.revokeObjectURL(url);
};
img.src = url; img.src = url;
// 触发加载 // 触发加载
}); });
@@ -465,6 +486,8 @@
placeholder.style.display = ''; placeholder.style.display = '';
placeholder.textContent = '点击「连接」连接装置'; placeholder.textContent = '点击「连接」连接装置';
// 隐藏 canvas,显示占位符 // 隐藏 canvas,显示占位符
canvasInitialized = false;
latestFrameId = 0;
} }
document.querySelectorAll('.dpad .btn, .action-btn').forEach(btn => { document.querySelectorAll('.dpad .btn, .action-btn').forEach(btn => {