💡 划重点
- 主控:ESP32,双核带 WiFi,适合 HTTP 拉取与屏幕刷新。
- 显示:2.4 寸 ILI9341 SPI 屏,常亮显示最近一班公交到站分钟。
- 交互:GPIO0 按键唤醒,短按刷新,长按切换线路。
- 联网:HTTP API 获取实时公交数据,JSON 解析后本地显示。
- 提醒:无源蜂鸣器在到站前 3 分钟短鸣,避免错过。
- 成本:整套物料控制在 70 元以内,适合上班族桌面使用。
需求洞察
目标用户是每天固定线路通勤的上班族。真实痛点不是“查不到公交”,而是“查公交这件事太麻烦”:早上出门前手里拿着早餐、背包、工牌,还要掏手机、解锁、打开 App、等广告、输入线路。冬天戴手套更不方便。很多时候只想知道“下一班还有几分钟”,却要付出十几秒操作成本。
另一个痛点是手机屏幕小、信息干扰多。放在桌面上的专用小屏,可以一直显示线路和到站时间,余光一瞥就能决定是现在出门还是再等两分钟。按键唤醒则解决夜间屏幕刺眼和功耗问题:平时深度睡眠,按下才刷新。这个项目值得做,因为它把高频、低信息量的查询变成零操作,成本低,结构简单,还能按自己线路定制。
方案设计
感知层:GPIO0 轻触按键,内部上拉,按下拉低。短按唤醒并刷新,长按切换预设线路。后续可加光敏电阻自动调亮度。
控制层:ESP32 负责 WiFi 连接、HTTP 请求、JSON 解析、屏幕驱动和蜂鸣器控制。使用深度睡眠降低功耗,按键通过 EXT0 唤醒。
执行层:ILI9341 显示线路号、最近一班、第二班、当前时间、网络状态。无源蜂鸣器在预计到站小于 3 分钟时响一声,小于 1 分钟时连响两声。
日志/联网层:串口输出调试日志;HTTP 请求设置超时与重试;WiFi 断线自动重连;预留 MQTT 或 HTTP 上报接口,方便以后把到站记录存到服务器。
物料清单
| 物料 | 数量 | 用途 | 估算单价 | 合计 |
|---|---|---|---|---|
| ESP32 DevKit V1 | 1 | 主控与联网 | 22 元 | 22 元 |
| ILI9341 2.4 寸 SPI 屏 | 1 | 显示到站信息 | 28 元 | 28 元 |
| 轻触按键 | 1 | 唤醒与刷新 | 0.5 元 | 0.5 元 |
| 无源蜂鸣器 | 1 | 到站提示音 | 1 元 | 1 元 |
| 面包板 | 1 | 免焊接搭建 | 5 元 | 5 元 |
| 杜邦线 | 若干 | 连接模块 | 5 元 | 5 元 |
| USB 电源线与适配器 | 1 | 供电 | 8 元 | 8 元 |
| 纸盒或 3D 打印外壳 | 1 | 桌面支架 | 0 元 | 0 元 |
| 合计 | 69.5 元 |
接线与结构
TFT 使用硬件 SPI:SCK=18,MOSI=23,CS=15,DC=2。RST 建议接 GPIO4,BL 接 3.3V 常亮,这样不占用额外 PWM 引脚。按键一端接 GPIO0,另一端接 GND,使用内部上拉。蜂鸣器正极接 GPIO5,负极接 GND。
注意 GPIO0 是启动模式引脚,上电时不要按住按键,否则可能进入下载模式。结构上把屏幕倾斜 15 度放入纸盒,按键放顶部,蜂鸣器开小孔。供电用 USB 适配器,放在桌面角落。
核心代码
以下为 Arduino 骨架,需在 TFT_eSPI 的 User_Setup.h 中配置 ILI9341 和上述引脚。
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <TFT_eSPI.h>
#include <SPI.h>
#define BTN_PIN 0
#define BUZZ_PIN 5
#define TFT_RST 4
TFT_eSPI tft = TFT_eSPI();
const char* ssid = "你的WiFi";
const char* password = "你的密码";
const char* apiUrl = "http://api.example.com/bus?line=123&stop;=abc";
void showMessage(String line1, String line2, uint16_t color) {
tft.fillScreen(TFT_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 30);
tft.println(line1);
tft.setTextSize(3);
tft.setCursor(10, 90);
tft.println(line2);
}
bool fetchBus(int &min1;, int &min2;) {
if (WiFi.status() != WL_CONNECTED) return false;
HTTPClient http;
http.begin(apiUrl);
http.setTimeout(5000);
int code = http.GET();
if (code != 200) { http.end(); return false; }
String payload = http.getString();
StaticJsonDocument<512> doc;
DeserializationError err = deserializeJson(doc, payload);
if (err) { http.end(); return false; }
min1 = doc["arrivals"][0]["minutes"] | -1;
min2 = doc["arrivals"][1]["minutes"] | -1;
http.end();
return true;
}
void beep(int times) {
for (int i = 0; i < times; i++) {
tone(BUZZ_PIN, 2000, 120);
delay(180);
}
}
void setup() {
Serial.begin(115200);
pinMode(BUZZ_PIN, OUTPUT);
tft.init();
tft.setRotation(1);
showMessage("连接WiFi", "...", TFT_YELLOW);
WiFi.begin(ssid, password);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) delay(500);
if (WiFi.status() == WL_CONNECTED) showMessage("已联网", "按按键刷新", TFT_GREEN);
else showMessage("联网失败", "检查热点", TFT_RED);
}
void loop() {
if (digitalRead(BTN_PIN) == LOW) {
delay(50);
if (digitalRead(BTN_PIN) == LOW) {
showMessage("刷新中", "...", TFT_CYAN);
int m1 = -1, m2 = -1;
if (fetchBus(m1, m2)) {
String a = String(m1) + " 分钟";
String b = "下一班 " + String(m2) + " 分钟";
showMessage(a, b, m1 <= 3 ? TFT_RED : TFT_GREEN);
if (m1 <= 3 && m1 >= 0) beep(m1 <= 1 ? 2 : 1);
} else {
showMessage("获取失败", "再按重试", TFT_ORANGE);
}
while (digitalRead(BTN_PIN) == LOW) delay(10);
}
}
delay(100);
}
调试步骤
- 先只接屏幕,烧录 TFT_eSPI 示例,确认引脚和旋转方向正确。
- 接按键,串口打印高低电平,确认按下为低,松开为高。
- 连接 WiFi,串口输出 IP 地址,确认热点兼容 2.4G。
- 用浏览器或串口调试 HTTP API,确认返回 JSON 字段名。
- 把真实字段填入
fetchBus,观察屏幕刷新和蜂鸣器。 - 加入深度睡眠与 EXT0 唤醒,测待机电流和唤醒成功率。
- 装入外壳,固定屏幕和按键,连续运行一天观察稳定性。