1. 项目概述:用ESP32和GTFS数据追踪纽约M15公交
如果你和我一样,是个喜欢鼓捣硬件的创客,同时又对城市公共交通的数据感兴趣,那么“用ESP32做一个基于GTFS的M15公交追踪器”这个项目,绝对能让你兴奋起来。这不仅仅是一个简单的“显示公交车到站时间”的设备,而是一个融合了物联网硬件、实时数据解析和本地化数据处理逻辑的综合性实践。它解决的核心问题是:如何用一个成本低廉、功耗可控的微控制器,稳定、准确地获取并展示一条特定公交线路的实时动态,而无需依赖复杂的服务器后端或昂贵的云服务。
这个项目非常适合那些已经玩过Arduino或ESP32基础项目,想挑战更复杂数据应用的朋友。你需要对C++(Arduino框架)有基本了解,对HTTP请求和JSON数据解析不陌生,并且愿意花点时间去理解公共交通数据规范。最终,你会得到一个可以放在桌面的小设备,它通过Wi-Fi连接到网络,从纽约大都会运输署(MTA)的公开数据接口抓取M15线路的实时到站信息,并在一个OLED屏幕上清晰地显示出来,比如“下一班车:3分钟后到达第2大道站”。整个过程,从硬件选型、数据获取到解析显示,你都将亲手实现,成就感十足。
2. 核心思路与方案选型:为什么是ESP32+GTFS?
2.1 硬件核心:ESP32为何是唯一选择
在这个项目中,微控制器的选型几乎没有悬念——ESP32是当前的最优解,甚至可以说是唯一可行的选择。原因在于项目对连接性和计算能力的双重需求。
首先,稳定的Wi-Fi连接是生命线。我们需要设备能持续、可靠地接入互联网,以轮询MTA的实时数据接口。虽然ESP8266也具备Wi-Fi功能,但其内存(通常只有80KB的用户RAM)和处理复杂JSON数据流的能力相对有限。MTA的GTFS实时数据反馈通常是Protobuf格式(一种比JSON更高效但需要专门解码的数据序列化格式),或者即便是JSON,其嵌套结构也可能比较复杂。ESP32系列(如ESP32-WROOM-32)通常拥有520KB的SRAM,双核处理器,这为运行Wi-Fi协议栈、HTTP客户端以及JSON/Protobuf解码库提供了充裕的空间,确保系统在长时间运行中不易因内存不足而崩溃。
其次,丰富的GPIO和接口为未来扩展留足余地。虽然基础版本只需要驱动一个I2C接口的OLED屏幕,但ESP32的引脚资源允许你未来轻松添加更多功能,比如一个物理按钮用于切换显示模式(查看不同方向的车辆)、一个蜂鸣器用于到站提醒,甚至一个温湿度传感器来做个环境监测小彩蛋。其硬件I2C、SPI接口也能保证与显示屏等外设通信的稳定性和速度。
最后,开发生态与功耗控制的平衡。ESP32支持Arduino IDE和ESP-IDF两种主流的开发框架,社区资源极其丰富,遇到任何问题几乎都能找到解决方案。同时,它支持深度睡眠模式,虽然在这个需要持续更新的追踪器中可能不常用,但这一特性为制作电池供电的便携版本提供了可能。相比之下,使用树莓派Pico等纯MCU需要额外添加Wi-Fi模块,增加了复杂性和成本;而使用树莓派Zero W等Linux板卡则显得“杀鸡用牛刀”,功耗和成本都更高。因此,ESP32在性能、功能、成本和功耗上取得了最佳平衡。
2.2 数据基石:深入理解GTFS与GTFS-Realtime
项目的灵魂在于数据,而这里我们依赖的是公共交通领域的开放数据标准:GTFS和GTFS-Realtime。
GTFS(通用公共交通数据规范)可以理解为公交系统的“静态课程表”。它是一系列CSV格式的文件,定义了公交线路、站点、时刻表等固定信息。例如,stops.txt文件包含了所有站点的ID、名称、经纬度;routes.txt定义了线路(如M15);trips.txt将具体的车次与线路、时刻表关联起来;stop_times.txt则精确描述了每趟车在每个站点的计划到达和离开时间。我们需要事先下载并解析纽约MTA的GTFS静态数据,从中提取出M15线路所有站点的stop_id,这是后续查询实时数据的“钥匙”。
GTFS-Realtime则是这个“课程表”的实时修正版。它通过Protocol Buffers(Protobuf)格式提供实时更新,包含几个核心信息源:
- TripUpdates(行程更新):提供某趟具体车次(Trip)的延误、取消或时刻表偏差信息。
- VehiclePositions(车辆位置):提供车辆当前的GPS位置、方向和状态。
- ServiceAlerts(服务警报):提供线路中断、改道等文本通知。
对于公交到站时间预测,我们主要关心TripUpdates。MTA会提供一个Protobuf数据流的URL端点。我们的ESP32需要定期(例如每30秒)访问这个端点,获取数据流,然后使用专门的GTFS-Realtime解析库(如pb_decode配合.proto定义文件,或Arduino社区封装好的库)将其解码。解码后,我们根据目标线路(M15)和方向筛选出相关的TripUpdate实体,再从中找到目标站点stop_id对应的StopTimeUpdate,其arrival.delay字段结合静态时刻表,就能计算出预测的到站时间。
注意:直接解析Protobuf在MCU上略有挑战。一个更简单但效率稍低的替代方案是,寻找MTA是否同时提供JSON格式的实时数据接口(有些第三方聚合API或MTA的某些端点会提供)。如果存在,我们可以直接使用ArduinoJson库来解析,这会大大简化开发难度,是快速原型验证的首选。
2.3 整体系统架构设计
整个系统的运行逻辑是一个清晰的闭环:
- 初始化:ESP32启动,连接Wi-Fi,初始化OLED显示屏,并从内部存储(如SPIFFS文件系统)或代码常量中加载预先处理好的M15线路关键站点
stop_id列表。 - 数据获取:通过HTTP/HTTPS客户端,向MTA的GTFS-Realtime数据接口发起GET请求。这里需要注意,MTA的接口通常需要API密钥,你需要提前申请并将其配置在代码中。
- 数据解析:
- 如果使用Protobuf接口,则调用GTFS-Realtime解析库,解码数据流,在内存中形成结构化的数据对象。
- 如果使用JSON接口,则使用ArduinoJson库反序列化HTTP响应体。
- 业务逻辑处理:遍历解析后的数据,筛选出
route_id为“M15”的行程更新。然后,针对我们关心的站点(例如用户家附近的车站),找到对应的StopTimeUpdate,提取预测到达时间戳或延迟秒数。结合当前系统时间(可能需要从NTP服务器同步),计算出“还有X分钟到达”的友好显示内容。 - 信息呈现:将计算出的到站时间、车辆位置、线路状态等信息,通过I2C总线发送到OLED屏幕上,以清晰的字体和布局进行显示。
- 循环与休眠:完成一次显示后,系统进入延时(例如等待25秒),然后重复步骤2-5,实现信息的周期性更新。为了节省功耗,可以在深夜等非运营时段让ESP32进入轻度睡眠模式。
这个架构的关键在于稳定性和错误处理。网络可能中断,API可能暂时不可用,数据格式可能意外变化。因此,代码中必须包含健壮的错误处理机制,比如网络重连逻辑、解析失败时的默认显示(“更新中…”或“暂无数据”)、以及看门狗定时器防止程序卡死。
3. 硬件准备与软件环境搭建
3.1 物料清单与硬件连接
要动手制作,你需要准备以下核心部件:
| 组件 | 型号/规格 | 数量 | 说明 |
|---|---|---|---|
| 主控板 | ESP32开发板(如NodeMCU-32S、ESP32-DevKitC) | 1 | 建议选择带有USB转串口芯片的版本,方便烧录和调试。 |
| 显示屏 | SSD1306 0.96寸或1.3寸 I2C接口OLED | 1 | 分辨率128x64足以显示多行信息,I2C接口接线简单。 |
| 连接线 | 杜邦线(母对母) | 若干 | 用于连接ESP32与OLED屏幕。 |
| 电源 | USB数据线(Micro-USB或Type-C) | 1 | 用于供电和程序烧录。5V/1A的USB充电器即可。 |
硬件连接非常简单,遵循I2C标准接线:
- OLED VCC->ESP32 3.3V(注意:绝大多数OLED屏是3.3V逻辑电平,切勿接5V!)
- OLED GND->ESP32 GND
- OLED SCL->ESP32 GPIO 22(这是ESP32上常用的I2C时钟引脚,也可自定义)
- OLED SDA->ESP32 GPIO 21(这是ESP32上常用的I2C数据引脚,也可自定义)
接好后,可以通过一个简单的I2C扫描程序来测试屏幕是否被正确识别。
3.2 开发环境配置与核心库安装
我们选择在Arduino IDE中进行开发,因为它对初学者友好,库管理方便。当然,使用PlatformIO(VSCode插件)是更专业的选择,它提供了更好的项目管理和依赖管理。
Arduino IDE 配置步骤:
- 安装Arduino IDE:从官网下载并安装最新版。
- 添加ESP32开发板支持:
- 打开
文件->首选项,在“附加开发板管理器网址”中输入:https://espressif.github.io/arduino-esp32/package_esp32_index.json - 打开
工具->开发板->开发板管理器,搜索“esp32”,找到由“Espressif Systems”提供的版本并安装。
- 打开
- 安装必备的库:
WiFi/HTTPClient:ESP32核心库已包含,无需单独安装。ArduinoJson(v6.x或v7.x):用于解析JSON格式的API响应。在库管理中搜索并安装。Adafruit SSD1306和Adafruit GFX Library:用于驱动OLED屏幕。在库管理中搜索“Adafruit SSD1306”和“Adafruit GFX”并安装。- (可选)
protobuf相关库:如果你决定直接解析Protobuf,需要寻找如nanopb或专门为GTFS-Realtime封装的Arduino库。这比JSON方案复杂,建议先从JSON接口入手。
实操心得:在安装库时,注意库的兼容性和版本。例如,新版
ArduinoJson(v7) 的API与v6有较大变化,网上教程代码可能不兼容。建议新手先使用v6.x稳定版。另外,Adafruit SSD1306库安装后,在示例代码中需要根据你的屏幕尺寸和I2C地址修改初始化参数,常见的I2C地址是0x3C。
3.3 获取并预处理MTA的GTFS静态数据
这是项目前期一项关键的数据准备工作。你需要从MTA的开发者门户(例如 MTA Developer Resources )下载最新的GTFS静态数据包。这是一个ZIP文件,解压后得到一堆CSV文件。
我们的目标是从中提取M15线路相关的信息。这个过程强烈建议在电脑上使用Python、Excel或任何你熟悉的工具完成,而不是在ESP32上实时处理,因为数据量很大。
预处理步骤:
- 找到M15的
route_id:打开routes.txt,找到route_short_name为“M15”的行,记下对应的route_id(例如“MTASBWY:M15”)。 - 找到M15的所有
trip_id:打开trips.txt,筛选出route_id等于上一步记录值的所有行,提取出trip_id列表。一个trip_id代表一个具体的车次(比如“工作日早高峰从南码头开往东哈莱姆的某趟车”)。 - 找到M15的站点序列:打开
stop_times.txt,这是一个非常大的文件。筛选出trip_id属于上一步列表的所有行。然后,选择某一个具有代表性的trip_id(比如第一个),按照stop_sequence排序,你就得到了这趟车经过的所有站点的stop_id顺序列表。 - 获取站点名称:打开
stops.txt,根据上一步得到的stop_id列表,找到每个站点对应的stop_name(如“Allen St / Delancey St”)。 - 生成配置文件:将你关心的几个站点(比如你家附近的两三个站)的
stop_id和stop_name,以简单的格式(如JSON或纯文本)保存下来。最终,你需要将这个配置文件内容硬编码到ESP32的Arduino代码中,或者上传到ESP32的SPIFFS文件系统里供程序读取。
// 示例:在代码中硬编码目标站点信息 struct BusStop { const char* stop_id; const char* stop_name; }; BusStop m15Stops[] = { {"MTASBWY:142756", "Allen St / Delancey St"}, {"MTASBWY:142757", "Allen St / Rivington St"}, // ... 添加更多你关心的站点 };完成这一步,你就为ESP32准备好了查询实时数据所必需的“地图”和“坐标”。
4. 核心代码实现与解析
4.1 网络连接与实时数据获取
ESP32连接Wi-Fi是第一步,代码很标准。关键在于获取实时数据。
假设我们使用MTA提供的JSON格式实时接口(例如,通过第三方聚合服务如https://api.mta.info/,你需要注册获取API密钥)。下面是一个简化的核心代码片段:
#include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> const char* ssid = "你的Wi-Fi名称"; const char* password = "你的Wi-Fi密码"; const char* mtaApiUrl = "https://api.mta.info/你的实时数据端点"; const char* mtaApiKey = "你的MTA_API_KEY"; // 需要在请求头中携带 void fetchRealTimeData() { if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi断开,尝试重连..."); WiFi.reconnect(); delay(2000); return; } HTTPClient http; http.begin(mtaApiUrl); http.addHeader("Authorization", mtaApiKey); // 添加API密钥头 http.addHeader("Accept", "application/json"); int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); Serial.println("收到数据,长度: " + String(payload.length())); // 调用解析函数 parseTripUpdates(payload); } else if (httpCode == HTTP_CODE_UNAUTHORIZED) { Serial.println("错误:API密钥无效或过期。"); displayError("API Key Error"); } else { Serial.printf("HTTP请求失败,错误码: %d\n", httpCode); displayError("HTTP " + String(httpCode)); } http.end(); }注意事项:MTA的官方GTFS-Realtime端点是Protobuf格式,URL类似
http://gtfsrt.prod.obanyc.com/tripUpdates?key=...。上面的JSON接口示例是一个假设。在实际操作中,你必须查阅MTA最新的官方文档,确认可用的数据格式和接入点。使用HTTPS时,ESP32可能需要更新根证书或使用http.begin(url, root_ca)指定证书。
4.2 GTFS-Realtime数据解析与业务逻辑
这是整个项目最核心、也最具挑战性的部分。我们需要从海量数据中精准定位到M15线路、特定方向、特定站点的预测时间。
void parseTripUpdates(String &jsonPayload) { // 使用ArduinoJson解析文档 DynamicJsonDocument doc(16384); // 根据实际数据大小调整,宁大勿小 DeserializationError error = deserializeJson(doc, jsonPayload); if (error) { Serial.print("JSON解析失败: "); Serial.println(error.c_str()); return; } // 假设JSON结构顶层有一个"entity"数组,每个元素是一个行程更新 JsonArray entities = doc["entity"]; if (!entities) { Serial.println("未找到‘entity’字段"); return; } // 遍历所有实体 for (JsonObject entity : entities) { // 检查是否有行程更新 if (entity.containsKey("trip_update")) { JsonObject tripUpdate = entity["trip_update"]; JsonObject trip = tripUpdate["trip"]; // 1. 筛选线路:检查route_id是否为M15 const char* routeId = trip["route_id"]; // 例如 "MTASBWY:M15" if (routeId == nullptr || strstr(routeId, "M15") == nullptr) { continue; // 不是M15线路,跳过 } // 2. 获取行程ID和方向(可选,用于区分上行/下行) const char* tripId = trip["trip_id"]; int directionId = trip["direction_id"]; // 通常0和1代表两个方向 // 3. 获取该行程的站点时间更新列表 JsonArray stopTimeUpdates = tripUpdate["stop_time_update"]; if (!stopTimeUpdates) continue; // 4. 遍历站点更新,匹配我们关心的站点 for (JsonObject stu : stopTimeUpdates) { const char* stuStopId = stu["stop_id"]; // 与我们预定义的站点列表进行匹配 for (int i = 0; i < numTargetStops; i++) { if (strcmp(stuStopId, m15Stops[i].stop_id) == 0) { // 找到匹配的站点! // 5. 提取预测到达时间 if (stu.containsKey("arrival")) { JsonObject arrival = stu["arrival"]; long long predictedArrivalTime = arrival["time"]; // Unix时间戳(秒) // 6. 计算剩余分钟数 time_t now = time(nullptr); // 需要先通过NTP同步系统时间 int minutesAway = (predictedArrivalTime - now) / 60; // 7. 存储或显示这个预测结果 updateDisplayForStop(i, minutesAway, directionId); break; // 找到该站点的预测,跳出内层循环 } } } } } } }关键逻辑解析:
- 数据筛选:通过
route_id快速过滤掉其他线路的数据,极大减少后续处理量。 - 时间处理:API返回的通常是Unix时间戳(秒)。ESP32需要通过NTP(网络时间协议)同步本地时间,
time(nullptr)才能获取正确的当前时间戳。计算差值后除以60得到分钟数。 - 方向判断:
direction_id字段很重要。M15可能有两个方向(南向和北向)。你需要根据你的位置,决定显示哪个方向的车辆。可以在代码中配置一个目标方向进行筛选。 - 错误处理:
arrival字段可能不存在(例如车辆已离站),time字段可能为0。代码中必须加入判断,避免程序崩溃。
4.3 OLED屏幕信息显示优化
显示信息要清晰、直观。我们可以设计一个简单的界面,分区域显示:
- 标题区:固定显示“M15 Bus Tracker”。
- 主要信息区:显示最近的两个预测。例如:
-> To Harlem Stop A: 3 min Stop B: 8 min - 状态区:显示最后更新时间、Wi-Fi信号强度或错误信息。
使用Adafruit_SSD1306和Adafruit_GFX库可以方便地绘制文本和图形。
#include <Adafruit_SSD1306.h> #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); void setupDisplay() { if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F("SSD1306分配失败")); for(;;); // 死循环 } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.display(); } void updateDisplayForStop(int stopIndex, int minutesAway, int direction) { display.clearDisplay(); // 绘制标题 display.setCursor(0, 0); display.setTextSize(2); display.print("M15"); display.setTextSize(1); display.print(" Tracker"); // 绘制方向指示 display.setCursor(0, 18); display.print(direction == 0 ? "To Uptown" : "To Downtown"); // 绘制站点和预测时间 display.setCursor(0, 30); display.print(m15Stops[stopIndex].stop_name); display.setCursor(90, 30); if(minutesAway <= 0) { display.print("Due"); } else if (minutesAway > 60) { display.print(">1h"); } else { display.print(minutesAway); display.print(" min"); } // 绘制分隔线和状态 display.drawLine(0, 48, 128, 48, SSD1306_WHITE); display.setCursor(0, 54); display.print("Updated:"); display.print(formatTime(now)); // 一个格式化时间的函数 display.display(); }实操心得:OLED屏幕是单色、分辨率有限,所以信息布局要精简。避免频繁全屏刷新,可以只刷新变化的部分以减少闪烁。另外,
display.display()是实际将缓冲区内容推送到屏幕的命令,比较耗时,应在所有绘制操作完成后一次性调用。
4.4 系统时间同步与定时任务管理
准确的预测依赖于准确的当前时间。ESP32需要通过NTP同步时间。
#include <time.h> const char* ntpServer = "pool.ntp.org"; const long gmtOffset_sec = -18000; // 纽约时区偏移(秒),例如-5*3600 = -18000 const int daylightOffset_sec = 3600; // 夏令时偏移 void syncNetworkTime() { configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); struct tm timeinfo; if(!getLocalTime(&timeinfo)){ Serial.println("获取时间失败"); return; } Serial.println(&timeinfo, "时间同步成功: %Y-%m-%d %H:%M:%S"); }在主循环loop()中,我们需要管理数据获取的节奏。不宜太频繁(增加MTA服务器负担,可能被限流),也不宜太慢(信息不及时)。每30秒到60秒获取一次是一个合理的区间。可以使用millis()进行非阻塞式定时,避免使用delay()导致程序卡死。
unsigned long previousMillis = 0; const long interval = 30000; // 30秒间隔 void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; fetchRealTimeData(); // 获取并解析数据 // 更新显示... } // 这里可以处理其他非紧急任务,如检查按钮输入 }5. 进阶优化与功能扩展
基础功能实现后,你可以考虑以下优化,让项目更稳定、更智能。
5.1 提升数据获取的稳定性与效率
- 错误重试与退避:网络请求失败时,不要立即无限重试。实现一个指数退避算法,比如第一次失败等2秒,第二次等4秒,第三次等8秒,以此类推,直到成功后再恢复常规间隔。
- 数据缓存:在内存中缓存上一次成功的预测结果。当本次网络请求失败时,继续显示缓存的信息,并标记为“可能过时”,提升用户体验。
- 差分更新:如果API支持,可以只请求自上次更新以来的变化数据(通过
If-Modified-Since头或类似机制),减少数据流量和解析开销。 - 使用更高效的Protobuf:如果追求极致的性能和数据新鲜度,最终应该切换到解析原生的GTFS-Realtime Protobuf流。这需要集成
nanopb库,并使用MTA提供的.proto文件定义来编译生成解码器。虽然步骤繁琐,但数据体积小、解析快。
5.2 丰富显示内容与交互
- 多站点滚动显示:如果你的OLED屏幕足够大(比如1.3寸),可以设计一个界面,轮流显示3-4个关注站点的预测信息。
- 车辆位置可视化:如果API提供了
VehiclePositions数据,你可以尝试在屏幕上用简单的点或线段,示意车辆在当前站点区间的大概位置。 - 物理按钮交互:增加一个按钮,用于切换显示方向(北行/南行),或者切换显示不同的关注站点。
- 声音提示:增加一个无源蜂鸣器,当车辆即将到站(例如3分钟内)时发出“滴滴”声提醒。
5.3 低功耗设计与便携化
如果你想制作一个电池供电的版本,需要深入优化功耗。
- 深度睡眠模式:在公交非运营时段(例如凌晨1点到5点),让ESP32进入深度睡眠(Deep Sleep)。可以设置一个定时器(RTC定时器或外部中断)在运营开始前唤醒它。
- 降低更新频率:在非高峰时段,将数据更新间隔从30秒延长到2-5分钟。
- 优化外设供电:使用ESP32的GPIO控制一个MOSFET开关,在睡眠期间彻底切断OLED屏幕的电源。
- 选择低功耗型号:使用ESP32-S2或ESP32-C3等单核型号,它们在深度睡眠下的功耗可能更低。
实现深度睡眠的代码片段:
#define uS_TO_S_FACTOR 1000000ULL // 微秒到秒的转换因子 #define TIME_TO_SLEEP 3600 // 睡眠时间(秒),例如1小时 void gotoSleep() { display.clearDisplay(); display.display(); display.ssd1306_command(SSD1306_DISPLAYOFF); // 关闭显示屏 esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); Serial.println("进入深度睡眠,ZZZ..."); delay(100); esp_deep_sleep_start(); } // 在setup()中,可以通过esp_sleep_get_wakeup_cause()判断唤醒原因,并决定是正常启动还是继续睡眠。6. 常见问题与故障排查实录
在实际制作和运行过程中,你几乎一定会遇到下面这些问题。这里记录了我的踩坑经验和解决方案。
6.1 编译与烧录问题
- 问题:编译时提示“
WiFi.h找不到”或“HTTPClient.h找不到”。- 排查:这通常是因为没有正确安装ESP32开发板支持包,或者Arduino IDE的板卡类型选择错误。
- 解决:确认
工具->开发板中选择了正确的ESP32型号(如“NodeMCU-32S”)。确保已按照前面步骤添加了开发板管理网址并安装了ESP32包。
- 问题:代码编译通过,但烧录时卡在“连接...”阶段,或提示“串口打开失败”。
- 排查:驱动问题或板卡bootloader模式未进入。
- 解决:
- 安装正确的CP210x或CH340 USB转串口驱动。
- 确保选择了正确的端口(
工具->端口)。 - 对于大多数ESP32开发板,在烧录时需要按住“BOOT”(或“IO0”)按钮,再按一下“EN”(复位)按钮,然后松开“EN”,再松开“BOOT”,即可进入烧录模式。有些板子有自动下载电路,则无需此操作。
6.2 网络与数据获取问题
- 问题:Wi-Fi连接不稳定,经常断开。
- 排查:信号强度弱、路由器设置问题或代码中重连逻辑不完善。
- 解决:
- 在代码开头添加
WiFi.setSleep(false);,禁用Wi-Fi睡眠模式,可以显著提升稳定性。 - 增强重连逻辑。在主循环中定期检查
WiFi.status(),如果断开,则尝试WiFi.reconnect(),如果多次失败则执行WiFi.begin(ssid, password)重新初始化。 - 考虑使用
WiFi.mode(WIFI_STA)明确设置为站点模式。
- 在代码开头添加
- 问题:HTTP请求返回403 Forbidden或401 Unauthorized。
- 排查:API密钥错误、过期或未正确添加到请求头中。
- 解决:仔细检查API密钥字符串是否正确,确保在请求头中的字段名(如
Authorization或x-api-key)符合API文档要求。去MTA开发者门户确认密钥是否有效。
- 问题:能收到数据,但
ArduinoJson解析失败,提示“NoMemory”或“InvalidInput”。- 排查:分配的
DynamicJsonDocument内存太小,或者JSON数据格式不符合预期。 - 解决:
- 先
Serial.println(payload)打印出原始数据,复制到在线JSON校验器(如jsonlint.com)检查格式。 - 根据打印出的数据长度,大幅增加
DynamicJsonDocument doc(16384);中的缓冲区大小。ESP32内存相对充裕,可以给大一点(如32768)。 - 检查JSON路径是否正确。使用
doc.containsKey()或doc[“key”].is<T>()进行安全访问。
- 先
- 排查:分配的
6.3 数据显示与逻辑问题
- 问题:时间预测完全不准,显示负数或极大值。
- 排查:系统时间未同步,或时区设置错误。
- 解决:确保在连接Wi-Fi后成功调用了
syncNetworkTime()。检查gmtOffset_sec和daylightOffset_sec设置是否正确。可以通过Serial.println(&timeinfo, “当前时间: %Y-%m-%d %H:%M:%S”)来验证。
- 问题:屏幕上只显示一个站点,或者显示的站点不对。
- 排查:站点
stop_id匹配失败,或者数据筛选逻辑有误。 - 解决:
- 在解析代码中,将匹配到的
route_id、trip_id、stop_id都打印出来,与你在预处理阶段得到的数据进行比对,确认完全一致。注意字符串末尾是否有空格。 - 确认你在遍历
stop_time_update时,正确地找到了包含arrival字段的更新。有时对于已过去的站点,只有departure字段。
- 在解析代码中,将匹配到的
- 排查:站点
- 问题:OLED屏幕不显示或显示乱码。
- 排查:I2C地址错误、接线松动或库初始化参数不对。
- 解决:
- 运行一个I2C扫描程序,确认屏幕的I2C地址(通常是
0x3C或0x3D)。 - 检查
display.begin()函数中的地址参数是否正确。 - 确认
SCL和SDA引脚定义与接线一致。ESP32上除了默认的21/22,其他引脚也可用作I2C,但需要在Wire.begin(SDA_PIN, SCL_PIN)中指定。
- 运行一个I2C扫描程序,确认屏幕的I2C地址(通常是
6.4 长期运行稳定性问题
- 问题:设备运行几天后死机或不更新。
- 排查:内存泄漏、看门狗超时或网络异常未恢复。
- 解决:
- 启用硬件看门狗:在
setup()中加入esp_task_wdt_init(10, true);和esp_task_wdt_add(NULL);,并在loop()中定期esp_task_wdt_reset()。这可以在程序卡死时自动重启。 - 防范内存泄漏:确保在每次HTTP请求后都调用
http.end();避免在循环中动态创建大量String对象,尽量使用字符数组或预留静态缓冲区。 - 加强异常处理:在
fetchRealTimeData和parseTripUpdates函数中,用try-catch(如果编译器支持)或细致的条件判断包裹可能出错的代码,确保单次失败不会导致整个循环崩溃。 - 定期软重启:可以在代码中设置一个计数器,连续运行24小时后,主动调用
ESP.restart()进行一次软重启,清理内存状态。
- 启用硬件看门狗:在
这个项目从想法到实现,每一步都需要耐心调试。最花时间的往往不是写代码,而是理解数据格式、处理网络异常和调试硬件连接。但当你的小设备第一次正确显示出“下一班车:5分钟后到达”时,所有的努力都是值得的。它不仅是一个实用的工具,更是一个涵盖了物联网全链路开发的绝佳学习案例。你可以在此基础上,尝试接入其他城市的GTFS数据,或者增加更多传感器,打造属于你自己的智能交通信息站。