尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于ESP8266与Pushbullet的物联网通知系统实现

基于ESP8266与Pushbullet的物联网通知系统实现 1. 项目缘起从“通知焦虑”到“万物互联”的轻量级实践不知道你有没有过这样的体验人不在电脑前手机却放在卧室充电突然一个重要的邮件通知或者服务器告警弹出来你完全错过了。或者你在工作室里埋头焊接电路板、调试代码手机静音放在一边家人发来的消息、快递的电话总是事后才看到。这种“信息延迟”带来的焦虑感在需要即时响应的场景下尤其恼人。传统的解决方案无非是买个智能手表或者把手机音量调到最大、永远放在手边。但这要么成本高要么干扰大。作为一名嵌入式开发者和“懒人科技”爱好者我一直在寻找一种更优雅、更自定义的解决方案能不能让我手边正在捣鼓的、最便宜的单片机开发板变成一个无声的、个性化的信息提示中心这就是本次项目的核心动机。我手头有一块Lifely Agrumino Lemon——这是一款基于ESP8266的、专门用于农业和植物监测的趣味开发板板载了土壤湿度、温度、光线等传感器还有个小小的OLED屏幕。但我想让它“跨界”一下。同时我也希望这个方案能通用于我抽屉里吃灰的其他ESP8266开发板比如NodeMCU、Wemos D1 mini等。目标很明确让任何ESP8266设备都能通过Wi-Fi接收来自互联网的通知并以某种形式比如点亮LED、屏幕显示提醒我且整个过程要足够轻量、稳定、可定制。而实现“接收互联网通知”这个功能需要一个“推送桥梁”。我选择了Pushbullet。可能现在很多朋友更熟悉Telegram Bot或者Server酱但Pushbullet有几个难以替代的优点它极其简单API清晰跨平台支持好尤其是对浏览器和桌面端的通知同步是原生强项并且免费 tier 对于个人低频使用完全足够。它就像一个轻量级的消息队列我们只需要让ESP8266作为一个客户端去“订阅”这个队列即可。所以“Pushbullet for Lifely Agrumino Lemon and all ESP8266”这个项目本质上就是打通Pushbullet服务与ESP8266硬件之间的通道。下面我将详细拆解如何从零开始实现它其中包含的坑、技巧和扩展思路适用于所有ESP8266设备。2. 核心架构解析ESP8266如何“收听”Pushbullet在写第一行代码之前我们必须理清数据流向。整个系统的架构并不复杂但理解每个环节的选择和原理能帮你避开很多后期的坑。2.1 推送链路的选择Polling轮询 vs. WebSocket长连接Pushbullet为消息获取提供了两种主要API方式/v2/pushes端点轮询这是一个标准的HTTP GET请求。客户端我们的ESP8266定期比如每10秒、30秒向这个接口发起请求询问“我有新消息吗”服务器返回最新的推送列表。WebSocket 实时流建立一个持久化的双向连接。一旦有新的推送服务器会主动通过这个连接“推”给客户端。对于ESP8266选择哪种轮询实现简单代码逻辑直观使用普通的HTTPClient库即可。缺点是实时性差并且频繁请求会消耗更多网络流量和电量虽然ESP8266接电源无所谓电量。更重要的是Pushbullet的API有请求频率限制过于频繁的轮询会导致被限流。WebSocket实时性极高几乎是瞬间送达。但ESP8266的WebSocket客户端实现相对复杂连接稳定性需要更多处理断线重连、心跳包对内存和代码空间占用也稍大。我的选择与理由对于通知提醒这种“准实时”即可的场景晚几秒收到提醒完全可接受我优先选择轮询。理由有三第一代码复杂度低更稳定适合在资源有限的ESP8266上运行第二我们可以通过“长轮询”技巧来优化第三更容易加入各种错误处理和状态判断。本项目将基于轮询实现。2.2 Pushbullet API关键点梳理要让ESP8266能读取消息你需要一个Pushbullet的Access Token。在Pushbullet官网账户设置里可以生成。这个Token是调用所有API的钥匙务必保密。核心的API调用是GET https://api.pushbullet.com/v2/pushes?modified_after{timestamp}modified_after参数是精髓所在。我们不需要每次获取全部推送历史只需要获取上次检查之后新产生的推送。因此我们需要在ESP8266上保存一个时间戳可以是本地时间也可以是服务器返回的推送created字段里的unix时间戳。每次请求时带上这个时间戳服务器就只返回比这个时间更新的推送。这极大地减少了网络数据传输量和服务器压力。2.3 ESP8266端的职责分解我们的ESP8266固件需要完成以下循环网络连接连接本地Wi-Fi。HTTP请求构造一个包含Access Token在Authorization头中格式为Bearer your_access_token和modified_after参数的HTTPS请求。解析响应服务器返回JSON格式的数据。我们需要一个JSON解析库如ArduinoJson来解析它提取出推送的title标题、body正文和type类型note/link/file等。触发动作根据解析出的推送内容执行本地动作。例如让LED闪烁特定模式、在OLED屏幕上滚动显示信息、控制一个蜂鸣器响一下或者通过串口打印出来。更新时间戳成功处理新推送后更新本地存储的modified_after时间戳为下一次轮询做准备。休眠与延迟进行一次完整的轮询后让ESP8266进入深度睡眠Deep Sleep以省电或者简单地用delay()函数等待一段时间然后进入下一个循环。3. 硬件准备与软件环境搭建3.1 硬件清单与选型考量主控任何一款ESP8266开发板。本项目以Lifely Agrumino Lemon和NodeMCUESP-12E为例进行说明。Agrumino Lemon其特殊之处在于板载了0.96英寸OLEDSSD1306驱动、一个可编程LED、一个用于土壤探针的模拟输入以及一个LiPo电池管理芯片。我们将主要利用其OLED和LED。NodeMCU更通用我们需要外接一个LED和一个OLED屏幕可选来验证效果。输出设备用于提示LED几乎所有开发板都有板载LED通常连接在GPIO2D4上。但注意ESP8266的板载LED大多是“低电平有效”即GPIO输出LOW时点亮。这是一个初期很容易踩的坑。OLED屏幕I2C接口这是显示推送内容的绝佳方式。Agrumino Lemon已集成NodeMCU需要外接。通常使用SDAD2、SCLD1引脚。其他USB数据线、面包板、杜邦线如需外接设备。3.2 软件开发环境配置我强烈推荐使用PlatformIO基于VSCode而非传统的Arduino IDE。PlatformIO对库管理、项目结构和多平台支持要好得多特别适合管理ESP8266 RTOS SDK等项目。安装VSCode和PlatformIO插件。创建新项目选择板子类型。对于Agrumino Lemon你可能需要搜索“ESP8266”并选择一个通用型号如NodeMCU 1.0因为PlatformIO可能没有它的专属配置。关键在于正确设置引脚定义。对于NodeMCU直接选择“NodeMCU 1.0 (ESP-12E Module)”即可。配置platformio.ini这是项目的核心配置文件。[env:nodemcuv2] ; 环境名称 platform espressif8266 board nodemcuv2 framework arduino ; 使用Arduino框架最简单 monitor_speed 115200 lib_deps bblanchon/ArduinoJson^6.21.3 ; JSON解析库 thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays^4.4.0 ; OLED驱动库注意如果你使用Agrumino Lemon并且想用其专属库例如用于读取电池电压可能需要添加agrumino/Agrumino库。但为了通用性本项目将直接操作硬件引脚。获取Pushbullet Access Token登录Pushbullet官网 - Settings - Account - Create Access Token。复制并保存好我们将在代码中使用它。4. 代码实现从Wi-Fi连接到消息解析让我们分步拆解核心代码。我将以Arduino框架为例因为它最通用。4.1 网络连接与基础配置首先包含必要的库并定义你的Wi-Fi凭证和Pushbullet Token。#include ESP8266WiFi.h #include ESP8266HTTPClient.h #include WiFiClientSecure.h #include ArduinoJson.h // 你的网络配置 const char* ssid Your_WiFi_SSID; const char* password Your_WiFi_Password; // Pushbullet配置 const char* pushbulletToken 你的Access_Token_粘贴在这里; const char* pushbulletHost api.pushbullet.com; const int pushbulletPort 443; const char* pushbulletPath /v2/pushes?activetruemodified_after; // 只获取活跃的推送 // 全局变量 unsigned long lastCheckTime 0; // 用于记录上次检查的时间戳本地毫秒 unsigned long lastModified 0; // 用于存储从服务器获取的最新推送时间戳Pushbullet的unix时间戳 bool ledState LOW; const int ledPin 2; // ESP8266常见板载LED引脚GPIO2低电平点亮 void setup() { Serial.begin(115200); pinMode(ledPin, OUTPUT); digitalWrite(ledPin, HIGH); // 初始熄灭LED假设低电平有效 // 连接Wi-Fi WiFi.begin(ssid, password); Serial.print(Connecting to WiFi); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); digitalWrite(ledPin, !digitalRead(ledPin)); // 连接过程中LED闪烁 } Serial.println(\nConnected! IP address: ); Serial.println(WiFi.localIP()); digitalWrite(ledPin, HIGH); // 连接成功熄灭LED }关键点1lastModified初始为0意味着第一次请求会获取所有活跃的推送。处理完后我们会用最新推送的created时间更新它。关键点2LED引脚逻辑。这里先设置为HIGH熄灭符合“低电平有效”的假设。在实际中你需要根据你的板子确认。4.2 构造HTTPS请求与获取数据在loop()函数中我们将定期执行轮询。void loop() { // 每30秒检查一次可根据需要调整 if (millis() - lastCheckTime 30000) { lastCheckTime millis(); checkForPushes(); } // 这里可以添加其他非阻塞任务 } void checkForPushes() { // 确保Wi-Fi连接正常 if (WiFi.status() ! WL_CONNECTED) { Serial.println(WiFi not connected, attempting reconnect...); WiFi.reconnect(); return; } WiFiClientSecure client; HTTPClient https; // 设置SSL/TLS证书ESP8266 Arduino核心已内置根证书但有时需要忽略验证用于测试 client.setInsecure(); // 跳过证书验证仅用于测试生产环境应正确处理。 // 构造完整的URL包含modified_after参数 String url https:// String(pushbulletHost) String(pushbulletPath) String(lastModified); Serial.print(Requesting URL: ); Serial.println(url); if (https.begin(client, url)) { // 添加必要的HTTP头 https.addHeader(Authorization, Bearer String(pushbulletToken)); https.addHeader(Content-Type, application/json); int httpCode https.GET(); Serial.printf(HTTP Response code: %d\n, httpCode); if (httpCode HTTP_CODE_OK) { String payload https.getString(); Serial.println(Payload received.); processPushes(payload); // 解析和处理推送 } else if (httpCode 429) { Serial.println(Rate limited! Slowing down.); // 遇到频率限制可以增加延迟时间 lastCheckTime millis() - 25000; // 让下次检查更快到来5秒后 } else { Serial.printf(HTTP GET failed, error: %s\n, https.errorToString(httpCode).c_str()); } https.end(); } else { Serial.println(Unable to connect to Pushbullet server.); } }关键点1client.setInsecure()这行代码很重要。它跳过了对服务器SSL证书的严格验证。在开发测试阶段这很方便但在产品中这是一个安全风险。对于最终项目你应该考虑将Pushbullet的根证书嵌入到代码中并使用client.setCACert()来设置。这里为了简化我们先使用setInsecure()。关键点2错误处理。我们特别处理了HTTP 429请求过多错误。当遇到这个错误时我们主动将lastCheckTime回调让下一次轮询提前进行实际上是缩短了等待间隔这是一种简单的退避策略。更复杂的策略可以实现指数退避。4.3 解析JSON与触发本地动作这是最有趣的部分。我们需要从返回的JSON中提取推送内容。void processPushes(String jsonPayload) { // 使用ArduinoJson解析 DynamicJsonDocument doc(2048); // 根据返回数据大小调整建议先打印payload长度来估算 DeserializationError error deserializeJson(doc, jsonPayload); if (error) { Serial.print(F(deserializeJson() failed: )); Serial.println(error.f_str()); return; } JsonArray pushes doc[pushes]; if (!pushes.isNull()) { unsigned long newestTime lastModified; bool hasNewPush false; // 逆序遍历因为API返回的是按创建时间倒序排列最新的在前。 // 但我们顺序处理也可以重点是找到最新的时间戳。 for (JsonObject push : pushes) { const char* type push[type]; // note, link, file const char* title push[title]; const char* body push[body]; unsigned long created push[created]; // 检查是否是新推送created lastModified if (created lastModified) { hasNewPush true; if (created newestTime) { newestTime created; } // 触发提醒动作 triggerNotification(type, title, body); } } // 如果有新推送更新lastModified时间戳 if (hasNewPush) { lastModified newestTime; Serial.printf(Updated lastModified to: %lu\n, lastModified); // 可以在这里将lastModified保存到EEPROM实现断电记忆 } else { Serial.println(No new pushes.); } } }关键点我们遍历所有推送但只处理那些created时间大于我们记录的lastModified的推送。同时我们记录下这批推送里最新的时间戳用于更新lastModified。这样下次请求就只会拿到这个时间点之后的新推送。4.4 设计本地提醒动作以LED和OLED为例triggerNotification函数是硬件交互的核心。void triggerNotification(const char* type, const char* title, const char* body) { Serial.println( New Push Notification ); Serial.printf(Type: %s\n, type); Serial.printf(Title: %s\n, title ? title : (无标题)); Serial.printf(Body: %s\n, body ? body : (无正文)); Serial.println(); // 动作1LED闪烁提示 blinkLED(3, 200); // 闪烁3次每次亮灭各200ms // 动作2在OLED屏幕上显示如果连接了屏幕 displayOnOLED(title, body); // 你可以在这里添加更多动作比如控制继电器响一下蜂鸣器或者通过WS2812灯带显示特定颜色。 } void blinkLED(int times, int interval) { for (int i 0; i times; i) { digitalWrite(ledPin, LOW); // 点亮 delay(interval); digitalWrite(ledPin, HIGH); // 熄灭 delay(interval); } }OLED显示函数需要安装并配置好SSD1306库#include SSD1306Wire.h SSD1306Wire display(0x3c, SDA, SCL); // I2C地址通常是0x3cSDA/SCL引脚根据你的板子定义 // 在setup()中初始化OLED void setup() { // ... 其他初始化代码 display.init(); display.flipScreenVertically(); // 根据屏幕安装方向调整 display.setFont(ArialMT_Plain_10); display.setTextAlignment(TEXT_ALIGN_LEFT); display.drawString(0, 0, Pushbullet Ready); display.display(); } void displayOnOLED(const char* title, const char* body) { display.clear(); display.setFont(ArialMT_Plain_10); display.setTextAlignment(TEXT_ALIGN_LEFT); // 显示标题 display.drawStringMaxWidth(0, 0, 128, title ? title : Notification); // 显示正文可能需要处理换行和长文本 String displayBody body ? String(body) : ; // 简单的截断处理实际可以做得更智能滚动、分页 if (displayBody.length() 100) { displayBody displayBody.substring(0, 100) ...; } display.drawStringMaxWidth(0, 20, 128, displayBody); display.display(); // 显示5秒后清屏或者保留直到下一条推送 delay(5000); display.clear(); display.display(); }5. 避坑指南与实战优化按照上面的步骤一个基础版本应该能跑通了。但真正让项目稳定、可靠还需要处理以下这些我踩过的坑。5.1 网络稳定性与错误恢复ESP8266的Wi-Fi连接在复杂环境中并不总是稳定的。你的代码必须能处理断线重连。心跳与重连除了在每次请求前检查WiFi.status()建议在loop()中增加一个独立的看门狗定时器定期比如每60秒检查连接状态如果断开则尝试重连。可以使用WiFi.disconnect()然后WiFi.begin()进行强制重连。非阻塞设计delay()在等待网络响应时会阻塞整个程序。对于需要更复杂交互的项目可以考虑使用非阻塞的定时器如Ticker库或者状态机来管理轮询周期让ESP8266在等待期间还能做其他事比如读取传感器。5.2 时间戳的管理与持久化lastModified时间戳是保证不重复处理消息的关键。目前它存储在RAM中一旦设备重启或断电就会丢失导致重新获取所有历史推送。使用EEPROM保存将lastModified写入ESP8266的EEPROM。每次启动时从中读取每次更新后写入。注意EEPROM有写入寿命约10万次不要每次轮询都写。#include EEPROM.h #define EEPROM_SIZE 4 // 只需要4字节存储一个unsigned long void setup() { EEPROM.begin(EEPROM_SIZE); // 从地址0读取lastModified EEPROM.get(0, lastModified); // 如果读取的值异常如第一次运行可以重置为0 if (lastModified 4294967295) { // 简单校验 lastModified 0; } } // 在更新lastModified的地方 void updateLastModified(unsigned long newTime) { if (newTime ! lastModified) { lastModified newTime; EEPROM.put(0, lastModified); EEPROM.commit(); // 必须调用commit } }使用文件系统LittleFS/SPIFFS对于需要存储更多数据比如缓存多条推送内容的情况使用文件系统是更好的选择。5.3 资源限制与内存管理ESP8266的内存非常有限通常~80KB用户可用RAM。大JSON响应或内存泄漏会导致崩溃。精确分配JsonDocument大小使用serializeJsonPretty(doc, Serial)打印一次完整的响应查看其大小然后为DynamicJsonDocument分配一个略大的值如数据大小20%。过小会解析失败过大会浪费内存。及时释放内存String对象在循环中拼接容易产生内存碎片。在处理HTTP响应时如果可能尝试使用Stream接口直接解析JSON而不是先读到String里。对于ArduinoJson确保doc在函数结束时离开作用域被自动销毁。5.4 Pushbullet API的限制与策略免费版Pushbullet API有每月500次请求的限制对于每分钟轮询一次每天1440次显然不够。所以我们的轮询间隔不能太短。合理设置轮询间隔根据通知的紧急程度设置为30秒、1分钟甚至5分钟都是合理的。这能大幅降低请求次数。使用modified_after参数这是节省请求配额的关键确保每次只拉取增量数据。处理429错误如前所述代码中已经实现了简单的退避。更完善的策略是当收到429响应时读取响应头中的Retry-After字段如果有并据此设置一个更长的等待时间。5.5 安全性考量Access Token绝对不能硬编码在代码里提交到公开的Git仓库。有几种方法使用const char*定义但只在本地编译。将敏感信息放在一个单独的secrets.h头文件中并将该文件加入.gitignore。使用Wi-Fi Manager库如WiFiManager的配置模式在设备首次启动时通过网页让用户输入Wi-Fi密码和Pushbullet Token并保存到文件系统。SSL证书验证如前所述生产环境应避免setInsecure()。你需要获取Pushbullet API的根证书或其证书链并将其以const char*字符串的形式嵌入代码使用client.setCACert()进行设置。6. 扩展思路让ESP8266推送通知更有趣基础功能实现后我们可以玩出更多花样。6.1 通知过滤与分类不是所有推送都值得让LED狂闪。可以在processPushes函数中添加过滤逻辑。// 只处理来自特定设备或包含关键字的推送 bool shouldNotify(const char* title, const char* body) { String titleStr String(title); String bodyStr String(body); // 例如只关注标题包含“[Alert]”或正文包含“error”的推送 if (titleStr.indexOf([Alert]) ! -1 || bodyStr.indexOf(error) ! -1) { return true; } return false; } // 在triggerNotification前调用判断 if (shouldNotify(title, body)) { triggerNotification(type, title, body); }6.2 丰富提醒方式多色LED/WS2812使用RGB LED或灯带用不同颜色代表不同应用的通知比如微信-绿色邮件-蓝色服务器告警-红色。蜂鸣器旋律为不同类型的推送设计不同的“叮咚”声。电子墨水屏e-ink功耗极低适合长期显示最后一条重要通知比如天气预报或日程提醒。舵机或物理装置来个“拍一拍”小木鱼或者让一个旗帜升起来硬件交互的想象力是无穷的。6.3 双向通信从ESP8266发送推送Pushbullet API也支持发送推送。这意味着你的ESP8266可以主动向你手机发消息例如当土壤湿度低于阈值时Agrumino Lemon可以给你手机发一条推送“该浇水了”void sendPushbulletNote(const char* title, const char* body) { WiFiClientSecure client; HTTPClient https; client.setInsecure(); // 同上生产环境需改 https.begin(client, https://api.pushbullet.com/v2/pushes); https.addHeader(Authorization, Bearer String(pushbulletToken)); https.addHeader(Content-Type, application/json); DynamicJsonDocument doc(256); doc[type] note; doc[title] title; doc[body] body; String requestBody; serializeJson(doc, requestBody); int httpCode https.POST(requestBody); // ... 处理响应 https.end(); }6.4 集成到自动化平台Home Assistant, Node-RED你可以将ESP8266作为一个通用的“通知接收终端”集成到更大的智能家居系统中。在Node-RED中使用node-red-contrib-pushbullet节点接收推送然后通过MQTT将消息发布到特定主题。ESP8266订阅这个MQTT主题即可。这样你就解耦了Pushbullet和硬件可以通过Node-RED做更复杂的消息路由和过滤。在Home Assistant中可以通过RESTful传感器集成来模拟这个功能或者直接使用ESPHome固件在其中编写自定义组件来与Pushbullet API通信。7. 针对特定板型Agrumino Lemon的适配Agrumino Lemon的引脚定义与NodeMCU略有不同且它有自己的库来方便使用其硬件功能。如果你想使用其原生库可以这样调整安装Agrumino库在PlatformIO的lib_deps中添加agrumino/Agrumino。初始化与使用#include Agrumino.h Agrumino agrumino; void setup() { agrumino.setup(); agrumino.turnBoardOn(); // 打开板载电源管理 } void triggerNotification(...) { // 使用板载LED agrumino.ledToggle(); // 它有自己控制LED的函数 // 使用板载OLEDAgrumino库可能封装了也可能需要直接操作Adafruit_SSD1306 // 通常它暴露了 display 对象 agrumino.display.clearDisplay(); agrumino.display.setTextSize(1); agrumino.display.setCursor(0,0); agrumino.display.println(title); agrumino.display.display(); }具体请参考Agrumino的官方示例。核心逻辑Wi-Fi连接、HTTP请求、JSON解析是完全通用的只需替换硬件操作部分即可。这个项目就像给你的旧ESP8266开发板赋予了新的生命让它从一个简单的联网模块变成了一个连接你和数字世界的物理桥梁。它成本极低可定制性极高并且充满了学习的乐趣。从网络请求到JSON解析从硬件IO到状态管理几乎涵盖了物联网入门的所有核心知识点。希望这份详细的指南能帮你顺利搭建起来并启发你做出更有趣的变种。
返回列表