1. 项目概述:用树莓派打造一块实时到离站信息板
几年前,我在一个本地社区中心做志愿者,那里有个老旧的纸质公交时刻表,信息更新不及时,经常让等车的人白等半天。当时我就想,能不能用身边现成的树莓派(Raspberry Pi)做一块电子屏,实时显示公交或火车的到站离站信息?这个想法就是今天要聊的“Pi based real-time arrivals and departures board”项目。简单说,这就是一个基于树莓派微型电脑,配合一块显示屏,通过网络获取公共交通的实时数据,并以清晰、美观的界面展示出来的信息板。
它解决的痛点非常直接:信息滞后。无论是家里的通勤者想掐点出门,还是小型办公室、咖啡馆、社区公告栏需要一个公共信息展示终端,一块能自动更新、免维护的实时信息板都非常实用。这个项目听起来有点极客,但其实技术栈非常亲民,核心就是树莓派、一个Web服务器(比如Apache)、以及处理数据和展示的PHP脚本。你不需要是嵌入式开发专家,只要对Linux命令行和Web开发有基本了解,就能跟着一步步做出来。它适合喜欢动手的开发者、创客,甚至是有点技术热情的公共交通爱好者,目标就是花最少的钱和精力,获得一个稳定、可定制且完全属于自己的实时信息显示方案。
2. 项目核心架构与方案选型
2.1 为什么选择LAMP(Linux, Apache, MySQL, PHP)栈?
在树莓派上实现一个Web应用,选择很多,比如Python的Flask/Django,或者Node.js。但我最终选择了经典的LAMP栈,特别是Apache+PHP的组合,原因有几个。首先,资源消耗与稳定性:树莓派(尤其是早期的型号)计算资源和内存有限。Apache虽然被诟病“重”,但在处理静态资源和并发连接不高的场景下(比如一块信息板,可能同时就几个内网用户访问),其成熟度和稳定性是经过时间考验的。配合PHP-FPM(FastCGI进程管理器)可以有效管理PHP进程,比用Python CGI或某些轻量级服务器更易于配置和维护。
其次,开发效率与生态:PHP在数据处理和文本渲染方面非常直接。我们的核心任务就是定时抓取API数据、解析、然后渲染成HTML页面。PHP内建的cURL函数、json_decode以及模板字符串能力,让这几步变得异常简单。市面上大量的公共交通API返回的都是JSON格式,PHP处理起来得心应手。此外,如果你想加入简单的数据缓存或日志记录,MySQL/MariaDB的集成也非常顺畅。
最后,部署与运维的简便性:在基于Debian的树莓派OS上,安装Apache、PHP、MySQL几乎就是几条apt-get命令的事。配置也都有全球开发者积累的成熟文档。整个系统可以看作一个标准的Web服务器,任何前端修改(调整CSS样式)或后端逻辑更新(修改抓取频率),都只需要更新文件并刷新浏览器即可,无需重启整个系统或重新编译,这对于后期维护和调试非常友好。
2.2 硬件选型与连接考量
硬件是项目的物理基础,选对了事半功倍。
- 树莓派型号:推荐Raspberry Pi 3B+ 或 4B。3B+性价比高,性能足够;4B的USB 3.0和更快的网络接口在处理数据时更有优势。如果追求极致低功耗和成本,Zero 2 W也行,但需注意其接口较少,可能需要额外的转接器。
- 显示屏:这是体验的关键。
- 官方触摸屏:接口简单(通过DSI连接),即插即用,但尺寸固定(7寸),价格较高。
- HDMI显示屏:选择最多,从10寸到24寸都有,可以通过HDMI线连接。建议选择带有IPS面板的型号,可视角度广,适合多人从不同角度观看。务必确认显示器供电方式,有些需要单独接电源,有些可以通过树莓派的USB口供电(功率需足够)。
- 电子墨水屏:如果追求超低功耗和类纸显示效果,可以考虑。但刷新率慢,不适合频繁更新的实时信息,更适合显示静态时刻表。
- 供电与网络:稳定的电源至关重要!建议使用官方电源或输出电流≥2.5A的优质电源适配器,供电不足会导致树莓派重启,显示中断。网络连接优先使用有线以太网,比Wi-Fi更稳定,延迟更低,对于定时抓取API数据来说可靠性更高。
- 外壳与散热:长期运行需要考虑散热。一个带有散热片或小风扇的亚克力外壳能有效防止CPU过热降频。如果放在公共场所,还需要考虑外壳的坚固性和安全性。
注意:在购买HDMI显示器时,留意其“休眠”功能。有些显示器长时间没有信号输入会自动休眠,需要修改树莓派配置或显示器设置来禁用此功能,确保屏幕常亮。
3. 软件环境搭建与核心配置
3.1 系统初始化与LAMP环境安装
首先,为树莓派安装最新的 Raspberry Pi OS Lite(无桌面版)以节省资源。烧录系统到SD卡后,首次启动通过raspi-config进行基础设置:扩展文件系统、更改密码、设置时区、启用SSH。接着,通过有线网络连接并获取IP地址,就可以用SSH客户端远程登录了。
安装LAMP环境是一系列命令:
sudo apt update && sudo apt upgrade -y sudo apt install apache2 -y sudo apt install php php-curl php-mysql php-mbstring php-xml php-gd -y sudo apt install mariadb-server -y安装Apache和PHP时,php-curl是抓取API数据的必备扩展,php-mbstring和php-xml用于处理可能的文本和XML格式数据(虽然现在JSON是主流),php-gd可能用于未来生成简单的图标。
安装完成后,验证Apache:在浏览器访问树莓派的IP地址,应看到Apache默认页面。验证PHP:创建一个info.php文件(sudo nano /var/www/html/info.php),内容为<?php phpinfo(); ?>,访问该文件应显示PHP配置信息。
3.2 Apache与PHP的关键配置调优
默认配置是为通用服务器设计的,我们需要为信息板这个单一应用做优化。
配置Apache虚拟主机(可选但推荐):在
/etc/apache2/sites-available/下创建一个新配置文件,例如arrivals.conf。这样可以将我们的应用隔离管理。<VirtualHost *:80> ServerName arrivals.local # 或你的树莓派IP DocumentRoot /var/www/html/arrivals-board <Directory /var/www/html/arrivals-board> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory> ErrorLog ${APACHE_LOG_DIR}/arrivals-error.log CustomLog ${APACHE_LOG_DIR}/arrivals-access.log combined </VirtualHost>然后启用该站点并禁用默认站点:
sudo a2ensite arrivals.conf,sudo a2dissite 000-default.conf,最后重启Apache:sudo systemctl restart apache2。调整PHP配置:编辑
/etc/php/版本号/apache2/php.ini(例如/etc/php/8.2/apache2/php.ini)。max_execution_time = 30:单个脚本最大执行时间。抓取API可能超时,可适当增大到60。memory_limit = 128M:内存限制。对于简单数据处理,128M足够,如果数据量巨大可调整。date.timezone = Asia/Shanghai:设置正确时区,这对显示时间至关重要。display_errors = Off和log_errors = On:生产环境务必关闭错误显示,防止敏感信息泄露,将错误记录到日志文件。
启用必要模块:确保
mod_rewrite(用于URL重写,未来可能用到)和headers模块已启用:sudo a2enmod rewrite headers。
3.3 数据源对接:公共交通API的选择与使用
这是项目的灵魂。你需要找到一个提供实时到离站数据的API。国内许多城市的公交集团或第三方平台(如高德、百度地图开放平台)都提供此类接口,通常需要申请API Key。
以获取某城市“123路公交车”即将到达“人民广场站”的数据为例,一个典型的请求和响应可能如下:请求:GET https://api.transit.com/realtime?route=123&stop=人民广场&key=YOUR_API_KEY响应(JSON):
{ "status": "success", "data": { "stop_name": "人民广场", "upcoming": [ { "route": "123", "destination": "火车站", "arrival_time": "2023-10-27T14:05:00+08:00", "estimated_arrival": "3", "vehicle_id": "BUS-1001" }, { "route": "123", "destination": "动物园", "arrival_time": "2023-10-27T14:15:00+08:00", "estimated_arrival": "13", "vehicle_id": "BUS-1002" } ] } }在PHP中,我们使用cURL来获取这个数据:
<?php $api_url = "https://api.transit.com/realtime?route=123&stop=人民广场&key=YOUR_API_KEY"; $ch = curl_init($api_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 仅用于测试,生产环境应验证SSL证书 curl_setopt($ch, CURLOPT_TIMEOUT, 10); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code == 200 && $response) { $data = json_decode($response, true); if ($data && $data['status'] == 'success') { $upcoming_buses = $data['data']['upcoming']; // 处理数据... } else { // 处理API返回的错误 $upcoming_buses = []; $error = 'API数据解析失败'; } } else { // 处理网络或请求错误 $upcoming_buses = []; $error = '网络请求失败 (HTTP ' . $http_code . ')'; } ?>实操心得:务必仔细阅读API文档,了解调用频率限制、数据格式、错误码。将API Key存储在环境变量或配置文件(如config.php)中,不要硬编码在脚本里。对于免费API,调用频率可能受限,需要合理设置缓存。
4. 核心功能实现与前端展示
4.1 后端数据抓取、解析与缓存策略
单纯每次页面刷新都去请求API是不现实的,既慢又容易触发频率限制。我们需要一个定时任务驱动的数据抓取和缓存机制。
创建核心数据抓取脚本:编写一个PHP脚本,比如
/var/www/html/arrivals-board/fetch_data.php。这个脚本负责调用API、解析JSON、并将处理后的数据存储起来。存储方式可以选择:- 文件缓存:将数据序列化后写入一个JSON文件。简单快速,适合单点部署。
- SQLite数据库:比文件更结构化,便于查询历史数据或管理多个车站信息。
- Redis:内存数据库,速度极快,但需要额外安装和配置。
这里以文件缓存为例:
// fetch_data.php 核心部分 function fetchAndCacheData() { // ... 上面的cURL请求代码 ... if (!empty($upcoming_buses)) { // 添加抓取时间戳 $cache_data = [ 'fetch_time' => time(), 'data' => $upcoming_buses ]; // 写入缓存文件 file_put_contents('/var/www/html/arrivals-board/cache/arrivals_cache.json', json_encode($cache_data)); file_put_contents('/var/www/html/arrivals-board/cache/arrivals_cache_backup.json', json_encode($cache_data)); // 备份 } } fetchAndCacheData();设置定时任务(Cron Job):让系统每分钟自动执行一次这个抓取脚本。
crontab -e添加一行:
* * * * * /usr/bin/php /var/www/html/arrivals-board/fetch_data.php > /dev/null 2>&1这样,数据每分钟更新一次,对于公交信息来说通常足够实时。
前端展示脚本读取缓存:负责显示页面的PHP脚本(如
index.php)不再直接请求API,而是读取本地的缓存文件。// index.php $cache_file = 'cache/arrivals_cache.json'; if (file_exists($cache_file) && (time() - filemtime($cache_file)) < 70) { // 缓存70秒内有效 $cache_content = file_get_contents($cache_file); $cached_data = json_decode($cache_content, true); $upcoming_buses = $cached_data['data']; $last_updated = date('H:i:s', $cached_data['fetch_time']); } else { // 缓存过期或不存在 $upcoming_buses = []; $last_updated = '数据更新中...'; // 可以尝试触发一次同步抓取,或仅显示错误信息 }
4.2 前端页面设计与自动刷新
前端的目标是清晰、易读、在显示屏上美观。
HTML结构:设计一个全屏显示的表格或卡片列表。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>实时公交信息 - 人民广场站</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <header> <h1>🚌 人民广场站 实时到站信息</h1> <p class="update-time">最后更新: <span id="lastUpdate"><?php echo $last_updated; ?></span></p> </header> <main> <?php if (empty($upcoming_buses)): ?> <div class="no-data">暂无即将到站车辆信息,或数据正在更新...</div> <?php else: ?> <table class="arrivals-table"> <thead>...</thead> <tbody> <?php foreach ($upcoming_buses as $bus): ?> <tr> <td class="route"><?php echo htmlspecialchars($bus['route']); ?></td> <td class="destination"><?php echo htmlspecialchars($bus['destination']); ?></td> <td class="arrival"> <?php // 计算分钟数并显示 $mins = intval($bus['estimated_arrival']); if ($mins <= 0) { echo '<span class="imminent">即将进站</span>'; } elseif ($mins <= 3) { echo '<span class="soon">' . $mins . ' 分钟</span>'; } else { echo $mins . ' 分钟'; } ?> </td> <td class="vehicle"><?php echo htmlspecialchars($bus['vehicle_id']); ?></td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif; ?> </main> <footer>...</footer> </div> <script src="script.js"></script> </body> </html>CSS样式:使用全屏布局,大字体、高对比度颜色(如黑底绿字或白底蓝字),确保在远处也能看清。隐藏滚动条,禁用文本选择。
body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; font-family: 'Arial', sans-serif; } .container { display: flex; flex-direction: column; height: 100vh; background: #111; color: #0f0; } .arrivals-table { width: 100%; border-collapse: collapse; font-size: 2.5vw; } .arrivals-table td, .arrivals-table th { padding: 20px; text-align: center; border-bottom: 1px solid #333; } .soon { color: #ff9900; font-weight: bold; } .imminent { color: #ff0000; font-weight: bold; animation: blink 1s infinite; } @keyframes blink { 50% { opacity: 0.5; } }JavaScript自动刷新:让页面定时(如每30秒)刷新数据,而无需手动刷新整个页面。使用
fetchAPI调用一个只返回数据的PHP端点(如api.php),然后更新DOM。// script.js function refreshData() { fetch('api.php') // 这个api.php只返回JSON格式的最新数据 .then(response => response.json()) .then(data => { // 更新表格中的行 updateTable(data.upcoming); document.getElementById('lastUpdate').textContent = data.last_updated; }) .catch(error => console.error('刷新数据失败:', error)); } // 每30秒刷新一次 setInterval(refreshData, 30000); // 页面加载后立即刷新一次 document.addEventListener('DOMContentLoaded', refreshData);
4.3 进阶功能:多站点支持与异常处理
基础功能完成后,可以考虑扩展。
- 多站点/多线路轮播:如果你关心多个车站的信息,可以修改
fetch_data.php,循环请求多个API,将数据合并缓存。前端则通过选项卡(Tab)或定时轮播(使用JavaScriptsetInterval切换显示的数据集)来展示。 - 网络异常处理:在抓取脚本中加入重试机制。如果第一次请求失败,等待几秒后重试1-2次。可以将失败日志写入文件,便于排查。
function fetchWithRetry($url, $maxRetries = 2) { for ($i = 0; $i <= $maxRetries; $i++) { $result = makeHttpRequest($url); // 封装好的请求函数 if ($result['success']) { return $result; } if ($i < $maxRetries) { sleep(3); // 等待3秒后重试 } } return ['success' => false, 'error' => '所有重试均失败']; } - 系统状态监控:创建一个简单的状态页面(
status.php),显示缓存文件最后更新时间、API最后成功调用时间、树莓派负载和温度等,方便远程检查。
5. 部署、优化与故障排查实录
5.1 系统部署与开机自启动
为了让信息板在树莓派开机后自动运行,我们需要将浏览器设置为全屏Kiosk模式。
- 安装轻量级桌面环境(可选):如果使用Lite版系统,需要先安装一个极简的桌面环境来运行浏览器,如
lxde或直接安装xserver-xorg和xinit。sudo apt install --no-install-recommends xserver-xorg x11-xserver-utils xinit openbox chromium-browser - 配置自动登录并启动浏览器:编辑
~/.bash_profile或创建~/.xinitrc文件。# ~/.xinitrc # 禁用屏幕保护和电源管理 xset s off xset -dpms xset s noblank # 以全屏模式启动Chromium,并隐藏警告和错误提示 chromium-browser --noerrdialogs --disable-infobars --kiosk http://localhost/arrivals-board/ - 设置自动启动X Window:编辑
/etc/systemd/system/autostart.service(需要sudo权限)。
然后启用服务:[Unit] Description=Start Arrivals Board Kiosk After=graphical.target [Service] Type=simple User=pi Environment=DISPLAY=:0 ExecStart=/usr/bin/startx /home/pi/.xinitrc Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.targetsudo systemctl enable autostart.service。
踩坑记录:Chromium在Kiosk模式下,有时会因GPU内存不足导致页面渲染问题。可以在
/boot/config.txt中增加gpu_mem=128来分配更多内存给GPU。如果遇到浏览器崩溃,可以考虑使用更稳定的Firefox ESR版本,或者使用unclutter工具隐藏鼠标指针。
5.2 性能优化与长期运行稳定性
树莓派7x24小时运行,优化很重要。
- 禁用不必要的服务:用
sudo systemctl disable关闭蓝牙、Wi-Fi(如果用有线)、Avahi-daemon等服务。 - 使用RAM磁盘存储临时文件:将频繁读写的小文件(如日志、临时缓存)挂载到内存中,减少SD卡磨损。
然后在PHP脚本中将日志路径指向这里。注意:重启后数据会丢失,只适合临时数据。sudo mkdir /tmp/ramdisk sudo mount -t tmpfs -o size=10m tmpfs /tmp/ramdisk - 监控SD卡健康:定期检查SD卡剩余空间和寿命。可以设置一个Cron任务,每周通过
df -h和sudo smartctl -a /dev/mmcblk0(如果支持)检查,并将报告发送到邮箱。 - 配置日志轮转:防止Apache或应用日志撑满磁盘。配置
/etc/logrotate.d/apache2和自定义应用日志。
5.3 常见问题与排查技巧
在实际部署中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 屏幕黑屏或无法显示 | 1. 显示器未通电或信号线松动。 2. 树莓派未启动。 3. 浏览器未成功启动。 | 1. 检查电源和HDMI线连接。 2. 观察树莓派ACT和PWR指示灯。通过SSH尝试登录。 3. 检查 autostart.service状态:sudo systemctl status autostart.service。查看Xorg日志:journalctl -u autostart。 |
| 页面显示“数据更新中”或空白 | 1. API请求失败。 2. 缓存文件权限问题。 3. Cron任务未执行。 | 1. 手动运行fetch_data.php看输出:php /path/to/fetch_data.php。检查网络和API Key。2. 确保Web服务器用户(如 www-data)对缓存目录有读写权限:sudo chown -R www-data:www-data /path/to/cache。3. 检查Cron日志: sudo grep CRON /var/log/syslog。 |
| 页面布局错乱,字体小 | CSS未正确加载或视口设置问题。 | 1. 检查浏览器开发者工具(F12)的Console和Network标签,看是否有CSS文件404错误。 2. 确认HTML中 <meta name="viewport">标签已设置。3. 检查CSS中的字体单位是否使用 vw或vh进行响应式适配。 |
| 信息更新延迟大 | 1. API本身延迟。 2. 树莓派网络慢或负载高。 3. 缓存时间设置过长。 | 1. 直接测试API响应速度。 2. 使用 htop查看CPU/内存占用,ping测试网络延迟。3. 调整Cron任务频率(如改为每30秒一次),并相应调整前端JS的刷新间隔。注意不要超过API调用频率限制。 |
| 树莓派运行一段时间后卡顿 | 1. 内存泄漏或某个进程占用过高。 2. CPU过热降频。 3. SD卡读写频繁导致性能下降。 | 1. 使用htop排序查看内存和CPU占用,重启异常进程。2. 安装散热片或风扇,使用 vcgencmd measure_temp监控温度。3. 将日志、临时文件移到RAM磁盘,减少对SD卡的写入。 |
个人调试心得:当问题出现时,日志是你的第一手资料。确保PHP错误日志(/var/log/apache2/error.log)、自定义的应用日志以及Cron任务的日志都配置好并定期查看。对于前端问题,别忘了树莓派上的浏览器也可以按F12打开开发者工具(可能需要接上键盘),这是调试JavaScript和CSS的利器。最后,保持耐心,硬件项目的调试往往需要结合软件和物理连接一起排查。