拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于树莓派与Telegram Bot的可靠电流监控系统设计与实现

1. 项目缘起一个被“转圈圈”逼出来的硬件项目最近在折腾一个物联网项目需要实时监测一块太阳能板的充电电流。数据采集和本地处理都搞定了但每次想看数据都得跑到设备跟前或者连上串口调试助手实在麻烦。我就琢磨着能不能让这些数据自己“跑”到我的手机上随时随地都能看一眼。这个念头一出来我首先想到的就是Telegram Bot。原因很简单它跨平台手机、电脑都能用通知推送及时几乎无延迟而且开发门槛极低用个简单的HTTP请求就能把消息发出去。对于这种轻量级的远程监控需求简直是量身定做。于是我很快用Python写了个脚本让树莓派定时读取电流传感器的数据然后通过Telegram Bot API把读数发到我的私人频道里。然而理想很丰满现实很骨感。项目跑起来没多久我就遇到了那个经典的“Telegram进不去了一直转圈圈”的问题。尤其是在一些网络环境不那么稳定的地方Bot发送消息的请求经常会超时失败。更头疼的是我的脚本是“只发不问”的模式一旦发送失败数据就丢了我根本不知道在某个关键时间点电流是不是出现了异常的波动或中断。这种不确定性对于监控系统来说是致命的。你无法区分是网络抽风了还是设备真的出了故障。正是这个痛点催生了“Telegram Supported Current Probe”这个项目。它的核心目标不再是简单地“发送数据”而是构建一个具备双向通信能力和本地缓存机制的可靠电流监控终端。“Supported”在这里有两层含义一是利用Telegram作为主要的人机交互与远程通知通道二是当这个通道不可用时系统必须有足够的能力“支撑”自己保证监控不中断数据不丢失并在恢复后能补报或告警。2. 系统架构设计从“发报机”到“智能终端”最初的方案只是一个简单的“发报机”读取传感器 - 格式化消息 - 调用API发送。为了达到“Supported”的可靠性整个架构需要重新设计。新的系统架构核心围绕“状态感知”、“队列缓冲”和“断链续传”这三个能力展开。整个系统运行在一个树莓派Zero W上它功耗低、体积小非常适合嵌入式监控场景。系统的软件架构可以分为以下几个层次数据采集层负责与硬件传感器对话。我选用的是基于ACS712的电流传感器模块它通过模拟电压输出对应电流值。树莓派通过GPIO上的ADC模数转换芯片如ADS1115以获得比板载ADC更高的精度定期读取这个电压值。数据处理与缓存层这是系统的“中枢神经”。采集到的原始数据会在这里进行校准、滤波例如使用滑动平均滤波消除毛刺并转换成带有时间戳的规范格式。处理后的数据包不会立即发送而是被推入一个内存队列中。同时一个独立的线程或进程负责消费这个队列尝试通过Telegram Bot API发送数据。通信管理层这是实现“Supported”的关键。它包含几个核心状态在线状态持续检测到Telegram API的可达性。可以通过定期发送心跳包或捕获发送函数的异常来实现。发送队列存储待发送的数据包。我选择使用Python的queue.Queue它本身是线程安全的简化了编程。本地持久化缓存当检测到网络异常发送失败时通信管理器不再尝试重发避免阻塞主线程而是将失败的数据包包括时间戳和读数写入一个本地的SQLite数据库文件中。这个数据库文件就充当了“黑匣子”的角色。恢复与补报机制当网络恢复检测到API重新可达后系统会首先检查本地缓存数据库。如果其中有未发送的数据则会按照时间顺序逐一取出并重新尝试发送。同时它会发送一条特殊的汇总通知给我例如“网络已恢复。补发过去2小时内缺失的3条读数。”人机交互层完全通过Telegram Bot实现。我不仅可以被动接收定时推送的电流读数还可以主动向Bot发送查询指令如/status查看系统状态当前读数、队列长度、缓存数据量、网络状态、/history 5获取最近5条记录甚至/calibrate在特定情况下触发校准流程需配合硬件操作。这个架构的转变使得整个系统从一个脆弱的“数据线”变成了一个健壮的“数据节点”。Telegram不再是唯一的数据出口而是变成了一个优先的、友好的交互界面。真正的数据保管和逻辑处理职责落在了本地的树莓派上。3. 硬件选型与电路连接要点电流探测的准确性和稳定性是硬件部分的基础。市面上常见的电流探测方案主要有分流电阻运放、霍尔效应传感器如ACS712、电流互感器等。针对我这个太阳能板充电监控的场景直流、非隔离、量程在0-5A左右我选择了ACS712ELCTR-05A这款霍尔效应电流传感器模块。为什么是ACS712首先它基于霍尔效应实现了电气隔离。初级侧被测电流流经的引脚和次级侧输出信号的芯片部分没有直接的电气连接这大大提高了安全性避免测量电路干扰主回路或发生共地问题。其次它模块化程度高通常市面上买的模块已经集成了必要的滤波电容和分压电阻输出是标准的模拟电压VCC/2为零点可以直接接入MCU的ADC省去了自己设计放大滤波电路的麻烦。最后它的精度典型值1.5%和响应速度对于我的应用来说完全足够。电路连接上的几个坑供电噪声ACS712和树莓派的ADC参考电压对电源噪声都很敏感。务必使用一个干净的LDO低压差线性稳压器为它们供电并在电源引脚就近放置去耦电容如100nF和10uF并联。如果直接从树莓派的5V或3.3V取电电机或其他大功率外设的启停可能会在读数上引入明显的毛刺。采样电阻与滤波树莓派本身的GPIO不具备高精度ADC所以外接ADC芯片是必须的。我选用ADS1115它是16位分辨率、I2C接口的ADC比树莓派自带的12位ADC如果可用精度高很多。在ADS1115的输入通道连接ACS712输出引脚上建议串联一个小的限流电阻如100Ω并并联一个0.1uF的电容到地构成一个简单的RC低通滤波器可以滤除一部分高频噪声。校准与零点漂移ACS712的零点即0A电流时的输出电压标称是VCC/2但受温度和个体差异影响会有微小漂移。因此上电初始化后的第一件事必须是自动零点校准。我的做法是在系统启动后、负载未开启的安静状态下连续采样100-200个点计算其平均值并将这个值作为当前的“软件零点”存储起来。后续的所有读数都要先减去这个零点值再进行比例换算。量程与过载保护我选的05A版本测量范围是±5A。虽然太阳能板充电电流一般不会超过这个值但也要考虑瞬时冲击。可以在ACS712的输入引脚上并联一个双向TVS管瞬态电压抑制二极管进行过压保护。同时在软件里设置阈值告警如果读数持续超过量程的90%就通过Telegram发送紧急告警。具体的接线很简单ACS712模块的VCC接5VGND接地OUT引脚接ADS1115的A0通道。ADS1115的VDD接3.3V与树莓派逻辑电平匹配GND接地SCL和SDA分别接树莓派的I2C时钟线和数据线。4. 核心软件实现状态机、队列与缓存软件部分是整个项目的灵魂我用Python来实现主要依赖几个库python-telegram-bot用于优雅地处理Bot交互、smbus2或Adafruit-ADS1x15用于操作ADS1115、sqlite3内置于Python用于本地缓存、threading和queue用于并发处理。4.1 数据采集与滤波线程这是一个独立的线程以固定的频率例如1Hz运行。import time import board import busio import adafruit_ads1x15.ads1115 as ADS from adafruit_ads1x15.analog_in import AnalogIn class CurrentSampler(threading.Thread): def __init__(self, data_queue): super().__init__() self.data_queue data_queue # 用于存放采样数据的队列 self.running True self.zero_offset 0.0 # 软件零点 self.calibrate_zero() # 启动时校准零点 def calibrate_zero(self): # 采集若干样本计算零点平均值 samples [] for _ in range(200): samples.append(self._read_raw_voltage()) time.sleep(0.01) self.zero_offset sum(samples) / len(samples) print(fZero offset calibrated to: {self.zero_offset:.3f}V) def _read_raw_voltage(self): # 通过ADS1115读取原始电压此处为示例需具体实现 i2c busio.I2C(board.SCL, board.SDA) ads ADS.ADS1115(i2c) chan AnalogIn(ads, ADS.P0) return chan.voltage def run(self): while self.running: raw_v self._read_raw_voltage() # 减去零点并根据ACS712灵敏度例如185mV/A转换为电流值 current_a (raw_v - self.zero_offset) / 0.185 # 简单的滑动平均滤波窗口大小为5 if not hasattr(self, filter_window): self.filter_window [] self.filter_window.append(current_a) if len(self.filter_window) 5: self.filter_window.pop(0) filtered_current sum(self.filter_window) / len(self.filter_window) timestamp time.time() data_packet {ts: timestamp, current: filtered_current} # 将数据包放入队列供发送线程消费 try: self.data_queue.put(data_packet, blockFalse) except queue.Full: # 如果队列满了丢弃最旧的数据或记录日志 pass time.sleep(1) # 1秒采样一次这个线程确保了数据源的稳定和初步处理。将数据放入队列实现了生产者-消费者模式解耦了数据采集和网络发送避免因网络延迟导致采样周期失控。4.2 通信管理器的状态机实现通信管理器是另一个核心线程它维护着一个简单的状态机ONLINE、OFFLINE、RECOVERING。class CommManager(threading.Thread): def __init__(self, data_queue, bot_token, chat_id): super().__init__() self.state ONLINE self.data_queue data_queue self.bot_token bot_token self.chat_id chat_id self.failed_queue queue.Queue() # 用于临时存放发送失败的数据包 self.db_conn sqlite3.connect(current_cache.db) self._init_db() self.last_heartbeat_ok time.time() def _init_db(self): # 创建缓存表 cursor self.db_conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS failed_messages (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL, current REAL, added_time DATETIME DEFAULT CURRENT_TIMESTAMP) ) self.db_conn.commit() def _send_to_telegram(self, message): # 尝试发送消息到Telegram url fhttps://api.telegram.org/bot{self.bot_token}/sendMessage payload {chat_id: self.chat_id, text: message} try: response requests.post(url, jsonpayload, timeout10) if response.status_code 200: return True else: print(fSend failed with status: {response.status_code}) return False except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: print(fNetwork error: {e}) return False def _save_to_cache(self, data_packet): # 将失败的数据包存入SQLite cursor self.db_conn.cursor() cursor.execute(INSERT INTO failed_messages (timestamp, current) VALUES (?, ?), (data_packet[ts], data_packet[current])) self.db_conn.commit() def _recover_from_cache(self): # 从缓存中恢复并发送数据 cursor self.db_conn.cursor() cursor.execute(SELECT id, timestamp, current FROM failed_messages ORDER BY timestamp ASC) rows cursor.fetchall() if not rows: return 0 success_count 0 for row in rows: msg_id, ts, curr row message f[Recovered] {time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(ts))} - Current: {curr:.2f}A if self._send_to_telegram(message): cursor.execute(DELETE FROM failed_messages WHERE id?, (msg_id,)) success_count 1 time.sleep(0.5) # 避免发送过快 else: break # 如果发送再次失败停止恢复等待下次 self.db_conn.commit() return success_count def run(self): while True: if self.state ONLINE: # 尝试发送心跳或处理队列 if not self._send_heartbeat(): self.state OFFLINE print(Network down, switching to OFFLINE mode.) continue # 处理主队列和失败队列 self._process_queues() elif self.state OFFLINE: # 离线状态持续检查网络 if self._check_network(): self.state RECOVERING print(Network back, switching to RECOVERING mode.) else: time.sleep(30) # 检查间隔长一些 elif self.state RECOVERING: # 恢复状态先补发缓存数据 recovered self._recover_from_cache() if recovered 0: self._send_to_telegram(fNetwork recovered. Successfully resent {recovered} cached readings.) self.state ONLINE time.sleep(5)这个状态机逻辑清晰地区分了系统的不同行为模式。在ONLINE状态下它积极消费队列并发送数据一旦检测到失败立即切换到OFFLINE状态并将数据转入本地缓存当网络恢复时进入RECOVERING状态优先处理积压数据确保历史记录的连续性然后再回归正常ONLINE状态。这种设计极大地提升了系统面对不稳定网络时的韧性。4.3 Telegram Bot交互功能的扩展除了被动接收数据主动查询让这个探头变得“智能”。使用python-telegram-bot库可以很方便地实现。from telegram.ext import Updater, CommandHandler, MessageHandler, Filters def start(update, context): update.message.reply_text(Hi! I am your current monitor bot. Use /status, /history num, or /calibrate.) def status(update, context): # 获取系统状态当前电流、队列长度、缓存数量、运行时间 current_reading get_latest_current() # 从共享变量或队列中获取最新读数 q_size data_queue.qsize() cache_count get_cache_count_from_db() uptime time.time() - start_time message (f⚡ Live Current: {current_reading:.2f} A\n f Queue Size: {q_size}\n f Cached Readings: {cache_count}\n f⏱️ Uptime: {uptime/3600:.1f} hours) update.message.reply_text(message) def history(update, context): try: num int(context.args[0]) if context.args else 5 num min(num, 20) # 限制最大查询数量 except ValueError: num 5 # 从数据库或内存中获取最近N条记录 history_data get_recent_readings_from_db(num) if history_data: msg_lines [f{ts}: {curr:.2f}A for ts, curr in history_data] update.message.reply_text(\n.join(msg_lines)) else: update.message.reply_text(No history data available.) def calibrate(update, context): # 这是一个危险操作需要确认 update.message.reply_text(Warning: This will recalibrate the zero point. Ensure no current is flowing. Type CONFIRM to proceed.) # 通常需要更复杂的状态机来处理这类交互确认 # 在主程序中设置Bot updater Updater(tokenYOUR_BOT_TOKEN, use_contextTrue) dp updater.dispatcher dp.add_handler(CommandHandler(start, start)) dp.add_handler(CommandHandler(status, status)) dp.add_handler(CommandHandler(history, history)) dp.add_handler(CommandHandler(calibrate, calibrate)) # 启动Bot的轮询线程与主程序并发运行 updater.start_polling()这样用户与探头的交互就变得非常直观和强大。/status命令让我对系统健康状况一目了然/history让我可以回溯数据而/calibrate则为后期维护提供了便利尽管需要谨慎使用。5. 部署、优化与实测心得将代码部署到树莓派上需要确保其能开机自启动。我使用systemd来管理这个Python服务。创建一个current-probe.service文件放在/etc/systemd/system/下内容如下[Unit] DescriptionTelegram Supported Current Probe Afternetwork.target [Service] Typesimple Userpi WorkingDirectory/home/pi/current_probe ExecStart/usr/bin/python3 /home/pi/current_probe/main.py Restarton-failure RestartSec10 [Install] WantedBymulti-user.target然后使用sudo systemctl enable current-probe.service启用它。这样即使树莓派意外重启监控服务也能自动恢复。在实际运行中我积累了几点非常重要的心得队列大小的权衡内存队列 (queue.Queue) 的大小需要仔细设置。设置太小网络短暂波动就可能导致丢数据设置太大则会消耗过多内存。我的经验是根据采样频率和你能容忍的“内存缓存时长”来定。例如1Hz采样希望缓存最多5分钟的数据队列大小可以设为 300。同时一定要处理队列满的情况我的策略是记录一条警告日志并丢弃最旧的一个数据包确保程序不会卡死。SQLite数据库的维护缓存数据库会随着时间增长。我增加了一个定时清理任务例如每天一次删除所有已成功补发超过7天的记录防止数据库文件无限膨胀。同时定期对数据库执行VACUUM;命令可以压缩文件回收空间。网络检测的“聪明”策略最初我简单地用“发送失败”来判断离线。但这可能导致频繁的状态切换例如一次短暂的超时。后来我改成了“心跳包失败计数器”的策略。每隔30秒发送一个极短的心跳消息如“.”连续3次失败才判定为离线同样连续3次心跳成功才判定为恢复在线。这增加了状态转换的“迟滞”避免了网络抖动造成的误判。功耗与散热树莓派Zero W本身功耗不高但长期运行仍需注意。我将其放在一个小散热壳里并禁用了一些不用的外设如HDMI、LED灯。对于太阳能供电场景这一点尤其重要。测量整个系统的待机电流有助于计算所需的电池或太阳能板容量。数据可视化补充Telegram推送的是即时数据和文本历史。对于长期趋势分析我额外增加了一个功能将数据同时上传到一个简单的InfluxDB时序数据库然后用Grafana搭建了一个仪表盘。这样我就能看到电流随时间变化的曲线图分析充电效率。Telegram负责告警和即时查看Grafana负责深度分析两者互补。经过几周的持续运行这个“Telegram Supported Current Probe”完美地解决了最初“转圈圈”导致的数据丢失焦虑。即使在网络不稳定的时段我也可以在恢复后收到完整的补发数据报告心里非常踏实。它不再是一个依赖单一云服务的脆弱玩具而是一个真正可靠、自持的工业级监控终端原型。这个项目的核心思想——本地缓存、状态感知、断点续传——完全可以迁移到其他需要远程监控的传感器项目上比如温度、湿度、电压甚至是门窗开关状态。关键在于要把智能和可靠性构建在边缘设备本身而云服务只作为高效的展示和通知通道。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门