131 lines
4.5 KiB
Python
131 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""在「以太网 2」(I210) 上关闭非 EtherCAT 绑定,测跳拍率;可 --restore 还原。"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
|
||
|
|
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||
|
|
LAB = os.path.join(ROOT, "lab", "skip_meter")
|
||
|
|
NIC = "以太网 2"
|
||
|
|
|
||
|
|
|
||
|
|
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 snapshot():
|
||
|
|
out, err = ps(
|
||
|
|
f"Get-NetAdapterBinding -Name '{NIC}' | "
|
||
|
|
"Select-Object DisplayName,ComponentID,Enabled | ConvertTo-Json -Compress"
|
||
|
|
)
|
||
|
|
data = json.loads(out)
|
||
|
|
return data if isinstance(data, list) else [data]
|
||
|
|
|
||
|
|
|
||
|
|
def disable(component_ids):
|
||
|
|
logs = []
|
||
|
|
for cid in component_ids:
|
||
|
|
out, err = ps(
|
||
|
|
f"Disable-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' -Confirm:$false; "
|
||
|
|
f"Get-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' | "
|
||
|
|
"Select-Object ComponentID,Enabled | ConvertTo-Json -Compress"
|
||
|
|
)
|
||
|
|
logs.append({"cid": cid, "out": out[:300], "err": err[:300]})
|
||
|
|
return logs
|
||
|
|
|
||
|
|
|
||
|
|
def enable(component_ids):
|
||
|
|
logs = []
|
||
|
|
for cid in component_ids:
|
||
|
|
out, err = ps(
|
||
|
|
f"Enable-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' -Confirm:$false; "
|
||
|
|
f"Get-NetAdapterBinding -Name '{NIC}' -ComponentID '{cid}' | "
|
||
|
|
"Select-Object ComponentID,Enabled | ConvertTo-Json -Compress"
|
||
|
|
)
|
||
|
|
logs.append({"cid": cid, "out": out[:300], "err": err[:300]})
|
||
|
|
return logs
|
||
|
|
|
||
|
|
|
||
|
|
def set_prio(level="RealTime"):
|
||
|
|
p = int(subprocess.check_output(
|
||
|
|
["powershell", "-NoProfile", "-Command", "(Get-Process ecat_native).Id"],
|
||
|
|
text=True).strip())
|
||
|
|
subprocess.check_output(
|
||
|
|
["powershell", "-NoProfile", "-Command",
|
||
|
|
f"$p=Get-Process -Id {p}; $p.PriorityClass='{level}'; $p.PriorityClass"],
|
||
|
|
text=True)
|
||
|
|
return p
|
||
|
|
|
||
|
|
|
||
|
|
def meter(tag, secs=18):
|
||
|
|
r = subprocess.run(
|
||
|
|
[sys.executable, os.path.join(ROOT, "scripts", "skip_rate_meter.py"),
|
||
|
|
"--tag", tag, "--secs", str(secs)],
|
||
|
|
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||
|
|
files = sorted(f for f in os.listdir(LAB) if f.startswith(tag + "_") and f.endswith(".json"))
|
||
|
|
return json.load(open(os.path.join(LAB, files[-1]), encoding="utf-8"))
|
||
|
|
|
||
|
|
|
||
|
|
# Offloads that are useless/dangerous for raw EtherCAT on a dedicated NIC
|
||
|
|
OFFLOADS = [
|
||
|
|
"vmware_bridge", # VMware Bridge — intercepts/injects, classic ECAT killer
|
||
|
|
"ms_pacer", # QoS packet scheduler
|
||
|
|
"ms_lldp", # LLDP
|
||
|
|
"ms_rspndr", # LLTD responder
|
||
|
|
"ms_lltdio", # LLTD mapper
|
||
|
|
"ms_server", # file/printer sharing
|
||
|
|
"ms_msclient", # Microsoft network client
|
||
|
|
# keep ms_tcpip / ms_tcpip6 for now in case anything uses IP on this NIC
|
||
|
|
# Npcap must stay: INSECURE_NPCAP
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
os.makedirs(LAB, exist_ok=True)
|
||
|
|
snap = snapshot()
|
||
|
|
snap_path = os.path.join(LAB, f"nic_bind_{int(time.time())}.json")
|
||
|
|
json.dump(snap, open(snap_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||
|
|
print("bind snapshot", snap_path)
|
||
|
|
|
||
|
|
if "--restore" in sys.argv:
|
||
|
|
print(enable(OFFLOADS))
|
||
|
|
return 0
|
||
|
|
|
||
|
|
print("disable", OFFLOADS)
|
||
|
|
print(json.dumps(disable(OFFLOADS), ensure_ascii=False, indent=2))
|
||
|
|
print("set RealTime")
|
||
|
|
print("pid", set_prio("RealTime"))
|
||
|
|
# binding change usually doesn't reset link; brief settle
|
||
|
|
time.sleep(3)
|
||
|
|
data = meter("bind_off", 18)
|
||
|
|
row = {
|
||
|
|
"late_per_s": data["late_per_s"],
|
||
|
|
"skip_per_s": data["skip_per_s"],
|
||
|
|
"late_frac_pct": data["late_frac_pct"],
|
||
|
|
"achieved_hz": data["achieved_hz"],
|
||
|
|
"n_late": data["n_late"],
|
||
|
|
"send_stall_frac": data["send_stall_frac"],
|
||
|
|
"rx_stall_frac": data["rx_stall_frac"],
|
||
|
|
"work_p50": data["window_cyc_us"]["work"]["p50"],
|
||
|
|
"rx_p50": data["window_cyc_us"]["rx"]["p50"],
|
||
|
|
"send_p50": data["window_cyc_us"]["send"]["p50"],
|
||
|
|
"work_p90": data["window_cyc_us"]["work"]["p90"],
|
||
|
|
}
|
||
|
|
print("ROW", json.dumps(row, ensure_ascii=False, indent=2))
|
||
|
|
out = os.path.join(LAB, f"summary_bind_off_{int(time.time())}.json")
|
||
|
|
json.dump({"row": row, "disabled": OFFLOADS, "snap": snap_path},
|
||
|
|
open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
||
|
|
print("WROTE", out)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|