Files
ESP32_COMM/tools/motor.html
T
小龙虾 e9cb7342f6 feat(motor): 支持多通道同节点号电机状态管理
支持同一节点号在多个CAN通道上的电机状态管理,采用node×channel双键存储机制。

- 修改motor_state.cpp中的槽位索引逻辑,从单节点索引改为(node-1)*MOTOR_CHANNELS+channel索引
- 增加MOTOR_CHANNELS和MOTOR_MAX_SLOTS宏定义,支持9节点×3通道共27个槽位
- 更新http_handlers.cpp中的二进制响应缓冲区为static类型,避免大缓冲区占用栈空间
- 调整循环遍历范围从MOTOR_MAX_NODES改为MOTOR_MAX_SLOTS
- 添加HTML页面的空favicon配置,避免浏览器404请求
- 新增CSS样式用于显示节点在其他通道在线的状态提示
2026-07-30 11:57:21 +08:00

350 lines
21 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=20260730a">
</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>
</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>
</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=20260730g"></script>
</body>
</html>