Files
ESP32_COMM/tools/motor.html
T
小龙虾 ef6d3a4be3 feat(can): 添加跨通道CAN突发发送功能支持多电机同步驱动
- 新增 /can/send_burst HTTP接口,支持一次SPI事务批量发送多帧CAN消息
- 实现canFdSendBurst函数,通过0xC7 SPI类型实现背靠背装载各通道邮箱
- 添加canBurstCapacity函数计算单次突发最大帧数限制
- 突发发送期间暂停can_feedback_task避免SPI抢占冲突

refactor(can_feedback_task): 改造为周期查询+事件消费模式

- 0xC4/0xC5从GD32主动推送改为ESP32周期查询(250ms/1s)
- 0xC6/0xD2仍保留事件驱动推送模式,由任务消费处理
- 消除OTA期间GD32悬空按铃导致的超时刷屏问题
- 电机OTA期间也暂停任务避免与0xD1抢夺SPI资源

perf(ota_relay): 增加重发次数上限提升传输可靠性

- 将kMaxDataSends从3次增加到5次,提高数据包传输成功率

chore(version): 更新固件版本号至0.3.78

- 版本查询checkVersionWait移到tickInFlight释放后异步触发
- 避免/motor/version请求拖长tick导致tickStuck误报通道闪黄
2026-08-10 00:54:12 +08:00

