现有工具纳入版本管理: - 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。
478 lines
22 KiB
HTML
478 lines
22 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>EtherCAT 控制台 — DCU LAN9252</title>
|
||
<link rel="icon" href="data:,">
|
||
<link rel="stylesheet" href="ecat.css?v=20260831a">
|
||
<style>
|
||
/* ecat 页面专属补充(基于 motor.css 设计体系) */
|
||
.ecat-grid { grid-template-columns: minmax(240px, 290px) minmax(0, 1fr) minmax(0, 1.1fr); }
|
||
.center-col { display: flex; flex-direction: column; min-height: 0; }
|
||
.center-col .global-log { flex: 1; min-height: 90px; max-height: none; }
|
||
.mode-op { background: var(--ok-dim); color: var(--ok); }
|
||
.mode-safeop { background: var(--warn-dim); color: var(--warn); }
|
||
.mode-preop { background: color-mix(in srgb, var(--primary) 16%, transparent); color: var(--primary); }
|
||
.eep { margin-top: var(--sp-3); padding: var(--sp-3); background: var(--bg-root);
|
||
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||
font-family: var(--font-mono); font-size: 0.74rem; line-height: 1.7;
|
||
color: var(--fg-muted); white-space: pre; overflow: auto; max-height: 170px; }
|
||
.btn-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-3); align-items: center; }
|
||
.br-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-2); margin-top: var(--sp-3); }
|
||
.br-row .big-reading { cursor: default; padding: var(--sp-2) var(--sp-2); }
|
||
.br-row .br-val { font-size: 0.95rem; }
|
||
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-2);
|
||
margin-top: var(--sp-3); }
|
||
.stat-cell { padding: var(--sp-2) var(--sp-3); background: var(--bg-root);
|
||
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||
font-family: var(--font-mono); font-size: 0.7rem; color: var(--fg-dim); }
|
||
.stat-cell b { display: block; font-size: 1rem; color: var(--fg); font-weight: 600; }
|
||
.stat-cell b.err { color: var(--danger); }
|
||
.statusbar .btn { padding: 2px 12px; font-size: .68rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ═══════════ TOOLBAR ═══════════ -->
|
||
<header class="toolbar">
|
||
<div class="toolbar-brand">
|
||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" aria-hidden="true">
|
||
<rect x="1" y="1" width="24" height="24" rx="6" stroke="var(--primary)" stroke-width="1.5" opacity=".6"/>
|
||
<path d="M8 7l-4.2 6L8 19M18 7l4.2 6L18 19" stroke="var(--primary)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<circle cx="13" cy="13" r="1.6" fill="var(--primary)"/>
|
||
</svg>
|
||
<span>EtherCAT 控制台</span>
|
||
<span class="sub">DCU LAN9252</span>
|
||
</div>
|
||
<div class="toolbar-sep"></div>
|
||
<div class="conn-group">
|
||
<span class="led" id="connLed"></span>
|
||
<input type="text" id="substr" value="I226" style="width:96px" aria-label="适配器匹配子串">
|
||
<button class="btn btn-primary" id="btnConn">连接</button>
|
||
<button class="btn" id="btnDisc" disabled>断开</button>
|
||
</div>
|
||
<div class="toolbar-sep"></div>
|
||
<div class="conn-group">
|
||
<span class="refresh-hint">ESC EEPROM 直读 · 烧录后断电重启生效</span>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<span class="node-badge" id="alBadge">AL —</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="net-banner" id="netBanner">
|
||
<span class="led led-err led-pulse" style="width:7px;height:7px"></span>
|
||
<span id="netBannerText">未连接 — 输入适配器匹配串(默认 I226)后点击连接</span>
|
||
</div>
|
||
|
||
<!-- ═══════════ MAIN GRID ═══════════ -->
|
||
<main class="main-grid ecat-grid">
|
||
|
||
<!-- LEFT: 从站身份 -->
|
||
<aside class="panel">
|
||
<div class="panel-head">
|
||
<span class="panel-title">从站身份</span>
|
||
<span style="margin-left:auto;font-size:.68rem;color:var(--fg-dim);font-family:var(--font-mono)" id="connDesc">—</span>
|
||
</div>
|
||
<div class="panel-body" style="padding:var(--sp-3)">
|
||
<div class="node-card" id="slaveCard">
|
||
<div class="nc-top">
|
||
<span class="nc-id">S0</span>
|
||
<span class="nc-mode mode-off" id="alMode">—</span>
|
||
</div>
|
||
<div class="nc-status-text" id="slaveName">未连接</div>
|
||
<div class="nc-readings">
|
||
<div class="nc-reading"><span class="lbl">VendorID</span><span class="val" id="rVid">—</span></div>
|
||
<div class="nc-reading"><span class="lbl">ProductCode</span><span class="val" id="rPid">—</span></div>
|
||
<div class="nc-reading"><span class="lbl">Revision</span><span class="val" id="rRev">—</span></div>
|
||
<div class="nc-reading"><span class="lbl">AL Status Code</span><span class="val" id="rCode">—</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="mode-desc" style="margin-top:var(--sp-3)">
|
||
身份字来自 SII(ESC EEPROM 接口直读, 无需断电重载)。<br>
|
||
全 F = EEPROM 无效 → 先在 EEPROM 页烧录。<br>
|
||
AL 进 PREOP 后 SDO 可用; OP 需 SII 的 SM/FMMU 配置有效。
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- CENTER: EEPROM + 全局日志 -->
|
||
<section class="panel center-col">
|
||
<div class="panel-head">
|
||
<span class="panel-title">EEPROM · SII / AT24C32</span>
|
||
<span style="margin-left:auto;font-size:.68rem;color:var(--fg-dim);font-family:var(--font-mono)">4096 B · word 寻址</span>
|
||
</div>
|
||
<div class="panel-body" style="padding:var(--sp-4);display:flex;flex-direction:column;min-height:0">
|
||
<div class="cmd-grid">
|
||
<div class="field">
|
||
<span class="field-label">start <span class="field-unit">word 地址</span></span>
|
||
<input type="text" id="eeStart" value="0x0000">
|
||
</div>
|
||
<div class="field">
|
||
<span class="field-label">count <span class="field-unit">word 数</span></span>
|
||
<input type="number" id="eeCount" value="32" min="1" max="2048">
|
||
</div>
|
||
<div class="field">
|
||
<span class="field-label"> </span>
|
||
<button class="btn" id="btnEeRead" disabled>读取</button>
|
||
</div>
|
||
</div>
|
||
<div class="eep" id="eeOut">—</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-danger" id="btnFlashGen" disabled>烧录 gen_eeprom.py 镜像</button>
|
||
<button class="btn btn-danger" id="btnFlashFile" disabled>烧录 bin 文件…</button>
|
||
<button class="btn" id="btnVerifyFile" disabled>校验 bin 文件…</button>
|
||
<input type="file" id="eeFile" style="display:none">
|
||
</div>
|
||
<div class="mode-desc" style="margin-top:var(--sp-2)">
|
||
烧录逐字校验约数秒; 完成后板子断电重启, ESC 才重载 PDI/SYNC 配置。
|
||
</div>
|
||
<div style="flex:1;min-height:0;display:flex;flex-direction:column;margin-top:var(--sp-4)">
|
||
<div class="panel-title" style="margin-bottom:var(--sp-2)">全局日志</div>
|
||
<div class="ota-log global-log" id="log"></div>
|
||
<div class="ota-log-toolbar">
|
||
<button id="btnClearLog">清空</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- RIGHT: PDO / SDO -->
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<span class="panel-title">过程数据 · 4×64bit</span>
|
||
<span style="margin-left:auto;font-size:.68rem;color:var(--fg-dim);font-family:var(--font-mono)" id="pdoSize">输出 — / 输入 —</span>
|
||
</div>
|
||
<div class="tabs-bar">
|
||
<button class="tab-btn active" data-tab="pdo">过程数据</button>
|
||
<button class="tab-btn" data-tab="sdo">SDO (CoE)</button>
|
||
</div>
|
||
<div class="tab-content">
|
||
<!-- PDO 页 -->
|
||
<div class="tab-page active" id="tabPdo">
|
||
<div class="cmd-grid">
|
||
<div class="field"><span class="field-label">输出 u64[0]</span><input type="text" id="o0" value="0x0"></div>
|
||
<div class="field"><span class="field-label">输出 u64[1]</span><input type="text" id="o1" value="0x0"></div>
|
||
<div class="field"><span class="field-label">输出 u64[2]</span><input type="text" id="o2" value="0x0"></div>
|
||
<div class="field"><span class="field-label">输出 u64[3]</span><input type="text" id="o3" value="0x0"></div>
|
||
</div>
|
||
<div class="btn-row">
|
||
<div class="field" style="min-width:110px">
|
||
<span class="field-label">周期 <span class="field-unit">ms</span></span>
|
||
<select id="rate"><option value="10">10</option><option value="50">50</option>
|
||
<option value="100" selected>100</option><option value="200">200</option></select>
|
||
</div>
|
||
<div class="field" style="min-width:150px">
|
||
<span class="field-label">DC SYNC0 <span class="field-unit">us</span></span>
|
||
<select id="dcCycle">
|
||
<option value="0" selected>关 (free-run)</option>
|
||
<option value="500">500</option><option value="1000">1000</option>
|
||
<option value="2000">2000</option><option value="4000">4000</option>
|
||
</select>
|
||
</div>
|
||
<div class="field" style="min-width:150px;justify-content:flex-end">
|
||
<span class="field-label"> </span>
|
||
<button class="btn btn-primary" id="btnOp" disabled>进入 OP 并循环</button>
|
||
</div>
|
||
</div>
|
||
<div class="br-row">
|
||
<div class="big-reading"><div class="br-val" id="in0">—</div><div class="br-lbl">IN u64[0]</div></div>
|
||
<div class="big-reading"><div class="br-val" id="in1">—</div><div class="br-lbl">IN u64[1]</div></div>
|
||
<div class="big-reading"><div class="br-val" id="in2">—</div><div class="br-lbl">IN u64[2]</div></div>
|
||
<div class="big-reading"><div class="br-val" id="in3">—</div><div class="br-lbl">IN u64[3]</div></div>
|
||
</div>
|
||
<div class="stat-row">
|
||
<div class="stat-cell">周期<b id="kCyc">0</b></div>
|
||
<div class="stat-cell">WKC 错误<b id="kErr">0</b></div>
|
||
<div class="stat-cell">输出区<b id="kOut">—</b></div>
|
||
<div class="stat-cell">输入区<b id="kIn">—</b></div>
|
||
<div class="stat-cell">SYNC0<b id="kSync0">—</b></div>
|
||
<div class="stat-cell">SYNC0/s<b id="kSyncRate">—</b></div>
|
||
</div>
|
||
<div class="eep" id="inHex" style="margin-top:var(--sp-3)">—</div>
|
||
<div class="mode-desc" style="margin-top:var(--sp-2)">
|
||
循环期间每周期都把上方 4 个输出值写入输出 PDO, 修改即时生效。
|
||
</div>
|
||
</div>
|
||
<!-- SDO 页 -->
|
||
<div class="tab-page" id="tabSdo">
|
||
<div class="cmd-grid">
|
||
<div class="field"><span class="field-label">index</span><input type="text" id="sdoIdx" value="0x1000"></div>
|
||
<div class="field"><span class="field-label">sub</span><input type="text" id="sdoSub" value="0"></div>
|
||
<div class="field"><span class="field-label">size <span class="field-unit">读</span></span><input type="number" id="sdoSize" value="8" min="1"></div>
|
||
<div class="field"><span class="field-label"> </span><button class="btn" id="btnSdoRead" disabled>SDO 读</button></div>
|
||
</div>
|
||
<div class="cmd-grid" style="margin-top:var(--sp-3)">
|
||
<div class="field"><span class="field-label">写 hex <span class="field-unit">字节流</span></span><input type="text" id="sdoHex" placeholder="AABBCC.."></div>
|
||
<div class="field"><span class="field-label"> </span><button class="btn" id="btnSdoWrite" disabled>SDO 写</button></div>
|
||
</div>
|
||
<div class="eep" id="sdoOut">—</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<!-- ═══════════ STATUSBAR ═══════════ -->
|
||
<footer class="statusbar">
|
||
<span class="led" id="sbLed"></span>
|
||
<span id="sbState">未连接</span>
|
||
<span>·</span>
|
||
<span id="sbIdent">identity —</span>
|
||
<span style="margin-left:auto" id="sbLast"></span>
|
||
<button class="btn" id="btnQuit" title="结束后台 Python 服务进程(隐藏窗口模式下的退出入口)">退出服务</button>
|
||
</footer>
|
||
|
||
<script>
|
||
"use strict";
|
||
const $=id=>document.getElementById(id);
|
||
let connected=false, running=false, timer=null, cyc=0, wkcErr=0;
|
||
|
||
/* ---- 基础 ---- */
|
||
function log(msg, cls){
|
||
const d=new Date(), t=[d.getHours(),d.getMinutes(),d.getSeconds()]
|
||
.map(v=>String(v).padStart(2,"0")).join(":");
|
||
const el=document.createElement("div");
|
||
if(cls) el.className=cls;
|
||
el.innerHTML=`<span style="color:var(--fg-dim)">${t}</span> ${msg}`;
|
||
const box=$("log"); box.appendChild(el);
|
||
box.scrollTop=box.scrollHeight;
|
||
while(box.children.length>400) box.removeChild(box.firstChild);
|
||
$("sbLast").textContent=msg.replace(/<[^>]*>/g,"").slice(0,90);
|
||
}
|
||
function parseNum(v){v=String(v).trim();return /^0x/i.test(v)?parseInt(v,16):parseInt(v,10);}
|
||
async function api(path, body){
|
||
const r=await fetch(path,{method:"POST",headers:{"Content-Type":"application/json"},
|
||
body:JSON.stringify(body||{})});
|
||
return await r.json();
|
||
}
|
||
function showLog(res){
|
||
(res.log||[]).forEach(l=>{
|
||
const err=/失败|FAIL|错误|不符|异常|无效|超/.test(l);
|
||
log(l, err?"log-err":(/OK|已进|已回|成功|全部一致/.test(l)?"log-ok":"log-info"));
|
||
});
|
||
}
|
||
function hex8(v){return "0x"+(v>>>0).toString(16).toUpperCase().padStart(8,"0");}
|
||
function setConnected(on, st){
|
||
connected=on;
|
||
["btnDisc","btnEeRead","btnFlashGen","btnFlashFile","btnVerifyFile",
|
||
"btnSdoRead","btnSdoWrite"].forEach(id=>$(id).disabled=!on);
|
||
$("btnOp").disabled=!on;
|
||
$("btnConn").disabled=on;
|
||
$("connLed").className="led "+(on?"led-on":"");
|
||
$("sbLed").className="led "+(on?"led-on":"led-err");
|
||
$("netBanner").classList.toggle("show",!on);
|
||
$("netBannerText").textContent=on?"":"未连接 — 输入适配器匹配串(默认 I226)后点击连接";
|
||
if(on&&st) updateSlave(st);
|
||
else if(!on){
|
||
$("slaveName").textContent="未连接"; $("alMode").textContent="—";
|
||
$("alMode").className="nc-mode mode-off"; $("alBadge").textContent="AL —";
|
||
["rVid","rPid","rRev","rCode"].forEach(id=>$(id).textContent="—");
|
||
$("connDesc").textContent="—"; $("sbState").textContent="未连接"; $("sbIdent").textContent="identity —";
|
||
}
|
||
}
|
||
function alInfo(al){
|
||
const name={0:"NONE",1:"INIT",2:"PREOP",3:"BOOT",4:"SAFEOP",8:"OP"}[al]||("0x"+al.toString(16));
|
||
const cls=al===8?"mode-op":(al===4?"mode-safeop":(al===2?"mode-preop":"mode-off"));
|
||
return {name, cls};
|
||
}
|
||
function updateSlave(st){
|
||
$("connDesc").textContent=st.desc||"—";
|
||
$("slaveName").textContent=st.name||"—";
|
||
const info=alInfo(st.state);
|
||
$("alMode").textContent=info.name; $("alMode").className="nc-mode "+info.cls;
|
||
$("alBadge").textContent="AL "+info.name;
|
||
$("rVid").textContent=hex8(st.man);
|
||
$("rPid").textContent=hex8(st.id);
|
||
$("rRev").textContent=(st.rev>>>0).toString(10);
|
||
$("rCode").textContent="0x"+(st.al_status>>>0).toString(16).toUpperCase().padStart(4,"0");
|
||
$("slaveCard").classList.toggle("fault", st.man===0xFFFFFFFF);
|
||
$("sbState").textContent="已连接";
|
||
$("sbIdent").textContent=`identity ${hex8(st.man)} / ${hex8(st.id)}`;
|
||
}
|
||
async function pollStatus(){
|
||
if(!connected||running) return;
|
||
try{const res=await api("/api/status"); if(res.ok) updateSlave(res.slave);}catch(e){}
|
||
}
|
||
setInterval(pollStatus,2000);
|
||
|
||
/* ---- 连接 ---- */
|
||
$("btnConn").onclick=async()=>{
|
||
const res=await api("/api/open",{substr:$("substr").value});
|
||
showLog(res);
|
||
if(res.ok){setConnected(true,res.slave); pollStatus();}
|
||
};
|
||
$("btnDisc").onclick=async()=>{
|
||
stopCycle(true);
|
||
const res=await api("/api/close");
|
||
showLog(res); setConnected(false);
|
||
};
|
||
let quitArmed=false, quitTimer=null;
|
||
$("btnQuit").onclick=async()=>{
|
||
if(!quitArmed){
|
||
quitArmed=true;
|
||
$("btnQuit").textContent="再点一次确认退出";
|
||
$("btnQuit").classList.add("btn-danger");
|
||
quitTimer=setTimeout(()=>{quitArmed=false;
|
||
$("btnQuit").textContent="退出服务";
|
||
$("btnQuit").classList.remove("btn-danger");},4000);
|
||
return;
|
||
}
|
||
clearTimeout(quitTimer); quitArmed=false;
|
||
$("btnQuit").textContent="退出服务";
|
||
$("btnQuit").classList.remove("btn-danger");
|
||
stopCycle(true);
|
||
const wv=(window.pywebview&&window.pywebview.api)?window.pywebview.api:null;
|
||
if(wv&&wv.quit_app){ log("正在退出…","log-info"); wv.quit_app(); return; }
|
||
try{await api("/api/shutdown");}catch(e){}
|
||
setConnected(false);
|
||
$("netBannerText").textContent="后台服务已退出 — 双击 ecat_web.vbs 重新启动";
|
||
log("后台服务已退出, 双击 ecat_web.vbs 重新启动","log-info");
|
||
};
|
||
|
||
/* ---- EEPROM ---- */
|
||
$("btnEeRead").onclick=async()=>{
|
||
const res=await api("/api/eeprom/read",
|
||
{start:parseNum($("eeStart").value),count:parseNum($("eeCount").value)});
|
||
showLog(res);
|
||
if(res.ok) $("eeOut").textContent=res.rows
|
||
.map(r=>`0x${r.addr.toString(16).padStart(4,"0")}: ${r.hex}`).join("\n");
|
||
};
|
||
function readBin(file,cb){
|
||
const r=new FileReader();
|
||
r.onload=()=>{const u=new Uint8Array(r.result);
|
||
cb(Array.from(u).map(b=>b.toString(16).padStart(2,"0")).join(""));};
|
||
r.readAsArrayBuffer(file);
|
||
}
|
||
$("eeFile").onchange=e=>{
|
||
const f=e.target.files[0]; if(!f) return;
|
||
readBin(f,async hex=>{
|
||
const isFlash=e.target.dataset.mode==="flash";
|
||
log(isFlash?"烧录上传的 bin…":"校验上传的 bin…");
|
||
const res=await api(isFlash?"/api/flash":"/api/eeprom/verify",{data:hex});
|
||
showLog(res);
|
||
if(res.ok&&isFlash) log(`烧录成功 ${res.written} 字, 记得板子断电重启`,"log-ok");
|
||
});
|
||
e.target.value="";
|
||
};
|
||
$("btnFlashGen").onclick=async()=>{
|
||
log("烧录 gen_eeprom.py 镜像…");
|
||
const res=await api("/api/flash",{});
|
||
showLog(res);
|
||
if(res.ok) log(`烧录成功 ${res.written} 字, 记得板子断电重启`,"log-ok");
|
||
};
|
||
$("btnFlashFile").onclick=()=>{$("eeFile").dataset.mode="flash";$("eeFile").click();};
|
||
$("btnVerifyFile").onclick=()=>{$("eeFile").dataset.mode="verify";$("eeFile").click();};
|
||
$("btnClearLog").onclick=()=>{$("log").innerHTML="";};
|
||
|
||
/* ---- SDO ---- */
|
||
$("btnSdoRead").onclick=async()=>{
|
||
const res=await api("/api/sdo",{op:"read",index:parseNum($("sdoIdx").value),
|
||
sub:parseNum($("sdoSub").value),size:parseNum($("sdoSize").value)});
|
||
showLog(res); $("sdoOut").textContent=res.data?res.data.toUpperCase():"—";
|
||
};
|
||
$("btnSdoWrite").onclick=async()=>{
|
||
const res=await api("/api/sdo",{op:"write",index:parseNum($("sdoIdx").value),
|
||
sub:parseNum($("sdoSub").value),hex:$("sdoHex").value.trim()});
|
||
showLog(res);
|
||
};
|
||
|
||
/* ---- tabs ---- */
|
||
document.querySelectorAll(".tab-btn").forEach(b=>{
|
||
b.onclick=()=>{
|
||
document.querySelectorAll(".tab-btn").forEach(x=>x.classList.remove("active"));
|
||
document.querySelectorAll(".tab-page").forEach(x=>x.classList.remove("active"));
|
||
b.classList.add("active");
|
||
$(b.dataset.tab==="pdo"?"tabPdo":"tabSdo").classList.add("active");
|
||
};
|
||
});
|
||
|
||
/* ---- PDO ---- */
|
||
function outHex(){
|
||
const parts=[];
|
||
for(let i=0;i<4;i++){
|
||
let v; try{v=BigInt($("o"+i).value.trim()||"0");}catch(e){v=0n;}
|
||
if(v<0n) v&=0xFFFFFFFFFFFFFFFFn;
|
||
const h=v.toString(16).padStart(16,"0");
|
||
parts.push(h.match(/../g).reverse().join(""));
|
||
}
|
||
return parts.join("");
|
||
}
|
||
function renderInput(hex){
|
||
if(!hex||hex.length<64){["in0","in1","in2","in3"].forEach(id=>$(id).textContent="—");
|
||
$("inHex").textContent=hex||"—";return;}
|
||
for(let i=0;i<4;i++){
|
||
const bytes=hex.substr(i*16,16).match(/../g).reverse().join("");
|
||
$("in"+i).textContent="0x"+BigInt("0x"+bytes).toString(16).toUpperCase();
|
||
}
|
||
$("inHex").textContent="raw: "+hex.toUpperCase();
|
||
}
|
||
$("btnOp").onclick=async()=>{
|
||
if(!running){
|
||
const dc=parseNum($("dcCycle").value);
|
||
log(dc?`进入 OP(DC SYNC0 ${dc}us)…`:"进入 OP(free-run)…");
|
||
const res=await api("/api/op/start",{dc_cycle_us:dc});
|
||
showLog(res); if(!res.ok) return;
|
||
running=true; cyc=0; wkcErr=0;
|
||
$("btnOp").textContent="停止并回 INIT"; $("btnOp").className="btn btn-danger";
|
||
$("dcCycle").disabled=true;
|
||
$("kOut").textContent=$("kIn").textContent="…";
|
||
timer=setInterval(doCycle,parseNum($("rate").value));
|
||
doCycle();
|
||
syncTimer=setInterval(pollSync,2000); pollSync();
|
||
}else stopCycle();
|
||
};
|
||
function stopCycle(silent){
|
||
if(timer){clearInterval(timer);timer=null;}
|
||
if(syncTimer){clearInterval(syncTimer);syncTimer=null;}
|
||
if(running){
|
||
running=false;
|
||
api("/api/op/stop").then(res=>{if(!silent)showLog(res);});
|
||
$("btnOp").textContent="进入 OP 并循环"; $("btnOp").className="btn btn-primary";
|
||
$("dcCycle").disabled=false;
|
||
$("kSync0").textContent=$("kSyncRate").textContent="—";
|
||
}
|
||
}
|
||
/* DC 通路观测: 运行期每 2s 经 SDO 读从站 0x2000 SYNC Diag(固件≥1.5.31)。
|
||
DC 模式下 SYNC0/s 应≈1e6/周期us(1000us→1000/s); free-run 恒 0。 */
|
||
let syncTimer=null;
|
||
async function pollSync(){
|
||
if(!running) return;
|
||
try{
|
||
const res=await api("/api/sync_diag");
|
||
if(!res.ok){
|
||
$("kSync0").textContent="—"; $("kSyncRate").textContent="—";
|
||
if(!pollSync.warned){pollSync.warned=true;log(res.log.join(" | "),"log-err");}
|
||
return;
|
||
}
|
||
pollSync.warned=false;
|
||
$("kSync0").textContent=res.sync0.toLocaleString();
|
||
$("kSyncRate").textContent=res.sync0_rate==null?"…":res.sync0_rate.toLocaleString();
|
||
}catch(e){/* 网络瞬断, 下轮重试 */}
|
||
}
|
||
async function doCycle(){
|
||
if(!running) return;
|
||
try{
|
||
const res=await api("/api/op/cycle",{outputs:outHex()});
|
||
if(!res.ok){showLog(res);stopCycle(true);log("循环已停止","log-err");return;}
|
||
cyc++;
|
||
if(res.wkc<res.expected_wkc) wkcErr++;
|
||
$("kCyc").textContent=cyc;
|
||
$("kErr").textContent=wkcErr;
|
||
$("kErr").className=wkcErr>0?"err":"";
|
||
$("kOut").textContent=res.out_len+"B"; $("kIn").textContent=res.in_len+"B";
|
||
renderInput(res.input);
|
||
}catch(e){/* 网络瞬断, 下个周期重试 */}
|
||
}
|
||
window.addEventListener("beforeunload",()=>{
|
||
if(running) navigator.sendBeacon("/api/op/stop");
|
||
});
|
||
|
||
/* ---- 初始状态 ---- */
|
||
setConnected(false);
|
||
log("页面就绪: 连接后可扫描 / 烧录 EEPROM / 收发过程数据。");
|
||
</script>
|
||
</body>
|
||
</html>
|