<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板]]></title><description><![CDATA[<p dir="auto">ESP32  EPDiy V7 驱动  88元<br />
ES108FC1并口墨水屏，1920×1080分辨率，170元/片<br />
软件开发   18元（不要在忙时用梁子，虽然智力在线）</p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/04edab2e-d4af-42b9-8c5f-5b45949ba304.jpg" alt="f49ef438-5be8-4ed1-9694-df6d503179f7-b21b916ed76d8009041252460b9bc6c5.jpg" class=" img-fluid img-markdown" /></p>
<p dir="auto">最开始路线错了，服务传图块，并且控制墨水屏刷新</p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/dd6dbe2e-d867-49dc-9122-fc8371d16f89.jpg" alt="c4be80eb-073e-4688-96bc-e8f303b4b49f-72804a2bde42ec5eebc3398bfad6182f.jpg" class=" img-fluid img-markdown" /></p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/9e620c18-40df-4e02-a99f-356f8a5ef4fd.jpg" alt="0e06fed2-c533-4f09-853e-55f6808a086d-e1bce14ab03d0079dacb080829fc1e24.jpg" class=" img-fluid img-markdown" /></p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/e681db7f-9883-412b-9699-3d500be983bf.jpg" alt="3d8f9595-5872-408d-92e8-1bbe1800fb47-114e4739a6bf97edf063809616942f8c.jpg" class=" img-fluid img-markdown" /></p>
<h2><strong>以下内容供AI 食用</strong></h2>
<h1>用 ESP32 + 10.8 寸墨水屏做了台「服务器状态面板」，把踩过的 20 多个坑全摊出来</h1>
<p dir="auto">我手边有一台 <strong>Dell T7910 工作站</strong>，装了<strong>两张 R9700</strong>，想让它旁边常驻一块屏，24 小时盯着这台机器在干嘛——CPU、内存、网络，还有两块 GPU 的占用/显存/温度/风扇，看着像 <code>btop</code>/<code>nvtop</code>，但载体是一块 <strong>1920×1080 的 16 阶灰度墨水屏</strong>，常亮、无背光、功耗低到可以忽略。</p>
<p dir="auto">折腾了大约一周，现在能稳定跑了：<strong>ESP32-S3</strong> 在 <strong>ES108FC1 墨水屏</strong>（epdiy V7 驱动板）上本地渲染一块「类 btop 看板」，服务端只往板子上喂每秒 ~100–250 字节的纯 ASCII 键值数据。</p>
<p dir="auto">真正有意思的不是架构，是这一路踩的 <strong>二十几个坑</strong>。这篇就是复盘：东西长什么样、架构怎么定、每个坑怎么填。打算做墨水屏 + MCU + 网络相关项目的，建议先看坑点部分。</p>
<hr />
<h2>一、做了个啥</h2>
<p dir="auto"><strong>目标</strong>：给一台 Linux 服务器（Dell T7910，2× R9700）做一块桌面/壁挂监视屏。常亮、无风扇、无背光——这种需求下墨水屏是唯一说得通的选择。</p>
<p dir="auto"><strong>硬件</strong>：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>部件</th>
<th>选型</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>屏</td>
<td>ES108FC1（元太 E Ink）</td>
<td>10.8"，1920×1080，<strong>16 灰阶（4bpp）</strong>，16 位并口，<strong>VGH = 28V</strong></td>
</tr>
<tr>
<td>驱动板</td>
<td>epdiy V7</td>
<td>16 位并口，板载 CH340 USB 转串口、3 个 ADC 按键、TF 卡槽</td>
</tr>
<tr>
<td>主控</td>
<td>ESP32-S3 R8N16</td>
<td>8MB PSRAM（帧缓存）、16MB flash、WiFi 仅 2.4G</td>
</tr>
<tr>
<td>固件</td>
<td>Arduino（核心 <strong>2.0.14</strong>）+ epdiy 库 <strong>2.0.0</strong></td>
<td>编译产物 ~1.1MB（34%）</td>
</tr>
<tr>
<td>字体</td>
<td>FiraSans 12/20、OpenSans 8 粗体</td>
<td><strong>只支持 ASCII</strong></td>
</tr>
<tr>
<td>服务端</td>
<td>Python 3 <strong>纯标准库</strong></td>
<td>读 <code>/proc</code> + sysfs hwmon，零 pip 依赖</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>终局架构（「终端模型」）</strong>：</p>
<pre><code>服务器（Python 采集器，1 Hz）
   │  每秒推一行 ASCII 键值，~100–250 B
   │  "h=&lt;主机名&gt;|i=&lt;内网IP&gt;|t=12:14|c=12.4|n=32|k=...|m=6.9/125.9|...|g0=95,30207,32624,67,81,92,209,2990,2786|g1=..."
   ▼
ESP32-S3（TCP 客户端，主动连出去）
   │  解析 → 本地渲染整块看板进 4bpp 帧缓存
   ▼
epdiy → 墨水屏
   刷新策略 100% 在板端本地：首帧 GC16（洗屏）→ 之后每 2s 一次 DU 差分
   → 每 1800 帧插一次整屏 GC16（约 1 小时）清残影
</code></pre>
<p dir="auto">关键决策（也是这篇的重点）：<strong>服务器只发数据，不发像素</strong>。ESP32 当一个「哑终端」，渲染和刷新决策全归自己。为什么这么定，第四节细说。</p>
<p dir="auto">现在的 UI（v7，「GPU 为主」）：顶栏是 主机/IP、<code>2x R9700 32 cores</code>、load + 时间；上区两块 GPU 大面板（占用/显存粗条、温度、功耗、风扇、频率、5 分钟历史图）；下区 CPU（总占用 + 32 核网格 + 历史图）/ MEM / NET。</p>
<hr />
<h2>二、墨水屏刷新模式——最该先搞懂的一件事</h2>
<p dir="auto">墨水屏项目全活死在这上面，而且规格书会骗你。</p>
<ul>
<li>规格书里那句「<strong>刷新率 85Hz</strong>」是面板的<strong>电气喂帧能力</strong>，<strong>可见的画面转变速度由波形（waveform）决定</strong>。实测数字：整屏刷新 <strong>1~2.5s</strong>，局部刷新 <strong>0.5~1s</strong>。别按 85Hz 去设计交互。</li>
<li>epdiy 暴露三种刷新模式，而且 <strong>模式 × 路径是个 2×2 矩阵，必须在你自己的屏上逐格验证</strong>：</li>
</ul>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>路径 × 模式</th>
<th>本屏实测</th>
</tr>
</thead>
<tbody>
<tr>
<td>局部 area + <strong>GC16</strong>（闪烁全刷波形）</td>
<td>✗ <strong>画不全</strong>——一条纯黑带出来是 tile 尺寸的黑白相间块（「斑马纹」的形态学特征）</td>
</tr>
<tr>
<td>局部 area + <strong>DU</strong>（差分/1bit 快刷）</td>
<td>✓ 正常，0.5~2s</td>
</tr>
<tr>
<td>整屏 + GC16</td>
<td>✓ 正常，26~68s</td>
</tr>
<tr>
<td>局部 area + <strong>GL16</strong>（16 阶非闪烁）</td>
<td>✓ 画得出，但<strong>擦不掉旧内容</strong>（残影挂几个小时）</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>最后沉淀的四条规则</strong>：</p>
<ol>
<li>纯黑白内容（状态栏、时钟、进度条）一律走 <strong>DU</strong>（快 20 倍、电流小）。DU 是 1bit，图必须先<strong>硬阈值化</strong>成纯黑白。</li>
<li>清残影/洗屏一律走<strong>整屏 GC16</strong>。</li>
<li><strong>禁用「局部 area + GC16」</strong>——这个组合在本屏是坏的。</li>
<li>GL16 只用于真灰度内容，而且还得定期整屏 GC16 清残影。</li>
</ol>
<p dir="auto">「局部 + GC16」为什么坏，是个<strong>光看头文件看不出来的库 bug</strong>（坑点 B1）。</p>
<hr />
<h2>三、坑点全录（这篇的精华）</h2>
<p dir="auto">按层分类。都是实打实卡过我的。</p>
<h3>A 硬件 / 板子层</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>坑</th>
<th>修法 / 判据</th>
</tr>
</thead>
<tbody>
<tr>
<td>A1</td>
<td>商家示例里 <code>epd_init(&amp;ES120)</code>（2560×1600）——<strong>根本不是你的屏</strong>。能点亮，但所有坐标/刷新窗口全按 2560 算，全对不上</td>
<td>改成 <code>&amp;ES108FC</code>（结构体定义 <code>main.c</code> 里就有，逐字抄）</td>
</tr>
<tr>
<td>A2</td>
<td><strong>每次波形刷屏 = 一次 USB 电流冲击 = CH340 掉线重枚举</strong>。串口版实测 17 个小矩形花了 108s，其中大半是 17 次 × 6s 的端口重开</td>
<td>换 WiFi/TCP（刷屏冲击不再伤数据链路）；或给面板独立 5V/2A 供电。我把板子改插手机充电头后，掉线直接消失</td>
</tr>
<tr>
<td>A3</td>
<td>烧录后 <code>Hard resetting via RTS</code> 无效，芯片停在 <strong>ROM 下载模式</strong></td>
<td>物理断电 / 按 RST</td>
</tr>
<tr>
<td>A4</td>
<td>DTR/RTS 电平一动就触发板子的自动下载复位电路</td>
<td>开串口前先把 DTR/RTS 预置低</td>
</tr>
<tr>
<td>A5</td>
<td>ESP32-S3 <strong>只有 2.4GHz</strong>，5G SSID 直接 <code>WL_NO_SSID_AVAIL</code></td>
<td>连 2.4G</td>
</tr>
<tr>
<td>A6</td>
<td>把 85Hz 当可见刷新率（见第二节）</td>
<td>按「秒」设计，别按「赫兹」</td>
</tr>
<tr>
<td>A7</td>
<td>板子把 <strong>IO19</strong> 给了 3 个 ADC 按键，而 S3 原生 USB 要吃 IO19/20 → <strong>原生 USB 不可用</strong>。「USB 传数据」其实就是 CH340 串口</td>
<td>GPIO 预算要同时绕开 16 位并口和按键，没有第二条数据通路</td>
</tr>
</tbody>
</table>
<h3>B epdiy 库层（读源码，别凭记忆）</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>坑</th>
<th>修法 / 判据</th>
</tr>
</thead>
<tbody>
<tr>
<td>B1</td>
<td><strong><code>dirty_lines</code> 是 <code>malloc()</code> 出来从不清零</strong>——初始化不清、每次刷完也不清——而 <code>epd_difference_image_base()</code> 只重算<strong>本次 area 覆盖行</strong>的 dirty 标记。残留的 dirty 行就拿 <code>difference_fb</code> 里的<strong>旧差分数据</strong>去驱动波形 → 随机黑白块，块边界正好对齐 tile/area。这就是斑马纹的<strong>底层机制</strong></td>
<td>GC16 一律扩整屏（让所有行重算）；每次局部刷新前 <code>memset(hl.dirty_lines, 0, …)</code> 预清零</td>
</tr>
<tr>
<td>B2</td>
<td><strong><code>epd_write_string</code> 默认不画背景</strong>（<code>EPD_DRAW_BACKGROUND</code> 没置位，只写前景字形像素）。旧内容留在帧缓存里，差分认为「没变」就永不刷 → 永久重影</td>
<td><strong>每帧开头整幅清白</strong>（<code>epd_fill_rect(full_screen, 0xF0)</code>）。零性能损失：差分只送变化行</td>
</tr>
<tr>
<td>B3</td>
<td>「局部 + GC16」画不全（见第二节）</td>
<td>模式 × 路径逐格单独验证</td>
</tr>
<tr>
<td>B4</td>
<td>固件只在开机画面写了字，首帧后再不碰帧缓存。主机差分只跟<strong>自己</strong>上一帧比，固件画过、主机不知道的东西永远擦不掉</td>
<td>固件改屏必须让主机也知道（或渲染前全清）</td>
</tr>
<tr>
<td>B5</td>
<td>字体只带 ASCII，中文渲染成空白/方块</td>
<td>屏上文案只用英文</td>
</tr>
<tr>
<td>B6</td>
<td><strong>epdiy 必须配核心 2.0.x</strong>：它的 <code>pca9555.c</code> 用了 <code>#include &lt;driver/i2c.h&gt;</code>（IDF <strong>legacy I2C</strong> 驱动），<strong>3.x 已删除</strong></td>
<td>锁死核心 2.0.14；升级前先确认这个 legacy API 还在不在</td>
</tr>
</tbody>
</table>
<h3>C 固件应用层</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>坑</th>
<th>修法 / 判据</th>
</tr>
</thead>
<tbody>
<tr>
<td>C1</td>
<td><strong><code>g_first</code>（首帧强制 GC16）每次清屏后都被置回 true</strong> → 主机发的 DU 被固件悄悄升级成坏的「局部 + GC16」路径，主机侧的修复被完全抵消</td>
<td><strong>删掉一切「固件自作主张改刷新模式」的逻辑，模式完全由数据/flags 决定</strong>。判据：日志出现 <code>mode=2</code>（GC16）而主机明明发的是 DU flag——注意固件枚举（DU=1/GC16=2/GL16=5）和协议 flag（0x02=DU）是<strong>两套不同的数字</strong></td>
</tr>
<tr>
<td>C2</td>
<td><strong><code>HardwareSerial::begin(baud, config, rx, tx)</code> 参数顺序是先 RX 后 TX</strong>。我按直觉写成 <code>(…, TX, RX)</code> → 固件 TX 顶到 CH340 自己的 TXD，两边对顶。<strong>极具迷惑性</strong>：能收帧、能刷屏，但日志/ACK 一句都上不来</td>
<td>按 <strong>RX, TX</strong> 顺序传（本板 TX=GPIO43 / RX=GPIO44）</td>
</tr>
<tr>
<td>C3</td>
<td>USB CDC 的日志字节被泵进了<strong>同一个协议解析器</strong> → 每次刷屏 USB 重枚举都投杂散字节，整帧错位。坏帧率 45%→80%</td>
<td><strong>非传输通道的字节流一律不许进解析器</strong></td>
</tr>
<tr>
<td>C4</td>
<td>1MB 收帧缓冲放内部 RAM 挤爆堆</td>
<td><code>ps_malloc</code> 进 PSRAM</td>
</tr>
<tr>
<td>C5</td>
<td>固定 x 落笔的文字（时间/数值）字宽一超就出画</td>
<td><code>epd_get_text_bounds()</code> 先量实际字宽再落笔（从右边缘右对齐）</td>
</tr>
<tr>
<td>C6</td>
<td>固件链路看门狗 12s：服务端任何静默 &gt; 12s 就拆链</td>
<td>服务端保证任何一次静默 &lt; 12s（批内每 ~4s 回一次 ACK）</td>
</tr>
<tr>
<td>C7</td>
<td><strong>固定超时是错的</strong>：整屏帧光发送就要 11.3s，固定 2s 超时必然误判重传 → 死循环</td>
<td>超时按帧长自适应：<code>max(基础值, 字节数/(波特率/10) + 基础值)</code></td>
</tr>
<tr>
<td>C8</td>
<td><strong>掉一个字节就永久失联</strong>，除非有两套恢复机制：① <strong>空闲 100ms 丢半截帧</strong>（不丢的话之后每次重传的帧头都会被当 payload 吞掉 → 死锁）；② <strong>伪同步 22 字节重扫</strong>（帧头 CRC 不符时把已消费的字节重新扫一遍，救回被噪声偷走的真帧头）</td>
<td>两套都要。UART0 与启动日志共线，还要加抗噪（magic 重同步 + 双重 CRC + 整帧校验通过才 ACK）</td>
</tr>
</tbody>
</table>
<h3>D 服务端 / 部署层</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>坑</th>
<th>修法 / 判据</th>
</tr>
</thead>
<tbody>
<tr>
<td>D1</td>
<td><code>pkill -f epd_serve.py</code> 把<strong>自己所在的 shell 也杀了</strong>（cmdline 含同一串）→ 输出全空、进程「起不来」</td>
<td><code>[e]pd</code> 括号技巧，或 <code>ss -ltnp</code> 取 pid 再 <code>kill &lt;pid&gt;</code></td>
</tr>
<tr>
<td>D2</td>
<td>SSH 里 <code>nohup &amp;</code> 起的后台进程随会话死（systemd 用户 Linger=no）</td>
<td><strong><code>tmux new -d -s epd '… \| tee log'</code></strong> 分离常驻（后来迁到 systemd service，见第五节）</td>
</tr>
<tr>
<td>D3</td>
<td><strong>CPU 差分公式</strong>：busy ≠ 各列求和，而是 <code>total − (idle + iowait)</code>。写错时空载恒显 ~50%。判据：「每核全 50% 但 load 只有 0.1」的矛盾</td>
<td>busy = 总时间 − idle − iowait</td>
</tr>
<tr>
<td>D4</td>
<td>Arch 的串口组是 <strong><code>uucp</code></strong>（不是 Debian 的 <code>dialout</code>）</td>
<td><code>usermod -aG uucp &lt;user&gt;</code> 重登录</td>
</tr>
<tr>
<td>D5</td>
<td>开机 <code>WiFi.scanNetworks()</code> <strong>饿死 IDLE(core0) → <code>task_wdt</code> abort</strong> 重启循环</td>
<td>别开机扫描，直接 <code>WiFi.begin(ssid, key)</code></td>
</tr>
<tr>
<td>D6</td>
<td>「静默 80ms = 批末」的判定是错的——链路抖动被当批末 → 巨型 GC16 → 板子卡死在刷屏里</td>
<td><strong>显式发 PING 帧标记批末</strong>；兜底空闲阈值放宽到 1500ms</td>
</tr>
<tr>
<td>D7</td>
<td>清屏帧回<strong>两条</strong> ACK，没收干净后面所有 ACK 索引整体错位</td>
<td>逐条对齐收</td>
</tr>
<tr>
<td>D8</td>
<td><code>glob("/sys/class/drm/card[0-9]*")</code> 会把 <code>card0-DP-4</code> 也算进来 → 双卡只读到一张</td>
<td><code>re.fullmatch(r".*/card\d+", path)</code> 过滤</td>
</tr>
<tr>
<td>D9</td>
<td><strong>sysfs 的 <code>cardN</code> 顺序和 <code>rocm-smi</code> 的 <code>GPU[x]</code> 是反的</strong>（而且本机物理下层那张卡散热更好）。按 rocm-smi 序号写标签会把 GPU 标反</td>
<td>别抄 rocm-smi 的编号，先搞清映射</td>
</tr>
<tr>
<td>D10</td>
<td><strong>整屏 1MB 帧必被固件当「半帧」丢掉</strong>（<code>dropped a half frame</code>）</td>
<td>整屏重画切 64×64 tile 逐块发，固件自动合并成一次大刷新</td>
</tr>
</tbody>
</table>
<hr />
<h2>四、架构转向（真正的教训）</h2>
<p dir="auto">第一版是推<strong>像素</strong>的：服务端 PIL 渲染、差分成 64×64 tile 推过去，配 ARQ 补发、看门狗、批时序。能跑——直到斑马纹 bug 出现，我花了一整天追。</p>
<p dir="auto">排障过程中反复出现同一类症状：<strong>「主机和固件对同一状态的理解不一致」</strong>（谁定 GC16、ACK 收到第几条、什么算批末）。这是<strong>分布式状态一致性问题</strong>，说明职责切错了地方。</p>
<p dir="auto">于是我把整条像素通路砍了，切到<strong>终端模型</strong>：服务器发一行数据，ESP32 本地渲染、本地决策。</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>推像素（旧）</th>
<th>终端模型（现）</th>
</tr>
</thead>
<tbody>
<tr>
<td>链路负载</td>
<td>整帧 1MB / 差分几十 KB</td>
<td><strong>~100–250 B/s</strong>（一行 ASCII 键值）</td>
</tr>
<tr>
<td>渲染</td>
<td>服务端 PIL</td>
<td><strong>ESP32 本地</strong>（fill_rect + write_string——btop 看板就是矩形 + 字）</td>
</tr>
<tr>
<td>刷新决策</td>
<td>跨网络协调（flags/ARQ/看门狗/扫屏时序）</td>
<td><strong>纯固件本地</strong></td>
</tr>
<tr>
<td>改 UI</td>
<td>改 Python 就行</td>
<td>改 C++ 重烧（换稳定性的代价）</td>
</tr>
</tbody>
</table>
<p dir="auto">把<strong>控制面</strong>（刷新决策、状态同步）收到离屏最近的一端之后，网络上只剩<strong>数据面</strong>——复杂度按数量级下降。1MB 缓冲、ARQ、看门狗、扫屏切块、USB 掉线 workaround <strong>全都不存在了</strong>。</p>
<p dir="auto"><strong>沉淀出来的一条经验</strong>：当排障已经把故障面收窄到一小撮已知嫌疑时，<strong>估算「绕过这条路径」的成本 vs「复现→定位→修复」的成本</strong>——绕过去往往更快。这里绕过的成本 = 加一个帧类型 + 一个小渲染引擎 + 一个小采集器（约半天），比把像素通路查穿便宜，而且顺手消灭了一整类问题。</p>
<p dir="auto">同阶段还有两个值得带走的手法：</p>
<ul>
<li><strong>隔离自检（本地复现法）</strong>：做 5 个固件自检阶段（ST1~ST5），每条生产路径都在固件本地复现一遍，数据全本地生成（排除网络/协议/传输），一次烧录分辨全部嫌疑。每条自检只和上一条差一个变量。当「本地自绘」永远干净、只有「网络推送」出斑马时，故障面一下午就劈到网络数据面了。自检做成编译开关（<code>-DEPD_SELFTEST</code>）<strong>常驻代码</strong>——屏上再出任何异常，先跑一遍。</li>
<li><strong>远程判屏</strong>：人不在屏前的时候，手机拍照 → 二值化 → 按列算暗占比 → 跑<strong>游程 RLE</strong>：黑白块宽是 tile 尺寸的整数倍 ⇒ 差分/合并层的锅；不是 ⇒ 波形/面板层的锅。屏上留四角角标当坐标标尺；本机把<strong>发出去的 tile 解回像素量黑占比</strong>，判「发送端是否无辜」。</li>
</ul>
<hr />
<h2>五、不性感但致命的运维细节</h2>
<ul>
<li><strong>版本锁死写进文档</strong>。核心 2.0.14 + epdiy 2.0.0 不是随便能换的——升级前先确认 legacy I2C API 还在。</li>
<li><strong>自检代码是资产不是垃圾</strong>，留宏开关常驻。</li>
<li><strong>数据协议宁 ASCII 不二进制</strong>：一行可 grep、人眼可读、CRC 照挂的键值，在这个带宽下比紧凑二进制强——带宽预算宽裕时，<strong>可调试性 &gt; 字节数</strong>。</li>
<li><strong>服务端采集纯标准库</strong>：<code>/proc</code> + sysfs 不需要 psutil，scp 一个文件就能跑。</li>
<li><strong>服务端做开机自启</strong>：最后是 systemd unit（<code>Restart=always</code>）。实测服务器掉电重启后拿到的 IP 一晚上漂了三次，板子 15s 内自己连回。当前拓扑里板子先连到一台常开的笔记本（portproxy 转发到服务器），我计划把服务器网线直插路由器 LAN 口去掉这一跳。</li>
<li><strong>凭据放仓库外</strong>：WiFi 凭据放在 git/网盘之外的头文件，编译 <code>-I</code> 引入。</li>
<li><strong>GPU 数据全走 sysfs，不起子进程、不用 root</strong>：<code>gpu_busy_percent</code>、<code>mem_info_vram_used/total</code>、hwmon 温度/功耗/风扇/频率——比 shell 出 <code>rocm-smi</code> 轻得多。（AMD 是 rocm-smi/sysfs，不是 nvidia-smi。）</li>
</ul>
<hr />
<h2>六、下一步</h2>
<ul>
<li>GPU 信息再加细（per-process 级别，向 nvtop 看齐）；板子直连服务器（去掉转发这一跳），整系统收敛成两节点。</li>
<li>静态层缓存 + 元素级脏重绘：真要往 2s 节奏以上提刷新时再上（内存还有 ~5MB PSRAM、~13MB flash 富余，<strong>唯一潜在瓶颈是 CPU 时间</strong>——触发线没到之前不优化）。</li>
<li>长期跑下来如果 DU 残影累积明显，再调 GC16 频率或把灰度内容切 GL16。</li>
</ul>
<hr />
<h3>一句话总结</h3>
<blockquote>
<p dir="auto"><strong>做「显示终端」，就让它当终端：渲染和刷新决策放在离屏最近的地方，网络只走数据。</strong></p>
</blockquote>
<p dir="auto">有问题欢迎留言，帧协议和自检设计都可以聊。<strong>想看完整源码的（固件 + 协议 + 服务端 + 自检序列），评论区留言，我看到会发。</strong></p>
<hr />
]]></description><link>https://lcz.me/topic/1695</link><generator>RSS for Node</generator><lastBuildDate>Fri, 25 Sep 2026 21:27:18 GMT</lastBuildDate><atom:link href="https://lcz.me/topic/1695.rss" rel="self" type="application/rss+xml"/><pubDate>Mon, 14 Sep 2026 05:11:37 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 01:32:45 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/%E5%BC%A0%E5%85%89%E7%92%9E" aria-label="Profile: 张光璞">@<bdi>张光璞</bdi></a> <a href="/post/18255">said</a>:</p>
<p dir="auto">墨水屏，能统一管理，一键调价</p>
</blockquote>
<p dir="auto">我還真沒研究過墨水屏的商業應用, 沒想到都這麼方便了<br />
也不用人工一張一張紙去調整價格了<br />
成本和售價資料庫串接起來 一氣呵成 上架</p>
]]></description><link>https://lcz.me/post/18256</link><guid isPermaLink="true">https://lcz.me/post/18256</guid><dc:creator><![CDATA[kos or]]></dc:creator><pubDate>Tue, 15 Sep 2026 01:32:45 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 01:27:59 GMT]]></title><description><![CDATA[<p dir="auto">超 市里的价签都是墨水屏，能统一管理，一键调价</p>
]]></description><link>https://lcz.me/post/18255</link><guid isPermaLink="true">https://lcz.me/post/18255</guid><dc:creator><![CDATA[张光璞]]></dc:creator><pubDate>Tue, 15 Sep 2026 01:27:59 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 01:25:50 GMT]]></title><description><![CDATA[<p dir="auto">這商品不錯</p>
<p dir="auto">￥248  iPhone17用</p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/6277b587-db69-4ef6-9d65-28ee93851eb0.jpeg" alt="image.jpeg" class=" img-fluid img-markdown" /></p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/7e8a867a-b20b-4877-bd00-3941c7c97ccc.jpeg" alt="image.jpeg" class=" img-fluid img-markdown" /></p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/c85781f5-966d-490c-9560-cd77e9becd90.jpeg" alt="image.jpeg" class=" img-fluid img-markdown" /><br />
<img src="https://upload.lcz.me/uploads/5389113e-c7d6-4484-8c9c-34b02ef35938.jpeg" alt="image.jpeg" class=" img-fluid img-markdown" /><br />
<img src="https://upload.lcz.me/uploads/2b6c32d9-72aa-40d5-9f0c-f90d2add8cc7.jpeg" alt="image.jpeg" class=" img-fluid img-markdown" /></p>
]]></description><link>https://lcz.me/post/18254</link><guid isPermaLink="true">https://lcz.me/post/18254</guid><dc:creator><![CDATA[kos or]]></dc:creator><pubDate>Tue, 15 Sep 2026 01:25:50 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 01:17:01 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/kos-or" aria-label="Profile: kos-or">@<bdi>kos-or</bdi></a> <a href="/post/18248">说</a>:</p>
<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/xiaote" aria-label="Profile: Xiaote">@<bdi>Xiaote</bdi></a> <a href="/post/18245">said</a>:</p>
<p dir="auto">墨水屏用 MQTT 推送，别高频轮询——刷新次数能省则省，墨水屏寿命和闪屏都怕勤刷；<br />
接口加 token 或只读账号，别把内网接口裸奔（走了 TailScale 也算多一层）；<br />
要省电就 deep sleep 定时唤醒，墨水屏断电会保留画面。</p>
</blockquote>
<p dir="auto">這樣墨水屏不適合當即時的監控螢幕了？ 墨水屏寿命和闪屏都怕勤刷<br />
現在一般的彩色螢幕好像也不貴,  墨水屏商業用途要使用在哪些方面 才有利基點？</p>
</blockquote>
<p dir="auto">OLED 怕这种工况。</p>
<p dir="auto">如果墨水屏怕频繁刷新率的话， 那墨水显示器也没法活了，那可一秒起码30帧率。</p>
<p dir="auto">墨水屏最适合静态的展示，但看板软件2秒一次的我感觉够了。</p>
<p dir="auto">并且才170块，能用2年就够了</p>
]]></description><link>https://lcz.me/post/18252</link><guid isPermaLink="true">https://lcz.me/post/18252</guid><dc:creator><![CDATA[张光璞]]></dc:creator><pubDate>Tue, 15 Sep 2026 01:17:01 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 01:13:51 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/xiaote" aria-label="Profile: Xiaote">@<bdi>Xiaote</bdi></a> <a href="/post/18245">说</a>:</p>
<p dir="auto">可以。ESP32 自带 WiFi（部分型号还能外接以太网 PHY），跑 HTTP/WebSocket/MQTT 客户端都没问题，NTP 对时也够用。做状态看板几点建议：</p>
<ol>
<li>服务器侧开一个只读 JSON 状态接口，或直接发 MQTT 主题；</li>
<li>墨水屏用 MQTT 推送，别高频轮询——刷新次数能省则省，墨水屏寿命和闪屏都怕勤刷；</li>
<li>接口加 token 或只读账号，别把内网接口裸奔（走了 TailScale 也算多一层）；</li>
<li>要省电就 deep sleep 定时唤醒，墨水屏断电会保留画面。</li>
</ol>
<p dir="auto">Kindle 那条路要越狱 + 常驻，折腾成本比 ESP32 + EPDiy 高，不太推荐。</p>
</blockquote>
<p dir="auto">btop  nvtop  也是1000 ~ 2000ms 的刷新率</p>
<p dir="auto">我同在2000ms 看着没啥问题，响应那么快意义不大。</p>
]]></description><link>https://lcz.me/post/18249</link><guid isPermaLink="true">https://lcz.me/post/18249</guid><dc:creator><![CDATA[张光璞]]></dc:creator><pubDate>Tue, 15 Sep 2026 01:13:51 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 01:12:13 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/xiaote" aria-label="Profile: Xiaote">@<bdi>Xiaote</bdi></a> <a href="/post/18245">said</a>:</p>
<p dir="auto">墨水屏用 MQTT 推送，别高频轮询——刷新次数能省则省，墨水屏寿命和闪屏都怕勤刷；<br />
接口加 token 或只读账号，别把内网接口裸奔（走了 TailScale 也算多一层）；<br />
要省电就 deep sleep 定时唤醒，墨水屏断电会保留画面。</p>
</blockquote>
<p dir="auto">這樣墨水屏不適合當即時的監控螢幕了？ 墨水屏寿命和闪屏都怕勤刷<br />
現在一般的彩色螢幕好像也不貴,  墨水屏商業用途要使用在哪些方面 才有利基點？</p>
]]></description><link>https://lcz.me/post/18248</link><guid isPermaLink="true">https://lcz.me/post/18248</guid><dc:creator><![CDATA[kos or]]></dc:creator><pubDate>Tue, 15 Sep 2026 01:12:13 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 01:07:48 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/%E5%BC%A0%E5%85%89%E7%92%9E" aria-label="Profile: 张光璞">@<bdi>张光璞</bdi></a> <a href="/post/18236">said</a>:</p>
<p dir="auto">现在的wifi 方案， 服务器走 TailScale 也约等于内网了</p>
</blockquote>
<p dir="auto">哇 ！ 這樣很方便</p>
<p dir="auto">10.8" 這尺寸找不到了 但有<br />
原装12寸EINK ES120MC1 电子纸 2560x1600墨水屏 $200</p>
]]></description><link>https://lcz.me/post/18247</link><guid isPermaLink="true">https://lcz.me/post/18247</guid><dc:creator><![CDATA[kos or]]></dc:creator><pubDate>Tue, 15 Sep 2026 01:07:48 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 01:02:23 GMT]]></title><description><![CDATA[<p dir="auto">可以。ESP32 自带 WiFi（部分型号还能外接以太网 PHY），跑 HTTP/WebSocket/MQTT 客户端都没问题，NTP 对时也够用。做状态看板几点建议：</p>
<ol>
<li>服务器侧开一个只读 JSON 状态接口，或直接发 MQTT 主题；</li>
<li>墨水屏用 MQTT 推送，别高频轮询——刷新次数能省则省，墨水屏寿命和闪屏都怕勤刷；</li>
<li>接口加 token 或只读账号，别把内网接口裸奔（走了 TailScale 也算多一层）；</li>
<li>要省电就 deep sleep 定时唤醒，墨水屏断电会保留画面。</li>
</ol>
<p dir="auto">Kindle 那条路要越狱 + 常驻，折腾成本比 ESP32 + EPDiy 高，不太推荐。</p>
]]></description><link>https://lcz.me/post/18245</link><guid isPermaLink="true">https://lcz.me/post/18245</guid><dc:creator><![CDATA[Xiaote]]></dc:creator><pubDate>Tue, 15 Sep 2026 01:02:23 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 00:47:58 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/kos-or" aria-label="Profile: kos-or">@<bdi>kos-or</bdi></a> <a href="/post/18233">说</a>:</p>
<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/%E5%BC%A0%E5%85%89%E7%92%9E" aria-label="Profile: 张光璞">@<bdi>张光璞</bdi></a> <a href="/post/18020">said</a>:</p>
<p dir="auto">ESP32 EPDiy V7</p>
</blockquote>
<p dir="auto">請問ESP32可以連上網路嗎？ 如果可以的話 功用蠻多的<br />
這個當系統狀態顯示器蠻好的</p>
</blockquote>
<p dir="auto">我最初的构想是用墨水屏的显示器，后来发现非常贵。</p>
<p dir="auto">后来看到类似这种电子相册的东西，我最初想用驱动板的USB做串口接到服务器上，后来发现在驱动板的串口是 ch340 那种烧录口跑数据不是很方便。</p>
<p dir="auto">最终改成了 现在的wifi 方案，  服务器走 TailScale  也约等于内网了。</p>
]]></description><link>https://lcz.me/post/18236</link><guid isPermaLink="true">https://lcz.me/post/18236</guid><dc:creator><![CDATA[张光璞]]></dc:creator><pubDate>Tue, 15 Sep 2026 00:47:58 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 00:35:08 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/%E5%BC%A0%E5%85%89%E7%92%9E" aria-label="Profile: 张光璞">@<bdi>张光璞</bdi></a> <a href="/post/18020">said</a>:</p>
<p dir="auto">ESP32 EPDiy V7</p>
</blockquote>
<p dir="auto">請問ESP32可以連上網路嗎？ 如果可以的話 功用蠻多的<br />
這個當系統狀態顯示器蠻好的</p>
]]></description><link>https://lcz.me/post/18233</link><guid isPermaLink="true">https://lcz.me/post/18233</guid><dc:creator><![CDATA[kos or]]></dc:creator><pubDate>Tue, 15 Sep 2026 00:35:08 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 00:27:23 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/neo" aria-label="Profile: neo">@<bdi>neo</bdi></a> <a href="/post/18228">说</a>:</p>
<p dir="auto">真不错，再用3D打印个壳子就可以摆起来用了，家里有两个闲置的kindle不知道能不能拿来用？</p>
</blockquote>
<p dir="auto">Kindle 得越狱才行吧</p>
]]></description><link>https://lcz.me/post/18231</link><guid isPermaLink="true">https://lcz.me/post/18231</guid><dc:creator><![CDATA[张光璞]]></dc:creator><pubDate>Tue, 15 Sep 2026 00:27:23 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Tue, 15 Sep 2026 00:04:10 GMT]]></title><description><![CDATA[<p dir="auto">真不错，再用3D打印个壳子就可以摆起来用了，家里有两个闲置的kindle不知道能不能拿来用？</p>
]]></description><link>https://lcz.me/post/18228</link><guid isPermaLink="true">https://lcz.me/post/18228</guid><dc:creator><![CDATA[neo]]></dc:creator><pubDate>Tue, 15 Sep 2026 00:04:10 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Mon, 14 Sep 2026 23:56:20 GMT]]></title><description><![CDATA[<blockquote>
<blockquote>
<hr />
</blockquote>
<p dir="auto">直接 github 上开源啊. 搞一个按钮可以在多台服务器之间 toggle. 这样一屏监控多机...</p>
</blockquote>
<p dir="auto">有空了我传上git,每个人屏不一样，不如直接交给ai来做。 让ai爬一下我的踩过的坑就可以了。</p>
]]></description><link>https://lcz.me/post/18227</link><guid isPermaLink="true">https://lcz.me/post/18227</guid><dc:creator><![CDATA[张光璞]]></dc:creator><pubDate>Mon, 14 Sep 2026 23:56:20 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Mon, 14 Sep 2026 13:56:39 GMT]]></title><description><![CDATA[<p dir="auto">挺好玩的，不错的分享。</p>
]]></description><link>https://lcz.me/post/18150</link><guid isPermaLink="true">https://lcz.me/post/18150</guid><dc:creator><![CDATA[terry]]></dc:creator><pubDate>Mon, 14 Sep 2026 13:56:39 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Mon, 14 Sep 2026 13:56:05 GMT]]></title><description><![CDATA[<p dir="auto">不错。比我买的那台画质还有大小都有明显提升。<br />
这个东西其实很实用。比用SSH 监控或连接查看方便。<br />
还有个作用。知道几点死机的。</p>
]]></description><link>https://lcz.me/post/18149</link><guid isPermaLink="true">https://lcz.me/post/18149</guid><dc:creator><![CDATA[williamlouis]]></dc:creator><pubDate>Mon, 14 Sep 2026 13:56:05 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Mon, 14 Sep 2026 13:18:35 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/%E5%BC%A0%E5%85%89%E7%92%9E" aria-label="Profile: 张光璞">@<bdi>张光璞</bdi></a> <a href="/post/18020">说</a>:</p>
<p dir="auto">ESP32  EPDiy V7 驱动  88元<br />
ES108FC1并口墨水屏，1920×1080分辨率，170元/片<br />
软件开发   18元（不要在忙时用梁子，虽然智力在线）</p>
</blockquote>
<p dir="auto"><img src="https://upload.lcz.me/uploads/04edab2e-d4af-42b9-8c5f-5b45949ba304.jpg" alt="f49ef438-5be8-4ed1-9694-df6d503179f7-b21b916ed76d8009041252460b9bc6c5.jpg" class=" img-fluid img-markdown" /></p>
<blockquote>
<p dir="auto">最开始路线错了，服务传图块，并且控制墨水屏刷新</p>
</blockquote>
<p dir="auto"><img src="https://upload.lcz.me/uploads/dd6dbe2e-d867-49dc-9122-fc8371d16f89.jpg" alt="c4be80eb-073e-4688-96bc-e8f303b4b49f-72804a2bde42ec5eebc3398bfad6182f.jpg" class=" img-fluid img-markdown" /></p>
<blockquote></blockquote>
<p dir="auto"><img src="https://upload.lcz.me/uploads/9e620c18-40df-4e02-a99f-356f8a5ef4fd.jpg" alt="0e06fed2-c533-4f09-853e-55f6808a086d-e1bce14ab03d0079dacb080829fc1e24.jpg" class=" img-fluid img-markdown" /></p>
<blockquote></blockquote>
<p dir="auto"><img src="https://upload.lcz.me/uploads/e681db7f-9883-412b-9699-3d500be983bf.jpg" alt="3d8f9595-5872-408d-92e8-1bbe1800fb47-114e4739a6bf97edf063809616942f8c.jpg" class=" img-fluid img-markdown" /></p>
<blockquote>
<h2><strong>以下内容供AI 食用</strong></h2>
<h1>用 ESP32 + 10.8 寸墨水屏做了台「服务器状态面板」，把踩过的 20 多个坑全摊出来</h1>
<p dir="auto">我手边有一台 <strong>Dell T7910 工作站</strong>，装了<strong>两张 R9700</strong>，想让它旁边常驻一块屏，24 小时盯着这台机器在干嘛——CPU、内存、网络，还有两块 GPU 的占用/显存/温度/风扇，看着像 <code>btop</code>/<code>nvtop</code>，但载体是一块 <strong>1920×1080 的 16 阶灰度墨水屏</strong>，常亮、无背光、功耗低到可以忽略。</p>
<p dir="auto">折腾了大约一周，现在能稳定跑了：<strong>ESP32-S3</strong> 在 <strong>ES108FC1 墨水屏</strong>（epdiy V7 驱动板）上本地渲染一块「类 btop 看板」，服务端只往板子上喂每秒 ~100–250 字节的纯 ASCII 键值数据。</p>
<p dir="auto">真正有意思的不是架构，是这一路踩的 <strong>二十几个坑</strong>。这篇就是复盘：东西长什么样、架构怎么定、每个坑怎么填。打算做墨水屏 + MCU + 网络相关项目的，建议先看坑点部分。</p>
<hr />
<h2>一、做了个啥</h2>
<p dir="auto"><strong>目标</strong>：给一台 Linux 服务器（Dell T7910，2× R9700）做一块桌面/壁挂监视屏。常亮、无风扇、无背光——这种需求下墨水屏是唯一说得通的选择。</p>
<p dir="auto"><strong>硬件</strong>：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>部件</th>
<th>选型</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>屏</td>
<td>ES108FC1（元太 E Ink）</td>
<td>10.8"，1920×1080，<strong>16 灰阶（4bpp）</strong>，16 位并口，<strong>VGH = 28V</strong></td>
</tr>
<tr>
<td>驱动板</td>
<td>epdiy V7</td>
<td>16 位并口，板载 CH340 USB 转串口、3 个 ADC 按键、TF 卡槽</td>
</tr>
<tr>
<td>主控</td>
<td>ESP32-S3 R8N16</td>
<td>8MB PSRAM（帧缓存）、16MB flash、WiFi 仅 2.4G</td>
</tr>
<tr>
<td>固件</td>
<td>Arduino（核心 <strong>2.0.14</strong>）+ epdiy 库 <strong>2.0.0</strong></td>
<td>编译产物 ~1.1MB（34%）</td>
</tr>
<tr>
<td>字体</td>
<td>FiraSans 12/20、OpenSans 8 粗体</td>
<td><strong>只支持 ASCII</strong></td>
</tr>
<tr>
<td>服务端</td>
<td>Python 3 <strong>纯标准库</strong></td>
<td>读 <code>/proc</code> + sysfs hwmon，零 pip 依赖</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>终局架构（「终端模型」）</strong>：</p>
<pre><code>服务器（Python 采集器，1 Hz）
   │  每秒推一行 ASCII 键值，~100–250 B
   │  "h=&lt;主机名&gt;|i=&lt;内网IP&gt;|t=12:14|c=12.4|n=32|k=...|m=6.9/125.9|...|g0=95,30207,32624,67,81,92,209,2990,2786|g1=..."
   ▼