372 lines
23 KiB
HTML
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.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>电机控制台 — Motor CAN FD Console</title>
<!-- 空 favicon:阻止浏览器自动请求 /favicon.ico(本地静态服务无此文件会 404) -->
<link rel="icon" href="data:,">
<link rel="stylesheet" href="css/motor.css?v=20260808c">
</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"/>
<circle cx="13" cy="13" r="5.5" stroke="var(--primary)" stroke-width="1.8"/>
<path d="M13 4v3.5M13 18.5V22M4 13h3.5M18.5 13H22" stroke="var(--primary)" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="13" cy="13" r="1.8" fill="var(--primary)"/>
</svg>
<span>电机控制台</span>
<span class="sub">Motor CAN FD</span>
</div>
<div class="toolbar-sep"></div>
<div class="conn-group" title="与 OTA 主页共用设备 IP,本页不提供连接配置">
<span class="led" id="connLed"></span>
<span class="conn-ip-label" id="ipLabel">IP --</span>
<span style="font-size:.72rem;color:var(--fg-dim);font-family:var(--font-mono)" id="devStatus">探测中…</span>
</div>
<div class="toolbar-sep"></div>
<div class="conn-group">
<label for="chSelect" style="font-size:.72rem;color:var(--fg-muted);font-family:var(--font-mono);letter-spacing:.06em">通道</label>
<select class="ch-select" id="chSelect">
<option value="0">CH0</option>
<option value="1">CH1</option>
<option value="2">CH2</option>
</select>
</div>
<div class="toolbar-sep"></div>
<div class="conn-group" title="电机状态刷新间隔(ms,50~5000)。同时驱动前端 HTTP 轮询与 GD32 0xC5 SPI 主动推送周期,端到端节拍一致">
<label for="refreshRate" class="refresh-hint">刷新速度</label>
<input type="number" id="refreshRate" class="refresh-input" value="250" min="50" max="5000" step="50" aria-label="电机状态刷新间隔(毫秒)" />
<span class="refresh-hint">ms</span>
</div>
<div class="toolbar-right">
<a class="btn btn-sm" href="index.html" title="返回 OTA 主页配置连接">返回 OTA 控制台</a>
<span class="node-badge" id="nodeBadge">NODE -</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">未连接 — 使用主页共享 IP 自动探测中… 请先在 OTA 主页配置并测试连接</span>
</div>
<!-- ═══════════ MAIN 3-ZONE GRID ═══════════ -->
<main class="main-grid">
<!-- LEFT: NODE OVERVIEW -->
<aside class="panel nodes-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="nodeSummary">0/9 在线 · CAN FD</span>
</div>
<div class="panel-body">
<!-- 独立 BOOT 状态:总线检测到 0x103 心跳时显示,不归属任何 Nx
显示/隐藏只靠 .boot-badge / .boot-badge.show,禁止内联 display -->
<div id="bootBadge" class="boot-badge" aria-hidden="true">
<span class="boot-dot" aria-hidden="true"></span>
<span class="boot-txt">电机在 BOOT</span>
<span class="boot-sub" id="bootBadgeSub">0x103</span>
</div>
<div class="node-list" id="nodeList" role="listbox" aria-label="电机节点列表 N1-N9(离线可选)">
<!-- N1–N9 由 JS 固定渲染,离线也可选中刷写 -->
</div>
</div>
</aside>
<!-- CENTER: CONTROL -->
<section class="panel center-panel">
<div class="panel-head">
<span class="panel-title">CMD 控制</span>
<span style="font-size:.68rem;color:var(--fg-dim);font-family:var(--font-mono);margin-left:auto" id="cmdIdLabel">ID 0x000-0200 · DLC 32</span>
</div>
<div class="cmd-section">
<div class="cmd-grid">
<div class="field">
<span class="field-label">Control_Mode <span class="field-unit">控制模式</span></span>
<select id="modeSelect" aria-label="控制模式">
<option value="0">0 — Disable 关闭</option>
<option value="1">1 — Angle 位置控制</option>
<option value="2">2 — Velocity 速度控制</option>
<option value="3" selected>3 — Velocity_Angle 速度+位置</option>
<option value="4">4 — MIT Mode</option>
<option value="5">5 — Current 电流/力矩</option>
<option value="6">6 — 开环速度</option>
<option value="7">7 — 开环速度 (2)</option>
</select>
<span class="mode-desc" id="modeDesc">速度+位置环联合控制,目标速度与目标角度同时生效</span>
</div>
<div class="field">
<span class="field-label">Limit_Voltage <span class="field-unit">V · 0~65.535</span></span>
<input type="number" id="inVoltage" value="24.000" step="0.5" min="0" max="65.535" aria-label="电压限制" />
</div>
<div class="field" id="fldCurrent">
<span class="field-label">Target_Current <span class="field-unit">mA</span></span>
<input type="number" id="inCurrent" value="0" step="100" aria-label="目标电流" />
</div>
<div class="field" id="fldVelocity">
<span class="field-label">Target_Velocity <span class="field-unit">deg/s</span></span>
<input type="number" id="inVelocity" value="360" step="10" aria-label="目标速度" />
</div>
<div class="field" id="fldAngle">
<span class="field-label">Target_Angle <span class="field-unit">deg</span></span>
<input type="number" id="inAngle" value="180.0" step="1" aria-label="目标角度" />
</div>
</div>
<div class="field" style="margin-top:var(--sp-4)">
<span class="field-label">PID 增益 <span class="field-unit">P × 3 环</span></span>
<div class="pid-group">
<div class="field"><span class="field-label" style="font-size:.62rem">电流环 P</span><input type="number" id="pidCurP" value="100" step="1" /></div>
<div class="field"><span class="field-label" style="font-size:.62rem">速度环 P</span><input type="number" id="pidVelP" value="100" step="1" /></div>
<div class="field"><span class="field-label" style="font-size:.62rem">角度环 P</span><input type="number" id="pidAngP" value="100" step="1" /></div>
</div>
</div>
<div class="cmd-actions">
<button class="btn btn-primary" id="sendCmdBtn">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M1.5 7h9M8 3.5L11.5 7 8 10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
发送 CMD
</button>
<button class="btn" id="sendRepeatBtn">周期发送</button>
<label class="repeat-period" title="周期发送间隔(最小 80ms)">
<input type="number" id="repeatPeriod" value="100" min="80" max="10000" step="10" />
<span>ms</span>
</label>
<span class="send-hint" id="sendHint">POST /can/send_batch · CH0</span>
</div>
</div>
<!-- TABS -->
<nav class="tabs-bar" aria-label="辅助功能">
<button class="tab-btn active" data-tab="config">配置操作</button>
<button class="tab-btn" data-tab="version">版本信息</button>
<button class="tab-btn" data-tab="nodeid">节点 ID</button>
<button class="tab-btn" data-tab="ota">电机 OTA</button>
<button class="tab-btn" data-tab="seq">命令序列</button>
</nav>
<div class="tab-content">
<!-- CONFIG TAB -->
<div class="tab-page active" id="tab-config">
<div class="config-grid">
<button class="config-btn danger-action" data-action="motReset">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true"><path d="M4 11a7 7 0 1 1 2 4.9M4 16v-5h5" stroke="var(--danger)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="cb-name">电机复位</span><span class="cb-sig">motReset · bit0</span>
</button>
<button class="config-btn" data-action="saveConfig">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true"><path d="M5 3h9l4 4v12H5V3z" stroke="var(--primary)" stroke-width="1.6" stroke-linejoin="round"/><path d="M8 3v5h6V3M8 18v-5h7v5" stroke="var(--primary)" stroke-width="1.4"/></svg>
<span class="cb-name">保存配置</span><span class="cb-sig">saveConfig · bit2</span>
</button>
<button class="config-btn" data-action="angleReset">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true"><circle cx="11" cy="11" r="7.5" stroke="var(--primary)" stroke-width="1.6"/><path d="M11 6v5l3.5 2" stroke="var(--primary)" stroke-width="1.5" stroke-linecap="round"/></svg>
<span class="cb-name">角度置零</span><span class="cb-sig">angle_reset · bit3</span>
</button>
<button class="config-btn" data-action="sleep">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true"><path d="M17 13.5A7 7 0 0 1 8.5 5a7 7 0 1 0 8.5 8.5z" stroke="var(--warn)" stroke-width="1.6" stroke-linejoin="round"/></svg>
<span class="cb-name">休眠</span><span class="cb-sig">SLEEP · bit56</span>
</button>
</div>
<p style="margin-top:var(--sp-4);font-size:.72rem;color:var(--fg-dim);line-height:1.6">
CMD_CONFIG (0x000<span class="cfgNodeNibble">-</span>0301) · DLC 24 · 每个操作发送单帧脉冲信号,操作前需确认
</p>
</div>
<!-- VERSION TAB -->
<div class="tab-page" id="tab-version">
<button class="btn btn-sm" id="queryVerBtn">
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true"><circle cx="6.5" cy="6.5" r="5" stroke="currentColor" stroke-width="1.3"/><path d="M6.5 4.2v.1M6.5 6v3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
查询版本 (GET_VERSION)
</button>
<div class="version-grid" style="margin-top:var(--sp-4)" id="versionGrid">
<div class="ver-card"><div class="ver-type">软件版本</div><div class="ver-date ver-empty" id="verSoft">--</div></div>
<div class="ver-card"><div class="ver-type">硬件版本</div><div class="ver-date ver-empty" id="verHard">--</div></div>
<div class="ver-card"><div class="ver-type">Bootloader</div><div class="ver-date ver-empty" id="verBoot">--</div></div>
</div>
<p style="margin-top:var(--sp-3);font-size:.72rem;color:var(--fg-dim)">请求/回报共用 0x000<span class="cfgNodeNibble">-</span>00FF(同 ID)· DLC 24 · major.minor.patch.build ×3 (soft/hard/boot,DBC v0.71) · GD32 解析后 0xC6 同步</p>
</div>
<!-- NODE ID TAB -->
<div class="tab-page" id="tab-nodeid">
<div class="nodeid-row">
<div class="field" style="min-width:140px">
<span class="field-label">目标 Node ID</span>
<input type="number" id="nodeIdInput" min="1" max="9" value="3" />
</div>
<button class="btn btn-primary btn-sm" id="setNodeIdBtn">设置 ID (0xN0400)</button>
<button class="btn btn-sm" id="getNodeIdBtn">读取 ID (0xN0401)</button>
</div>
<div class="nodeid-result" id="nodeIdResult">连接设备后读取节点 ID</div>
<p style="margin-top:var(--sp-3);font-size:.72rem;color:var(--fg-dim);line-height:1.6">
SET_NODE_ID (0x000N0400 DLC2) / GET_NODE_ID (0x000N0401 DLC2)。写入后 CAN ID 前缀变为 0x000Nxxxx。
</p>
</div>
<!-- OTA TAB -->
<div class="tab-page" id="tab-ota">
<div class="ota-stages" id="otaStages" aria-label="OTA阶段">
<div class="ota-stage" data-stage="upload"><span class="st-dot">1</span>上传固件</div>
<div class="ota-stage" data-stage="boot"><span class="st-dot">2</span>进入BOOT</div>
<div class="ota-stage" data-stage="handshake"><span class="st-dot">3</span>转发握手</div>
<div class="ota-stage" data-stage="erase"><span class="st-dot">4</span>擦除Flash</div>
<div class="ota-stage" data-stage="transfer"><span class="st-dot">5</span>数据传输</div>
<div class="ota-stage" data-stage="app"><span class="st-dot">6</span>完成</div>
</div>
<div class="ota-file-row">
<button class="btn btn-sm" id="otaFileBtn">选择固件 (.bin)</button>
<input type="file" id="otaFileInput" accept=".bin" hidden />
<span class="ota-file-name" id="otaFileName">未选择固件</span>
<button class="btn btn-primary btn-sm" id="otaStartBtn">开始 OTA</button>
<button class="btn btn-danger btn-sm" id="otaCancelBtn" disabled>取消</button>
</div>
<div class="ota-progress-wrap">
<div class="ota-progress-bar"><div class="ota-progress-fill" id="otaProgressFill"></div></div>
<div class="ota-stats">
<span>包 <span class="hl" id="otaPktNum">0</span> / <span id="otaPktTotal">0</span></span>
<span>速率 <span class="hl" id="otaSpeed">—</span></span>
<span>状态 <span id="otaCrc">—</span></span>
</div>
</div>
<div class="ota-log-toolbar">
<button id="otaLogCopyBtn" title="复制全部日志">复制</button>
<button id="otaLogClearBtn" title="清空日志">清空</button>
</div>
<div class="ota-log" id="otaLog" aria-live="polite">
<div class="log-info">[就绪] 电机 Bootloader OTA · 先上传固件到 ESP32 → 发 0x000N0000 进 BOOT → 触发转发 → GD32 本地 relay(AES-128 + CAN stop-and-wait)</div>
<div>[信息] 选固件 → 上传并比对 flash SHA-256(失败重传≤2次)→ 发进BOOT命令 → 触发 /motor/ota/relay → GD32 完成握手/加密/传输 · 每包 48 B · 48 KB 上限</div>
</div>
</div>
<!-- 命令序列 TAB -->
<div class="tab-page" id="tab-seq">
<div class="seq-toolbar">
<label class="repeat-period" title="整序列循环次数,0 = 无限循环">
<span>循环</span>
<input type="number" id="seqLoops" value="0" min="0" max="9999" step="1" />
<span>次(0=无限)</span>
</label>
<button class="btn btn-sm" id="seqAddBtn">+ 添加步骤</button>
<button class="btn btn-sm" id="seqClearBtn">清空</button>
<button class="btn" id="seqStartBtn">▶ 开始序列</button>
<span class="seq-status" id="seqStatus">未运行</span>
</div>
<div class="seq-list" id="seqList"></div>
<p class="seq-hint">
命令序列:每步为一个「同步动作组」,组内每个动作 = 一台电机目标(通道/节点/模式/电压/电流/速度/角度/PID);
执行本步时组内全部动作经一次突发请求近同时下发(同组偏差微秒级;超过单次容量自动分组,组间约 1-2ms),
发送成功后保持「保持 ms」再进入下一步。每步可「+」添加多个电机动作,支持多通道、多电机混合搭配,独立于左侧选中节点。
循环 0 = 无限循环,与「周期发送」互斥。「+ 添加步骤」会复制当前 CMD 表单已提交的值及当前通道/选中节点。
</p>
</div>
</div>
</section>
<!-- RIGHT: STATUS FEEDBACK -->
<section class="panel status-panel">
<div class="panel-head">
<span class="panel-title">STATUS 实时反馈</span>
<span style="font-size:.68rem;color:var(--fg-dim);font-family:var(--font-mono);margin-left:auto" id="statusIdLabel">0x000-0100 · 1 Hz</span>
</div>
<div class="panel-body">
<div class="offline-overlay show" id="offlineOverlay">
<svg class="off-icon" width="40" height="40" viewBox="0 0 40 40" fill="none" aria-hidden="true"><circle cx="20" cy="20" r="16" stroke="var(--fg-dim)" stroke-width="1.5" stroke-dasharray="4 4"/><path d="M14 26l12-12M26 26L14 14" stroke="var(--fg-dim)" stroke-width="1.5" stroke-linecap="round"/></svg>
<span style="font-size:.85rem" id="offlineTitle">节点离线 — 未收到 STATUS 帧</span>
<span style="font-size:.72rem" id="offlineSub">检查电机供电与 CAN 总线连接</span>
</div>
<div class="status-live hide" id="statusLive">
<div class="big-readings">
<div class="big-reading" data-signal="angle" title="点击展开/收起角度波形">
<div class="br-val" id="rtAngle">--</div>
<div class="br-unit">deg</div>
<div class="br-lbl">RT_Angle <span class="br-toggle">▾ 波形</span></div>
<div class="br-scope"><canvas id="miniAngle" aria-label="角度波形" role="img"></canvas></div>
</div>
<div class="big-reading" data-signal="velocity" title="点击展开/收起速度波形">
<div class="br-val" id="rtVelocity">--</div>
<div class="br-unit">deg/s</div>
<div class="br-lbl">RT_Velocity <span class="br-toggle">▾ 波形</span></div>
<div class="br-scope"><canvas id="miniVelocity" aria-label="速度波形" role="img"></canvas></div>
</div>
<div class="big-reading" data-signal="current" title="点击展开/收起电流波形">
<div class="br-val" id="rtCurrent">--</div>
<div class="br-unit">mA</div>
<div class="br-lbl">RT_Current <span class="br-toggle">▾ 波形</span></div>
<div class="br-scope"><canvas id="miniCurrent" aria-label="电流波形" role="img"></canvas></div>
</div>
</div>
<div class="status-row">
<div class="temp-block" data-signal="mcuTemp" title="点击展开/收起 MCU 温度曲线">
<div class="temp-head"><span class="temp-name">MCU_Temp<span class="temp-toggle">▾ 曲线</span></span><span class="temp-val" id="mcuTemp">-- °C</span></div>
<div class="temp-bar"><div class="temp-fill" id="mcuTempBar" style="width:0%"></div></div>
<div class="br-scope"><canvas id="miniMcuTemp" aria-label="MCU温度曲线" role="img"></canvas></div>
</div>
<div class="temp-block" data-signal="motTemp" title="点击展开/收起电机温度曲线">
<div class="temp-head"><span class="temp-name">Motor_Temp<span class="temp-toggle">▾ 曲线</span></span><span class="temp-val" id="motTemp">-- °C</span></div>
<div class="temp-bar"><div class="temp-fill" id="motTempBar" style="width:0%"></div></div>
<div class="br-scope"><canvas id="miniMotTemp" aria-label="电机温度曲线" role="img"></canvas></div>
</div>
</div>
<div class="phase-currents">
<div class="pc-head">
<span class="pc-title">三相电流 U / V / W</span>
<div class="pc-live-vals">
<span class="pc-chip" style="--ch-color:var(--primary)">U <b id="valU">+0</b> mA</span>
<span class="pc-chip" style="--ch-color:var(--ok)">V <b id="valV">+0</b> mA</span>
<span class="pc-chip" style="--ch-color:var(--warn)">W <b id="valW">+0</b> mA</span>
</div>
</div>
<div class="scope-wrap">
<canvas id="scopeCanvas" aria-label="三相电流实时波形" role="img"></canvas>
<div class="scope-grid-labels"><span>+5A</span><span>0</span><span>-5A</span></div>
</div>
</div>
<div class="misc-grid">
<div class="misc-item"><span class="mi-lbl">Power_Voltage</span><span class="mi-val" id="pwrVolt">-- V</span></div>
<div class="misc-item"><span class="mi-lbl">test_float</span><span class="mi-val" id="testFloat">--</span></div>
<div class="misc-item"><span class="mi-lbl">test_U16</span><span class="mi-val" id="testU16">--</span></div>
<div class="cpu-ring-wrap" style="grid-column:2;grid-row:2">
<div class="cpu-ring">
<svg width="52" height="52" viewBox="0 0 52 52" role="img" aria-label="CPU占用率">
<circle class="ring-bg" cx="26" cy="26" r="21"/>
<circle class="ring-fg" id="cpuRingFg" cx="26" cy="26" r="21" stroke-dasharray="131.9" stroke-dashoffset="131.9"/>
</svg>
<span class="cpu-pct" id="cpuPct">--%</span>
</div>
<div class="cpu-info"><span class="mi-lbl">CPU_Usage</span><span class="mi-sub" id="cpuSub">-- </span></div>
</div>
</div>
<div class="flag-lights">
<div class="flag-item" id="flagItemMot"><span class="flag-led" id="flagMot"></span>motStatus 使能<span class="flag-state" id="flagMotState">—</span></div>
<div class="flag-item" id="flagItemMag"><span class="flag-led" id="flagMag"></span>magStatus 磁编码<span class="flag-state" id="flagMagState">—</span></div>
<div class="flag-item" id="flagItemThermal"><span class="flag-led" id="flagThermal"></span>thermal 过温<span class="flag-state" id="flagThermalState">—</span></div>
<div class="flag-item" id="flagItemEeprom"><span class="flag-led" id="flagEeprom"></span>EEPROM 存储<span class="flag-state" id="flagEepromState">—</span></div>
</div>
</div>
</div>
</section>
</main>
<!-- ═══════════ STATUS BAR ═══════════ -->
<footer class="statusbar">
<div class="sb-item"><span class="led" id="sbLed" style="width:7px;height:7px"></span> <span id="sbCh">CH0</span> <span class="sb-val" id="sbRx">RX --</span></div>
<div class="sb-sep"></div>
<div class="sb-item">TX <span class="sb-val" id="sbTx">--</span></div>
<div class="sb-sep"></div>
<div class="sb-item">ERR <span class="sb-val" id="sbErr">--</span> · TEC <span id="sbTec">--</span> / REC <span id="sbRec">--</span></div>
<div class="sb-sep"></div>
<div class="sb-item">BUS <span class="sb-val" id="sbBus">--</span></div>
<span class="sb-last-send" id="sbLastSend">尚未发送</span>
</footer>
<!-- TOAST & MODAL -->
<div class="toast-container" id="toastContainer" aria-live="polite"></div>
<div class="modal-mask" id="modalMask" role="dialog" aria-modal="true">
<div class="modal-box">
<div class="modal-title" id="modalTitle">确认操作</div>
<div class="modal-desc" id="modalDesc"></div>
<div class="modal-actions">
<button class="btn" id="modalCancel">取消</button>
<button class="btn btn-primary" id="modalConfirm">确认执行</button>
</div>
</div>
</div>
<script type="module" src="js/motor/main.js?v=20260808d"></script>
</body>
</html>