diff --git a/templates/admin/wall.html b/templates/admin/wall.html
index 34ec3b7..e057783 100644
--- a/templates/admin/wall.html
+++ b/templates/admin/wall.html
@@ -391,14 +391,18 @@ function updateCtrlHead(){
document.getElementById('ctrl-sub').textContent=_ctrlSerial+(d.present?'':'(离线)');
}
// 坐标映射:显示尺寸 -> 设备原生分辨率
+// 图片 object-fit:contain 时实际绘制区域小于盒子(居中留黑边),
+// 必须按绘制区域换算,否则黑边/偏移导致点击错位
function ctrlCoords(ev){
const img=document.getElementById('ctrl-img');
- if(!img.naturalWidth)return[null,null];
+ if(!img.naturalWidth||!img.naturalHeight)return[null,null];
const r=img.getBoundingClientRect();
- return[
- Math.round((ev.clientX-r.left)/r.width*img.naturalWidth),
- Math.round((ev.clientY-r.top)/r.height*img.naturalHeight),
- ];
+ const scale=Math.min(r.width/img.naturalWidth,r.height/img.naturalHeight);
+ const drawW=img.naturalWidth*scale, drawH=img.naturalHeight*scale;
+ const ox=(r.width-drawW)/2, oy=(r.height-drawH)/2; // contain 偏移
+ const px=ev.clientX-r.left, py=ev.clientY-r.top;
+ if(pxox+drawW||pyoy+drawH)return[null,null]; // 黑边点击忽略
+ return[Math.round((px-ox)/scale),Math.round((py-oy)/scale)];
}
function ctrlTap(x,y){
if(x==null||!x)return;
diff --git a/web/monitor.py b/web/monitor.py
index 653ec78..52f33c8 100644
--- a/web/monitor.py
+++ b/web/monitor.py
@@ -416,10 +416,34 @@ def api_screen_stream():
mimetype="multipart/x-mixed-replace; boundary=frame")
+_screen_u2_cache = {} # serial -> (ts, device):大屏高频触控复用连接(60s TTL)
+
+
def _screen_get_device(serial):
- """远程看屏用的 u2 连接(连接失败抛异常由调用方转 503)。"""
+ """远程看屏用的 u2 连接(60s 缓存复用,避免每次 tap/swipe 都重建连接)。
+
+ 大屏高频操作(点击/滑动/按键)每次 u2.connect 有 1-3s 握手开销(atx-agent
+ 探测),体验卡顿。缓存 60s 内复用;连接失败(设备离线/重启)时清缓存重建,
+ 由调用方把异常转 503。
+ """
+ import time
import uiautomator2 as u2
- return u2.connect(serial)
+ now = time.time()
+ cached = _screen_u2_cache.get(serial)
+ if cached and now - cached[0] < 60:
+ return cached[1]
+ d = u2.connect(serial)
+ _screen_u2_cache[serial] = (now, d)
+ if len(_screen_u2_cache) > 30: # 惰性清理过期项
+ for k in list(_screen_u2_cache):
+ if now - _screen_u2_cache[k][0] >= 60:
+ del _screen_u2_cache[k]
+ return d
+
+
+def _screen_invalidate(serial):
+ """u2 调用失败时清掉缓存(设备离线/atx-agent 重启),下次请求重新连接。"""
+ _screen_u2_cache.pop(serial, None)
@bp.route("/api/screen/thumb")
@perm_required(PERM_DEVICES)
@@ -464,6 +488,7 @@ def api_screen_tap():
_screen_get_device(serial).click(int(x), int(y))
return jsonify({"ok": True})
except Exception as e:
+ _screen_invalidate(serial)
return jsonify({"ok": False, "error": f"点击失败: {e}"}), 503
@bp.route("/api/screen/swipe", methods=["POST"])
@@ -480,6 +505,7 @@ def api_screen_swipe():
duration=float(data.get("duration", 0.2)))
return jsonify({"ok": True})
except Exception as e:
+ _screen_invalidate(serial)
return jsonify({"ok": False, "error": f"滑动失败: {e}"}), 503
@@ -498,6 +524,7 @@ def api_screen_key():
_screen_get_device(serial).press(key)
return jsonify({"ok": True})
except Exception as e:
+ _screen_invalidate(serial)
return jsonify({"ok": False, "error": f"按键失败: {e}"}), 503
@bp.route("/api/screen/text", methods=["POST"])
@@ -528,8 +555,32 @@ def _current_focus_pkg(serial):
except Exception:
return ""
+_screen_u2_cache = {} # serial -> (ts, device):大屏高频触控复用连接(60s TTL)
+
+
def _screen_get_device(serial):
- """远程看屏用的 u2 连接(连接失败抛异常由调用方转 503)。"""
+ """远程看屏用的 u2 连接(60s 缓存复用,避免每次 tap/swipe 都重建连接)。
+
+ 大屏高频操作(点击/滑动/按键)每次 u2.connect 有 1-3s 握手开销(atx-agent
+ 探测),体验卡顿。缓存 60s 内复用;连接失败(设备离线/重启)时清缓存重建,
+ 由调用方把异常转 503。
+ """
+ import time
import uiautomator2 as u2
- return u2.connect(serial)
+ now = time.time()
+ cached = _screen_u2_cache.get(serial)
+ if cached and now - cached[0] < 60:
+ return cached[1]
+ d = u2.connect(serial)
+ _screen_u2_cache[serial] = (now, d)
+ if len(_screen_u2_cache) > 30: # 惰性清理过期项
+ for k in list(_screen_u2_cache):
+ if now - _screen_u2_cache[k][0] >= 60:
+ del _screen_u2_cache[k]
+ return d
+
+
+def _screen_invalidate(serial):
+ """u2 调用失败时清掉缓存(设备离线/atx-agent 重启),下次请求重新连接。"""
+ _screen_u2_cache.pop(serial, None)