公司动态

基于ESP32与GTFS-Realtime的公交追踪器:物联网硬件与实时数据解析实践

📅 2026/8/19 8:56:34
基于ESP32与GTFS-Realtime的公交追踪器:物联网硬件与实时数据解析实践
1. 项目概述用ESP32和GTFS数据追踪纽约M15公交如果你和我一样是个喜欢鼓捣硬件的创客同时又对城市公共交通的数据感兴趣那么“用ESP32做一个基于GTFS的M15公交追踪器”这个项目绝对能让你兴奋起来。这不仅仅是一个简单的“显示公交车到站时间”的设备而是一个融合了物联网硬件、实时数据解析和本地化数据处理逻辑的综合性实践。它解决的核心问题是如何用一个成本低廉、功耗可控的微控制器稳定、准确地获取并展示一条特定公交线路的实时动态而无需依赖复杂的服务器后端或昂贵的云服务。这个项目非常适合那些已经玩过Arduino或ESP32基础项目想挑战更复杂数据应用的朋友。你需要对CArduino框架有基本了解对HTTP请求和JSON数据解析不陌生并且愿意花点时间去理解公共交通数据规范。最终你会得到一个可以放在桌面的小设备它通过Wi-Fi连接到网络从纽约大都会运输署MTA的公开数据接口抓取M15线路的实时到站信息并在一个OLED屏幕上清晰地显示出来比如“下一班车3分钟后到达第2大道站”。整个过程从硬件选型、数据获取到解析显示你都将亲手实现成就感十足。2. 核心思路与方案选型为什么是ESP32GTFS2.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定义了线路如M15trips.txt将具体的车次与线路、时刻表关联起来stop_times.txt则精确描述了每趟车在每个站点的计划到达和离开时间。我们需要事先下载并解析纽约MTA的GTFS静态数据从中提取出M15线路所有站点的stop_id这是后续查询实时数据的“钥匙”。GTFS-Realtime则是这个“课程表”的实时修正版。它通过Protocol BuffersProtobuf格式提供实时更新包含几个核心信息源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-DevKitC1建议选择带有USB转串口芯片的版本方便烧录和调试。显示屏SSD1306 0.96寸或1.3寸 I2C接口OLED1分辨率128x64足以显示多行信息I2C接口接线简单。连接线杜邦线母对母若干用于连接ESP32与OLED屏幕。电源USB数据线Micro-USB或Type-C1用于供电和程序烧录。5V/1A的USB充电器即可。硬件连接非常简单遵循I2C标准接线OLED VCC-ESP32 3.3V(注意绝大多数OLED屏是3.3V逻辑电平切勿接5V)OLED GND-ESP32 GNDOLED SCL-ESP32 GPIO 22(这是ESP32上常用的I2C时钟引脚也可自定义)OLED SDA-ESP32 GPIO 21(这是ESP32上常用的I2C数据引脚也可自定义)接好后可以通过一个简单的I2C扫描程序来测试屏幕是否被正确识别。3.2 开发环境配置与核心库安装我们选择在Arduino IDE中进行开发因为它对初学者友好库管理方便。当然使用PlatformIOVSCode插件是更专业的选择它提供了更好的项目管理和依赖管理。Arduino IDE 配置步骤安装Arduino IDE从官网下载并安装最新版。添加ESP32开发板支持打开文件-首选项在“附加开发板管理器网址”中输入https://espressif.github.io/arduino-esp32/package_esp32_index.json打开工具-开发板-开发板管理器搜索“esp32”找到由“Espressif Systems”提供的版本并安装。安装必备的库WiFi/HTTPClientESP32核心库已包含无需单独安装。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”].isT()进行安全访问。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)中指定。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数据或者增加更多传感器打造属于你自己的智能交通信息站。