Files
DCU_DesktopController/scripts/skip_nic_experiment.py
T
cbb e91d53e0fc feat(ecat): 启动后把 ecat_native 提到 RealTime;跳拍优化实验脚本与报告
- esp32_server: Popen 后外部 SetPriorityClass(RT/High),native 自设会被钳成 High
- 入库 skip_* 标定/实验脚本与 REPORT_ecat_skip_opt_20260917
- 前端 WIP:layout/app/polling/boot_ecat/index
- 不含 lab/skip_meter 原始采样与 preview 截图
2026-09-17 02:32:20 +08:00

313 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""可回滚的 I210 网卡参数实验:停环 → 改高级属性 → 恢复 OP → 标定跳拍率。
默认只测、不永久破坏:原值会写入 restore json;`--restore` 按快照还原。
改网卡会触发短暂链路重协商,期间从站可能掉 OP,脚本会等 link+OP 恢复。
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import time
import urllib.request
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
LAB = os.path.join(ROOT, "lab", "skip_meter")
NIC = "以太网 2"
BASE = os.environ.get("ECAT_BASE", "http://127.0.0.1:8090")
def post(path, body=None, timeout=8.0):
data = json.dumps(body or {}).encode()
req = urllib.request.Request(
BASE + path, data=data, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode())
def ps(cmd, timeout=60):
r = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
)
return (r.stdout or "").strip(), (r.stderr or "").strip()
def nic_snapshot():
out, err = ps(
f"Get-NetAdapterAdvancedProperty -Name '{NIC}' | "
"Select-Object RegistryKeyword,DisplayValue,RegistryValue | "
"ConvertTo-Json -Compress"
)
if not out:
return []
data = json.loads(out)
if isinstance(data, dict):
return [data]
return data
def nic_set(keyword: str, value: str):
# value as string; Intel accepts "0"/"1"/numbers
cmd = (
f"Set-NetAdapterAdvancedProperty -Name '{NIC}' "
f"-RegistryKeyword '{keyword}' -RegistryValue '{value}' -NoRestart"
)
out, err = ps(cmd)
return out, err
def nic_restart():
out, err = ps(
f"Restart-NetAdapter -Name '{NIC}' -Confirm:$false; Start-Sleep -Seconds 2; "
f"Get-NetAdapter -Name '{NIC}' | Select-Object Status,LinkSpeed | ConvertTo-Json -Compress"
)
return out, err
def wait_link(timeout=20.0):
t0 = time.time()
while time.time() - t0 < timeout:
out, _ = ps(
f"Get-NetAdapter -Name '{NIC}' | Select-Object Status,LinkSpeed | ConvertTo-Json -Compress"
)
try:
j = json.loads(out)
if isinstance(j, list):
j = j[0]
if str(j.get("Status", "")).lower() == "up":
return True, j
except Exception:
pass
time.sleep(0.5)
return False, None
def wait_ecat_healthy(timeout=25.0):
t0 = time.time()
last = None
while time.time() - t0 < timeout:
try:
h = post("/api/native/health")
last = h
if h.get("loop") == 1 and int(h.get("wkc") or 0) >= 3 and h.get("op") == 1:
# also require cycles advancing
c0 = h.get("cycles") or 0
time.sleep(0.4)
h2 = post("/api/native/health")
c1 = h2.get("cycles") or 0
if c1 > c0:
return True, h2
except Exception as e:
last = {"err": str(e)}
time.sleep(0.5)
return False, last
def stop_bus():
logs = []
for path in ("/api/motor/stop", "/api/op/stop"):
try:
r = post(path)
logs.append((path, r.get("ok"), (r.get("log") or r)[:2]))
except Exception as e:
logs.append((path, False, str(e)))
return logs
def start_bus():
logs = []
try:
r = post("/api/op/start")
logs.append(("/api/op/start", r.get("ok"), (r.get("log") or [""])[:2]))
except Exception as e:
logs.append(("/api/op/start", False, str(e)))
try:
r = post("/api/motor/start")
logs.append(("/api/motor/start", r.get("ok"), (r.get("log") or [""])[:2]))
except Exception as e:
logs.append(("/api/motor/start", False, str(e)))
return logs
def meter(tag: str, secs: float):
# reuse skip_rate_meter as subprocess for isolation
cmd = [
sys.executable,
os.path.join(ROOT, "scripts", "skip_rate_meter.py"),
"--tag",
tag,
"--secs",
str(secs),
]
r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
# last json file for tag
files = sorted(
[f for f in os.listdir(LAB) if f.startswith(tag + "_") and f.endswith(".json")]
)
data = None
if files:
with open(os.path.join(LAB, files[-1]), encoding="utf-8") as f:
data = json.load(f)
return data, r.stdout, r.stderr
def apply_batch(changes: list[tuple[str, str]]):
results = []
for k, v in changes:
out, err = nic_set(k, v)
results.append({"key": k, "value": v, "out": out[:300], "err": err[:300]})
return results
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--restore", action="store_true", help="restore from snap json")
ap.add_argument("--snap", default=None, help="snapshot json for restore")
ap.add_argument("--exp", default=None, help="named experiment")
ap.add_argument("--secs", type=float, default=16.0)
ap.add_argument("--list-exp", action="store_true")
args = ap.parse_args()
os.makedirs(LAB, exist_ok=True)
# experiment table: list of (keyword, value)
EXPS = {
"itr_off": [
("*InterruptModeration", "0"),
("ITR", "0"),
],
"itr_low": [
("*InterruptModeration", "1"),
("ITR", "400"), # valid: 65535,3600,2000,950,400,200,0
],
"itr_200": [
("*InterruptModeration", "1"),
("ITR", "200"),
],
"itr_950": [
("*InterruptModeration", "1"),
("ITR", "950"),
],
"eee_off": [
("EEELinkAdvertisement", "0"),
],
"flow_off": [
("*FlowControl", "0"),
],
"txbuf_max": [
("*TransmitBuffers", "4096"),
],
"rxbuf_max": [
("*ReceiveBuffers", "1024"),
],
"combo_lowlat": [
("*InterruptModeration", "0"),
("ITR", "0"),
("EEELinkAdvertisement", "0"),
("*FlowControl", "0"),
("*TransmitBuffers", "2048"),
("*ReceiveBuffers", "512"),
],
}
if args.list_exp:
print(json.dumps({k: v for k, v in EXPS.items()}, indent=2))
return 0
# always keep a fresh snapshot before mutating
snap = nic_snapshot()
snap_path = os.path.join(LAB, f"nic_before_{int(time.time())}.json")
with open(snap_path, "w", encoding="utf-8") as f:
json.dump(snap, f, ensure_ascii=False, indent=2)
print(f"NIC snapshot → {snap_path}")
if args.restore:
path = args.snap or snap_path
with open(path, encoding="utf-8") as f:
data = json.load(f)
# stop, apply all, restart
print("restore: stop bus")
print(stop_bus())
n = 0
for row in data:
kw = row.get("RegistryKeyword")
val = row.get("RegistryValue")
if not kw or val is None:
continue
if isinstance(val, list):
val = val[0] if val else ""
val = str(val)
apply_batch([(kw, val)])
n += 1
print("restore applied", n, "keys; restarting NIC")
print(nic_restart())
print(wait_link())
print("start bus")
print(start_bus())
ok, h = wait_ecat_healthy()
print("healthy", ok, h)
return 0
exp = args.exp or "itr_off"
if exp not in EXPS:
print("unknown exp", exp, "use --list-exp")
return 2
changes = EXPS[exp]
print(f"\n=== EXP {exp} {changes} ===")
print("stop bus...")
print(stop_bus())
time.sleep(0.3)
print("apply...")
print(apply_batch(changes))
print("restart nic...")
print(nic_restart())
ok, info = wait_link()
print("link", ok, info)
print("start bus...")
print(start_bus())
ok, h = wait_ecat_healthy(timeout=30)
print("healthy", ok, h)
if not ok:
print("ABORT: bus not healthy after change")
return 1
# settle longer: NIC restart + re-enumeration leaves ~2–5s of elevated skip
time.sleep(8.0)
data, out, err = meter(f"exp_{exp}", args.secs)
print(out)
if err:
print("meter stderr", err[:500])
if data:
summary = {
"exp": exp,
"changes": changes,
"late_per_s": data.get("late_per_s"),
"skip_per_s": data.get("skip_per_s"),
"late_frac_pct": data.get("late_frac_pct"),
"send_stall_frac": data.get("send_stall_frac"),
"rx_stall_frac": data.get("rx_stall_frac"),
"achieved_hz": data.get("achieved_hz"),
"n_late": data.get("n_late"),
}
sp = os.path.join(LAB, f"summary_{exp}_{int(time.time())}.json")
with open(sp, "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print("SUMMARY", json.dumps(summary, ensure_ascii=False))
print("WROTE", sp)
return 0
if __name__ == "__main__":
raise SystemExit(main())