feat: 正式池断联设备自动重连——①扫描线程每轮顺带对断联的网络设备 adb_connect_light(幂等,连上即恢复,无需人工;此前只有启动时预连接一次,断联后不会自己回来);②断联设备不删不进待连接池(pending 语义=未授权新设备),GET discovery 返回 pool_offline(serial/型号/备注);③POST /api/devices/discovery/reconnect 手动立即重连;④发现面板加「设备池断联设备」区块(自动重连中 + 立即重连按钮)

This commit is contained in:
2026-09-05 09:58:42 +08:00
parent 4865ed1d31
commit edfd529c59
4 changed files with 108 additions and 3 deletions
+41 -2
View File
@@ -22,7 +22,7 @@ from config import (USB_ADB_HOST,
DISCOVERY_PORT, DISCOVERY_SUBNETS, DISCOVERY_INTERVAL) DISCOVERY_PORT, DISCOVERY_SUBNETS, DISCOVERY_INTERVAL)
from core.adb_helper import _adb, adb_connect_light from core.adb_helper import _adb, adb_connect_light
from core.logger import get_logger from core.logger import get_logger
from core.models import db, PendingDevice from core.models import db, Device, PendingDevice
_log = get_logger("core.disc") _log = get_logger("core.disc")
@@ -220,8 +220,14 @@ def scan_once(manual=False):
first_seen=now, last_seen=now)) first_seen=now, last_seen=now))
added += 1 added += 1
db.session.commit() db.session.commit()
# 6. 正式池断联设备自动重连:adb connect 会因 WiFi 波动/设备重启/
# adb 服务重启而断开——扫描线程每轮顺带重试(幂等轻量,内部
# 全局锁串行),连上即恢复在线,无需人工干预。pending 池是给
# 「未授权新设备」的,正式池设备断联不进 pending,而是自动重连。
back = _reconnect_offline(configured)
_log.info(f"发现: 探测开放 {len(open_ips)} 台,可连 {len(verified)} 台," _log.info(f"发现: 探测开放 {len(open_ips)} 台,可连 {len(verified)} 台,"
f"新增待连接 {added} 台") f"新增待连接 {added} 台"
+ (f",自动重连恢复 {len(back)} 台 {back}" if back else ""))
_last_scan = (now, len(open_ips), len(verified), added) _last_scan = (now, len(open_ips), len(verified), added)
_last_error = "" _last_error = ""
return True, {"found": len(open_ips), "verified": len(verified), "new": added} return True, {"found": len(open_ips), "verified": len(verified), "new": added}
@@ -240,6 +246,39 @@ def device_pool_list_configured():
return device_pool.list_configured() return device_pool.list_configured()
# ================== 正式池断联设备:自动重连 ==================
def _reconnect_offline(configured):
"""对正式池中断联的网络设备逐个 adb 重连,返回恢复的 serial 列表。
只重连网络设备(IP:5555;USB 设备插着就在,无需 connect)。
幂等轻量:内部 adb 全局锁串行,失败静默(下轮扫描再试)。
"""
online_now = _parse_adb_devices(_adb("devices"))
targets = [s for s in (configured or [])
if ":" in s and s not in online_now]
if not targets:
return []
for serial in targets:
adb_connect_light(serial)
online_after = _parse_adb_devices(_adb("devices"))
return [s for s in targets if s in online_after]
def list_pool_offline():
"""正式设备池中断联的设备(serial + 型号 + 备注名),面板展示用。
断联设备仍是正式池成员(不删除、不进 pending)——pending 是给未授权
新设备的;它们由扫描线程每轮自动重连,也可前端手动立即重连。
"""
online = _parse_adb_devices(_adb("devices"))
with _ctx():
rows = [d.to_dict() for d in Device.query.filter_by(enabled=True)
.order_by(Device.serial).all()]
return [{"serial": r["serial"], "model": r.get("model") or "",
"name": r.get("name") or ""}
for r in rows if r["serial"] not in online]
# ================== 定时扫描线程 ================== # ================== 定时扫描线程 ==================
def _discovery_loop(): def _discovery_loop():
"""定时扫描 daemon 线程。每轮重读配置(开关/周期即时生效)。 """定时扫描 daemon 线程。每轮重读配置(开关/周期即时生效)。
+23
View File
@@ -81,6 +81,29 @@ function loadDiscovery(){
+'<button class="btn btn-xs" data-serial="'+esc(x.serial)+'" onclick="ignoreDiscoveryDev(this.dataset.serial)">忽略</button></td></tr>'; +'<button class="btn btn-xs" data-serial="'+esc(x.serial)+'" onclick="ignoreDiscoveryDev(this.dataset.serial)">忽略</button></td></tr>';
}).join(''); }).join('');
} }
// 正式池断联设备:仍在设备池(不删除),扫描线程自动重连,可手动立即重连
const offTb=document.getElementById('tb-pool-offline');
if(offTb){
const off=(r.pool_offline||[]);
if(!off.length){
offTb.innerHTML='<tr><td colspan="5" class="empty">全部在线</td></tr>';
}else{
offTb.innerHTML=off.map(x=>{
return '<tr><td style="font-family:monospace">'+esc(x.serial)+'</td>'
+'<td>'+esc(x.model||'-')+'</td><td>'+esc(x.name||'')+'</td>'
+'<td><span class="label label-danger">断联·自动重连中</span></td>'
+'<td><button class="btn btn-xs" data-serial="'+esc(x.serial)+'" onclick="reconnectPoolDev(this.dataset.serial)">立即重连</button></td></tr>';
}).join('');
}
}
});
}
function reconnectPoolDev(serial){
apiPost('/api/devices/discovery/reconnect',{serial}).then(r=>{
if(r&&r.ok){
showToast('重连已启动(约 5-15 秒生效)','success');
setTimeout(loadDiscovery,8000);
}else showToast('重连失败: '+((r&&r.error)||''),'error');
}); });
} }
function toggleDiscovery(checked){ function toggleDiscovery(checked){
+9
View File
@@ -766,6 +766,15 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
</table> </table>
<div class="help">扫描局域网/Tailscale 网段中开放 adb 5555 的设备进入<b>待连接池</b>; <div class="help">扫描局域网/Tailscale 网段中开放 adb 5555 的设备进入<b>待连接池</b>;
<b>确认后才加入正式设备池</b>(不自动连接、不参与任务调度)。未授权设备(手机未接受 RSA 弹窗)不会出现。</div> <b>确认后才加入正式设备池</b>(不自动连接、不参与任务调度)。未授权设备(手机未接受 RSA 弹窗)不会出现。</div>
<!-- 正式池断联设备:仍在设备池(不删除、不进待连接池),自动重连中 -->
<div style="margin-top:10px">
<div style="font-size:12.5px;font-weight:600;margin-bottom:6px">设备池断联设备
<span class="text-muted" style="font-weight:400">(仍在设备池内,扫描线程每轮自动重连;也可手动立即重连)</span></div>
<table class="table table-hover table-sm">
<thead><tr><th>serial</th><th>型号</th><th>备注</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="tb-pool-offline"><tr><td colspan="5" class="empty">全部在线</td></tr></tbody>
</table>
</div>
</div> </div>
<div class="toolbar"> <div class="toolbar">
<input type="text" id="devpool-serial" class="form-control" style="width:220px;font-family:monospace" <input type="text" id="devpool-serial" class="form-control" style="width:220px;font-family:monospace"
+35 -1
View File
@@ -118,14 +118,16 @@ def api_devices_pool_refresh_models():
@bp.route("/api/devices/discovery", methods=["GET"]) @bp.route("/api/devices/discovery", methods=["GET"])
@perm_required(PERM_DEVICES) @perm_required(PERM_DEVICES)
def api_devices_discovery(): def api_devices_discovery():
"""发现状态 + 待连接列表(前端 10s 轮询一次请求)。""" """发现状态 + 待连接列表 + 正式池断联设备(前端 10s 轮询一次请求)。"""
try: try:
status = device_discovery.get_status() status = device_discovery.get_status()
pending = device_discovery.list_pending() pending = device_discovery.list_pending()
pool_offline = device_discovery.list_pool_offline()
except Exception as e: except Exception as e:
_log.warning(f"discovery 状态获取失败: {e}") _log.warning(f"discovery 状态获取失败: {e}")
return jsonify({"ok": False, "error": str(e)[:120]}), 503 return jsonify({"ok": False, "error": str(e)[:120]}), 503
status["pending"] = pending status["pending"] = pending
status["pool_offline"] = pool_offline
return jsonify({"ok": True, **status}) return jsonify({"ok": True, **status})
@@ -182,6 +184,38 @@ def api_devices_discovery_ignore():
return jsonify({"ok": True, "msg": msg}) return jsonify({"ok": True, "msg": msg})
@bp.route("/api/devices/discovery/reconnect", methods=["POST"])
@perm_required(PERM_DEVICES)
def api_devices_discovery_reconnect():
"""手动立即重连正式池断联设备(后台 adb connect + 采型号)。
日常无需手动——发现线程每轮(默认 60s)自动重连断联设备;
本端点用于不想等下一轮扫描时立即拉起。
"""
serial = (request.json or {}).get("serial", "").strip()
if not serial:
return jsonify({"ok": False, "error": "缺少 serial"}), 400
from core import device_pool
if serial not in device_pool.list_configured():
return jsonify({"ok": False, "error": "设备不在设备池中"}), 404
def _reconnect():
try:
if ":" in serial:
adb_connect(serial)
# 无论是否连上,采一次型号(模型失败静默)
try:
device_pool.refresh_model(serial)
except Exception:
pass
except Exception as e:
_log.warning(f"手动重连 {serial} 异常: {e}")
threading.Thread(target=_reconnect, daemon=True).start()
_log.info(f"设备池管理: 手动重连 {serial}")
return jsonify({"ok": True, "msg": "重连已启动(约 5-15 秒生效)"})
@bp.route("/api/devices/discovery/settings", methods=["POST"]) @bp.route("/api/devices/discovery/settings", methods=["POST"])
@perm_required(PERM_DEVICES) @perm_required(PERM_DEVICES)
def api_devices_discovery_settings(): def api_devices_discovery_settings():