Claude Meter with XIAO ePaper DIY Kit (ESP32-S3) EE05
如果你手里有 Claude API 的额度,又觉得每次都要打开网页去看剩余用量太麻烦,那这个项目应该正合你意。我花了一个周末把 XIAO ePaper DIY Kit(EE05 套装)拼成了一台桌面 Claude Meter,一块 1.54 寸黑白电子墨水屏,随时显示 API 剩余额度、已使用量、限额和当天状态。它自己会隔一段时间去拉一次数据,平时就静静躺在桌上,不亮不闪不吵,非常适合放在显示器旁边当一种“安静的监控器”。
这个项目本质上是三件事的交叉:ESP32-S3 开发板的低功耗联网与驱动墨水屏、Claude API 端点的数据获取、以及电子墨水屏显示策略的调优。如果你熟悉 Arduino 的基本操作,哪怕不太熟悉 ESP32-S3 和 ePaper 的配合方式,跟着这篇文章走也能完成一套能用的桌面仪表盘。如果你完全没玩过墨水屏,那正好,我用这篇文章补上这个领域最容易踩坑的那部分经验。
1. 为什么要做一台 Claude Meter:桌面监控的刚需与选型思路
1.1 需求拆解:API 用量监控到底缺什么
Claude API 的用量信息在官方后台可以看到,但问题在于:后台页面的数据存在一定延迟,而且你得主动打开浏览器、登录、再点进对应的页面。对于经常写脚本、调接口的人来说,这种“需要主动去查”的方式很容易被忽略,往往是到了请求返回 429(速率限制)或者额度耗尽时才发现,这时候再调整节奏就已经晚了。
我当时的想法很简单:做一个东西放在桌面上,让它在视觉范围内被动地展示数据。人眼扫一眼就能知道“今天还剩多少量”,不需要任何主动查询动作。电子墨水屏是这个场景最合适的载体,因为它显示静态内容时完全不耗电,刷新频率低,也不会像 LCD 屏幕那样常亮造成光污染。
这个需求拆下来其实只有四层:
- 数据层:从 Claude API 的某个端点拿到额度、用量、剩余量这些信息。
- 传输层:ESP32-S3 通过 Wi-Fi 定期去获取最新数据。
- 显示层:墨水屏渲染出清晰的量化信息,比如剩余比例条、数字、状态标记。
- 供电层:能长时间离线运行,不依赖电源线。
1.2 硬件选型逻辑:为什么是 XIAO ESP32-S3 和 EE05 套装
市面上的开发板很多,墨水屏套件也不少,我选 XIAO ESP32-S3 + ePaper DIY Kit 有几个具体原因:
第一,主控芯片是 ESP32-S3,Wi-Fi 和 BLE 都有,跑 Arduino 生态非常顺,不需要外挂无线模块。这对一个需要联网的桌面仪表盘来说是刚需。ESP32 系列的低功耗模式也支持得比较好,可以做到测完一次数据后进入深度睡眠,一整天下来耗电量非常小。
第二,EE05 套装里配的扩展板和墨水屏是焊接好的,省去了自己飞线、焊排针的时间。扩展板上集成了 SEESAW 芯片,负责驱动墨水屏和读取电池电压,这意味着 ESP32-S3 不用直接和墨水屏的时序打交道,主机只需要通过 I2C 和 SEESAW 通信就行。这对开发效率的提升是巨大的,因为 ePaper 的驱动时序本身非常容易出问题,什么温度补偿、刷新帧率、LUT 表,让一个专门芯片去管,主控这边轻松很多。
第三,1.54 寸的黑白墨水屏分辨率是 152x152,显示一排数字和一个进度条绰绰有余。黑白屏刷新速度快于三色屏,残影也更少,数据看板用黑白完全够了。
1.3 整体架构与数据流的走向
整个系统的运行逻辑可以描述成这样:
Claude API 服务端 -> 代理层(可选的轻量服务) -> ESP32-S3 -> SEESAW -> 墨水屏理想情况下,ESP32-S3 直接请求 Claude API 也可以,但我不建议这么做。原因后面章节会展开讲。这里先给出我最终采用的方案:
- 在一台常开的小主机(或者在云函数平台)上跑一个非常轻的 Flask 服务,定时从 Claude API 拉取用量数据并缓存。
- ESP32-S3 每 15 分钟通过 HTTP GET 这个代理服务的接口,拿到一小段 JSON。
- 解析后驱动墨水屏渲染。
这种设计的核心考虑是:把复杂的事情都放在有完整网络栈和证书库的设备上,ESP32 这边只做最简单的事——拉取一段小文本,显示出来。
2. EE05 套件装配与开发环境准备:动手前的关键细节
2.1 包装里有什么
EE05 这个 DIY Kit 不是单纯的“开发板”,它是一个完整的电子墨水屏扩展套件。拆包后你会看到这些组件:
- XIAO ESP32-S3 主控板,板载 Wi-Fi/BLE,USB-C 接口。
- XIAO 扩展板(带 SEESAW),上面已经焊好了墨水屏排线座。
- 1.54 寸黑白电子墨水屏模块。
- 一根短的排线(连接扩展板和墨水屏)。
- 18650 电池座(部分版本有),用于离线供电。
安装流程基本是“物理插入”,不需要电烙铁。把 XIAO ESP32-S3 插到扩展板的母座上,再用排线把墨水屏接到扩展板,结构就完成了。整个套件是叠层设计,组装完厚度大概和一个硬币差不多,适合放桌上。
2.2 安装开发环境与驱动库
软件部分我用的 Arduino IDE 2.x。ESP32-S3 的板级支持包需要先安装好,这个在 Arduino IDE 的“开发板管理器”里搜索esp32就能找到。安装完成后选择开发板型号为XIAO_ESP32S3(不同版本的板级包命名可能略有差异,选带 XIAO ESP32S3 字样的即可)。
接下来是库依赖,这里我整理一份清单:
| 库名 | 用途 | 安装方式 |
|---|---|---|
| Adafruit seesaw | 通过 I2C 控制 SEESAW 芯片,驱动墨水屏和 ADC | 库管理器搜索安装 |
| Adafruit EPD | 墨水屏显示控制 | 库管理器安装 |
| ArduinoJson | 解析代理服务返回的 JSON 数据 | 库管理器安装 |
| WiFiClientSecure | 走 HTTPS 请求时需要 | 板级支持包自带 |
安装完这些库后,先跑一个最简单的扫描 I2C 地址的程序,确认 ESP32-S3 能和扩展板通信上。这个步骤很容易被跳过,但跳过之后如果后面显示不出来,你根本分不清是排线松了还是地址不对。
#include <Wire.h> void setup() { Serial.begin(115200); Wire.begin(); for (uint8_t addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.printf("Found I2C device at 0x%02X\n", addr); } } } void loop() { delay(1000); }正常情况下,SEESAW 的地址是 0x49。如果扫描不到,大概率是排线方向插反了,或者扩展板没接触好。
2.3 SEESAW 驱动墨水屏和电压读取的基本方式
SEESAW 芯片在这里扮演一个“翻译官”的角色。ESP32-S3 不需要直接去操作墨水屏的每个像素时序,只需要告诉 SEESAW“我要在 (x,y) 画一个点”或者“刷新屏幕”就行了。
具体使用方式大致如下:
#include "Adafruit_seesaw.h" #include "Adafruit_EPD.h" Adafruit_seesaw ss; Adafruit_EPD_154 epd(152, 152, &ss, 15);这里的Adafruit_EPD_154是墨水屏驱动对象,传入的&ss让它知道底层的传输通道是 SEESAW,而不是直接的 SPI 引脚。这个封装让上层代码非常干净,画图、写文字、局部刷新都变得非常直观。
SEESAW 还自带了 ADC 通道,可以读取电池电压。XIAO 扩展板上有分压电路,你可以直接通过ss.analogRead()拿到原始值,然后根据分压比例换算成电池电压,再映射成电量百分比。这个功能非常重要——桌面仪表盘如果哪天突然不显示了,多半不是程序问题,而是电池没电了,但因为墨水屏断电后仍然保持最后画面,你根本看不出来它已经“死”了。
3. Claude API 的数据端设计:为什么要加一个【代理层】而不是直接请求
3.1 直接让 ESP32 请求 Claude API 的四个问题
一开始我以为最简单的方案是在 ESP32-S3 里直接调用 Claude API,毕竟只是 GET 一下限额数据,看起来并不复杂。但在实际操作中发现这条路是不好走的,主要有四个原因:
第一,密钥安全。Claude API 的密钥相当于你的钱包钥匙,把它烧录进 ESP32 的固件里,意味着任何拿到这个固件的人都能通过串口导出二进制,从而提取到密钥。桌面设备不同于服务器,它可能被带到各种场合,风险偏高。即使只在你自己桌上放,固件备份、日志输出等场景都有泄露风险。
第二,TLS 握手的内存负担。Claude API 强制 HTTPS,ESP32-S3 虽然能跑 TLS,但握手过程需要占用不少内存。如果你的程序里同时缓存了大字符串、加载了字体数据、还留着 JSON 解析缓冲,内存余量会非常紧张,容易出现随机重启或莫名的段错误。
第三,CORS 和数据格式的频繁变化。Claude API 的头部要求(如x-api-key、anthropic-version)是固定的,但数据字段在不同阶段可能有调整。如果你用的是一个代理层,这些变化只需要在服务器端适配一次,ESP32 端永远只消费固定的 JSON 格式,哪怕上游字段变了,代理层改一下就完事。
第四,请求频率控制。Claude API 对请求频率有限制,如果你把刷新间隔调成 5 分钟一次,一天就是 288 次请求,虽然不是特别夸张,但完全没必要。加一个代理层来缓存结果,ESP32 的请求可以先打到代理层,代理层只需要每 10 分钟去上游拉一次,整体请求次数大幅度减少。
3.2 代理服务端代码:能跑就行,能缓存就行
代理层我用的是 Flask,跑在一个常开的 Python 环境里。启动后监听局域网端口,ESP32 通过 WiFi 访问它。这个代理服务做的事很简单:
- 定时从 Claude API 获取用量信息。
- 把 JSON 存到内存里。
- ESP32 访问时直接返回缓存的 JSON。
核心代码可以压缩到很短:
import time import requests from flask import Flask, jsonify app = Flask(__name__) cache = {"data": None, "updated_at": 0} INTERVAL = 600 # 每 10 分钟刷新一次 API_KEY = "你的Claude API密钥" ANTHROPIC_VERSION = "2023-06-01" def fetch_usage(): headers = { "x-api-key": API_KEY, "anthropic-version": ANTHROPIC_VERSION, } url = "https://api.anthropic.com/v1/organizations/usage" # 根据实际端点调整 resp = requests.get(url, headers=headers, timeout=15) if resp.status_code == 200: return resp.json() return None @app.route("/meter") def meter(): now = time.time() if cache["data"] is None or (now - cache["updated_at"]) > INTERVAL: data = fetch_usage() if data: cache["data"] = data cache["updated_at"] = now return jsonify(cache["data"]) if __name__ == "__main__": app.run(host="0.0.0.0", port=8080)这里有几个细节值得说明。fetch_usage()里的端点路径只是示例,实际上 Claude API 的限额和用量接口在不同时间段可能有不同的入口,重要的是把握两个思路:一是透过官方 API 文档找到能直接返回“用量/限额”的可用端点,二是如果你找不到现成的用量端点,可以退而求其次用请求后返回的响应头里的anthropic-ratelimit-*字段来拼凑数据。响应头里一般包含anthropic-ratelimit-requests-limit、anthropic-ratelimit-requests-remaining、anthropic-ratelimit-tokens-limit等字段,这些数字也能支撑一台 meter 的显示。
3.3 ESP32-S3 侧的 JSON 拉取与解析
ESP32 侧我直接用HTTPClient库发起 GET 请求,拿到响应后扔给ArduinoJson解析。为了省内存,我使用静态分配的文档模型,而不是动态占比很大的那种解析方式。
#include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> const char* wifi_ssid = "你的WiFi"; const char* wifi_pass = "你的密码"; const char* proxy_url = "http://192.168.1.100:8080/meter"; void fetchAndDisplay() { HTTPClient http; http.begin(proxy_url); http.setConnectTimeout(5000); http.setTimeout(10000); int code = http.GET(); if (code != 200) { Serial.printf("HTTP error: %d\n", code); http.end(); return; } String payload = http.getString(); http.end(); JsonDocument doc; DeserializationError err = deserializeJson(doc, payload); if (err) { Serial.println("JSON parse failed"); return; } float limit = doc["limit"] | 0.0f; float used = doc["used"] | 0.0f; float remaining = limit - used; if (remaining < 0) remaining = 0; int ratio = (limit > 0) ? (int)(remaining / limit * 100) : 0; Serial.printf("Used: %.2f, Limit: %.2f, Remaining: %.2f (%d%%)\n", used, limit, remaining, ratio); // 这里将数值交给显示函数 renderMeter(used, limit, remaining, ratio); }解析过程比较简单,但要特别注意:代理层返回的字段名必须固定,比如limit、used。不要直接透传上游 API 的字段名,因为上游改字段名的频率比想象中高。代理层做一次“翻译”,把上游字段统一成这台 meter 认识的固定 schema,后续维护几乎零成本。
4. 墨水屏显示与刷新策略:不闪、不卡、续航久的调校心得
4.1 电子墨水屏初体验:为什么第一次全屏刷新会闪得厉害
如果你的墨水屏上手后第一次全屏刷新,你可能会被吓一跳——整个屏幕先变成全黑,再变成全白,再显示内容,这个过程大概持续一秒钟左右,视觉上非常“闪”。这不是故障,而是电子墨水屏的工作方式决定的。墨水屏的每个像素是带正负电荷的黑色粒子与白色粒子,刷新时需要让所有粒子先归位到一种初始状态,再重新排列成目标图案,所以会经历多次“全体翻转”。
从显示效果角度,这个闪烁隔几分钟来一次还可以接受,但如果每 15 分钟就闪一下,放桌面上就像是给旁边的人不停眨眼,反而不安静了。处理方式有两种。
第一种是采用局部刷新模式。对于墨水屏来说,局部刷新只更新变化的那一小块区域,其他区域保持原有画面。坏处是长时间局部刷新会导致残影积累,本来应该显示白色的地方慢慢变成灰色。第二种是设定合理的全量刷新节奏:大部分时间用局部刷新更新数字和进度条,每隔两小时左右才做一次全局清屏,清掉累积残影。
实际体验下来,最舒服的策略是:每次数据更新只重绘数值区和进度条,保持背景和边框不动;每 10 次更新后做一次全屏刷新,把残影洗净。这种策略下,屏幕看起来几乎是“悄悄变化”的,安安静静地就把数字换了。
4.2 布局设计与渲染代码
1.54 寸、152x152 的屏幕,放下一套数据看板是完全够的。我自己设计的布局分成三块区域:
- 顶部区域:标题
CLAUDE METER加当前电量图标。 - 中间区域:剩余额度占比的百分比数字(大字号,通常 48px 以上)。
- 下方区域:一个横向进度条和“已用/限额”的小字标注。
墨水屏是黑白两色的,想做层次感只有一种工具:反色。我通常把底色设为白、文字设为黑,进度条用黑底白字来凸显填充效果。渲染代码大致如下:
#include "Adafruit_EPD.h" Adafruit_EPD_154 epd(152, 152, &ss, 15); void renderMeter(float used, float limit, float remaining, int ratio) { epd.begin(); epd.clearBuffer(); // 标题 epd.setTextSize(2); epd.setCursor(20, 10); epd.print("CLAUDE"); epd.setCursor(20, 34); epd.print("METER"); // 剩余百分比 epd.setTextSize(6); epd.setCursor(24, 70); epd.print(ratio); epd.setTextSize(2); epd.setCursor(100, 78); epd.print("%"); // 进度条 int barX = 14; int barY = 110; int barW = 124; int barH = 14; int fillW = (int)(barW * ratio / 100.0f); epd.fillRect(barX, barY, fillW, barH, EPD_BLACK); epd.drawRect(barX, barY, barW, barH, EPD_BLACK); // 小字标注 epd.setTextSize(1); epd.setCursor(14, 130); epd.printf("USED %.1f / LIMIT %.1f", used, limit); epd.display(); }这里 EPD 库的fillRect、drawRect、print等接口和常规图形库类似,学过 TFT 的都不陌生。唯一的差别在于:display()这行才会真正把缓冲区推给墨水屏,之前的操作都是画在内存缓冲区里的。
4.3 局部刷新与全局刷新的取舍参数
Adafruit EPD 库对 EPD 1.54 提供了两种刷新模式:全屏刷新和部分刷新。部分刷新的调用方式通常是在display()之前设定一个刷新窗口,或者使用库中提供的setPartialWindow()方法。
我踩过的一个坑是:部分刷新窗口的位置必须和实际修改的像素区域完全一致,否则会出现画面残影错位。如果只想更新百分比数字这一小块,就要精确地把窗口设到数字所在的行列范围,而不是图省事把整个上半屏都设为局部刷新窗口。
另外一个参数是刷新频率。我的最终配置是:
- 常规更新:15 分钟一次局部刷新,只刷数字和进度条。
- 深度清理:每 2 小时一次全局刷新。
- 夜间(22:00-08:00):不刷新,让 ESP32 直接睡眠,等第二天早上再唤醒。
这样一天大概只有 40 次左右的局部刷新和 4 次全局刷新,对墨水屏颗粒寿命影响很小,电池也能坚持很久。
4.4 低功耗策略:深度睡眠与唤醒计划
ESP32-S3 最棒的一点是深度睡眠模式下电流可以低到几十微安级别。我的运行逻辑是:
void setup() { // 初始化、联网、拉数据、渲染 fetchAndDisplay(); // 进入深度睡眠 esp_sleep_enable_timer_wakeup(15 * 60 * 1000000ULL); esp_deep_sleep_start(); } void loop() { // 不会到达这里 }是的,整个程序就是“跑一次就睡”,没有loop()的持续轮询。每次醒来后,ESP32 重新启动、连接 Wi-Fi、拉取数据、更新屏幕,然后继续睡。这种模式比在loop()里写delay()要省电得多,原因是 CPU 在整个睡眠期间都处于空闲状态。
实测下来,一块 2000mAh 的电池,在这种节奏下能用两周左右。如果你把刷新间隔拉到 30 分钟,甚至能用一个月,基本达到“充电一次忘掉一个月”的效果。我第一次测到两周没掉电的时候,意识到深度睡眠 + 墨水屏的组合确实是小功耗设备的最优解。
5. 实际运行中的问题与调优记录
5.1 代理层偶尔假死:ESP32 侧的重试与超时设计
跑了几周后,我发现一个典型问题:代理服务偶尔会假死,原因大多是上游 API 超时或者网络抖动,Flask 进程卡在requests.get,既不返回也不报错。这时 ESP32 请求代理层就会出现连接超时,屏幕上保留的还是上一次的数据——这其实不算坏事,因为墨水屏显示静态内容,不会出现“显示一半卡住”的情况。
但性子急的我不能接受。我做了两处改进。第一处是在 Flask 的请求上加更严格超时:requests.get(..., timeout=(3, 5)),连接超时 3 秒,读超时 5 秒。第二处是 ESP32 侧的回退机制——如果第一次请求代理失败,等待 30 秒再请求一次;如果两次都失败,就什么都不渲染,直接回睡眠,等下个周期再试。这样避免了连续失败时的“抖动风暴”,也保证不会阻塞电池寿命。
5.2 字体太小,墨水屏上看不清:调整字号与排版密度
1.54 寸屏幕如果放太多信息,很容易变成“只有贴近看才看得清”的状态。这个尺寸只有 152x152 像素,如果小字放得太多,像素字号只有 1 的时候,实际看起来像一堆毛边字,近看费劲,远看更是完全不行。我的调整是:核心信息只有“剩余百分比”和“进度条”两样,其他统统列为次要信息,小字只显示一行USED 12.3 / LIMIT 100.0,如果剩余额度太低,再来一行警告文字。宁缺毋滥,这是桌面仪表盘的设计原则。
5.3 夜间不显示的问题:无感静默设计
夜间刷新其实是纯浪费,因为没人盯着屏幕看。但如果你不加限制,ESP32 半夜还是会按时唤醒、联网、拉数据、刷新屏幕,白白消耗电量。深度睡眠的定时唤醒是可以预先判断时间的:
struct tm tminfo; getLocalTime(&tminfo, 0); if (tminfo.tm_hour >= 22 || tminfo.tm_hour < 8) { esp_sleep_enable_timer_wakeup(6 * 60 * 60 * 1000000ULL); // 睡 6 小时 esp_deep_sleep_start(); }这个逻辑让设备在夜间直接躺平,白天才干活。而且一旦早上醒来,屏幕上会自动显示最新数据,不会有“半夜更新了但没人看”的浪费。实测下来,这个改动让续航直接翻了近一倍。
6. 如果重做一次,我会改什么
这个项目整体很满意,但如果再动手做一遍,有三个地方我会调整。
第一个是显示内容。当前偏重“剩余额度”,但实际使用中我发现自己更想去关注“这周每天的消耗趋势”。墨水屏做趋势图其实可行,152x152 的像素画一条 7 个点的折线完全没问题。下次我会在代理层多缓存几天的用量数据,ESP32 每次拿到后画一个小型的柱状图或折线图。
第二个是外壳。EE05 套件是裸板,直接放桌面上虽然不影响功能,但长期看容易积灰。我会设计一个 3D 打印的斜支架外壳,让屏幕角度更直观,顺便遮住背面的芯片,看起来像是一个完整的产品而非开发板搭建。
第三个是显示亮度。黑白墨水屏的对比度在光线充足的条件下很棒,但在昏暗环境里可读性差。接下来我想加一个微型 LED 背光或者干脆在座子旁放一个 USB 小灯,照亮屏幕,让它全天候可读。这个属于锦上添花,不影响核心功能,但对于桌面体验的提升是实打实的。
最后说一个经验:做这种小型 DIY 看板,最重要的不是代码多漂亮,而是把“不打扰”这件事做好。墨水屏最大的优势就是静态显示不耗电、不发光,你要顺着它的特性去设计刷新策略,而不是试图让它像手机屏幕一样动不动就闪。跑通数据链路只是第一步,把刷新节奏调到让人几乎感觉不到它的存在,这台 meter 才算真正做好了。