反馈刷新率(配套电机固件 v0.4.1:0x0400 DLC4/0x0401 DLC2 扩展格式, 选择器 0x0100=STATUS · 0x0101=CONFIG 帧,协议值为周期 ms): - 「节点 ID」页签新增 STATUS/CONFIG 刷新率读取/写入;界面单位一律 Hz (写入按 周期ms=round(1000/Hz) 换算下发,读取反算 Hz 并附原始周期, 取整有损如实展示),0=停发,写入掉电保存;配置值以「读取」回读电机为准 - WiFi 应答回执走 0xC1 per-ID 快照(/can/feedback 全量 entries): GD32 0xC0 ACK 仅 1 字节状态、不携带电机回帧;同 ID 回帧快照只存最新 一条,故一次快照只配一次请求(STATUS/CONFIG 分开读写) - ECAT 回执走批量接收区 takeNodeIdReport,按长度分派 2B 旧格式/4B GET/5B SET ACK - GET_NODE_ID 迁移到扩展选择器 0x0000,兼容旧固件 2B 节点 ID 应答 (SET DLC2 旧格式行为不变) - transport-wifi 新增 fetchFeedbackFull(全量 CAN 反馈快照) EtherCAT/基础设施(密排槽固件 1.6.21 配套): - 快照槽位 5→9 一拍整轮(扫描周期数 6→1);判活阈值随实测 seq 间隔 自适应 clamp(3×EWMA, 2s, 30s),秒级回报档不再被固定 2s 误判离线; STATUS 面板 Hz 明确为网页到达口径(按槽 seq 实测电机回报率的方案已否掉) - esp32_server.py:ecat_native 单实例收紧(后起独占 + taskkill 残留实例, 双实例并存会抢 Npcap 致后起者抓不到从站)+ native stdout 落盘 - flash_gd32.py:刷写后校验「GD32 真的在跑新代码」(版本变化或 uptime 归零), 防"写入≠在跑"假台账 - lab:新增 LAN9252 电源诊断台、PDO 布局页;SPI 测速并入 EtherCAT 接收看板 并更名「SPI/ECAT 通信测速」;移除 continuity_test.html;新增 ECAT 单拍耗时 优化报告;/_local/ 排除入库;测试同步更新(62+58 项全过)
135 lines
5.8 KiB
HTML
135 lines
5.8 KiB
HTML
<!--
|
|
tabs.html - ESP32 调试控制台「单窗口多标签」外壳
|
|
顶部一排标签, 下方 iframe 承载各功能页(pages/ 下功能页, lab/ 下实验页)。
|
|
各页面原样复用, 互不干扰; 页面内的"新窗口打开"/普通链接会在 iframe 内跳转,
|
|
外壳监听 iframe 的 load 事件同步高亮当前标签。
|
|
激活标签存 localStorage(dcu_tab), 下次打开自动恢复。
|
|
-->
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ESP32 调试控制台 — DCU</title>
|
|
<link rel="icon" href="data:,">
|
|
<link rel="stylesheet" href="css/base.css?v=857f83a5">
|
|
<style>
|
|
html, body { height: 100%; margin: 0; }
|
|
body {
|
|
background: var(--bg-root); color: var(--text);
|
|
font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
|
display: flex; flex-direction: column; overflow: hidden;
|
|
}
|
|
.tabbar {
|
|
display: flex; align-items: stretch; gap: 2px;
|
|
background: var(--bg-panel); border-bottom: 1px solid var(--border);
|
|
padding: 0 8px; height: 42px; flex: 0 0 auto;
|
|
user-select: none; -webkit-user-select: none;
|
|
}
|
|
.tabbar .brand {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 0 12px 0 4px; margin-right: 6px;
|
|
border-right: 1px solid var(--border);
|
|
font-weight: 600; font-size: .86rem; color: var(--text-dim);
|
|
letter-spacing: .02em;
|
|
}
|
|
.tabbar .brand .dot { width: 9px; height: 9px; border-radius: 50%;
|
|
background: var(--esp32); box-shadow: 0 0 8px var(--esp32); }
|
|
.tab {
|
|
appearance: none; border: none; background: transparent; cursor: pointer;
|
|
color: var(--text-dim); font-size: .84rem; font-family: inherit;
|
|
padding: 0 16px; display: flex; align-items: center; gap: 7px;
|
|
border-bottom: 2px solid transparent; transition: color .12s, background .12s;
|
|
white-space: nowrap;
|
|
}
|
|
.tab:hover { color: var(--text); background: var(--bg-hover); }
|
|
.tab.active { color: var(--text); border-bottom-color: var(--esp32); background: var(--bg-card); }
|
|
.tab .ico { font-size: .95rem; opacity: .9; }
|
|
.tab .accent { width: 7px; height: 7px; border-radius: 50%; opacity: .55; }
|
|
.tab.active .accent { opacity: 1; }
|
|
.tabbar .spacer { flex: 1 1 auto; }
|
|
.tabbar .hint { display: flex; align-items: center; color: var(--text-label);
|
|
font-size: .72rem; padding: 0 8px; }
|
|
#view { flex: 1 1 auto; width: 100%; border: none; background: var(--bg-root); display: block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="tabbar" id="tabbar">
|
|
<div class="brand"><span class="dot"></span>ESP32 调试控制台</div>
|
|
<!-- 标签由 TABS 数组渲染 -->
|
|
<div class="spacer"></div>
|
|
<div class="hint">⟳ 刷新当前页 · F5</div>
|
|
</nav>
|
|
<iframe id="view" title="功能页" src="about:blank"></iframe>
|
|
|
|
<script>
|
|
(function(){
|
|
var TABS = [
|
|
{ page: 'pages/index.html', label: 'OTA 中继', ico: '⬆', color: 'var(--esp32)' },
|
|
{ page: 'pages/can.html', label: 'CAN 通信', ico: '⇄', color: 'var(--can)' },
|
|
{ page: 'pages/can_monitor.html', label: 'CAN 监控', ico: '👁', color: 'var(--can)' },
|
|
{ page: 'pages/motor.html', label: '电机控制', ico: '⚙', color: 'var(--motor)' },
|
|
{ page: 'lab/lan9252_pm.html', label: 'LAN9252 电源', ico: '⏻', color: '#a88bfa' },
|
|
{ page: 'lab/spi_test.html', label: 'SPI/ECAT 通信测速', ico: '⚡', color: 'var(--ble)' }
|
|
];
|
|
var bar = document.getElementById('tabbar');
|
|
var view = document.getElementById('view');
|
|
var spacer = bar.querySelector('.spacer');
|
|
var LS = 'dcu_tab';
|
|
|
|
function baseName(p){ return (p || '').replace(/^.*\//, '').split('?')[0]; }
|
|
|
|
/* 按文件名匹配, 与目录层级无关: iframe 里读到的是绝对 pathname
|
|
* (/pages/can.html), 而旧版 localStorage 里存的是裸文件名(can.html),
|
|
* 两者都能落到同一个标签。返回 TABS 中登记的相对路径。 */
|
|
function pageToTab(path){
|
|
var b = baseName(path);
|
|
if (b === '') return 'pages/index.html';
|
|
for (var i=0;i<TABS.length;i++) if (baseName(TABS[i].page) === b) return TABS[i].page;
|
|
return null; // 未知页(如从页面内跳到别的 html)
|
|
}
|
|
function setActive(path){
|
|
var tabs = bar.querySelectorAll('.tab');
|
|
for (var i=0;i<tabs.length;i++){
|
|
tabs[i].classList.toggle('active', tabs[i].getAttribute('data-page') === path);
|
|
}
|
|
}
|
|
function go(path, save){
|
|
view.src = path;
|
|
setActive(path);
|
|
if (save !== false) { try { localStorage.setItem(LS, path); } catch(e){} }
|
|
}
|
|
|
|
// 渲染标签
|
|
TABS.forEach(function(t){
|
|
var b = document.createElement('button');
|
|
b.className = 'tab'; b.type = 'button';
|
|
b.setAttribute('data-page', t.page);
|
|
b.innerHTML = '<span class="accent" style="background:'+t.color+'"></span>'
|
|
+ '<span class="ico">'+t.ico+'</span>' + t.label;
|
|
b.onclick = function(){ go(t.page); };
|
|
bar.insertBefore(b, spacer);
|
|
});
|
|
|
|
// iframe 内导航(页面里的链接/新窗口打开覆写)后, 同步高亮对应标签
|
|
view.addEventListener('load', function(){
|
|
var p = null;
|
|
try { p = view.contentWindow.location.pathname; } catch(e){ return; }
|
|
var tab = pageToTab(p);
|
|
if (tab) { setActive(tab); try { localStorage.setItem(LS, tab); } catch(e){} }
|
|
});
|
|
|
|
// 初始标签: 上次记忆, 否则 OTA 主页。
|
|
// 取 pageToTab 的返回值而非 localStorage 原值, 这样旧版存的裸文件名
|
|
// (can.html) 会被自动迁移成新的 pages/can.html。
|
|
var init = 'pages/index.html';
|
|
try { var s = localStorage.getItem(LS); var m = s && pageToTab(s); if (m) init = m; } catch(e){}
|
|
go(init, false);
|
|
})();
|
|
</script>
|
|
<!-- app 窗口租约: 本外壳窗口活着期间保持与本地服务的连接,
|
|
全部窗口关闭后服务端自动停 EtherCAT PDO 循环(见 js/common/app-lease.js) -->
|
|
<script src="js/common/app-lease.js"></script>
|
|
</body>
|
|
</html>
|