Files
DCU_DesktopController/lab/continuity_test.html
T

755 lines
33 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LAN9252 通断测试</title>
<style>
/* === WebGL 液态玻璃 (参考 yzrt/html-liquid-glass-effect-webgl) ===
参数:IOR=1.5 / Thickness=50 / Normal=10 / Blur=0 / Highlight=4 / Radius=26
实现:SVG feDisplacementMap 做真实背景像素折射位移 + saturate/contrast 色散
+ 边缘 4px 白色高光带 (Highlight Width=4)
+ 边缘 1px 暗描边 (Normal Strength=10)
所有组件统一用此风格,旧 yzrt .box 多层 inset 阴影已舍弃
*/
:root{
--grid:#E1E1E1;
--text:#f0f0f5;
--text-2:#c8c8d0;
--text-3:#8a8a95;
--separator:rgba(255,255,255,.08);
--blue:#5b9eff;
--green:#30d158;
--red:#ff453a;
--orange:#ff9f0a;
--purple:#bf5af2;
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transition:.25s;}
html,body{height:100%;}
body{
font-family:"Microsoft YaHei","SF Pro Display","Segoe UI",sans-serif;
color:var(--text);font-size:15px;line-height:1.5;
min-height:100vh;padding:32px 20px 80px;
position:relative;overflow-x:hidden;
/* 彩色 blur 球 + 细点阵 — 让液态玻璃折射位移清晰可见
色球提供颜色参照(折射色散),点阵提供位置参照(位移扭曲) */
background:
/* 细点阵:让折射位移有参照物 */
radial-gradient(circle at 50% 50%, rgba(255,255,255,.12) 1px, transparent 1.5px) 0 0/24px 24px,
/* 彩色 blur 球:中饱和度,不要太鲜艳以免干扰文字 */
radial-gradient(circle at 15% 20%, #4a90e2 0%, transparent 35%),
radial-gradient(circle at 85% 15%, #b06ab3 0%, transparent 38%),
radial-gradient(circle at 75% 80%, #e27d60 0%, transparent 40%),
radial-gradient(circle at 20% 75%, #41d3a3 0%, transparent 38%),
radial-gradient(circle at 50% 50%, #f0a500 0%, transparent 45%),
/* 底色渐变 */
linear-gradient(135deg, #2d3561 0%, #2d4a6b 50%, #1a2a45 100%);
background-attachment:fixed;
}
/* 大色球:增强折射参照(放固定层,不会被玻璃遮挡) */
body::before{
content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
background:
radial-gradient(circle at 30% 40%, rgba(255,200,100,.18) 0%, transparent 20%),
radial-gradient(circle at 70% 60%, rgba(100,200,255,.15) 0%, transparent 25%);
}
.wrap{max-width:1100px;margin:0 auto;position:relative;z-index:1;}
h1{
font-size:36px;font-weight:700;letter-spacing:-.03em;margin-bottom:6px;color:#fff;
text-shadow:0 2px 8px rgba(0,0,0,.4);
}
.sub{color:rgba(255,255,255,.85);font-size:15px;margin-bottom:24px;font-weight:500;text-shadow:0 1px 4px rgba(0,0,0,.3);}
/* === 玻璃基础类 — 纯 WebGL 液态玻璃(无辅助效果) ===
IOR1.5/Thick50 → SVG feDisplacementMap 背景折射位移 + saturate/contrast 色散
Blur0 → 不模糊
Highlight4 → 边缘 8px 白色高光带(略扩散)
Normal10 → 边缘 2px 暗影
Radius26 → 圆角 */
.glass,.glass-strong{
position:relative;z-index:0;isolation:isolate;
border-radius:26px;
border:none;
background:transparent;
backdrop-filter:url(#glassRefract) saturate(1.4) contrast(1.15);
-webkit-backdrop-filter:url(#glassRefract) saturate(1.4) contrast(1.15);
box-shadow:
inset 0 0 8px rgba(255,255,255,.6), /* Highlight Width=4 → 略向中间扩散 */
inset 0 0 2px rgba(0,0,0,.6), /* Normal Strength=10 */
0 4px 12px rgba(0,0,0,.18);
}
.glass-strong{
box-shadow:
inset 0 0 10px rgba(255,255,255,.7),
inset 0 0 2px rgba(0,0,0,.6),
0 6px 16px rgba(0,0,0,.2);
}
.glass-subtle{
position:relative;z-index:0;isolation:isolate;
border-radius:18px;
border:none;
background:transparent;
backdrop-filter:url(#glassRefract) saturate(1.3) contrast(1.1);
-webkit-backdrop-filter:url(#glassRefract) saturate(1.3) contrast(1.1);
box-shadow:
inset 0 0 6px rgba(255,255,255,.5),
inset 0 0 2px rgba(0,0,0,.5),
0 2px 8px rgba(0,0,0,.14);
}
.card{padding:24px;margin-bottom:16px;}
.card > *{position:relative;z-index:1;}
.card h2{
font-size:22px;font-weight:700;letter-spacing:-.02em;margin-bottom:6px;color:#fff;
text-shadow:0 1px 6px rgba(0,0,0,.3);
}
.card-desc{font-size:13px;color:var(--text-2);margin-bottom:16px;}
/* 警告条 — 纯 WebGL 玻璃 + 橙红染色 */
.warn{
color:#fff;font-size:13px;margin-bottom:24px;padding:14px 18px;
position:relative;z-index:0;isolation:isolate;
background:linear-gradient(135deg,rgba(255,159,10,.35),rgba(255,69,58,.3));
backdrop-filter:url(#glassRefract) saturate(1.4) contrast(1.15);
-webkit-backdrop-filter:url(#glassRefract) saturate(1.4) contrast(1.15);
border:none;
border-radius:26px;
box-shadow:
inset 0 0 8px rgba(255,255,255,.5),
inset 0 0 2px rgba(0,0,0,.5),
0 4px 12px rgba(0,0,0,.16);
display:flex;align-items:flex-start;gap:10px;font-weight:500;
text-shadow:0 1px 3px rgba(0,0,0,.4);
}
.warn > *{position:relative;z-index:1;}
.warn-icon{font-size:16px;flex-shrink:0;}
/* === 按钮 — 纯 WebGL 液态玻璃(无辅助效果) === */
button{
--btn-glow:rgba(0,0,0,.3);
position:relative;z-index:0;isolation:isolate;
background:transparent;
backdrop-filter:url(#glassRefract) saturate(1.4) contrast(1.15);
-webkit-backdrop-filter:url(#glassRefract) saturate(1.4) contrast(1.15);
color:var(--text);
border:none;
border-radius:26px;
padding:11px 22px;
font-size:14px;font-weight:600;font-family:inherit;
cursor:pointer;letter-spacing:-.01em;
box-shadow:
inset 0 0 8px rgba(255,255,255,.6),
inset 0 0 2px rgba(0,0,0,.6),
0 2px 6px rgba(0,0,0,.16);
}
button:hover{
transform:translateY(-1px);
backdrop-filter:url(#glassRefract) saturate(1.5) contrast(1.2);
-webkit-backdrop-filter:url(#glassRefract) saturate(1.5) contrast(1.2);
box-shadow:
inset 0 0 10px rgba(255,255,255,.7),
inset 0 0 2px rgba(0,0,0,.6),
0 4px 10px rgba(0,0,0,.2);
}
button:active{transform:translateY(0);}
/* 液体弹动:点击时像液态玻璃被按压后弹性回弹
注:用 transform-origin:center 保证对称变形;
background 用 background-size:200% 避免渐变被拉伸变形;
box-shadow 跟随收缩,保持形状一致 */
button.pressed{
animation:liquidPress .6s cubic-bezier(.25,1.5,.5,1) 1;
background-size:200% 200% !important;
background-position:center !important;
}
@keyframes liquidPress{
0%{transform:scale(1,1);box-shadow:inset 0 0 8px rgba(255,255,255,.5),inset 0 0 2px rgba(0,0,0,.6),0 3px 10px var(--btn-glow,rgba(0,0,0,.3));}
25%{transform:scale(.90,.86);border-radius:32px;box-shadow:inset 0 0 6px rgba(255,255,255,.4),inset 0 0 2px rgba(0,0,0,.6),0 1px 4px var(--btn-glow,rgba(0,0,0,.3));}
45%{transform:scale(1.06,.94);border-radius:22px;box-shadow:inset 0 0 10px rgba(255,255,255,.6),inset 0 0 2px rgba(0,0,0,.6),0 4px 12px var(--btn-glow,rgba(0,0,0,.3));}
65%{transform:scale(.97,1.03);border-radius:28px;box-shadow:inset 0 0 9px rgba(255,255,255,.55),inset 0 0 2px rgba(0,0,0,.6),0 3px 9px var(--btn-glow,rgba(0,0,0,.3));}
82%{transform:scale(1.01,.99);border-radius:25px;box-shadow:inset 0 0 8px rgba(255,255,255,.5),inset 0 0 2px rgba(0,0,0,.6),0 3px 10px var(--btn-glow,rgba(0,0,0,.3));}
100%{transform:scale(1,1);border-radius:26px;box-shadow:inset 0 0 8px rgba(255,255,255,.5),inset 0 0 2px rgba(0,0,0,.6),0 3px 10px var(--btn-glow,rgba(0,0,0,.3));}
}
button.selected{
--btn-glow:rgba(37,99,235,.45);
background:linear-gradient(135deg,rgba(37,99,235,.82),rgba(29,78,216,.82));
background-size:200% 200%;
color:#fff;
box-shadow:
inset 0 0 8px rgba(255,255,255,.5),
inset 0 0 2px rgba(0,0,0,.6),
0 3px 10px rgba(37,99,235,.45);
}
button.selected:hover{background:linear-gradient(135deg,rgba(29,78,216,.88),rgba(37,99,235,.88));}
button.confirm-btn{
--btn-glow:rgba(22,163,74,.4);
background:linear-gradient(135deg,rgba(22,163,74,.8),rgba(21,128,61,.8));
background-size:200% 200%;
color:#fff;
box-shadow:
inset 0 0 8px rgba(255,255,255,.5),
inset 0 0 2px rgba(0,0,0,.6),
0 3px 10px rgba(22,163,74,.4);
}
button.confirm-btn:hover{background:linear-gradient(135deg,rgba(21,128,61,.88),rgba(22,163,74,.88));}
button.confirm-btn:disabled{
--btn-glow:rgba(0,0,0,.2);
background:rgba(140,140,145,.3);color:rgba(255,255,255,.8);
cursor:not-allowed;filter:brightness(.85);
box-shadow:inset 0 0 2px rgba(0,0,0,.4);transform:none;
}
button.loop-btn{
--btn-glow:rgba(124,58,237,.4);
background:linear-gradient(135deg,rgba(124,58,237,.8),rgba(109,40,217,.8));
background-size:200% 200%;
color:#fff;
box-shadow:
inset 0 0 8px rgba(255,255,255,.5),
inset 0 0 2px rgba(0,0,0,.6),
0 3px 10px rgba(124,58,237,.4);
}
button.loop-btn:hover{background:linear-gradient(135deg,rgba(109,40,217,.92),rgba(124,58,237,.92));}
button.loop-btn.active{
--btn-glow:rgba(234,88,12,.5);
background:linear-gradient(135deg,rgba(234,88,12,.92),rgba(194,65,12,.92));
background-size:200% 200%;
animation:pulse 2s infinite;
}
button.stop-btn{
--btn-glow:rgba(220,38,38,.4);
background:linear-gradient(135deg,rgba(220,38,38,.8),rgba(185,28,28,.8));
background-size:200% 200%;
color:#fff;
box-shadow:
inset 0 0 8px rgba(255,255,255,.5),
inset 0 0 2px rgba(0,0,0,.6),
0 3px 10px rgba(220,38,38,.4);
}
button.stop-btn:hover{background:linear-gradient(135deg,rgba(185,28,28,.88),rgba(220,38,38,.88));}
@keyframes pulse{
0%,100%{box-shadow:inset 0 0 2px rgba(0,0,0,.7),0 0 0 0 rgba(234,88,12,.5);}
50%{box-shadow:inset 0 0 2px rgba(0,0,0,.7),0 0 0 12px rgba(234,88,12,0);}
}
.btn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px;}
.ctrl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;}
.section-label{
font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:.08em;
margin:18px 0 10px;font-weight:700;
}
.section-label:first-child{margin-top:0;}
/* SVG 引脚图容器 — 纯 WebGL 玻璃 */
.pin-svg-wrap{
display:flex;justify-content:center;margin:12px 0;padding:20px;
position:relative;z-index:0;isolation:isolate;
background:transparent;
backdrop-filter:url(#glassRefract) saturate(1.4) contrast(1.15);
-webkit-backdrop-filter:url(#glassRefract) saturate(1.4) contrast(1.15);
border-radius:18px;
border:none;
box-shadow:
inset 0 0 6px rgba(255,255,255,.5),
inset 0 0 2px rgba(0,0,0,.5),
0 2px 8px rgba(0,0,0,.14);
}
.pin-svg-wrap svg{max-width:100%;height:auto;}
.pin-rect{stroke:rgba(255,255,255,.3);stroke-width:.5;transition:fill .3s ease;}
.pin-num{fill:rgba(255,255,255,.7);font-size:12px;font-family:ui-monospace,"SF Mono",monospace;}
.pin-name{fill:rgba(255,255,255,.9);font-size:11px;font-family:ui-monospace,"SF Mono",monospace;}
.pin-name-data{fill:#7fd3ff;font-weight:600;}
.pin-name-ctrl{fill:#ffb74d;font-weight:600;}
.chip-body{fill:url(#chipGrad);stroke:rgba(255,255,255,.25);stroke-width:1;}
.chip-label{fill:#fff;font-size:22px;font-weight:700;text-anchor:middle;}
.chip-sub{fill:rgba(255,255,255,.7);font-size:10px;text-anchor:middle;}
.pin1-dot{fill:var(--blue);}
.pin-legend{
display:flex;gap:18px;justify-content:center;flex-wrap:wrap;margin-top:16px;
font-size:12px;color:var(--text-2);padding-top:16px;
border-top:1px solid var(--separator);font-weight:500;
position:relative;z-index:1;
}
.pin-legend span{display:flex;align-items:center;gap:6px;}
.pin-legend .swatch{
width:14px;height:14px;border-radius:4px;display:inline-block;
box-shadow:0 1px 2px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.5);
}
/* 连接 / 操作 */
.connect-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:14px;position:relative;z-index:1;}
.connect-label{color:var(--text-2);font-weight:500;}
.connect-ip{
color:var(--blue);font-weight:700;
font-family:ui-monospace,"SF Mono",monospace;font-size:15px;
background:rgba(37,99,235,.15);padding:5px 14px;border-radius:26px;
border:none;
box-shadow:inset 0 0 6px rgba(255,255,255,.4),inset 0 0 2px rgba(0,0,0,.3);
}
.connect-hint{color:var(--text-3);font-size:12px;}
.action-row{display:flex;gap:10px;flex-wrap:wrap;position:relative;z-index:1;}
.action-desc{
font-size:13px;color:var(--text-2);margin-top:16px;padding-top:14px;
border-top:1px solid var(--separator);line-height:1.7;position:relative;z-index:1;
}
.action-desc strong{color:var(--text);font-weight:600;}
/* 日志面板 — 纯 WebGL 玻璃 */
.log-panel{padding:0;overflow:hidden;}
.log-panel-header{
padding:18px 24px 14px;border-bottom:1px solid var(--separator);
display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;
background:transparent;position:relative;z-index:1;
}
.log-panel-title{font-size:17px;font-weight:700;letter-spacing:-.01em;}
.log-filters{display:flex;gap:6px;}
.log-filter{
position:relative;z-index:0;isolation:isolate;
padding:5px 14px;border-radius:26px;font-size:12px;font-weight:600;
border:none;background:transparent;color:var(--text-2);
cursor:pointer;font-family:inherit;
backdrop-filter:url(#glassRefract) saturate(1.3) contrast(1.1);
-webkit-backdrop-filter:url(#glassRefract) saturate(1.3) contrast(1.1);
box-shadow:
inset 0 0 5px rgba(255,255,255,.4),
inset 0 0 2px rgba(0,0,0,.4),
0 1px 3px rgba(0,0,0,.1);
}
.log-filter.active{
background:linear-gradient(135deg,rgba(37,99,235,.82),rgba(29,78,216,.82));
color:#fff;
box-shadow:
inset 0 0 8px rgba(255,255,255,.5),
inset 0 0 2px rgba(0,0,0,.6),
0 2px 6px rgba(37,99,235,.35);
}
.log-filter:hover:not(.active){color:var(--text);}
.log-scroll{
padding:14px 24px;font-family:ui-monospace,"SF Mono","Cascadia Code",monospace;
font-size:12px;line-height:1.8;max-height:380px;overflow-y:auto;
background:transparent;position:relative;z-index:1;
}
.log-scroll::-webkit-scrollbar{width:6px;}
.log-scroll::-webkit-scrollbar-track{background:transparent;}
.log-scroll::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2);border-radius:3px;}
.log-scroll::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.35);}
.log-line{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.05);}
.log-line .lt{color:var(--text-3);flex-shrink:0;}
.log-line .lv{font-weight:700;min-width:18px;flex-shrink:0;}
.log-line .lv.E{color:var(--red);}
.log-line .lv.I{color:var(--blue);}
.log-line .lg{color:var(--purple);flex-shrink:0;}
.log-line .lm{color:#fff;word-break:break-all;}
.log-foot{
display:flex;justify-content:space-between;align-items:center;
padding:12px 24px;border-top:1px solid var(--separator);
font-size:12px;color:var(--text-3);font-weight:500;
background:transparent;position:relative;z-index:1;
}
</style>
</head>
<body>
<!-- SVG 折射滤镜:模拟 WebGL IOR=1.5 Thickness=50 的背景位移折射 -->
<svg style="position:absolute;width:0;height:0;overflow:hidden" aria-hidden="true">
<defs>
<filter id="glassRefract" x="-20%" y="-20%" width="140%" height="140%">
<feTurbulence type="fractalNoise" baseFrequency="0.012 0.012" numOctaves="2" seed="7" result="noise"/>
<feGaussianBlur in="noise" stdDeviation="1.5" result="softNoise"/>
<feDisplacementMap in="SourceGraphic" in2="softNoise" scale="14" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
</svg>
<div class="wrap">
<h1>LAN9252 通断测试</h1>
<div class="sub">通过 ESP32 /spi/send → SPI 0xA2 → GD32 驱动 EXMC 引脚,万用表测 LAN9252 端电压</div>
<div class="warn">
<span class="warn-icon">⚠️</span>
<span>不点按钮时 GD32 引脚为高阻输入态(安全)。点击模式后先预览引脚图,点"确认应用"才实际驱动引脚。测量完毕请点"停止/恢复"释放。</span>
</div>
<!-- 连接 -->
<div class="glass card">
<div class="connect-row">
<span class="connect-label">设备 IP</span>
<span id="ipDisplay" class="connect-ip">读取中...</span>
<span class="connect-hint">共用主页"设备连接"配置的 IP</span>
</div>
</div>
<!-- 引脚图 -->
<div class="glass-strong card">
<h2>LAN9252 64-QFN 引脚图</h2>
<p class="card-desc">俯视,Pin1 右上角。点击下方模式按钮预览引脚电压,点"确认应用"发送命令。绿色=3.3V,红色=0V,橙色=控制线测试中。</p>
<div class="pin-svg-wrap">
<svg id="pinSvg" width="720" height="720" viewBox="0 0 720 720">
<defs>
<linearGradient id="chipGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="rgba(60,60,80,.6)"/>
<stop offset="100%" stop-color="rgba(40,40,60,.5)"/>
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
</svg>
</div>
<div class="pin-legend">
<span><span class="swatch" style="background:#16a34a;"></span> HIGH 3.3V</span>
<span><span class="swatch" style="background:#dc2626;"></span> LOW 0V</span>
<span><span class="swatch" style="background:#ea580c;"></span> 控制线测试</span>
<span><span class="swatch" style="background:rgba(232,232,237,.8);"></span> 数据线空闲</span>
<span><span class="swatch" style="background:rgba(255,245,230,.8);"></span> 控制线空闲</span>
<span><span class="swatch" style="background:rgba(240,240,243,.8);"></span> 电源/其他</span>
<span><span class="swatch" style="background:#2563eb;border-radius:50%;"></span> Pin1</span>
</div>
</div>
<!-- 模式选择 -->
<div class="glass card">
<h2>模式选择</h2>
<p class="card-desc">点击预览引脚图,再点"确认应用"发送命令</p>
<div class="section-label">数据线 pattern · D0-D15</div>
<div class="btn-grid">
<button class="mode-btn" data-action="01">0xFFFF 全高</button>
<button class="mode-btn" data-action="02">0x0000 全低</button>
<button class="mode-btn" data-action="03">0xAAAA 奇数高</button>
<button class="mode-btn" data-action="04">0x5555 偶数高</button>
</div>
<div class="section-label">控制线测试</div>
<div class="ctrl-grid">
<button class="mode-btn" data-action="05">CS 拉低 · PD7→Pin28</button>
<button class="mode-btn" data-action="06">RD 拉低 · PD4→Pin31</button>
<button class="mode-btn" data-action="07">WR 拉低 · PD5→Pin30</button>
<button class="mode-btn" data-action="08">A1 拉高 · PD11→Pin25</button>
<button class="mode-btn" data-action="09">A2 拉高 · PD12→Pin29</button>
</div>
<div class="section-label">读周期保持 · 判定芯片是否驱动总线</div>
<div class="ctrl-grid">
<button class="mode-btn" data-action="10">读保持 · CS#+RD# 持续低</button>
</div>
<p class="card-desc" style="margin-top:10px;">
数据线切为高阻输入、CS#+RD# 持续拉低(读 IDX_0)。用万用表在 <strong>LAN9252 芯片引脚</strong> 上量数据线:
芯片活着 → D2(Pin12)、D4(Pin49)、D5(Pin50)、D9(Pin19)、D12(Pin21) ≈ 3.3V,其余数据脚 ≈ 0V(全部确定电平,即 0x1234);
全部中电平/漂移 → 芯片未驱动总线。图上预览色 = 芯片应答的期望值。
</p>
</div>
<!-- 操作 -->
<div class="glass card">
<h2>操作</h2>
<div class="action-row">
<button class="confirm-btn" id="confirmBtn" disabled>✓ 确认应用</button>
<button class="loop-btn" id="loopBtn">🔄 循环切换</button>
<button class="stop-btn" id="stopBtn">⏹ 停止 / 恢复</button>
</div>
<p class="action-desc">
<strong>确认应用</strong> · 发送当前选中的模式到 GD32<br>
<strong>循环切换</strong> · 每5s在 0x0000 ↔ 当前选中模式 之间切换(需先选数据线 pattern)<br>
<strong>停止 / 恢复</strong> · 恢复 EXMC 正常模式
</p>
</div>
<!-- 日志 -->
<div class="glass log-panel">
<div class="log-panel-header">
<span class="log-panel-title">操作日志</span>
<div class="log-filters">
<button class="log-filter active" onclick="setLogFilter('all',this)">全部</button>
<button class="log-filter" onclick="setLogFilter('E',this)">错误</button>
<button class="log-filter" onclick="setLogFilter('I',this)">信息</button>
<button class="log-filter" onclick="clearLogs()">清除</button>
</div>
</div>
<div class="log-scroll" id="logScroll"></div>
<div class="log-foot">
<span>共 <span id="logCount">0</span> 条</span>
<span style="font-family:ui-monospace,monospace;">SPI 0xA2 · UDP 6666</span>
</div>
</div>
</div><!-- /.wrap -->
<script>
/* ===== LAN9252 64-QFN 引脚表 ===== */
var pinData=[
{n:1,name:"OSCI",type:"other"},{n:2,name:"OSCO",type:"other"},
{n:3,name:"VDD12",type:"power"},{n:4,name:"VSS",type:"gnd"},
{n:5,name:"VDD33",type:"power"},{n:6,name:"VDDCR",type:"power"},
{n:7,name:"REG_EN",type:"other"},{n:8,name:"FXLOSEN",type:"other"},
{n:9,name:"FXSDA",type:"other"},{n:10,name:"FXSDB",type:"other"},
{n:11,name:"RST#",type:"other"},{n:12,name:"D2",type:"data",d:2},
{n:13,name:"D1",type:"data",d:1},{n:14,name:"VDDIO",type:"power"},
{n:15,name:"D14",type:"data",d:14},{n:16,name:"D13",type:"data",d:13},
{n:17,name:"D0",type:"data",d:0},{n:18,name:"SYNC1",type:"other"},
{n:19,name:"D9",type:"data",d:9},{n:20,name:"VDDIO",type:"power"},
{n:21,name:"D12",type:"data",d:12},{n:22,name:"D11",type:"data",d:11},
{n:23,name:"D10",type:"data",d:10},{n:24,name:"VDDCR",type:"power"},
{n:25,name:"A1",type:"ctrl",ctrl:"A1"},{n:26,name:"A3",type:"other"},
{n:27,name:"A4",type:"other"},{n:28,name:"CS",type:"ctrl",ctrl:"CS"},
{n:29,name:"A2",type:"ctrl",ctrl:"A2"},{n:30,name:"WR",type:"ctrl",ctrl:"WR"},
{n:31,name:"RD",type:"ctrl",ctrl:"RD"},{n:32,name:"VDDIO",type:"power"},
{n:33,name:"D15",type:"data",d:15},{n:34,name:"SYNC0",type:"other"},
{n:35,name:"D3",type:"data",d:3},{n:36,name:"D6",type:"data",d:6},
{n:37,name:"VDDIO",type:"power"},{n:38,name:"VDDCR",type:"power"},
{n:39,name:"D7",type:"data",d:7},{n:40,name:"D8",type:"data",d:8},
{n:41,name:"TESTMODE",type:"other"},{n:42,name:"EESDA",type:"other"},
{n:43,name:"EESCL",type:"other"},{n:44,name:"IRQ",type:"other"},
{n:45,name:"RUNLED",type:"other"},{n:46,name:"MODE1",type:"other"},
{n:47,name:"VDDIO",type:"power"},{n:48,name:"MODE0",type:"other"},
{n:49,name:"D4",type:"data",d:4},{n:50,name:"D5",type:"data",d:5},
{n:51,name:"TXRX1",type:"power"},{n:52,name:"TXNA",type:"other"},
{n:53,name:"TXPA",type:"other"},{n:54,name:"RXNA",type:"other"},
{n:55,name:"RXPA",type:"other"},{n:56,name:"VDD12",type:"power"},
{n:57,name:"RBIAS",type:"other"},{n:58,name:"VDD33B",type:"power"},
{n:59,name:"VDD12",type:"power"},{n:60,name:"RXPB",type:"other"},
{n:61,name:"RXNB",type:"other"},{n:62,name:"TXPB",type:"other"},
{n:63,name:"TXNB",type:"other"},{n:64,name:"TXRX2",type:"power"}
];
var actionMap={
"01":{t:"data",val:0xFFFF,label:"0xFFFF 全高"},"02":{t:"data",val:0x0000,label:"0x0000 全低"},
"03":{t:"data",val:0xAAAA,label:"0xAAAA 奇数高"},"04":{t:"data",val:0x5555,label:"0x5555 偶数高"},
"05":{t:"ctrl",val:"CS",label:"CS 拉低"},"06":{t:"ctrl",val:"RD",label:"RD 拉低"},
"07":{t:"ctrl",val:"WR",label:"WR 拉低"},"08":{t:"ctrl",val:"A1",label:"A1 拉高"},
"09":{t:"ctrl",val:"A2",label:"A2 拉高"},
"10":{t:"readhold",val:0x1234,label:"读周期保持(CS#+RD# 持续低)"},
"00":{t:"stop",label:"停止/恢复"}
};
/* ===== SVG ===== */
var SVG_NS="http://www.w3.org/2000/svg";
var CHIP_X1=120,CHIP_Y1=120,CHIP_X2=600,CHIP_Y2=600;
var CHIP_W=CHIP_X2-CHIP_X1,PIN_GAP=CHIP_W/16,PIN_OFF=PIN_GAP/2;
var DEFAULT_COLORS={data:"rgba(200,200,210,.5)",ctrl:"rgba(255,200,150,.5)",power:"rgba(220,220,230,.5)",gnd:"rgba(180,180,190,.5)",other:"rgba(210,210,220,.5)"};
function el(tag,attrs,text){
var e=document.createElementNS(SVG_NS,tag);
for(var k in attrs)e.setAttribute(k,attrs[k]);
if(text!==undefined)e.textContent=text;
return e;
}
function getPinPos(n){
if(n>=1&&n<=16){
/* 顶部:引脚上方,名称在上、编号在下 */
var x=CHIP_X2-PIN_OFF-(n-1)*PIN_GAP;
return{rx:x-7,ry:CHIP_Y1-14,rw:14,rh:14,tx:x,ty:CHIP_Y1-16,anchor:"middle",ny:CHIP_Y1-32};
}else if(n>=17&&n<=32){
/* 左侧:引脚左方,编号在上、名称在下(竖向堆叠) */
var y=CHIP_Y1+PIN_OFF+(n-17)*PIN_GAP;
return{rx:CHIP_X1-14,ry:y-7,rw:14,rh:14,tx:CHIP_X1-18,ty:y-1,anchor:"end",ny:y+13};
}else if(n>=33&&n<=48){
/* 底部:引脚下方,编号在上、名称在下 */
var x=CHIP_X1+PIN_OFF+(n-33)*PIN_GAP;
return{rx:x-7,ry:CHIP_Y2,rw:14,rh:14,tx:x,ty:CHIP_Y2+22,anchor:"middle",ny:CHIP_Y2+38};
}else{
/* 右侧:引脚右方,编号在上、名称在下(竖向堆叠) */
var y=CHIP_Y2-PIN_OFF-(n-49)*PIN_GAP;
return{rx:CHIP_X2,ry:y-7,rw:14,rh:14,tx:CHIP_X2+18,ty:y-1,anchor:"start",ny:y+13};
}
}
var pinRects={};
function buildSvg(){
var svg=document.getElementById("pinSvg");
svg.appendChild(el("rect",{class:"chip-body",x:CHIP_X1,y:CHIP_Y1,width:CHIP_W,height:CHIP_W,rx:14}));
svg.appendChild(el("text",{class:"chip-label",x:360,y:345},"LAN9252"));
svg.appendChild(el("text",{class:"chip-sub",x:360,y:368},"64-QFN"));
svg.appendChild(el("text",{class:"chip-sub",x:360,y:386},"Pin1 → 右上角"));
svg.appendChild(el("circle",{class:"pin1-dot",cx:CHIP_X2-14,cy:CHIP_Y1+14,r:5}));
for(var i=0;i<pinData.length;i++){
var p=pinData[i],pos=getPinPos(p.n);
var rect=el("rect",{class:"pin-rect",x:pos.rx,y:pos.ry,width:pos.rw,height:pos.rh,rx:3,id:"pin-"+p.n});
rect.setAttribute("fill",DEFAULT_COLORS[p.type]||DEFAULT_COLORS.other);
svg.appendChild(rect);
pinRects[p.n]=rect;
svg.appendChild(el("text",{class:"pin-num",x:pos.tx,y:pos.ty,"text-anchor":pos.anchor},String(p.n)));
var nc="pin-name";
if(p.type==="data")nc="pin-name pin-name-data";
else if(p.type==="ctrl")nc="pin-name pin-name-ctrl";
svg.appendChild(el("text",{class:nc,x:pos.tx,y:pos.ny||pos.ty,"text-anchor":pos.anchor},p.name));
}
}
function updatePinColors(actionHex){
var act=actionMap[actionHex];if(!act)return;
for(var i=0;i<pinData.length;i++){
var p=pinData[i],color=DEFAULT_COLORS[p.type]||DEFAULT_COLORS.other;
var glow=false;
if(act.t==="data"&&p.type==="data"){
color=(act.val&(1<<p.d))?"#30d158":"#ff453a";glow=true;
}else if(act.t==="ctrl"&&p.type==="ctrl"&&p.ctrl===act.val){
color="#ff9f0a";glow=true;
}else if(act.t==="readhold"){
/* CS/RD 由 GD32 拉低(橙), 数据线 = 芯片应答期望值 0x1234 */
if(p.type==="ctrl"&&(p.ctrl==="CS"||p.ctrl==="RD")){color="#ff9f0a";glow=true;}
else if(p.type==="data"){color=(act.val&(1<<p.d))?"#30d158":"#ff453a";glow=true;}
}
pinRects[p.n].setAttribute("fill",color);
if(glow)pinRects[p.n].setAttribute("filter","url(#glow)");
else pinRects[p.n].removeAttribute("filter");
}
}
function resetPinColors(){
for(var i=0;i<pinData.length;i++){
var p=pinData[i];
pinRects[p.n].setAttribute("fill",DEFAULT_COLORS[p.type]||DEFAULT_COLORS.other);
pinRects[p.n].removeAttribute("filter");
}
}
/* ===== IP ===== */
var LS_ESP_IP="dcu_esp_ip",DEFAULT_IP="192.168.31.127";
function getIp(){try{var s=localStorage.getItem(LS_ESP_IP);if(s&&s.trim())return s.trim();}catch(e){}return DEFAULT_IP;}
document.getElementById("ipDisplay").textContent=getIp();
window.addEventListener("storage",function(e){if(e.key===LS_ESP_IP)document.getElementById("ipDisplay").textContent=getIp();});
/* ===== 日志 ===== */
var logScroll=document.getElementById("logScroll"),logCountEl=document.getElementById("logCount"),logCount=0,logFilter="all";
function esc(s){return String(s==null?"":s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");}
function addLog(level,tag,msg){
var line=document.createElement("div");
line.className="log-line";
if(logFilter!=="all"&&level!==logFilter){line.style.display="none";}
/* msg/tag 含设备响应内容(如 /spi/send JSON),拼 innerHTML 前必须转义 */
line.innerHTML='<span class="lt">['+new Date().toLocaleTimeString()+']</span>'+
'<span class="lv '+esc(level)+'">'+esc(level)+'</span>'+
'<span class="lg">['+esc(tag)+']</span>'+
'<span class="lm">'+esc(msg)+'</span>';
logScroll.insertBefore(line,logScroll.firstChild);
logCount++;logCountEl.textContent=logCount;
while(logScroll.children.length>200)logScroll.removeChild(logScroll.lastChild);
}
function setLogFilter(f,btn){
logFilter=f;
document.querySelectorAll(".log-filter").forEach(function(b){b.classList.remove("active");});
btn.classList.add("active");
logScroll.querySelectorAll(".log-line").forEach(function(l){
var lv=l.querySelector(".lv");
if(f==="all"||(lv&&lv.classList.contains(f)))l.style.display="";
else l.style.display="none";
});
}
function clearLogs(){logScroll.innerHTML="";logCount=0;logCountEl.textContent=0;}
/* ===== 按钮:液体弹动效果 ===== */
document.addEventListener("click",function(e){
var btn=e.target.closest("button");
if(!btn)return;
btn.classList.remove("pressed");
void btn.offsetWidth; /* 强制 reflow,允许重复触发 */
btn.classList.add("pressed");
var done=function(){
btn.classList.remove("pressed");
btn.removeEventListener("animationend",done);
};
btn.addEventListener("animationend",done);
},true);
/* ===== 模式选择 ===== */
var selectedAction=null,lastModeBtn=null;
document.querySelectorAll(".mode-btn").forEach(function(btn){
btn.addEventListener("click",function(){
var action=btn.getAttribute("data-action");
if(lastModeBtn)lastModeBtn.classList.remove("selected");
btn.classList.add("selected");
lastModeBtn=btn;
selectedAction=action;
updatePinColors(action);
document.getElementById("confirmBtn").disabled=false;
addLog("I","预览","选中: "+actionMap[action].label);
});
});
/* ===== 发送 ===== */
var busy=false;
function sendAction(actionHex){
if(busy)return;
var ip=getIp();if(!ip){addLog("E","错误","请先在主页配置 ESP32 IP");return;}
busy=true;
var label=actionMap[actionHex]?actionMap[actionHex].label:actionHex;
addLog("I","发送","action=0x"+actionHex+" ("+label+")");
fetch("http://"+ip+"/spi/send",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({type:"A2",data:actionHex,timeout_ms:2000}),
signal:AbortSignal.timeout(3000)
})
.then(function(r){if(!r.ok)throw new Error("HTTP "+r.status);return r.json();})
.then(function(j){
if(j.status==="ok"&&j.resp_type==="80")
addLog("I","成功",label+" → ACK"+(j.recv?" 0x"+j.recv:""));
else if(j.status==="ok"&&j.resp_type==="82")
addLog("E","失败",label+" → GD32 ERROR");
else
addLog("E","异常",label+" → "+JSON.stringify(j));
})
.catch(function(err){addLog("E","网络",label+" → "+err.message);})
.then(function(){busy=false;});
}
document.getElementById("confirmBtn").addEventListener("click",function(){
if(selectedAction)sendAction(selectedAction);
});
document.getElementById("stopBtn").addEventListener("click",function(){
stopLoop();
sendAction("00");
resetPinColors();
if(lastModeBtn)lastModeBtn.classList.remove("selected");
lastModeBtn=null;selectedAction=null;
document.getElementById("confirmBtn").disabled=true;
addLog("I","恢复","EXMC 恢复正常模式");
});
/* ===== 循环: 0x0000 ↔ 当前模式 ===== */
var loopTimer=null,loopToggle=false;
var loopBtn=document.getElementById("loopBtn");
loopBtn.addEventListener("click",function(){
if(loopTimer)stopLoop();else startLoop();
});
function startLoop(){
if(!selectedAction||actionMap[selectedAction].t!=="data"){
addLog("E","循环","请先选择一个数据线 pattern 模式");
return;
}
stopLoop();
var baseAction=selectedAction;
loopBtn.textContent="⏸ 停止循环";
loopBtn.classList.add("active");
addLog("I","循环","开始:每5s在 0x0000 ↔ "+actionMap[baseAction].label+" 切换");
loopToggle=false;
loopBtn.setAttribute("data-base",baseAction);
doLoopStep();
loopTimer=setInterval(doLoopStep,5000);
}
function stopLoop(){
if(loopTimer){clearInterval(loopTimer);loopTimer=null;}
loopBtn.textContent="🔄 循环切换";
loopBtn.classList.remove("active");
loopBtn.removeAttribute("data-base");
}
function doLoopStep(){
var baseAction=loopBtn.getAttribute("data-base");if(!baseAction)return;
loopToggle=!loopToggle;
var action=loopToggle?baseAction:"02";
if(lastModeBtn)lastModeBtn.classList.remove("selected");
var btn=document.querySelector('[data-action="'+action+'"]');
if(btn){btn.classList.add("selected");lastModeBtn=btn;}
selectedAction=action;
updatePinColors(action);
sendAction(action);
}
buildSvg();
addLog("I","系统","通断测试工具就绪");
</script>
</body>
</html>