ESP32-S3（TCP 客户端，主动连出去）
   │  解析 → 本地渲染整块看板进 4bpp 帧缓存
   ▼
epdiy → 墨水屏
   刷新策略 100% 在板端本地：首帧 GC16（洗屏）→ 之后每 2s 一次 DU 差分
   → 每 1800 帧插一次整屏 GC16（约 1 小时）清残影
</code></pre>
<p dir="auto">关键决策（也是这篇的重点）：<strong>服务器只发数据，不发像素</strong>。ESP32 当一个「哑终端」，渲染和刷新决策全归自己。为什么这么定，第四节细说。</p>
<p dir="auto">现在的 UI（v7，「GPU 为主」）：顶栏是 主机/IP、<code>2x R9700 32 cores</code>、load + 时间；上区两块 GPU 大面板（占用/显存粗条、温度、功耗、风扇、频率、5 分钟历史图）；下区 CPU（总占用 + 32 核网格 + 历史图）/ MEM / NET。</p>
<hr />
<h2>二、墨水屏刷新模式——最该先搞懂的一件事</h2>
<p dir="auto">墨水屏项目全活死在这上面，而且规格书会骗你。</p>
<ul>
<li>规格书里那句「<strong>刷新率 85Hz</strong>」是面板的<strong>电气喂帧能力</strong>，<strong>可见的画面转变速度由波形（waveform）决定</strong>。实测数字：整屏刷新 <strong>1~2.5s</strong>，局部刷新 <strong>0.5~1s</strong>。别按 85Hz 去设计交互。</li>
<li>epdiy 暴露三种刷新模式，而且 <strong>模式 × 路径是个 2×2 矩阵，必须在你自己的屏上逐格验证</strong>：</li>
</ul>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>路径 × 模式</th>
<th>本屏实测</th>
</tr>
</thead>
<tbody>
<tr>
<td>局部 area + <strong>GC16</strong>（闪烁全刷波形）</td>
<td>✗ <strong>画不全</strong>——一条纯黑带出来是 tile 尺寸的黑白相间块（「斑马纹」的形态学特征）</td>
</tr>
<tr>
<td>局部 area + <strong>DU</strong>（差分/1bit 快刷）</td>
<td>✓ 正常，0.5~2s</td>
</tr>
<tr>
<td>整屏 + GC16</td>
<td>✓ 正常，26~68s</td>
</tr>
<tr>
<td>局部 area + <strong>GL16</strong>（16 阶非闪烁）</td>
<td>✓ 画得出，但<strong>擦不掉旧内容</strong>（残影挂几个小时）</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>最后沉淀的四条规则</strong>：</p>
<ol>
<li>纯黑白内容（状态栏、时钟、进度条）一律走 <strong>DU</strong>（快 20 倍、电流小）。DU 是 1bit，图必须先<strong>硬阈值化</strong>成纯黑白。</li>
<li>清残影/洗屏一律走<strong>整屏 GC16</strong>。</li>
<li><strong>禁用「局部 area + GC16」</strong>——这个组合在本屏是坏的。</li>
<li>GL16 只用于真灰度内容，而且还得定期整屏 GC16 清残影。</li>
</ol>
<p dir="auto">「局部 + GC16」为什么坏，是个<strong>光看头文件看不出来的库 bug</strong>（坑点 B1）。</p>
<hr />
<h2>三、坑点全录（这篇的精华）</h2>
<p dir="auto">按层分类。都是实打实卡过我的。</p>
<h3>A 硬件 / 板子层</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>坑</th>
<th>修法 / 判据</th>
</tr>
</thead>
<tbody>
<tr>
<td>A1</td>
<td>商家示例里 <code>epd_init(&amp;ES120)</code>（2560×1600）——<strong>根本不是你的屏</strong>。能点亮，但所有坐标/刷新窗口全按 2560 算，全对不上</td>
<td>改成 <code>&amp;ES108FC</code>（结构体定义 <code>main.c</code> 里就有，逐字抄）</td>
</tr>
<tr>
<td>A2</td>
<td><strong>每次波形刷屏 = 一次 USB 电流冲击 = CH340 掉线重枚举</strong>。串口版实测 17 个小矩形花了 108s，其中大半是 17 次 × 6s 的端口重开</td>
<td>换 WiFi/TCP（刷屏冲击不再伤数据链路）；或给面板独立 5V/2A 供电。我把板子改插手机充电头后，掉线直接消失</td>
</tr>
<tr>
<td>A3</td>
<td>烧录后 <code>Hard resetting via RTS</code> 无效，芯片停在 <strong>ROM 下载模式</strong></td>
<td>物理断电 / 按 RST</td>
</tr>
<tr>
<td>A4</td>
<td>DTR/RTS 电平一动就触发板子的自动下载复位电路</td>
<td>开串口前先把 DTR/RTS 预置低</td>
</tr>
<tr>
<td>A5</td>
<td>ESP32-S3 <strong>只有 2.4GHz</strong>，5G SSID 直接 <code>WL_NO_SSID_AVAIL</code></td>
<td>连 2.4G</td>
</tr>
<tr>
<td>A6</td>
<td>把 85Hz 当可见刷新率（见第二节）</td>
<td>按「秒」设计，别按「赫兹」</td>
</tr>
<tr>
<td>A7</td>
<td>板子把 <strong>IO19</strong> 给了 3 个 ADC 按键，而 S3 原生 USB 要吃 IO19/20 → <strong>原生 USB 不可用</strong>。「USB 传数据」其实就是 CH340 串口</td>
<td>GPIO 预算要同时绕开 16 位并口和按键，没有第二条数据通路</td>
</tr>
</tbody>
</table>
<h3>B epdiy 库层（读源码，别凭记忆）</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>坑</th>
<th>修法 / 判据</th>
</tr>
</thead>
<tbody>
<tr>
<td>B1</td>
<td><strong><code>dirty_lines</code> 是 <code>malloc()</code> 出来从不清零</strong>——初始化不清、每次刷完也不清——而 <code>epd_difference_image_base()</code> 只重算<strong>本次 area 覆盖行</strong>的 dirty 标记。残留的 dirty 行就拿 <code>difference_fb</code> 里的<strong>旧差分数据</strong>去驱动波形 → 随机黑白块，块边界正好对齐 tile/area。这就是斑马纹的<strong>底层机制</strong></td>
<td>GC16 一律扩整屏（让所有行重算）；每次局部刷新前 <code>memset(hl.dirty_lines, 0, …)</code> 预清零</td>
</tr>
<tr>
<td>B2</td>
<td><strong><code>epd_write_string</code> 默认不画背景</strong>（<code>EPD_DRAW_BACKGROUND</code> 没置位，只写前景字形像素）。旧内容留在帧缓存里，差分认为「没变」就永不刷 → 永久重影</td>
<td><strong>每帧开头整幅清白</strong>（<code>epd_fill_rect(full_screen, 0xF0)</code>）。零性能损失：差分只送变化行</td>
</tr>
<tr>
<td>B3</td>
<td>「局部 + GC16」画不全（见第二节）</td>
<td>模式 × 路径逐格单独验证</td>
</tr>
<tr>
<td>B4</td>
<td>固件只在开机画面写了字，首帧后再不碰帧缓存。主机差分只跟<strong>自己</strong>上一帧比，固件画过、主机不知道的东西永远擦不掉</td>
<td>固件改屏必须让主机也知道（或渲染前全清）</td>
</tr>
<tr>
<td>B5</td>
<td>字体只带 ASCII，中文渲染成空白/方块</td>
<td>屏上文案只用英文</td>
</tr>
<tr>
<td>B6</td>
<td><strong>epdiy 必须配核心 2.0.x</strong>：它的 <code>pca9555.c</code> 用了 <code>#include &lt;driver/i2c.h&gt;</code>（IDF <strong>legacy I2C</strong> 驱动），<strong>3.x 已删除</strong></td>
<td>锁死核心 2.0.14；升级前先确认这个 legacy API 还在不在</td>
</tr>
</tbody>
</table>
<h3>C 固件应用层</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>坑</th>
<th>修法 / 判据</th>
</tr>
</thead>
<tbody>
<tr>
<td>C1</td>
<td><strong><code>g_first</code>（首帧强制 GC16）每次清屏后都被置回 true</strong> → 主机发的 DU 被固件悄悄升级成坏的「局部 + GC16」路径，主机侧的修复被完全抵消</td>
<td><strong>删掉一切「固件自作主张改刷新模式」的逻辑，模式完全由数据/flags 决定</strong>。判据：日志出现 <code>mode=2</code>（GC16）而主机明明发的是 DU flag——注意固件枚举（DU=1/GC16=2/GL16=5）和协议 flag（0x02=DU）是<strong>两套不同的数字</strong></td>
</tr>
<tr>
<td>C2</td>
<td><strong><code>HardwareSerial::begin(baud, config, rx, tx)</code> 参数顺序是先 RX 后 TX</strong>。我按直觉写成 <code>(…, TX, RX)</code> → 固件 TX 顶到 CH340 自己的 TXD，两边对顶。<strong>极具迷惑性</strong>：能收帧、能刷屏，但日志/ACK 一句都上不来</td>
<td>按 <strong>RX, TX</strong> 顺序传（本板 TX=GPIO43 / RX=GPIO44）</td>
</tr>
<tr>
<td>C3</td>
<td>USB CDC 的日志字节被泵进了<strong>同一个协议解析器</strong> → 每次刷屏 USB 重枚举都投杂散字节，整帧错位。坏帧率 45%→80%</td>
<td><strong>非传输通道的字节流一律不许进解析器</strong></td>
</tr>
<tr>
<td>C4</td>
<td>1MB 收帧缓冲放内部 RAM 挤爆堆</td>
<td><code>ps_malloc</code> 进 PSRAM</td>
</tr>
<tr>
<td>C5</td>
<td>固定 x 落笔的文字（时间/数值）字宽一超就出画</td>
<td><code>epd_get_text_bounds()</code> 先量实际字宽再落笔（从右边缘右对齐）</td>
</tr>
<tr>
<td>C6</td>
<td>固件链路看门狗 12s：服务端任何静默 &gt; 12s 就拆链</td>
<td>服务端保证任何一次静默 &lt; 12s（批内每 ~4s 回一次 ACK）</td>
</tr>
<tr>
<td>C7</td>
<td><strong>固定超时是错的</strong>：整屏帧光发送就要 11.3s，固定 2s 超时必然误判重传 → 死循环</td>
<td>超时按帧长自适应：<code>max(基础值, 字节数/(波特率/10) + 基础值)</code></td>
</tr>
<tr>
<td>C8</td>
<td><strong>掉一个字节就永久失联</strong>，除非有两套恢复机制：① <strong>空闲 100ms 丢半截帧</strong>（不丢的话之后每次重传的帧头都会被当 payload 吞掉 → 死锁）；② <strong>伪同步 22 字节重扫</strong>（帧头 CRC 不符时把已消费的字节重新扫一遍，救回被噪声偷走的真帧头）</td>
<td>两套都要。UART0 与启动日志共线，还要加抗噪（magic 重同步 + 双重 CRC + 整帧校验通过才 ACK）</td>
</tr>
</tbody>
</table>
<h3>D 服务端 / 部署层</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>坑</th>
<th>修法 / 判据</th>
</tr>
</thead>
<tbody>
<tr>
<td>D1</td>
<td><code>pkill -f epd_serve.py</code> 把<strong>自己所在的 shell 也杀了</strong>（cmdline 含同一串）→ 输出全空、进程「起不来」</td>
<td><code>[e]pd</code> 括号技巧，或 <code>ss -ltnp</code> 取 pid 再 <code>kill &lt;pid&gt;</code></td>
</tr>
<tr>
<td>D2</td>
<td>SSH 里 <code>nohup &amp;</code> 起的后台进程随会话死（systemd 用户 Linger=no）</td>
<td><strong><code>tmux new -d -s epd '… \| tee log'</code></strong> 分离常驻（后来迁到 systemd service，见第五节）</td>
</tr>
<tr>
<td>D3</td>
<td><strong>CPU 差分公式</strong>：busy ≠ 各列求和，而是 <code>total − (idle + iowait)</code>。写错时空载恒显 ~50%。判据：「每核全 50% 但 load 只有 0.1」的矛盾</td>
<td>busy = 总时间 − idle − iowait</td>
</tr>
<tr>
<td>D4</td>
<td>Arch 的串口组是 <strong><code>uucp</code></strong>（不是 Debian 的 <code>dialout</code>）</td>
<td><code>usermod -aG uucp &lt;user&gt;</code> 重登录</td>
</tr>
<tr>
<td>D5</td>
<td>开机 <code>WiFi.scanNetworks()</code> <strong>饿死 IDLE(core0) → <code>task_wdt</code> abort</strong> 重启循环</td>
<td>别开机扫描，直接 <code>WiFi.begin(ssid, key)</code></td>
</tr>
<tr>
<td>D6</td>
<td>「静默 80ms = 批末」的判定是错的——链路抖动被当批末 → 巨型 GC16 → 板子卡死在刷屏里</td>
<td><strong>显式发 PING 帧标记批末</strong>；兜底空闲阈值放宽到 1500ms</td>
</tr>
<tr>
<td>D7</td>
<td>清屏帧回<strong>两条</strong> ACK，没收干净后面所有 ACK 索引整体错位</td>
<td>逐条对齐收</td>
</tr>
<tr>
<td>D8</td>
<td><code>glob("/sys/class/drm/card[0-9]*")</code> 会把 <code>card0-DP-4</code> 也算进来 → 双卡只读到一张</td>
<td><code>re.fullmatch(r".*/card\d+", path)</code> 过滤</td>
</tr>
<tr>
<td>D9</td>
<td><strong>sysfs 的 <code>cardN</code> 顺序和 <code>rocm-smi</code> 的 <code>GPU[x]</code> 是反的</strong>（而且本机物理下层那张卡散热更好）。按 rocm-smi 序号写标签会把 GPU 标反</td>
<td>别抄 rocm-smi 的编号，先搞清映射</td>
</tr>
<tr>
<td>D10</td>
<td><strong>整屏 1MB 帧必被固件当「半帧」丢掉</strong>（<code>dropped a half frame</code>）</td>
<td>整屏重画切 64×64 tile 逐块发，固件自动合并成一次大刷新</td>
</tr>
</tbody>
</table>
<hr />
<h2>四、架构转向（真正的教训）</h2>
<p dir="auto">第一版是推<strong>像素</strong>的：服务端 PIL 渲染、差分成 64×64 tile 推过去，配 ARQ 补发、看门狗、批时序。能跑——直到斑马纹 bug 出现，我花了一整天追。</p>
<p dir="auto">排障过程中反复出现同一类症状：<strong>「主机和固件对同一状态的理解不一致」</strong>（谁定 GC16、ACK 收到第几条、什么算批末）。这是<strong>分布式状态一致性问题</strong>，说明职责切错了地方。</p>
<p dir="auto">于是我把整条像素通路砍了，切到<strong>终端模型</strong>：服务器发一行数据，ESP32 本地渲染、本地决策。</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>推像素（旧）</th>
<th>终端模型（现）</th>
</tr>
</thead>
<tbody>
<tr>
<td>链路负载</td>
<td>整帧 1MB / 差分几十 KB</td>
<td><strong>~100–250 B/s</strong>（一行 ASCII 键值）</td>
</tr>
<tr>
<td>渲染</td>
<td>服务端 PIL</td>
<td><strong>ESP32 本地</strong>（fill_rect + write_string——btop 看板就是矩形 + 字）</td>
</tr>
<tr>
<td>刷新决策</td>
<td>跨网络协调（flags/ARQ/看门狗/扫屏时序）</td>
<td><strong>纯固件本地</strong></td>
</tr>
<tr>
<td>改 UI</td>
<td>改 Python 就行</td>
<td>改 C++ 重烧（换稳定性的代价）</td>
</tr>
</tbody>
</table>
<p dir="auto">把<strong>控制面</strong>（刷新决策、状态同步）收到离屏最近的一端之后，网络上只剩<strong>数据面</strong>——复杂度按数量级下降。1MB 缓冲、ARQ、看门狗、扫屏切块、USB 掉线 workaround <strong>全都不存在了</strong>。</p>
<p dir="auto"><strong>沉淀出来的一条经验</strong>：当排障已经把故障面收窄到一小撮已知嫌疑时，<strong>估算「绕过这条路径」的成本 vs「复现→定位→修复」的成本</strong>——绕过去往往更快。这里绕过的成本 = 加一个帧类型 + 一个小渲染引擎 + 一个小采集器（约半天），比把像素通路查穿便宜，而且顺手消灭了一整类问题。</p>
<p dir="auto">同阶段还有两个值得带走的手法：</p>
<ul>
<li><strong>隔离自检（本地复现法）</strong>：做 5 个固件自检阶段（ST1~ST5），每条生产路径都在固件本地复现一遍，数据全本地生成（排除网络/协议/传输），一次烧录分辨全部嫌疑。每条自检只和上一条差一个变量。当「本地自绘」永远干净、只有「网络推送」出斑马时，故障面一下午就劈到网络数据面了。自检做成编译开关（<code>-DEPD_SELFTEST</code>）<strong>常驻代码</strong>——屏上再出任何异常，先跑一遍。</li>
<li><strong>远程判屏</strong>：人不在屏前的时候，手机拍照 → 二值化 → 按列算暗占比 → 跑<strong>游程 RLE</strong>：黑白块宽是 tile 尺寸的整数倍 ⇒ 差分/合并层的锅；不是 ⇒ 波形/面板层的锅。屏上留四角角标当坐标标尺；本机把<strong>发出去的 tile 解回像素量黑占比</strong>，判「发送端是否无辜」。</li>
</ul>
<hr />
<h2>五、不性感但致命的运维细节</h2>
<ul>
<li><strong>版本锁死写进文档</strong>。核心 2.0.14 + epdiy 2.0.0 不是随便能换的——升级前先确认 legacy I2C API 还在。</li>
<li><strong>自检代码是资产不是垃圾</strong>，留宏开关常驻。</li>
<li><strong>数据协议宁 ASCII 不二进制</strong>：一行可 grep、人眼可读、CRC 照挂的键值，在这个带宽下比紧凑二进制强——带宽预算宽裕时，<strong>可调试性 &gt; 字节数</strong>。</li>
<li><strong>服务端采集纯标准库</strong>：<code>/proc</code> + sysfs 不需要 psutil，scp 一个文件就能跑。</li>
<li><strong>服务端做开机自启</strong>：最后是 systemd unit（<code>Restart=always</code>）。实测服务器掉电重启后拿到的 IP 一晚上漂了三次，板子 15s 内自己连回。当前拓扑里板子先连到一台常开的笔记本（portproxy 转发到服务器），我计划把服务器网线直插路由器 LAN 口去掉这一跳。</li>
<li><strong>凭据放仓库外</strong>：WiFi 凭据放在 git/网盘之外的头文件，编译 <code>-I</code> 引入。</li>
<li><strong>GPU 数据全走 sysfs，不起子进程、不用 root</strong>：<code>gpu_busy_percent</code>、<code>mem_info_vram_used/total</code>、hwmon 温度/功耗/风扇/频率——比 shell 出 <code>rocm-smi</code> 轻得多。（AMD 是 rocm-smi/sysfs，不是 nvidia-smi。）</li>
</ul>
<hr />
<h2>六、下一步</h2>
<ul>
<li>GPU 信息再加细（per-process 级别，向 nvtop 看齐）；板子直连服务器（去掉转发这一跳），整系统收敛成两节点。</li>
<li>静态层缓存 + 元素级脏重绘：真要往 2s 节奏以上提刷新时再上（内存还有 ~5MB PSRAM、~13MB flash 富余，<strong>唯一潜在瓶颈是 CPU 时间</strong>——触发线没到之前不优化）。</li>
<li>长期跑下来如果 DU 残影累积明显，再调 GC16 频率或把灰度内容切 GL16。</li>
</ul>
<hr />
<h3>一句话总结</h3>
<blockquote>
<p dir="auto"><strong>做「显示终端」，就让它当终端：渲染和刷新决策放在离屏最近的地方，网络只走数据。</strong></p>
</blockquote>
<p dir="auto">有问题欢迎留言，帧协议和自检设计都可以聊。<strong>想看完整源码的（固件 + 协议 + 服务端 + 自检序列），评论区留言，我看到会发。</strong></p>
<hr />
</blockquote>
<p dir="auto">直接 github 上开源啊. 搞一个按钮可以在多台服务器之间 toggle. 这样一屏监控多机...</p>
]]></description><link>https://lcz.me/post/18126</link><guid isPermaLink="true">https://lcz.me/post/18126</guid><dc:creator><![CDATA[laobenxiong]]></dc:creator><pubDate>Mon, 14 Sep 2026 13:18:35 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Mon, 14 Sep 2026 06:21:07 GMT]]></title><description><![CDATA[<p dir="auto">再也过不上玩一天只要几块钱的日子了</p>
]]></description><link>https://lcz.me/post/18037</link><guid isPermaLink="true">https://lcz.me/post/18037</guid><dc:creator><![CDATA[张光璞]]></dc:creator><pubDate>Mon, 14 Sep 2026 06:21:07 GMT</pubDate></item><item><title><![CDATA[Reply to 今天服务器很不稳定，要么挂死，要么重启。用墨水屏写了一个看板 on Mon, 14 Sep 2026 05:27:19 GMT]]></title><description><![CDATA[<p dir="auto">梁文峰<img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/274c.png?v=ecb7c61779a" class="not-responsive emoji emoji-android emoji--x" style="height:23px;width:auto;vertical-align:middle" title=":x:" alt="❌" /><br />
梁文谷<img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/2714.png?v=ecb7c61779a" class="not-responsive emoji emoji-android emoji--heavy_check_mark" style="height:23px;width:auto;vertical-align:middle" title=":heavy_check_mark:" alt="✔" /></p>
]]></description><link>https://lcz.me/post/18028</link><guid isPermaLink="true">https://lcz.me/post/18028</guid><dc:creator><![CDATA[benton yi]]></dc:creator><pubDate>Mon, 14 Sep 2026 05:27:19 GMT</pubDate></item></channel></rss>