feat: 添加局域网设备发现功能和链路胶囊优化
- 实现局域网设备发现面板,支持探测ESP32中继板并自动切换端点 - 添加POST /api/lan/probe接口用于设备探测,返回三态判决结果 - 实现快扫和全段扫描两种模式,支持种子地址优先探测 - 添加端点唯一写手机制,防止多处同时修改设备IP - 优化链路胶囊UI显示WiFi/EtherCAT/BLE连接状态和设备名称 - 修复EtherCAT链路OTA刷写期间PDO循环暂停和AL状态处理问题 - 调整PDO周期设置范围为0.125-4ms,支持亚毫秒显示为微秒 - 添加ESP32 WiFi使能开关控件样式和数据来源角标显示
This commit is contained in:
+58
-2
@@ -69,6 +69,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# EtherCAT 客户端 / 进程治理模块(R8)
|
||||
import ecat_client
|
||||
import ecat_proc
|
||||
import lan_probe
|
||||
|
||||
ecat_proc.init(BASE_DIR)
|
||||
|
||||
@@ -495,6 +496,11 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
p, rel = UI_STATE_ROUTES[path_l]
|
||||
self._handle_ui_state_post(p, rel)
|
||||
return
|
||||
if path_l == "/api/lan/probe":
|
||||
# 局域网设备发现: 无状态, 一次请求探一批(≤24), 探完就回。
|
||||
# 编排(候选表/分批/进度/取消/缓存)全在页面 js/common/lan-picker.js。
|
||||
self._handle_lan_probe()
|
||||
return
|
||||
if path.startswith(ECAT_PROXY_PREFIX):
|
||||
# 电机页的 EtherCAT 传输: 同源转发到 EtherCAT 后端(见 ecat_client)。
|
||||
# 超时给足: /api/op/start 要走 PREOP→SAFEOP→OP, 内部还会自动重试一次,
|
||||
@@ -504,9 +510,14 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
except ValueError:
|
||||
length = 0
|
||||
raw = self.rfile.read(length) if length > 0 else b"{}"
|
||||
# /api/open 正常只要 ~0.1s(实测 107ms),但刷写 commit 后那一拍不算正常:
|
||||
# 新 APP 起板看到 warm 残值会自己补一发 LAN9252 RST#(esc_lan9252.cpp:441),
|
||||
# 芯片+PHY 整颗复位 → I210 链路重协商 1~2s,open 的广播/statecheck 全撞上
|
||||
# 去,实测能跨过 6s 档 → 代理假超时 → 非幂等只回 503(2026-10-09 21:49 现场)。
|
||||
timeout = 30.0 if path.endswith(("/op/start", "/motor/start",
|
||||
"/flash", "/eeprom/write",
|
||||
"/eeprom/read", "/eeprom/verify")) else 6.0
|
||||
"/eeprom/read", "/eeprom/verify",
|
||||
"/open")) else 6.0
|
||||
# EtherCAT 直灌 BOOT: run 要走 519 个邮箱帧 + 整区预擦, 几十秒量级。
|
||||
# 默认 6s 会在"正在正常刷写"的过程中假失败 —— 浏览器报错离场, native
|
||||
# 却还在往 flash 里写, 比不刷更糟。同一路由下的 open/push 都是纯内存
|
||||
@@ -637,6 +648,51 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return
|
||||
self._post_json(json.dumps(result, ensure_ascii=False).encode("utf-8"))
|
||||
|
||||
def _handle_lan_probe(self):
|
||||
"""POST /api/lan/probe — 局域网设备探测原语(一批 ≤24 个 IP,探完就回)。
|
||||
|
||||
刻意不建 job 表 / 不常驻线程 / 不落缓存:编排全在页面
|
||||
(js/common/lan-picker.js),这里由每连接一线程的 ThreadingHTTPServer
|
||||
自然限流。页面中途关标签最坏是让本线程跑满这一批的截止时长。
|
||||
"""
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
except ValueError:
|
||||
length = 0
|
||||
raw = self.rfile.read(min(length, 65536)) if length > 0 else b""
|
||||
if length == 0 or length > 65536:
|
||||
self._post_json(json.dumps({"ok": False, "errors": ["bad-body"]},
|
||||
ensure_ascii=False).encode("utf-8"), 400)
|
||||
return
|
||||
try:
|
||||
obj = json.loads(raw.decode("utf-8", "replace"))
|
||||
except ValueError:
|
||||
obj = {}
|
||||
if not isinstance(obj, dict):
|
||||
obj = {}
|
||||
ips = obj.get("ips") if isinstance(obj.get("ips"), list) else []
|
||||
ports = obj.get("ports") if isinstance(obj.get("ports"), list) else None
|
||||
if not ports:
|
||||
ports = [80]
|
||||
try:
|
||||
tmo = int(obj.get("timeout_ms") or 750)
|
||||
except (TypeError, ValueError):
|
||||
tmo = 750
|
||||
try:
|
||||
out = lan_probe.probe_batch(
|
||||
ips, ports=ports, path=obj.get("path") or lan_probe.PROBE_PATH,
|
||||
timeout_ms=tmo)
|
||||
except Exception as e: # noqa: BLE001
|
||||
out = {"ok": False, "results": [], "scan": {"candidates": len(ips)},
|
||||
"errors": ["%s: %s" % (type(e).__name__, e)]}
|
||||
if obj.get("local"):
|
||||
# 同一次往返带回"本机在哪段",页面才说得出扫的是哪段、依据是什么。
|
||||
try:
|
||||
out["local"] = lan_probe.local_net()
|
||||
except Exception as e: # noqa: BLE001
|
||||
out["local"] = {"ok": False, "err": type(e).__name__}
|
||||
self._post_json(json.dumps(out, ensure_ascii=False).encode("utf-8"))
|
||||
|
||||
def _handle_ui_state_post(self, path, rel):
|
||||
"""配置快照 POST(R11:motor/lab 共用,表驱动)。"""
|
||||
self._write_ui_state_route(path, rel)
|
||||
@@ -735,7 +791,7 @@ def _run_browser(args):
|
||||
url = f"http://127.0.0.1:{port}/tabs.html"
|
||||
print(f"PORT={port}", flush=True) # 窗口模式父进程据此得知端口
|
||||
print(f"ESP32 调试控制台: {url} (Ctrl+C 退出)")
|
||||
print("页面里的设备 IP 决定 API 目标(默认 192.168.31.127), 本服务只提供页面")
|
||||
print("设备端点由页面决定(顶栏『发现』选择或 IP 输入框手填); 本服务另有 /api/lan/probe 探测原语")
|
||||
if not args.no_browser:
|
||||
threading.Timer(0.5, lambda: webbrowser.open(url)).start()
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user