Files
DCU_ECAT_Master/ecat_tool.py
T
cbb d594b8fd94 init: DCU LAN9252 EtherCAT 主站工具集
现有工具纳入版本管理:
- ecat_tool.py: 一体化命令行主站(status/flash/eeprom/sdo/send)
- ecat_server.py + ecat.html/css: 网页/桌面控制台(端口 8090)
- ecat_scan.py / ecat_esc_regs.py / ecat_sniff.py: bring-up 扫描、
  网络侧 ESC 寄存器诊断、裸帧抓包对照
- ecat_flash_eeprom.py: 经网线烧写 SII 镜像 + 逐字校验
- *.bat / *.vbs 启动入口

新增 .gitignore(排除 venv/ 与 __pycache__)、.gitattributes(不改写换行符)、
requirements.txt(锁定 pysoem 1.1.13 等依赖)、README.md。
2026-09-03 01:56:29 +08:00

563 lines
24 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
"""ecat_tool.py — DCU LAN9252 EtherCAT 一体化主站工具(连接/收发/烧录)。
子命令:
adapters 列出 Npcap 适配器
status [substr] 扫描从站: 身份字/AL 状态/邮箱/PDO 尺寸
flash [--bin FILE] [substr] 烧录 EEPROM(gen_eeprom.py 镜像或 --bin)+逐字校验
eeprom read START COUNT [--out FILE] [substr]
eeprom write FILE [substr] 写 bin(奇数尾补 0xFF)+回读校验
eeprom verify FILE [substr] 只读校验
sdo read IDX SUB [--size N] [substr] CoE SDO 读(需 PREOP+,邮箱来自 SII)
sdo write IDX SUB HEX [substr] CoE SDO 写(HEX=字节流)
send [--hex H | --u64 a,b,c,d] [--rate-ms N] [--duration S | --forever]
[--dc-cycle-us N] [--dc-shift-us N] [substr]
进 OP 循环发送输出 PDO 并回显输入 PDO
monitor [--rate-ms N] [--duration S] [--dc-cycle-us N] [substr]
进 OP 只读输入(输出保持 0)
substr 默认 "I226"(按适配器描述模糊匹配)。send/monitor 的输出默认全 0。
--dc-cycle-us 非 0 时启用分布时钟: 从站 SYNC0 引脚按该周期输出脉冲,
GD32 侧 Sync0_Isr 才会被触发(默认 free-run 模式下 SYNC0 完全不打)。
验证: 主站设 1000us, 从站 [ECAT][SYNC] 日志应显示 sync0 ≈ 1000/s。
"""
import argparse
import os
import struct
import sys
import time
try:
import pysoem
except ImportError:
print("pysoem 无法加载:请先安装 Npcap(本目录 npcap-1.88.exe,"
"保持勾选 'Install WinPcap API-compatible Mode')")
sys.exit(1)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
EEPROM_SIZE = 4096 # AT24C32
# state_check 的 timeout 单位是微秒(pysoem: "Timeout value in us");
# 2000000us = 2s,与 SOEM 的 EC_TIMEOUTSTATE 默认值一致。
# 此前各处传 2000 实际只等 2ms,能通过纯属从站 ack 快,属侥幸。
EC_TIMEOUT_STATE_US = 2_000_000
AL_STATE = {0x00: "NONE", 0x01: "INIT", 0x02: "PREOP",
0x03: "BOOT", 0x04: "SAFEOP", 0x08: "OP"}
def al_name(code):
return AL_STATE.get(code, f"0x{code:02X}")
def find_adapter(substr):
for a in pysoem.find_adapters():
desc = a.desc.decode() if isinstance(a.desc, bytes) else a.desc
if substr.lower() in desc.lower():
return a
return None
def open_master(substr):
adapter = find_adapter(substr)
if adapter is None:
print(f"没有找到描述包含 {substr!r} 的适配器,可用:")
list_adapters()
sys.exit(1)
desc = adapter.desc.decode() if isinstance(adapter.desc, bytes) else adapter.desc
master = pysoem.Master()
master.open(adapter.name)
try:
n = master.config_init()
except Exception as e:
raise SystemExit(f"总线上没有发现从站({type(e).__name__}: {e})——"
"检查 DCU 板供电、网线是否接到该网口")
if not n:
raise SystemExit("总线上没有发现从站——检查 DCU 板供电、网线是否接到该网口")
return master, desc
def list_adapters():
for i, a in enumerate(pysoem.find_adapters()):
desc = a.desc.decode() if isinstance(a.desc, bytes) else a.desc
print(f"[{i}] {desc}\n {a.name}")
def print_slave_info(master):
print(f"从站: {len(master.slaves)} 个")
for i, s in enumerate(master.slaves):
print(f" [{i}] name={s.name!r}")
print(f" VendorID=0x{s.man:08X} ProductCode=0x{s.id:08X} Rev={s.rev}")
print(f" AL state={al_name(s.state & 0x0F)} AL code=0x{s.al_status:04X}")
try:
print(f" 输出 {len(bytes(s.output))}B / 输入 {len(bytes(s.input))}B "
f"(config_map 后有效)")
except Exception:
pass
# ---------------------------------------------------------------- status
def cmd_status(args):
if args.substr in ("-l", "--list"):
list_adapters()
return
master, desc = open_master(args.substr)
print(f"适配器: {desc}")
print_slave_info(master)
if master.slaves and master.slaves[0].man == 0xFFFFFFFF:
print("\n提示: 身份字全 F = EEPROM 无效。先执行: ecat_tool.py flash")
# ---------------------------------------------------------------- eeprom
def eeprom_read_word(slave, word):
return bytes(slave.eeprom_read(word))[:2]
def flash_image(slave, img, label):
if len(img) % 2 != 0:
img += b"\xFF"
if len(img) > EEPROM_SIZE:
raise SystemExit(f"镜像 {len(img)}B 超过 EEPROM 容量 {EEPROM_SIZE}B")
# 字节序探针(写入 0x1234 找读回偏移,随后恢复擦除态)
test = b"\x34\x12"
slave.eeprom_write(0x0200, test)
rb = bytes(slave.eeprom_read(0x0200))
off = rb.find(test)
if off < 0:
raise SystemExit(f"EEPROM 写探针失败: 读回 {rb.hex().upper()}")
slave.eeprom_write(0x0200, b"\xFF\xFF")
print(f"字节序探针 OK (读回偏移 {off})")
n_written = n_fail = 0
fails = []
for word in range(len(img) // 2):
val = struct.unpack_from("<H", img, word * 2)[0]
if val == 0xFFFF:
continue
data = struct.pack("<H", val)
try:
slave.eeprom_write(word, data)
except Exception as e:
fails.append((word, data.hex(), f"WRITE EXC {type(e).__name__}: {e}"))
n_fail += 1
continue
if eeprom_read_word(slave, word) != data:
fails.append((word, data.hex(), eeprom_read_word(slave, word).hex()))
n_fail += 1
n_written += 1
if n_written % 64 == 0:
print(f" ... 已写 {n_written} 字")
print(f"[{label}] 刷写完成: 成功 {n_written} 字, 失败 {n_fail} 字")
if fails:
for w, exp, act in fails[:20]:
print(f" word 0x{w:04X}: 期望 {exp.upper()} 实际 {act.upper()}")
raise SystemExit(3)
print(">>> 板子断电重新上电后 ESC 才会重载配置(身份字/PDI/SYNC)")
def cmd_flash(args):
if args.bin:
with open(args.bin, "rb") as f:
img = f.read()
label = os.path.basename(args.bin)
if len(img) % 2:
img += b"\xFF"
else:
from ecat_flash_eeprom import load_image # 自带 gen_eeprom.py 自校验
img = load_image()
label = "gen_eeprom.py"
print(f"镜像: {label}, {len(img)} 字节")
master, desc = open_master(args.substr)
print(f"适配器: {desc}, 从站应答正常")
flash_image(master.slaves[0], img, label)
def cmd_eeprom(args):
master, _ = open_master(args.substr)
slave = master.slaves[0]
if args.op == "read":
start, count = args.start, args.count
rows = []
for w in range(start, start + count):
rows.append(struct.unpack("<H", eeprom_read_word(slave, w))[0])
for base in range(0, count, 8):
chunk = rows[base:base + 8]
print(f" 0x{start + base:04X}: " +
" ".join(f"{v:04X}" for v in chunk))
if args.out:
with open(args.out, "wb") as f:
f.write(struct.pack(f"<{count}H", *rows))
print(f"已保存 {args.out} ({count} word)")
elif args.op == "write":
with open(args.file, "rb") as f:
data = f.read()
if len(data) % 2:
data += b"\xFF"
if len(data) > EEPROM_SIZE:
raise SystemExit(f"{len(data)}B 超过 EEPROM 容量 {EEPROM_SIZE}B")
n = n_fail = 0
for word in range(len(data) // 2):
val = struct.unpack_from("<H", data, word * 2)[0]
if val == 0xFFFF:
continue
raw = struct.pack("<H", val)
slave.eeprom_write(word, raw)
if eeprom_read_word(slave, word) != raw:
print(f" word 0x{word:04X} 校验失败")
n_fail += 1
n += 1
print(f"写 {n} 字, 失败 {n_fail} 字" + ("" if not n_fail else " -> FAIL"))
sys.exit(0 if n_fail == 0 else 3)
elif args.op == "verify":
with open(args.file, "rb") as f:
data = f.read()
if len(data) % 2:
data += b"\xFF"
diffs = 0
for word in range(min(len(data), EEPROM_SIZE) // 2):
expect = struct.unpack_from("<H", data, word * 2)[0]
actual = struct.unpack("<H", eeprom_read_word(slave, word))[0]
if expect != actual:
print(f" word 0x{word:04X}: 文件 0x{expect:04X} != 芯片 0x{actual:04X}")
diffs += 1
print("verify: " + ("OK, 全部一致" if diffs == 0 else f"{diffs} 字不符 -> FAIL"))
sys.exit(0 if diffs == 0 else 3)
# ---------------------------------------------------------------- SDO
def cmd_sdo(args):
master, desc = open_master(args.substr)
s = master.slaves[0]
if args.op == "read":
try:
data = bytes(s.sdo_read(args.index, args.sub, args.size))
except Exception as e:
raise SystemExit(f"SDO 读失败: {type(e).__name__}: {e}")
print(f"0x{args.index:04X}:{args.sub} = {data.hex().upper()} ({len(data)}B)")
else:
try:
payload = bytes.fromhex(args.hex.replace(" ", ""))
except ValueError:
raise SystemExit("HEX 非法: 只能是十六进制字节流")
try:
s.sdo_write(args.index, args.sub, payload)
except Exception as e:
raise SystemExit(f"SDO 写失败: {type(e).__name__}: {e}")
print(f"0x{args.index:04X}:{args.sub} <- {payload.hex().upper()} ({len(payload)}B) OK")
# ---------------------------------------------------------------- PDO 收发
def enter_op(master, dc_cycle_us=0, dc_shift_us=0):
"""config_map → (可选 DC) → SAFEOP → OP。
dc_cycle_us=0 时保持 free-run(SYNC0 引脚无脉冲,从站 Sync0_Isr 不会触发)。
非 0 时先 config_dc() 测量传播延迟,再对从站下发 SYNC0 周期,
必须在 config_map() 之后、进 SAFEOP 之前完成(ETG.1020 DC 配置顺序)。
"""
master.state = pysoem.PREOP_STATE | pysoem.STATE_ACK
master.write_state()
if master.state_check(pysoem.PREOP_STATE, EC_TIMEOUT_STATE_US) != pysoem.PREOP_STATE:
# read_state 拿真实 AL Status Code(广播 state_check 不刷新 al_status,
# 陈旧缓存恒显示 0x0000)。首败常见 0x0016/0x001D 类:停止回 INIT 时 SSC
# 禁用了邮箱 SM0/SM1,CheckSmSettings 拒绝 INIT→PREOP,重新 config_init 即恢复。
try:
master.read_state()
except Exception:
pass
codes = [(al_name(sl.state & 0x0F), f"0x{sl.al_status:04X}") for sl in master.slaves]
raise SystemExit(f"进 PREOP 失败: AL={codes}(真实值) —— 从站未 ack AL Control=PREOP。"
"0x0016/0x001D 类=邮箱 SM0/SM1 被停止时禁用(重跑即恢复);"
"抓 GD32 串口看 [ECAT][AL] evt/req")
# free-run 时清残留 DC 激活(2026-09-03 00:27 卡死根因之一):之前跑过 DC 的
# 0x0981 激活位跨重连不清,从站会按 DC 同步模式等 PLL,free-run 下永不进 OP。
# 必须在 config_map(其内部请求 SAFEOP)之前清除。
if not dc_cycle_us:
try:
master.slaves[0].dc_sync(False, 0, 0)
print("已清除残留 DC 激活(free-run)")
except Exception as e:
print(f"WARN: 清 DC 激活失败({type(e).__name__}: {e})")
master.config_map()
# 不要写 0x1C32:01 / 0x1C33:01 去改 SyncType:该子索引只允许在 PREOP 写
# (ACCESS_WRITE_PREOP),而 config_map() 已把从站推到 SAFEOP,实测返回
# SdoError 0x08000022(当前状态不可写)。而且没必要——SSC 在 PREOP→SAFEOP 的
# StartInputHandler() 会自动按 DC 寄存器/PDO 长度推导出 SM_SYNCHRON
# (ecatslv.c:849-880),coeappl.c 里的 FREERUN 只是上电初值。
if dc_cycle_us:
has_dc = master.config_dc()
if not has_dc:
raise SystemExit("config_dc(): 总线上没有支持 DC 的从站 —— "
"检查 SII word1 高字节(0x0151 Sync/Latch 配置)是否为 0xCC")
master.slaves[0].dc_sync(True, dc_cycle_us * 1000, dc_shift_us * 1000)
print(f"DC 已启用: SYNC0 周期 {dc_cycle_us}us"
+ (f", shift {dc_shift_us}us" if dc_shift_us else "")
+ f", 主站 DC 时间={master.dc_time}ns")
# 显式带 ACK 写 SAFEOP(config_map 内部已推过一次,这里再立一次并顺带清掉
# 上一轮残留的错误位——SSC 只有带 ACK 位的写才能清错+迁移,普通写会被忽略)
master.state = pysoem.SAFEOP_STATE | pysoem.STATE_ACK
master.write_state()
if master.state_check(pysoem.SAFEOP_STATE, EC_TIMEOUT_STATE_US) != pysoem.SAFEOP_STATE:
try:
master.read_state() # 真实 AL Status Code(广播 state_check 不刷新)
except Exception:
pass
codes = [(al_name(sl.state & 0x0F), f"0x{sl.al_status:04X}") for sl in master.slaves]
raise SystemExit(f"进 SAFEOP 失败: AL={codes}(真实值) —— 检查 SII 的 SM/FMMU 类别"
"(EEPROM 是否已刷)与 GD32 SSC 是否运行")
# 主站侧直写过程数据看门狗(双保险,SOEM set_watchdog → FPWR 0x0420 = 1000ms)。
# 从站固件 ≥1.5.45 也会写 1s,但板上固件未更新时 100ms 复位默认值会在 OP 等待期
# 触发 0x001B 拖回 SAFEOP;主站每次连接直接写,不依赖固件版本。
try:
master.slaves[0].set_watchdog('processdata', 1000.0)
except Exception as e:
print(f"WARN: 主站侧写看门狗失败({type(e).__name__}: {e})")
# SAFEOP -> OP:从站有输出 PDO 时,SSC 要求先收到至少一帧过程数据才 ack OP
# (ecatslv.c:1993 的 `nPdOutputSize == 0 || bEcatFirstOutputsReceived`),
# 否则迁移被挂起、随后回落,表现为 "进 OP 失败 ... code=0x0000"。
# 所以必须边发 processdata 边等,不能只 write_state 后干等。
# AL Control 一律带 ACK 位(0x10):SSC 对"错误位已置+无 ACK+请求≠INIT"的请求
# 直接忽略(ecatslv.c:1376-1384),残留的 0x001B 错误位不清掉,写 OP 无人应答。
master.state = pysoem.OP_STATE | pysoem.STATE_ACK
master.write_state()
# 状态查询用 read_state() 而非 state_check():后者 slave=0 广播模式不更新
# slavelist[0].ALstatuscode(报错里的 code 是陈旧缓存,恒 0x0000),且内部
# 1ms 轮询发帧风暴。read_state 无错误时 1 帧 BRD,有错误位时逐从站 FPRD
# 拿**真实 AL Status Code**。失败时输出真实错误码+状态轨迹。
deadline = time.time() + 12.0 # 12s:覆盖从站 ESM 超时(9s,固件≥1.5.47)+恢复重试
trail = []
last_key = None
state = None
code = 0
wkc_samples = []
next_rewrite = 0.0 # OP|ACK 重写节流:每 200ms 最多一次(避免 AL Control 写风暴)
while time.time() < deadline:
master.send_processdata()
wkc = master.receive_processdata(2000)
if not wkc_samples or wkc_samples[-1] != (wkc, master.expected_wkc):
wkc_samples.append((wkc, master.expected_wkc))
try:
master.read_state()
except Exception:
pass
state = master.slaves[0].state & 0x0F
code = master.slaves[0].al_status
key = (state, code)
if key != last_key:
trail.append((al_name(state), code))
last_key = key
if state == pysoem.OP_STATE and code == 0:
break
# 跌回 PREOP/INIT 时:带 ACK 重写 SAFEOP 立起状态,再带 ACK 重请 OP
if state < pysoem.SAFEOP_STATE:
master.state = pysoem.SAFEOP_STATE | pysoem.STATE_ACK
master.write_state()
master.state_check(pysoem.SAFEOP_STATE, 500_000)
next_rewrite = 0.0
if time.time() >= next_rewrite:
master.state = pysoem.OP_STATE | pysoem.STATE_ACK
master.write_state()
next_rewrite = time.time() + 0.2
time.sleep(0.005) # 循环节奏 ~7ms:LRW 间隔稳低于任何看门狗配置
if state != pysoem.OP_STATE or code != 0:
try:
master.read_state()
except Exception:
pass
st = master.slaves[0]
trail.append((al_name(st.state & 0x0F), st.al_status))
try:
wd_ms = st.get_watchdog('processdata')
wd_info = f", 从站 PD 看门狗={wd_ms:.0f}ms"
except Exception:
wd_info = ""
wkc_info = " / ".join(f"wkc={w}/{e}" for w, e in wkc_samples[:6])
raise SystemExit(
f"进 OP 失败: AL={al_name(st.state & 0x0F)} code=0x{st.al_status:04X}(真实值{wd_info}) —— "
f"等待期轨迹: {' -> '.join(f'{n}+0x{c:04X}' for n, c in trail)}; LRW WKC: {wkc_info}。"
"0x001B=SM watchdog;0x001A=DC 残留激活;0x001D/0x001E=SM 配置不匹配;"
"0x0000=从站在等首帧输出。请抓 GD32 串口 [ECAT][AL] evt/req 日志对照")
def leave_init(master):
try:
master.state = pysoem.INIT_STATE
master.write_state()
except Exception:
pass
def run_pdo(master, out_data, rate_ms, duration, label, dc_cycle_us=0, dc_shift_us=0):
out_len = len(bytes(master.slaves[0].output))
if out_data is None:
out_data = bytes(out_len)
if len(out_data) < out_len:
out_data += bytes(out_len - len(out_data))
try:
master.slaves[0].output = out_data
except Exception as e:
raise SystemExit(f"写输出缓冲失败({type(e).__name__}: {e}) —— output 长度={out_len}")
enter_op(master, dc_cycle_us, dc_shift_us)
mode = f"DC {dc_cycle_us}us" if dc_cycle_us else "free-run"
print(f"[{label}] 已进 OP({mode}): 输出 {out_len}B / 输入 "
f"{len(bytes(master.slaves[0].input))}B, expected_wkc={master.expected_wkc}"
f"{', Ctrl+C 退出' if duration is None else f', 运行 {duration}s'}")
t_end = None if duration is None else time.time() + duration
cycles = wkc_errs = 0
last = 0.0
try:
while True:
master.send_processdata()
wkc = master.receive_processdata(2000)
cycles += 1
if wkc < master.expected_wkc:
wkc_errs += 1
now = time.time()
if now - last >= 0.5:
last = now
inp = bytes(master.slaves[0].input)
extra = ""
if len(inp) >= 32:
u64 = struct.unpack("<4Q", inp[:32])
extra = " u64=" + ",".join(f"0x{x:X}" for x in u64)
print(f"cycles={cycles} wkc_err={wkc_errs} in={inp.hex().upper()}{extra}")
if t_end is not None and now >= t_end:
break
time.sleep(rate_ms / 1000.0)
except KeyboardInterrupt:
print("\n用户中断")
finally:
leave_init(master)
print(f"[{label}] 结束: {cycles} 周期, wkc_err={wkc_errs}, 已回 INIT")
def parse_output_data(args):
if args.hex:
try:
return bytes.fromhex(args.hex.replace(" ", ""))
except ValueError:
raise SystemExit("--hex 非法: 只能是十六进制字节流")
if args.u64:
try:
vals = [int(v, 0) for v in args.u64.split(",")]
except ValueError:
raise SystemExit("--u64 非法: 逗号分隔的整数(支持 0x 前缀)")
if len(vals) > 4:
raise SystemExit("--u64 最多 4 个(4×64bit=32B)")
return struct.pack(f"<{len(vals)}Q", *vals)
return None
def cmd_send(args):
master, desc = open_master(args.substr)
print(f"适配器: {desc}")
run_pdo(master, parse_output_data(args), args.rate_ms,
None if args.forever else args.duration, "send",
args.dc_cycle_us, args.dc_shift_us)
def cmd_monitor(args):
master, desc = open_master(args.substr)
print(f"适配器: {desc}")
run_pdo(master, None, args.rate_ms,
None if args.forever else args.duration, "monitor",
args.dc_cycle_us, args.dc_shift_us)
# ---------------------------------------------------------------- main
def main():
p = argparse.ArgumentParser(description="DCU LAN9252 EtherCAT 主站工具")
sub = p.add_subparsers(dest="cmd", required=False)
sub.add_parser("adapters", help="列出 Npcap 适配器")
sp = sub.add_parser("status", help="扫描从站身份/AL 状态")
sp.add_argument("substr", nargs="?", default="I226")
sp = sub.add_parser("flash", help="烧录 EEPROM + 逐字校验")
sp.add_argument("--bin", help="指定 bin(默认用 gen_eeprom.py 镜像)")
sp.add_argument("substr", nargs="?", default="I226")
sp = sub.add_parser("eeprom", help="EEPROM read/write/verify")
sp.add_argument("op", choices=["read", "write", "verify"])
sp.add_argument("file_or_start", nargs="?", default=None)
sp.add_argument("count_or_file", nargs="?", default=None)
sp.add_argument("--out")
sp.add_argument("substr", nargs="?", default="I226")
sp = sub.add_parser("sdo", help="CoE SDO read/write")
sp.add_argument("op", choices=["read", "write"])
sp.add_argument("index", type=lambda v: int(v, 0))
sp.add_argument("sub", type=lambda v: int(v, 0))
sp.add_argument("hex", nargs="?", default=None)
sp.add_argument("--size", type=lambda v: int(v, 0), default=8)
sp.add_argument("substr", nargs="?", default="I226")
sp = sub.add_parser("send", help="进 OP 循环发送输出 PDO")
sp.add_argument("--hex", help="输出字节流(≤输出区长度)")
sp.add_argument("--u64", help="4 个 64bit, 逗号分隔, 如 0x1,0x2,0x3,0x4")
sp.add_argument("--rate-ms", type=int, default=100)
sp.add_argument("--duration", type=float, default=5.0)
sp.add_argument("--forever", action="store_true")
sp.add_argument("--dc-cycle-us", type=int, default=0,
help="启用 DC 并设 SYNC0 周期(us); 0=free-run(默认, SYNC0 不打)")
sp.add_argument("--dc-shift-us", type=int, default=0,
help="SYNC0 相移(us), 仅在 --dc-cycle-us 非 0 时有效")
sp.add_argument("substr", nargs="?", default="I226")
sp = sub.add_parser("monitor", help="进 OP 只读输入 PDO")
sp.add_argument("--rate-ms", type=int, default=200)
sp.add_argument("--duration", type=float, default=10.0)
sp.add_argument("--forever", action="store_true")
sp.add_argument("--dc-cycle-us", type=int, default=0,
help="启用 DC 并设 SYNC0 周期(us); 0=free-run(默认, SYNC0 不打)")
sp.add_argument("--dc-shift-us", type=int, default=0,
help="SYNC0 相移(us), 仅在 --dc-cycle-us 非 0 时有效")
sp.add_argument("substr", nargs="?", default="I226")
args = p.parse_args()
if args.cmd is None:
# 双击/无参数时默认执行 status
print("(未指定子命令, 默认执行 status; 完整用法: ecat_tool.py -h)\n")
args.cmd = "status"
args.substr = "I226"
if args.cmd == "adapters":
list_adapters()
elif args.cmd == "status":
cmd_status(args)
elif args.cmd == "flash":
cmd_flash(args)
elif args.cmd == "eeprom":
if args.op == "read":
if args.file_or_start is None or args.count_or_file is None:
raise SystemExit("用法: eeprom read START COUNT [--out FILE]")
args.start = int(args.file_or_start, 0)
args.count = int(args.count_or_file, 0)
else:
if args.file_or_start is None:
raise SystemExit(f"用法: eeprom {args.op} FILE")
args.file = args.file_or_start
cmd_eeprom(args)
elif args.cmd == "sdo":
if args.op == "write" and args.hex is None:
raise SystemExit("用法: sdo write IDX SUB HEX")
cmd_sdo(args)
elif args.cmd == "send":
cmd_send(args)
elif args.cmd == "monitor":
cmd_monitor(args)
if __name__ == "__main__":
main()