🖥️ ESP32C3 Linux IP Display
A USB-powered OLED display for showing Linux server IP address in real time.
当服务器没有显示器、IP 经常变、无法固定 IP 时,只需插上这块小屏幕即可立刻看到当前服务器的 LAN/WAN IP。
📸 实物展示

使用 ESP32C3 Super Mini + 72×40 SSD1306 OLED,USB 供电 + USB CDC 串口通讯。
✨ Features
🎯 自动检测 USB ACM 设备(VID=303A)
📡 实时显示 IP (LAN/WLAN)
🔄 每秒刷新,不占用 CPU
💡 新消息 LED 闪烁指示
🔌 即插即用,断开自动恢复
🖥️ OLED 自动换行,支持 4 行内容
🛠️ 完整的 systemd 服务,开机自启
🔧 代码仅依赖 Python3 + ESP32 Arduino
📦 目录结构
.
├── esp32_firmware/
│ └── ip_display.ino # ESP32C3 程序
├── linux_client/
│ └── acm_report.py # Linux 端脚本
├── systemd/
│ └── acm_report.service # systemd 启动脚本
└── README.md
🚀 项目原理
Linux Server ESP32C3 + OLED
┌────────────────────────────┐ ┌─────────────────────────┐
│ Python Script │ USB │ USB CDC 串口接收字符串 │
│ 获取所有网卡 IPv4 地址 │──────▶│ 分行显示到 SSD1306 OLED │
│ 格式化为 L:xxx W:xxx │ │ 最近 1 分钟有数据则闪灯 │
│ 写入 /dev/ttyACM* │ └─────────────────────────┘
└────────────────────────────┘
ESP32C3 通过串口接受字符串并显示,Linux 每秒发一次 IP 状态。
🛠️ 1. ESP32C3 / ESP32C3 Super Mini 固件
硬件连接:
Signal ESP32C3 Pin
SDA GPIO5
SCL GPIO6
LED GPIO8
OLED I2C (0x3C)
使用 Arduino 框架。
📌 代码(ip_display.ino)
#include <U8g2lib.h>
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
#define SDA_PIN 5
#define SCL_PIN 6
// LED 引脚(按你要求用 IO8)
#define LED_PIN 8
// 按你之前的屏幕型号
U8G2_SSD1306_72X40_ER_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
const int MAX_LINES = 4;
String linesBuf[MAX_LINES];
int16_t dispWidth;
// LED 闪烁控制
unsigned long lastMsgTime = 0; // 最近一次收到新消息的时间
const unsigned long activeWindow = 60000UL; // 1 分钟内有新消息才闪灯
const unsigned long blinkInterval = 500UL; // 闪烁周期(毫秒)
bool ledState = false;
void u8g2_prepare(void) {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontRefHeightExtendedText();
u8g2.setDrawColor(1);
u8g2.setFontPosTop();
u8g2.setFontDirection(0);
}
// 去掉字符串右侧空白
void trimRight(String &s) {
while (s.length() > 0) {
char c = s.charAt(s.length() - 1);
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
s.remove(s.length() - 1);
} else {
break;
}
}
}
// 把一整行 text 按显示宽度拆成最多 4 行
void wrapTextIntoLines(String text) {
for (int i = 0; i < MAX_LINES; i++) linesBuf[i] = "";
text.trim(); // 去掉首尾空白
for (int lineNo = 0; lineNo < MAX_LINES && text.length() > 0; lineNo++) {
// 去掉前导空格
while (text.length() > 0 && text.charAt(0) == ' ') text.remove(0, 1);
if (text.length() == 0) break;
int fit = text.length();
// 找最大能放得下的前缀
while (fit > 0) {
String part = text.substring(0, fit);
if (u8g2.getStrWidth(part.c_str()) <= dispWidth) break;
fit--;
}
if (fit == 0) {
// 理论上不会发生,保险:至少放 1 个字符
fit = 1;
} else {
// 尽量在空格处断行
int lastSpace = text.substring(0, fit).lastIndexOf(' ');
if (lastSpace > 0) fit = lastSpace;
}
String chunk = text.substring(0, fit);
trimRight(chunk);
linesBuf[lineNo] = chunk;
text = text.substring(fit);
}
}
void displayText(const String &s) {
wrapTextIntoLines(s);
u8g2.clearBuffer();
const int line_spacing = 10; // 6x10 字体行高约 10 像素
for (int i = 0; i < MAX_LINES; i++) {
if (linesBuf[i].length() > 0) {
u8g2.drawStr(0, i * line_spacing, linesBuf[i].c_str());
}
}
u8g2.sendBuffer();
}
void setup() {
Serial.begin(115200);
Serial.setTimeout(200); // readStringUntil 超时
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW); // 初始灭灯
Wire.begin(SDA_PIN, SCL_PIN);
u8g2.begin();
u8g2_prepare();
dispWidth = u8g2.getDisplayWidth();
displayText("Ready. Send text with newline.");
lastMsgTime = millis(); // 上电时算作有一次“消息”,先允许闪灯
}
void loop() {
// 1. 串口读取和屏幕更新
if (Serial.available()) {
// 和你测试回显程序一样,用 '\n' 作为一行结束
String line = Serial.readStringUntil('\n');
line.trim();
if (line.length() > 0) {
Serial.print("Display: [");
Serial.print(line);
Serial.println("]");
displayText(line);
// 记录最近收到消息的时间
lastMsgTime = millis();
}
}
// 2. LED 闪烁逻辑:1 分钟内有新消息则闪烁,超过 1 分钟不闪(灭)
unsigned long now = millis();
if (now - lastMsgTime <= activeWindow) {
// 在“有消息”的时间窗内,按照 blinkInterval 闪烁
static unsigned long lastBlink = 0;
if (now - lastBlink >= blinkInterval) {
lastBlink = now;
ledState = !ledState;
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
}
} else {
// 超过 1 分钟没新消息,保持熄灭
ledState = false;
digitalWrite(LED_PIN, LOW);
}
delay(20);
}
🖥️ 2. Linux 端—IP 自动上报脚本
位置:
/usr/local/bin/acm_report.py
功能:
自动发现 VID=303A 的 ACM
每秒扫描 IPv4,提取 enp*(LAN)、wlp*(WLAN)
写入 JSON 格式可读串口文本
📌 代码
#!/usr/bin/env python3
import os
import time
import subprocess
import glob
TARGET_VID = "303a"
SLEEP_TIME = 1
def get_ip_map():
"""扫描所有接口 IPv4,返回字典:iface -> ip"""
ipmap = {}
try:
out = subprocess.check_output(["ip", "-4", "-o", "addr"]).decode()
for line in out.splitlines():
parts = line.split()
iface = parts[1]
ip = parts[3].split("/")[0]
ipmap[iface] = ip
except Exception:
pass
return ipmap
def pick_ip_for_prefix(ipmap, prefix):
"""选择名字以 prefix 开头的网卡 IPv4 地址"""
for iface, ip in ipmap.items():
if iface.startswith(prefix):
return ip
return "0.0.0.0"
def find_acm_device():
"""查找 VID=303A 的 /dev/ttyACM* 设备"""
for acm in glob.glob("/dev/ttyACM*"):
sysfs_path = f"/sys/class/tty/{os.path.basename(acm)}/device/../idVendor"
if os.path.exists(sysfs_path):
try:
with open(sysfs_path, "r") as f:
if f.read().strip().lower() == TARGET_VID:
return acm
except:
pass
return None
def main():
acm_dev = None
while True:
# Step 1:找 ACM
if acm_dev is None:
acm_dev = find_acm_device()
if acm_dev is None:
print("未找到 VID=303A 的 ACM 设备... 等待中...")
time.sleep(SLEEP_TIME)
continue
else:
print(f"找到设备:{acm_dev}")
# Step 2:获取 IP 地址
ipmap = get_ip_map()
lan_ip = pick_ip_for_prefix(ipmap, "enp") # LAN
wan_ip = pick_ip_for_prefix(ipmap, "wlp") # WAN(你的系统用 wlp2s0)
msg = f"L:{lan_ip} W:{wan_ip}"
# Step 3:写入 ACM
try:
os.system(f'echo "{msg}" > {acm_dev}')
print("写入:", msg)
except Exception as e:
print("写入失败:", e)
time.sleep(SLEEP_TIME)
if __name__ == "__main__":
main()
🔧 3. systemd 服务(开机自启)
创建:
sudo nano /etc/systemd/system/acm_report.service
内容:
[Unit]
Description=ACM Report Service
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/bin/acm_report.py
Restart=always
RestartSec=2
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
启用:
sudo systemctl daemon-reload
sudo systemctl enable acm_report.service
sudo systemctl start acm_report.service
查看状态:
sudo systemctl status acm_report.service
🧩 效果展示
插上 ESP32C3 后屏幕将自动显示:

如果一分钟内收到新消息,LED 会闪烁指示设备在线。
🔌 热插拔无障碍
即使 Linux 重启、断开 USB、换端口,也能自动识别:
自动寻找 /dev/ttyACM*
自动验证 idVendor = 303A
自动恢复显示
无需人工干预。
5111

被折叠的 条评论
为什么被折叠?



