1. 项目缘起当网络“清道夫”遇上电子墨水屏如果你和我一样家里部署了Pi-hole来拦截广告和追踪器享受清爽的网络环境那么你大概率也会遇到一个“幸福的烦恼”如何直观地、优雅地、低功耗地查看它的工作状态每天拦截了多少请求哪些域名是“黑名单常客”网络延迟怎么样把这些关键数据摆在桌面上不仅是一种极客的仪式感更是快速诊断网络问题的利器。传统的方案无外乎几种在浏览器里打开Pi-hole的管理后台网页或者SSH登录到树莓派上用命令行查看。前者需要打开电脑或手机后者不够直观。直到我看到了角落里吃灰的一块电子墨水屏E-ink Display一个绝妙的想法诞生了为何不把Pi-hole的实时数据“画”在这块屏幕上电子墨水屏的特性——超低功耗、类纸质感、静态显示不伤眼——让它成为显示这类不频繁更新的状态信息的绝佳载体。它就像一块永不断电的“数字仪表盘”安静地立在桌角随时告诉你家庭网络的“健康”状况。这个项目我称之为“Pi-hole e-ink Dashboard”。它的核心目标很简单利用一块廉价的电子墨水屏比如Waveshare的2.13英寸或7.5英寸型号通过Python脚本定时从Pi-hole的API获取数据经过处理和排版后渲染到屏幕上。整个过程完全自动化屏幕每隔几分钟刷新一次你得到的就是一个专属的、实体的网络数据监视器。2. 硬件选型与连接找到那块对的“画布”电子墨水屏种类繁多尺寸、分辨率、颜色黑白/三色、驱动接口各不相同。选择哪一款直接决定了项目的复杂度、显示效果和最终成本。经过一番对比和实测我推荐以下几款它们各有优劣你可以根据预算和需求选择。2.1 入门之选Waveshare 2.13英寸 V3 (250x122)这款屏幕堪称电子墨水屏界的“Hello World”。它价格极其低廉分辨率尚可驱动简单。其接口通常是SPI这意味着你只需要连接树莓派或运行Pi-hole的设备上的少数几根GPIO针脚即可。对于只想显示核心数据如总请求数、拦截数、拦截百分比的极简主义者来说它完全够用。缺点是屏幕较小能显示的信息有限刷新时有明显的全屏闪烁这是电子墨水屏的物理特性。2.2 平衡之选Waveshare 7.5英寸 V2 (800x480)这是我最终选择的型号。7.5英寸的尺寸足够大可以清晰地展示多组数据甚至能画出简单的趋势图表。800x480的分辨率让字体渲染更加细腻。它同样采用SPI接口驱动库成熟。更大的屏幕带来了更长的刷新时间约4-6秒但对于几分钟更新一次的状态屏来说完全可以接受。它的功耗依然极低静态显示时几乎为零。2.3 进阶之选Inky Impression (7.3英寸, 7色)如果你追求色彩和更好的视觉效果可以关注Pimoroni的Inky Impression系列。它能显示7种颜色视觉效果更接近印刷品。其驱动库对Python支持非常友好提供了高级的绘图函数。当然价格也水涨船高。需要注意的是彩色电子墨水屏的刷新时间通常更长。2.4 硬件连接实战以Waveshare 7.5英寸 V2 树莓派为例连接本身并不复杂但引脚对接必须准确。以下是接线表屏幕引脚树莓派 GPIO (物理引脚号)功能说明VCC3.3V (Pin 1)电源正极务必接3.3V接5V会烧屏GNDGND (Pin 6)电源地线DINMOSI (GPIO10, Pin 19)SPI数据输入CLKSCLK (GPIO11, Pin 23)SPI时钟信号CSCE0 (GPIO8, Pin 24)SPI片选0DCGPIO25 (Pin 22)数据/命令选择RSTGPIO17 (Pin 11)复位引脚BUSYGPIO24 (Pin 18)忙状态指示注意不同型号的屏幕引脚定义可能略有不同务必以屏幕附带的说明书为准。连接前请确保树莓派已关机。连接好后开机进入系统。首先需要启用SPI接口。在终端执行sudo raspi-config选择Interface Options-SPI-Yes启用它。重启后可以通过ls /dev/spi*命令检查是否出现了/dev/spidev0.0等设备文件有则说明SPI驱动加载成功。3. 软件环境搭建从驱动到绘图库硬件就绪后我们需要在软件层面搭建起从数据获取到屏幕渲染的完整管道。这个过程主要分为三步安装屏幕驱动库、安装HTTP请求和数据处理库、准备Pi-hole的API访问凭证。3.1 安装电子墨水屏驱动库Waveshare为其大部分屏幕提供了完善的Python库。我们可以通过pip直接安装。首先更新pip并安装必要的系统依赖sudo apt update sudo apt install python3-pip python3-pil python3-numpy pip3 install --upgrade pip然后安装Waveshare的驱动库。对于7.5英寸V2屏幕对应的库是waveshare-epdpip3 install waveshare-epd这个库包含了epd7in5_V2这个类我们将用它来控制屏幕。如果你用的是其他型号请在Waveshare的官方Wiki上找到对应的库名和示例代码。3.2 安装数据处理与网络请求库我们需要requests库来调用Pi-hole的API用Pillow(PIL) 库来生成图像。它们通常已经随驱动库一起安装了但为了保险起见可以显式安装pip3 install requests Pillow3.3 获取Pi-hole API令牌Pi-hole的管理后台提供了丰富的API。为了安全地访问我们需要一个API令牌。登录你的Pi-hole管理后台通常是http://pi.hole/admin。在左侧菜单栏找到Settings。切换到API / Web interface标签页。在API子标签下你会看到Show API token按钮点击它并复制显示出来的长字符串。这个令牌相当于密码请妥善保管不要泄露。有了令牌我们就可以通过向http://pi.hole/admin/api.php发送GET请求来获取数据了。例如获取摘要信息的API调用是http://pi.hole/admin/api.php?summaryauth你的API令牌。4. 核心脚本剖析数据获取、处理与渲染一切准备就绪现在我们来编写核心的Python脚本。这个脚本将周期性执行完成“获取数据 - 处理数据 - 生成图片 - 刷新屏幕”的完整流程。我将分模块详细解释。4.1 脚本框架与导入首先创建一个新文件比如pihole_dashboard.py。开头导入所有必需的库。#!/usr/bin/env python3 # -*- coding: utf-8 -*- import time import json import requests from PIL import Image, ImageDraw, ImageFont from waveshare_epd import epd7in5_V2 import logging import sys4.2 配置常量与初始化这里定义所有可配置的变量方便日后修改。最重要的是你的Pi-hole地址和API令牌。# 配置区域 PIHOLE_HOST http://192.168.1.100 # 你的Pi-hole地址 API_TOKEN YOUR_API_TOKEN_HERE # 替换为你的真实令牌 REFRESH_INTERVAL 300 # 刷新间隔单位秒300秒5分钟 # 屏幕尺寸 EPD_WIDTH 800 EPD_HEIGHT 480 # 字体路径 (树莓派上可用的字体) FONT_PATH_LARGE /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf FONT_PATH_MEDIUM /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf FONT_PATH_SMALL /usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf # 初始化日志 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s: %(message)s)注意将PIHOLE_HOST和API_TOKEN替换成你自己的。字体路径可能需要根据你的系统进行调整可以使用fc-list命令查看可用字体。4.3 数据获取函数这个函数负责调用Pi-hole的API并处理可能出现的网络错误。def fetch_pihole_data(): 从Pi-hole获取摘要数据 url f{PIHOLE_HOST}/admin/api.php?summaryauth{API_TOKEN} try: response requests.get(url, timeout10) response.raise_for_status() # 如果状态码不是200抛出异常 data response.json() # 检查API返回是否成功 if data.get(status) ! enabled: logging.warning(Pi-hole API returned non-enabled status.) return data except requests.exceptions.RequestException as e: logging.error(fFailed to fetch data from Pi-hole: {e}) return None except json.JSONDecodeError as e: logging.error(fFailed to parse JSON response: {e}) return None4.4 数据解析与格式化API返回的JSON数据包含很多字段我们需要从中提取关键信息并格式化成适合显示的字符串。def parse_and_format_data(api_data): 解析API数据返回格式化后的字典 if not api_data: return {error: No data available} stats {} try: # 核心指标 stats[domains_blocked] f{int(api_data.get(domains_being_blocked, 0)):,} stats[dns_queries] f{int(api_data.get(dns_queries_today, 0)):,} stats[ads_blocked] f{int(api_data.get(ads_blocked_today, 0)):,} stats[ads_percentage] f{float(api_data.get(ads_percentage_today, 0.0)):.1f}% # 查询类型分布 (简化示例) query_types api_data.get(querytypes, {}) stats[query_a] query_types.get(A, 0) stats[query_aaaa] query_types.get(AAAA, 0) # 客户端信息 (前5名) top_clients api_data.get(top_sources, []) stats[top_clients] [] for i, client in enumerate(top_clients[:5]): # client格式: [192.168.1.10, 1234] stats[top_clients].append(f{client[0]}: {client[1]}) # 状态 stats[status] api_data.get(status, unknown) stats[gravity_last_updated] api_data.get(gravity_last_updated, {}).get(relative, {}).get(days, N/A) except (KeyError, TypeError, ValueError) as e: logging.error(fError parsing API data: {e}) stats[error] fParse error: {e} return stats4.5 图像生成函数这是项目的核心视觉部分。我们使用Pillow库在内存中创建一张和屏幕分辨率一致的图像然后在上面“作画”。def create_dashboard_image(stats): 根据统计数据生成PIL图像 # 创建一张新的黑白图像模式为1 (1位像素黑白) image Image.new(1, (EPD_WIDTH, EPD_HEIGHT), 255) # 255为白色 draw ImageDraw.Draw(image) # 加载字体 (指定大小) try: font_large ImageFont.truetype(FONT_PATH_LARGE, 42) font_medium ImageFont.truetype(FONT_PATH_MEDIUM, 28) font_small ImageFont.truetype(FONT_PATH_SMALL, 22) font_mono ImageFont.truetype(FONT_PATH_SMALL, 18) except IOError: logging.warning(Specified fonts not found, using default bitmap font.) font_large ImageFont.load_default() font_medium ImageFont.load_default() font_small ImageFont.load_default() font_mono ImageFont.load_default() # 绘制标题 title Pi-hole Network Dashboard title_bbox draw.textbbox((0, 0), title, fontfont_large) title_width title_bbox[2] - title_bbox[0] draw.text(((EPD_WIDTH - title_width) // 2, 10), title, fontfont_large, fill0) # 绘制分隔线 draw.line([(20, 70), (EPD_WIDTH - 20, 70)], fill0, width2) y_offset 90 # 起始Y坐标 # 绘制核心数据块 if error in stats: draw.text((50, y_offset), fError: {stats[error]}, fontfont_medium, fill0) return image # 第一行总览 overview_text fQueries Today: {stats[dns_queries]} | Blocked: {stats[ads_blocked]} ({stats[ads_percentage]}) draw.text((50, y_offset), overview_text, fontfont_medium, fill0) y_offset 50 # 第二行拦截域名数 blocked_text fDomains in Blocklist: {stats[domains_blocked]} draw.text((50, y_offset), blocked_text, fontfont_medium, fill0) y_offset 50 # 绘制另一个分隔线 draw.line([(20, y_offset), (EPD_WIDTH - 20, y_offset)], fill0, width1) y_offset 30 # 左侧查询类型 draw.text((50, y_offset), Query Types:, fontfont_small, fill0) draw.text((50, y_offset35), fA: {stats.get(query_a, N/A)}, fontfont_mono, fill0) draw.text((50, y_offset60), fAAAA: {stats.get(query_aaaa, N/A)}, fontfont_mono, fill0) # 右侧Top Clients draw.text((EPD_WIDTH // 2, y_offset), Top Clients:, fontfont_small, fill0) client_y y_offset 35 for client in stats.get(top_clients, [])[:5]: draw.text((EPD_WIDTH // 2, client_y), client, fontfont_mono, fill0) client_y 25 y_offset max(y_offset100, client_y) 20 # 底部状态信息 status_line fStatus: {stats[status]} | Gravity Updated: {stats[gravity_last_updated]} days ago | {time.strftime(%Y-%m-%d %H:%M)} draw.text((EPD_WIDTH // 2 - draw.textbbox((0,0), status_line, fontfont_mono)[2] // 2, EPD_HEIGHT - 40), status_line, fontfont_mono, fill0) return image4.6 主循环与屏幕控制最后我们将所有部分串联起来并加入屏幕初始化和刷新逻辑。def main(): logging.info(Pi-hole E-ink Dashboard started) epd None try: # 初始化屏幕 epd epd7in5_V2.EPD() epd.init() epd.Clear() # 首次清屏 logging.info(E-Paper initialized and cleared.) last_update_time 0 while True: current_time time.time() # 达到刷新间隔或首次运行 if current_time - last_update_time REFRESH_INTERVAL: logging.info(Fetching new data...) # 1. 获取数据 api_data fetch_pihole_data() # 2. 解析数据 formatted_stats parse_and_format_data(api_data) # 3. 生成图像 dashboard_image create_dashboard_image(formatted_stats) # 4. 显示图像 # 注意为了减少残影电子墨水屏刷新通常需要先显示全白再显示新内容 # 但waveshare库的display方法内部通常已处理此流程 epd.display(epd.getbuffer(dashboard_image)) logging.info(Dashboard updated on E-Paper.) last_update_time current_time # 休眠一段时间避免CPU空转 time.sleep(30) # 每30秒检查一次是否该更新了 except KeyboardInterrupt: logging.info(Program interrupted by user.) except Exception as e: logging.exception(fAn unexpected error occurred: {e}) finally: # 程序退出时让屏幕进入睡眠模式以省电并显示再见信息 if epd is not None: try: sleep_image Image.new(1, (EPD_WIDTH, EPD_HEIGHT), 255) draw ImageDraw.Draw(sleep_image) font ImageFont.load_default() draw.text((100, EPD_HEIGHT//2), Dashboard Offline, fontfont, fill0) epd.display(epd.getbuffer(sleep_image)) epd.sleep() # 进入睡眠模式 logging.info(E-Paper put to sleep.) except Exception as e: logging.error(fFailed to put E-Paper to sleep: {e}) if __name__ __main__: main()5. 部署、优化与避坑指南脚本写好了但让它稳定、可靠、美观地运行起来还需要一些额外的步骤和技巧。这里分享我踩过的一些坑和优化方案。5.1 系统服务化让脚本开机自启我们肯定不希望每次重启树莓派都手动去运行这个Python脚本。最好的方法是将其注册为一个系统服务。创建一个服务文件sudo nano /etc/systemd/system/pihole-dashboard.service写入以下内容请根据你的实际路径修改WorkingDirectory和ExecStart[Unit] DescriptionPi-hole E-ink Dashboard Service Afternetwork.target multi-user.target Wantsnetwork.target [Service] Typesimple Userpi WorkingDirectory/home/pi/pihole_dashboard # 你的脚本所在目录 ExecStart/usr/bin/python3 /home/pi/pihole_dashboard/pihole_dashboard.py Restarton-failure RestartSec10 StandardOutputsyslog StandardErrorsyslog SyslogIdentifierpihole-dashboard [Install] WantedBymulti-user.target保存退出后启用并启动服务sudo systemctl daemon-reload sudo systemctl enable pihole-dashboard.service sudo systemctl start pihole-dashboard.service现在你的仪表盘就会在系统启动时自动运行了。你可以使用sudo systemctl status pihole-dashboard.service来查看运行状态和日志。5.2 解决电子墨水屏的“残影”与“鬼影”问题电子墨水屏在长时间显示静态图像后切换新画面时可能会留下上一幅画面的浅色痕迹这就是“残影”。这是其物理特性导致的但可以通过软件策略缓解。全屏刷新Full Refresh vs 局部刷新Partial Refresh大多数驱动库提供两种刷新模式。局部刷新快、无闪烁但长期使用会积累残影。全屏刷新慢、有闪烁但能彻底清除残影。我的策略在脚本中混合使用。例如每5次局部刷新后强制进行一次全屏刷新。Waveshare库中epd.init()默认可能是局部刷新模式可以查阅文档看是否有epd.init(full_updateTrue)这样的全刷初始化方法或者在循环中定期调用epd.Clear()后再显示。图像反色在生成图像时偶尔比如每10次将背景色和前景色对调黑白反转显示一次也有助于减轻残影。这可以通过在create_dashboard_image函数中动态决定fill参数是0黑还是255白来实现。5.3 界面布局与美化的进阶技巧最初的文本堆砌可能比较单调。我们可以利用Pillow库做出更专业的仪表盘。使用图标寻找或自己绘制一套简洁的黑白图标PNG格式1位色深用Image.open()加载并粘贴到画布上可以让“请求数”、“拦截数”等指标更直观。绘制进度条用draw.rectangle()函数可以轻松画出进度条直观显示拦截百分比。计算好矩形框的起始和结束坐标即可。绘制简单图表虽然复杂图表不适合但简单的柱状图或趋势线用draw.line()连接历史数据点是可以实现的。你需要将最近一段时间比如24小时的数据存储下来可以写入一个简单的JSON文件然后在每次刷新时绘制。字体渲染优化电子墨水屏分辨率有限抗锯齿效果不好。使用等宽字体如DejaVuSansMono显示数据对齐更整齐。避免使用过小的字号建议不小于16像素。5.4 网络与API调用的稳定性处理脚本需要长期无人值守运行网络波动或Pi-hole服务重启都可能导致API调用失败。增加重试机制在fetch_pihole_data函数中可以使用retrying库或简单的for循环实现有限次数的重试。设置超时requests.get(timeout10)中的超时设置非常重要避免脚本因网络无响应而长期挂起。异常降级显示当无法获取数据时create_dashboard_image函数应该显示一个有意义的错误信息如“Network Error”或“Pi-hole Unreachable”并附带上次成功更新的时间戳而不是显示空白或崩溃。缓存上一次成功的数据可以将最后一次成功获取的数据写入文件。当本次获取失败时使用缓存的数据进行显示并在界面上标注“数据可能不是最新的”。5.5 功耗与性能考量虽然电子墨水屏本身功耗极低但树莓派和持续运行的脚本仍会消耗电量。优化刷新间隔状态信息不需要秒级更新。将REFRESH_INTERVAL设置为300秒5分钟甚至600秒10分钟是完全合理的这能显著减少屏幕刷新和CPU活动。使用time.sleep()在主循环中使用time.sleep(30)让脚本在检查间隔之间休眠而不是忙等待可以降低CPU占用。考虑使用更省电的设备如果你的Pi-hole运行在树莓派4上驱动一个电子墨水屏绰绰有余。你也可以考虑使用更省电的树莓派 Zero 2 W 来专门运行这个仪表盘脚本通过局域网访问主Pi-hole的API。6. 扩展思路不止于Pi-hole这个项目的框架具有很强的通用性。一旦你打通了“数据获取 - 图像生成 - 屏幕渲染”这个流程就可以轻松地将屏幕变成其他信息的展示终端。系统监控仪表盘使用psutil库获取树莓派本身的CPU、内存、磁盘、温度信息并显示在屏幕上。天气预报站调用公开的天气API如OpenWeatherMap显示当前温度、湿度、未来几小时的预报。日历与待办事项与Google Calendar或Todoist等API同步显示今日日程和任务。智能家居状态中心如果你使用Home Assistant它的REST API非常强大可以获取所有智能设备的状态并集中展示。要实现这些你只需要修改fetch_pihole_data和create_dashboard_image这两个函数其余部分硬件驱动、主循环、服务管理几乎可以复用。这种将特定应用与通用显示框架解耦的设计让这个小项目拥有了巨大的可玩性。我个人在稳定运行Pi-hole仪表盘几周后就把它改造成了一个混合仪表盘上半部分显示Pi-hole状态下半部分滚动显示系统温度和未来3小时的天气。一块屏幕多种信息它已经成了我工作台上不可或缺的“信息窗口”。整个过程从硬件连接到软件调试虽然遇到了一些小波折但看到最终成品稳定工作时那种成就感正是DIY项目的乐趣所在。希望这份详细的指南能帮你绕过我踩过的坑顺利打造出属于你自己的电子墨水屏信息中心。