ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

自制低成本短信转发器:ESP32+SIM800C全流程解析

自制低成本短信转发器:ESP32+SIM800C全流程解析 1. 先泼一盆冷水为什么你需要一个短信转发器先说个场景。去年年底我换了双卡双待的主力机工作号和生活号分开了但日常刷手机的重心基本全在生活号这台机器上。工作号那张卡偶尔会收到验证码、银行通知、服务器告警短信我人又不可能一天到晚盯着另一台手机。漏掉一条验证码的后果轻则多等一分钟重发重则卡在某个流程上干着急。刚开始我想过买现成的方案比如那种插SIM卡的短信转发硬件或者某些云平台提供的虚拟号服务。但研究了一圈发现要么是封闭的黑盒不知道数据往哪传、有没有被留存要么是按月收费一条短信转发还要抽成再要么就是硬件本身贵而且绑定厂商的App哪天厂商不更新了设备就是电子垃圾。后来才意识到这件事本质上特别简单一张SIM卡收到短信把它通过网络转发到另一台设备或者即时通讯工具上仅此而已。既然硬件和云服务都不那么可控那我干脆自己做一个开源的自托管方案。这不只是省钱的问题——短信里经常带验证码属于敏感信息自己掌握转发链路至少知道数据走到哪了。这个项目做完之后我把代码开源了GitHub 上地址是 https://github.com/lanr。很多朋友私信问是怎么实现的、用的什么板子、稳定性怎么样今天这篇就把整个方案拆开聊一聊。如果你也有备用机收短信但不想随身带着的需求或者想在嵌入式设备上跑一个实用的网络服务这篇文章应该能给你一条可以直接照抄的路径。提示本文用到的硬件、软件均为开源或通用方案通信链路完全自主可控不依赖任何第三方短信转发平台。在动手之前先把需求理清楚。我要做的转发器需要满足几个条件能插实体SIM卡而不是依赖eSIM或者云号码因为备用卡就是实体卡。能联网把收到的短信通过Wi-Fi或以太网发出。接收端要方便——最好能直接推到微信、Telegram、钉钉这类日常在用的工具上。硬件成本控制在几十块钱级别坏了能随时换。源码要开放不藏着掖着。这几个条件一框方案基本就定了带GSM模块的嵌入式开发板加一个开源的消息推送通道。下面是我做完之后的整体架构。层级组件作用硬件层ESP32 开发板主控处理串口数据、联网、执行转发逻辑通信层SIM800C 模块插SIM卡接收短信通过串口与ESP32通信软件层MicroPython 固件简化开发串口解析、HTTP请求、状态管理推送层Server酱 / 企业微信群机器人把短信内容以消息形式推到手机App这张表看起来很简单但每一层都有不少坑。比如SIM800C供电不稳会直接重启ESP32的串口电平不兼容可能烧模块推送接口的Token泄漏会导致别人也能给你发消息。这些细节后面挨个讲。2. 硬件选型与电路连接为什么别用树莓派也别用纯Arduino很多第一次做这类项目的朋友会问为什么不用树莓派树莓派当然也能做——插个USB短信猫装个Linux跑个脚本就完事。但你要考虑一个实际问题这个转发器是7x24小时挂在那的树莓派的功耗、体积、价格都比一块ESP32高一个量级。而ESP32自带Wi-Fi主频也够跑HTTP请求价格十几块钱坏了不心疼。2.1 主控板选择ESP32 的不可替代性ESP32 几乎是这类自托管小硬件的事实标准。理由有几条自带2.4G Wi-Fi和蓝牙不需要额外接网卡。有多个UART串口可以跟GSM模块通信。支持MicroPython开发效率比写C/C高很多。3.3V逻辑电平和SIM800C的TTL电平兼容这一点需要确认下面细说。社区资料极其丰富踩过坑的人多解决方案也就多。同类的替代品有ESP8266但它只有单核、内存小跑MicroPython加串口长数据解析会吃力还有STM32性能强但需要自己搭Wi-Fi方案复杂度一下就上去了。所以我选ESP32具体型号是ESP32 DevKitC V44MB Flash版本。2.2 GSM模块选型SIM800C 为什么比SIM7600更合适GSM模块我选的是SIM800C。很多人会问SIM800C是2G模块现在运营商都退2G网了还能用吗这个问题我实测过国内三大运营商在大部分地区的2G基站还在保留尤其用于物联网设备但确实有些城市核心区域的2G信号已经变弱。所以如果你是放在家里固定使用先确认你家附近2G信号强度如果你是放在车里或者随身带建议换成SIM7600系列支持4G Cat-1价格贵一些但更保险。我的场景是放在家里书架上实测2G信号稳定短信收发正常所以果断用了SIM800C。它有这几个优点便宜模块加转接板一共二三十块钱。成熟AT指令集文档到处都是网上案例多。功耗适中待机电流很小适合长期挂机。短信收发走串口逻辑简单直接。如果你用4G模块AT指令会稍有区别比如网络注册指令从ATCREG变成ATCEREG但整体设计思路一致代码里也能平滑替换。2.3 接线图与电平匹配接线之前必须确认一个关键点SIM800C的串口逻辑电平是TTL 3.3V/5V兼容还是必须3.3V我用的这块SIM800C转接板标注的是TTL电平兼容3.3V/5V但我仍然建议串一个1kΩ电阻在TX线路上做分压保护。因为ESP32的GPIO引脚不是完全5V耐受的万一模块输出实际是5V电平直接接有可能烧引脚。电阻一加稳得很。具体接线如下ESP32 引脚SIM800C 引脚说明GPIO16 (UART2 TX)RX发送AT指令给模块GPIO17 (UART2 RX)TX接收模块返回的数据GNDGND必须共地5VESP32板载VCC模块供电模块峰值电流较大时可能不稳---电源适配器5V/2A建议外部供电给SIM800C这里有个特别容易踩的坑SIM800C在打电话或搜网时峰值电流能达到2A如果和ESP32共用同一个USB口供电电压一掉模块就自动重启。我一开始就是这么干的结果每次发短信就重启折腾了两天才反应过来。解决方案是给SIM800C单独供电用5V/2A的电源适配器接模块的VCC脚同时模块的GND和ESP32的GND连在一起。如果你手上没有单独电源至少要用一个大电容1000μF以上并在供电线上能缓一下峰值电流的问题。另外天线一定要接。别以为室内信号好就能不接天线SIM800C没有天线根本注册不上网络。我第一次就忘了插天线卡在ATCREG?一直返回0未注册排查了很久才意识到是天线的问题。2.4 硬件成本清单把整个硬件成本列出来方便你参考。物料参考价格元备注ESP32 DevKitC V415-25国产兼容板更便宜SIM800C模块含转接板20-35注意区分2G/4G版本5V/2A电源适配器10-15非必需但强烈建议天线3-8部分模块套装自带SIM卡月租因人而异建议用老卡或低月租卡合计50-100不含SIM卡月租这个成本比市面上那种几百块的短信转发硬件低了一半还多更重要的是完全自主可控。3. MicroPython 开发环境搭建与底层驱动封装硬件电路接好之后接下来就是软件部分。我不建议你用Arduino IDE C语言去写这个项目虽然也能跑但串口解析字符串、JSON组装、HTTP请求这些操作在C里都要手动处理代码量翻倍。MicroPython把这些琐碎的事封装好了能让你把精力聚焦在业务逻辑上。3.1 烧录MicroPython固件首先给ESP32烧录MicroPython固件。这一步有几个细节值得注意。从官网下载匹配你板子的固件我用的是ESP32_GENERIC系列的esp32-20230426-v1.20.0.bin。烧录工具我用的是esptool.py命令行操作# 进入bootloader模式 # 大多数板子按BOOT键再上电即可或通过命令行 esptool.py --port /dev/ttyUSB0 --baud 460800 erase_flash esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 esp32-20230426-v1.20.0.bin这里有两个容易出错的地方如果你用的是Windows端口通常是COM3或COM4别选错。烧录前必须擦除Flash否则新固件和旧数据冲突可能启动崩溃。烧录完成之后用串口工具连接波特率115200应该能看到提示符说明MicroPython已经跑起来了。3.2 用脚本初始化SIM800C从发送AT指令到处理阻塞GSM模块的核心操作都通过AT指令完成。MicroPython里用UART对象发指令、收回复。但AT指令有个特点模块处理需要时间比如搜网可能要几十秒发短信要几秒。所以驱动代码里必须处理等待回复的逻辑。我封装了一个最简单的指令函数from machine import UART, Pin import time uart UART(2, baudrate9600, txPin(16), rxPin(17)) uart.init(bits8, parityNone, stop1, timeout500) def send_at(cmd, wait0.5, expectOK): uart.write((cmd \r\n).encode()) time.sleep(wait) data uart.read() if data: print(Response:, data.decode(errorsignore)) if expect in data.decode(errorsignore): return True return False # 初始化流程 send_at(AT) # 测试模块是否应答 send_at(ATCPIN?) # 检查SIM卡是否识别 send_at(ATCREG?) # 检查网络注册状态 send_at(ATCMGF1) # 设置短信为文本模式 send_at(ATCNMI2,1,0,0,0) # 新短信主动上报挨个说明AT返回OK说明串口通信正常。ATCPIN?返回CPIN: READY说明SIM卡没被锁。ATCREG?返回第二个参数是1或5表示注册到了本地网络。ATCMGF1把短信设置成文本模式否则收到的是PDU格式的二进制解析起来麻烦。ATCNMI2,1,0,0,0是关键设置成新短信来的时候主动通过串口推送。这样ESP32不需要轮询短信列表效率高很多。初始化完成之后模块会开始主动往串口丢数据格式类似CMTI: SM,3表示新短信存储在第3个位置。这时候需要用ATCMGR3去读取短信内容。3.3 解析短信内容与编码问题短信内容如果是纯英文直接ASCII解析就完事。但我们日常收到的验证码短信都是中文的这里就有一个坑SIM800C文本模式返回中文时可以是GSM7编码或UCS2编码。UCS2就是Unicode的16位表示每两个字节对应一个汉字例如验证码的UCS2编码是6A8C 8BC1 7801。从串口读回来是一串十六进制字符需要自己转成可读文本。这一步我在代码里这样处理def decode_ucs2(hex_str): hex_str hex_str.replace( , ).strip() bytes_data bytes.fromhex(hex_str) try: return bytes_data.decode(utf-16-be) except: return hex_str读取短信的完整逻辑def read_sms(index): send_at(ATCMGR str(index), wait0.8) # 返回格式CMGR: REC UNREAD,8613800138000,,24/05/13,10:30:2032 # 换行后跟短信内容 data uart.read() if data: raw data.decode(errorsignore) lines raw.split(\r\n) if len(lines) 2: header lines[0] body lines[1] # 解析手机号 import re m re.search(r(\?\d), header) phone m.group(1) if m else unknown # 如果内容是hex格式使用UCS2解码 if body.startswith(CMGR) False: # 检查body是否全是hex字符 if all(c in 0123456789ABCDEF for c in body.strip()): body decode_ucs2(body.strip()) return phone, body return None, None这个解析方法在绝大多数场景下够用了。但如果你遇到PDU模式的短信比如某些特殊号码发的还是建议用ATCMGF1强制文本模式避免额外处理。3.4 转发逻辑不落盘、不存储、直接推送收到短信之后我的原则是不落盘、不存储直接在内存里组装数据然后通过HTTPS推送出去。这样即使设备丢了也不会泄露历史短信。推送目标我选了两个Server酱sct.ftqq.com简单微信扫码绑定提供一个SendKeyGET请求就能发消息适合自己用。企业微信群机器人Webhook地址可以POST JSON支持Markdown格式适合多人接收。两条链路都保留代码里做插件式设计想用哪条就配置哪条。import urequests import json def push_to_wecom(webhook, title, content): payload { msgtype: markdown, markdown: { content: **【短信转发】**\n 来自{}\n 内容{}.format(title, content) } } headers {Content-Type: application/json} try: r urequests.post(webhook, datajson.dumps(payload), headersheaders) r.close() except Exception as e: print(push failed:, e)Server酱的推送类似GET请求拼参数即可。注意一点Webhook的URL里带Token这个Token一定要保密别写死在公开的代码仓库里。我的做法是放在板子上的config.py文件里这个文件不进Git仓库。4. 核心链路实测从SIM卡收到短信到手机App弹出消息代码写完、硬件装好之后进入最关键的环节实测整条链路。这一步能暴露非常多理论上想不到的问题。我把自己实际调试的过程完整写下来你可以照着排查。4.1 第一次实测模块重启的噩梦上电后我先用串口监视器给SIM800C发AT正常返回OK。接着发ATCPIN?返回READY。一切看起来正常。然后给SIM800C发ATCNMI2,1,0,0,0也返回OK。我就用另一台手机往这张卡发了一条测试短信结果不到两秒模块直接断电重启了。这个问题我当时排查了好久。原因是供电不足SIM800C在接收短信之前要先做一次射频发射——也就是向基站确认短信已接收这个时候电流瞬时会冲到2A左右而我的USB口只提供了500mA电压瞬间被拉低模块就重启了。解决办法就是前面说的给SIM800C单独接5V/2A电源。如果你用可调电源可以把电流检测打开会看到短信到达瞬间电流确实冲到1.8A左右非常直观。4.2 第二次实测短信内容乱码供电问题解决后短信能收到了但中文内容乱码。串口打出来是一串形如6A8C 8BC1 7801的十六进制我知道是UCS2编码但转出来的汉字不对。后来排查发现我读取短信时没有指定ATCSMP参数导致模块用了默认的编码方式输出。虽然ATCMGF1设置了文本模式但编码方式还是可能因SIM卡运营商而异。加一行指令强制UCS2ATCSMP0,0,0,25这里的第四个参数25即0x19表示UCS2编码。设置完之后短信内容返回的就是标准的UCS2十六进制串解码正常。这个参数很多教程不会写但它就是「中文验证码短信乱码」的经典解法。4.3 第三次实测推送延迟与重试链路通了之后我最关心的是延迟。实测数据是从短信到达SIM卡到手机App弹出推送平均耗时2-4秒。这个时间主要是由运营商下发短信、模块上报、HTTP请求三部分组成。但有的时候推送会失败。原因通常是Wi-Fi信号弱或者HTTPS握手超时。我在推送函数里加了重试逻辑失败后隔2秒、5秒、10秒分别重试最多试3次。超过3次就丢弃并在日志里记录。这里有个取舍如果推送失败导致短信丢了呢会不会漏掉关键验证码我的方案是推送失败后短信内容还留在SIM卡的存储里不会主动删除。空闲时可以用ATCMGLREC UNREAD查一下有没有未读短信作为兜底。但这也意味着SIM卡存储空间会慢慢占满所以我的定时清扫任务会在每天凌晨清理一遍旧短信。4.4 长时间运行的稳定性看门狗与自动重连这个转发器不是跑几分钟就关的而是要一直挂机。实测连续运行30天的过程中我最担心的是两种情况Wi-Fi断连后没有重连。SIM800C进入省电模式后不再响应。针对Wi-Fi断连我在主循环里做心跳检测每30秒Ping一次网关不通就主动断开重连。同时MicroPython的machine.WDT()也启用一旦主线程卡死硬件看门狗会自动重启系统。针对SIM800C我不能让模块进入省电模式ATCFUN0就是省电否则短信来了可能不主动上报。所以我保持模块在CFUN1满功能状态并且每一小时发一次AT指令确认模块还活着如果连续3次无响应就用GPIO控制模块的复位引脚强制重启。30天运行下来的统计数据是重启过2次均为Wi-Fi重连触发的看门狗复位短信转发成功率99.5%以上漏掉的几次是因为运营商信号在某个时间段确实波动了。这个稳定性水平作为个人用途完全够了。5. 避坑手册从开门到刷机的完整排错思路做这个项目过程中踩了不少坑整理成清单希望能给你省掉几天的调试时间。5.1 SIM800C收不到短信的排查链如果你遇到收不到短信的问题按这个顺序排查确认SIM卡有信号ATCSQ返回信号强度第一个参数大于10才算勉强可用。确认网络注册ATCREG?第二个参数为1或5才算注册成功。确认短信存储位置ATCPMS?看SIM卡存储是否已满。确认主动上报开启ATCNMI2,1,0,0,0设置成功并返回OK。确认天线接牢天线没接好信号强度可能显示正常但实际收发不稳定。5.2 ESP32烧录失败的处理烧录时最常见的错误是Failed to connect to ESP32: Timed out waiting for packet header。原因通常是芯片进入了正常启动模式而不是下载模式。解决办法是按住板子上的BOOT按钮不放然后再点烧录看到连接成功后松开。如果还是失败检查串口驱动是否安装。国产板子多为CH340芯片需要装CH340驱动官方板用CP2102也需要对应驱动。5.3 推送接口的稳定性与限流Server酱和企微机器人都有频控限制。Server酱目前是免费版每天有次数限制如果你短信特别多建议配合关键词过滤比如只转发包含验证码的短信其他的丢弃或只做本地记录。企微机器人的限制是每分钟最多20条消息。我自己做了个简单的限流同一分钟内最多推送10条超出部分排队延迟发送。虽然大部分时候用不到但防止某些营销短信半夜轰炸导致接口被封。5.4 安全加固别把验证码裸奔在公网短信转发链路里最危险的是两个环节本地存储和网络传输。我的做法是板子上不存明文短信最多在内存里保留最近一条作为调试用途。HTTPS是必须的不要用HTTP明文请求否则验证码在网络上可以被抓包。Webhook Token不要提交到Git仓库用独立配置文件。如果条件允许给ESP32的Web管理页面加简单的HTTP Basic Auth防止局域网内其他人乱动配置。其实短信验证码属于高敏感信息哪怕是自己搭的转发器也要尽量做到用后即焚。我在收到短信推送给手机之后会立即发送ATCMGDindex删除该条短信SIM卡上不留副本。这样即使卡被拔走也没法读历史记录。6. 进阶玩法当转发器不只是转发器短信转发这个功能做完之后你会发现这块板子还有很大的扩展空间。既然ESP24小时在线SIM800C也能收发短信为什么不顺便做点别的下面这几个方向是我觉得最实用、代码改动量最小的。6.1 上行指令发短信控制家电买一个继电器模块接到ESP32的GPIO上再写一条逻辑如果收到的短信内容是KAI#就开继电器如果是GUAN#就关。这样你在外面用手机发一条短信就能远程控制家里的设备。这个场景的应用价值在于不需要依赖任何云服务哪怕你的Wi-Fi断了只要GSM有信号短信指令依然能到达。比某些智能家居的云优先方案可靠得多。代码改动量极小核心就是一个字符串判断if body.strip() KAI#: relay.on() send_sms(phone, relay: ON) elif body.strip() GUAN#: relay.off() send_sms(phone, relay: OFF)6.2 短信过滤与智能摘要收件箱里不全是重要短信营销广告、快递通知、银行流水各占一部分。我的转发器里加了一个简单的关键词过滤表只转发包含以下关键词的短信验证码余额异常登录报警其他短信直接标记为已读并删除。这样推送频率大大降低也不会被垃圾短信轰炸。更进阶一点可以用正则表达式提取短信中的验证码数字然后直接拼成标题推送。比如收到您的验证码是12345610分钟内有效手机上直接显示验证码123456一眼就能看到重点。import re code_match re.search(r(\d{4,6}), body) if code_match: title 验证码: code_match.group(1)6.3 多卡多模从单卡转发到短信网关一块ESP32只能带一个SIM800C但你可以用USB或者串口扩展多个模块。理论上把4块ESP32挂到同一个路由器下面每块板子分别工作就能组成一个4卡的短信收发集群。配合一个简单的HTTP接口就能实现收发短信的多路负载均衡。对个人用户来说这么做性价比不高但如果你有极客朋友需要做群控、接码、自动化测试这是个很好的原型。我给开源仓库里加了多路转发的示例代码有兴趣可以去看看。6.4 固件OTA升级改逻辑不用拆机器硬件放在书架的角落如果每次改代码都要拆下来刷固件太痛苦了。所以我在网上找了一个轻量级的OTA方案板子启动时先检查云端一个固定URL是否有新版本固件有就下载并写入新分区然后重启。这样以后调代码只需要把新固件传上去板子会自动完成升级。这个功能实现起来也不复杂MicroPython自带ota相关库或者直接用urequests下载固件写到OTA分区。不过要注意空中升级有变砖风险升级前一定要在本地测试充分别拿正式运行的板子当小白鼠。7. 从个人需求到开源项目我的一些体会这个项目从动手到跑稳定前后花了两周时间。硬件选型半天接线加写代码两天剩下的时间全花在调试供电、乱码、稳定性这些细节上。但正因为踩过这些坑我对整个链路有了非常清楚的认识——信道的稳定性、编码的兼容性、设备的自主可控这些是任何现成产品都很难给我的。开源之后GitHub仓库地址在 https://github.com/lanr 。我把原理图、MicroPython源码、AT指令初始化流程、配置文件模板都传上去了。你拿到代码之后只需要改一下Wi-Fi名称、密码、Webhook Token烧录进ESP32接好SIM800C就能跑起来。仓库里我会持续整理大家提上来的Issue常见问题也会写进README。如果你有别的板子或者别的推送渠道欢迎提交PR一起把这套方案做得更通用。最后说一句真心话自己做这个东西省下来的不止是钱。当你知道一条验证码是从SIM卡到模块、再到你手机全程可控时那种踏实感是买成品设备永远给不了的。如果你也一直想折腾一点小硬件这个项目是个非常合适的起点——难度不高、成本很低、每天都能用得上做完之后你会有种这才是我的设备的感觉。
返回列表