开源短信与来电转发网关:基于Modem与AT指令的旧手机改造实战
发布时间:2026/10/1 4:06:27 作者:尧图编辑部 阅读量:1,286

开源、短信转发、来电转发这三个词凑在一起基本就是一台“永不熄火的备用手机”刚需。我理解这个项目的目标也是一直想自己折腾一套的东西让一台常年插着充电线、放在家里的旧手机变成消息和电话的“哨兵站”——人在办公室、在高铁上、在开会所有打到这张卡上的电话和新进来的短信都能第一时间转到你当前的主用设备上。听起来像是个小工具但背后其实牵扯到硬件选型、AT指令解析、消息状态机、服务守护一环扣一环踩坑点非常多。这篇文章就把整个开源项目的拆解思路、实现原理、部署实操和排坑经验一次性讲透适合手里正好有一台闲置手机、日常需要双卡双待但不想再带一台手机的人也适合想入门嵌入式通信开发的折腾党。1. 项目到底解决什么问题核心需求拆解我在动手之前先把需求掰开了揉碎了想过一遍。标题写的是“短信转发来电转发”看起来就两件事但实际往里一挖每个需求底下都藏着一堆小需求。1.1 短信转发的真实场景短信这个东西看起来很老旧但至今依然是验证码、银行通知、快递取件码、App登录凭证的主要载体。真正难受的不是短信本身而是“短信躺在那台不带在身边的手机里”。我见过很典型的场景家里留了一台旧手机插着工作卡人在外面急需要一条验证码不得不打电话让家里人帮忙看一眼短信。这就是最原始的“人工转发”。这个开源项目的短信转发本质上是把“人工转发”变成“自动转发”。手机收到新短信之后后台程序立刻抓取发送号码、短信正文、接收时间然后通过一个或者多个通知渠道推出去。推给谁推到哪里这就引出了第二个隐藏需求目标渠道要灵活。可以推到企业微信、钉钉、飞书群机器人可以推到邮箱也可以推到自定义的Webhook。因为这些渠道本身都是跨平台的你人在哪台设备上通知就能在哪台设备上弹出。1.2 来电转发的真实场景来电转发比短信转发麻烦至少两个量级。麻烦在哪短信是“被动接收一条数据”来电是“一个实时发生的呼叫事件”处理这事有两层需求第一层是“知道有来电”。运营商当然有自带的呼叫转移功能设置一个“无条件转移”或者“无应答转移”的目的号码来电就会转过去。但问题是这个功能依赖运营商侧配置而且把话费也一起带过去了另外你根本没机会知道“是谁打进来的”。这个开源项目要解决的不只是转移呼叫而是在转移之前先把主叫号码解析出来。第二层是“如何通知”。我理想中的体验是来电进入时旧手机解析出主叫号码然后把“某某号码正在来电”推送出去与此同时程序可以根据预设规则决定是否接听、是否挂断、是否转发到另一台手机。国内实际能落地的做法是用软件模拟接听把通话“接起来再挂断”让对方知道这台手机是有反应的同时把主叫号码推给你。这中间涉及对Modem状态的实时监听比纯短信转发复杂得多。1.3 一台常年插着充电线的手机为什么是首选硬件方案这里有个关键的硬件思路问题为什么非要用一台手机当转发网关而不是直接在服务器上挂一个4G上网模块我在论证阶段认真比较过这两条路线。服务器 4G模块的方案其实更“极客”但坑也在那摆着模块和运营商之间的兼容性参差不齐很多模块对语音通话的支持不完整VoLTE更是看运气遇到需要输入SIM卡PIN码、需要处理运营商个性短信的情况调试成本非常高。用一台现成的手机当网关等于把信号处理、SIM卡管理、语音通话能力这些最麻烦的事情全部交给手机厂商调好的Modem开源程序只需要坐在上层跟Modem通信就行。这是典型的“把复杂交给地基把简单留给业务”。所以这个项目的核心思路在我这里很明确利用旧手机的Modem能力上层用开源软件做“感知 转发”。这也是项目选择嵌入式开源路线而不是纯服务器路线的根本原因。2. 项目整体架构与技术选型搞清楚需求之后接下来就是技术方案怎么落地的问题。这一节我重点讲架构设计逻辑以及为什么最终选择了这样一套组合很多取舍是我自己对比过之后才定的。2.1 硬件选型不是所有手机都适合当网关先聊硬件。从稳定性角度说旧手机当网关最重要的不是性能而是“能长期稳定在线”。我实际测下来几类硬件各有明显差异老款安卓手机系统开放性高搞到root权限很容易后台杀进程的毛病在刷了精简ROM之后基本可控。缺点是电池长期满电充电容易鼓包建议直接把电池拆掉用电源模块供电。旧iPhoneModem接口封闭iOS上做不到系统级的短信监听除非用企业签名的后台VoIP方式钻空子但稳定性很差不建议选。带通话功能的4G模块开发板体积小、功耗低但对前端射频的调校、天线布局要求高如果只是想解决实际问题优先手机。树莓派 4G Hat可玩性高Python生态好但依然要面对Modem兼容性问题纯接收短信还凑合来电检测在部分Hat上没有语音通道。我实际跑过一块SIM7600CE短信很稳来电检测也是靠AT指令硬解析能用但体验一般。最终我的选择是一台骁龙636的旧安卓机6GB内存跑Android 9。理由很简单这类机子便宜、方案成熟、能root、Modem稳定。把开发版当副机日常插着电源放家里当短信和来电的汇聚点。2.2 软件层面的模块划分整个软件架构我在脑子里画过一条完整链路手机Modem ↓ AT指令 / RIL事件 开源守护进程核心引擎 ↓ 解析事件 执行规则 转发处理器渠道适配器 ↓ 企业微信 / 钉钉 / 飞书 / 邮件 / Webhook这个项目之所以能把短信和来电“全搞定”核心是它把这两块能力合并到了一个守护进程里而不是拆成两个小程序。合在一起有个非常大的好处共享一套配置、一套日志、一套去重机制。比如来电转发的短信确认功能来电通知出去之后对方可能会回复一条指令短信“查收件箱”这台网关手机收到短信后同一个进程就能直接解析指令、回传结果。拆成两个程序去写光协调两个进程之间的状态就能把人搞疯。2.3 为什么使用AT指令而不是纯RIL接口Android系统下面有两套跟Modem交互的方案一套是RILRadio Interface Layer直接跟系统电话服务挂钩另一套是AT指令。这个项目选择AT指令作为主要交互手段我完全认同原因有三个第一权限边界清晰。RIL方向的接入要写系统级服务要过SELinux策略调试一次要重启一次手机AT指令只要打开Modem的串口通过读写端口就能完成事件监听和状态查询应用层权限就能搞定。第二事件回调天然适合。短信来了Modem会推送CMTI来电来了会推送CLIP或者RING这些通知是异步的程序挂在串口上持续读取即可架构上省掉一层轮询逻辑更干净。第三调试手段直观。我把Modem串口的波特率和设备路径配好用电脑上的串口工具直接发AT指令立刻就能看到回报。真实环境里用minicom或者echo AT的组合就可以验证行为不用走系统日志一层层翻。2.4 转发链路设计状态机与去重短信和来电的转发链路我抽出来看本质是一个“事件→判断→动作”的状态机。这个状态机设计是这个项目的灵魂。先看短信“收到新短信”事件被Modem上报后状态机当前状态是“待识别”程序提取短信内容判断发件人是否在黑名单、内容是否匹配某个规则然后就进入“待发送”状态。这个状态里有个关键的“去重机制”——同一个短信ID如果在短时间内连续上报两次有些ROM的广播会重复发送第二次直接丢弃。之前没做去重的时候出现过一条验证码被推到企业微信上三遍的情况非常尴尬。再看来电Modem上报RING之后状态机进入“来电中”程序从串口读取CLIP里携带的主叫号码把号码套进转发规则。如果规则配置的是“转发来电并发送通知”程序就会先把来电信息推送到通知渠道再根据配置决定是否用AT指令去接听ATA或拒绝ATH。通话结束后状态机回到“空闲”。这中间最怕的是来电过程中又来第二条来电状态机如果不支持“来电中叠加”的状态很容易漏掉第二个主叫号码。我在实际跑的时候就把来电状态设计成可叠加的铃声状态下又来一个Call WaitingModem会再报一次RING程序会把它当独立事件再推送一次。这套状态机逻辑是区分“玩具项目”和“真正能用”的分水岭。很多人写短信转发就写个“读取收件箱→推送”一旦遇到重复广播、来电阻断、信号不稳立刻露出破绽。这个项目敢把来电转发也收进来大概率是在状态机这里下了功夫的。3. 完整部署过程与核心环节实现前两节聊的是思路和设计这节开始动真格的。我按自己实际折腾下来的完整路径把部署一个可用的短信来电转发网关的流程拆开来讲。我会带着参数和配置一起给你可以照着抄。3.1 硬件准备与系统安装硬件这块我的配置清单是这样旧安卓手机一台要求能rootAndroid 8以上一张用于接收的SIM卡5V/2A的持续供电方案我直接拆了电池用电源模块供电避免电池鼓包可选一个散热底座夏天长时间充电热量还是有点可观系统安装这块我刷的是类原生系统LineageOS 16核心原因是类原生系统对第三方应用没有那么多管控后台存活率显著高于国产定制ROM。刷机过程不细说网上资料一大把我重点提示三件事刷完系统后第一步先解锁Bootloader、允许ADB调试、然后进系统把“保持唤醒状态”和“充电时不休眠”打开。我上车后干的第一件事是确认Modem串口能不能访问。用ADB shell进到系统执行ls /dev/smd*或者ls /dev/ttyUSB*不同芯片方案设备名不一样。我这台高通芯片的设备节点是/dev/smd7和/dev/smd11其中 smd7 就是AT指令通道。确认节点存在后我用chmod 666 /dev/smd7临时放开权限然后接着用echo -e AT\r /dev/smd7测试。设备回了一个OK的时候我知道这一关已经过了。权限这块有个陷阱权限每次重启后都会重置不能靠chmod一劳永逸正确的做法是写一个udev规则root后直接放/system/etc/udev/rules.d/下面或者干脆在启动脚本里执行chmod 666。我用的是后面这种虽然粗暴但最省事。3.2 核心依赖安装与Modem通信调通Modem串口能通之后下一步就是把AT指令用得顺手。这里的关键参数有两个波特率和流控。高通的Modem串口常见波特率是 115200也有个别方案是 9600流控通常是关闭硬件流控RTS/CTS用纯软件流控XON/XOFF。我直接用stty -F /dev/smd7 115200 raw -echo设置参数设置成功后再手动发AT指令验证。系统的短信通知是CMTI。为了让Modem主动上报新短信需要先通过AT指令设置ATCNMI2,1,0,0,0这个指令的意思是新短信到达时主动上报并把短信存储在SIM卡或Modem内存中。设完之后随便给自己发一条测试短信串口上就会立刻弹出一行CMTI: SM,3这个返回值的含义是新短信存储在SMSIM卡存储区序号是3。接下来需要根据这个序号把短信内容读出来ATCMGL3返回的RAW数据类似CMGL: 3,REC UNREAD,8613800138000,,23/08/17,10:30:0032 测试短信内容到这里“读取短信”这一环已经全部打通。我第一版调试脚本就是靠监听CMTI事件、读取短信、然后拼成通知推送出去的。来电检测需要设置CLIPATCLIP1开启之后来电到来时Modem会先打印一行RING随后紧跟着一行CLIP: 8613800138000,145,,0,,0括号里第一个字符串就是主叫号码145是号码类型标识国际格式或国内格式后面的字段是号码显示限制等信息。从这一行里我可以提取主叫号码再交给上层逻辑处理。实测过程中如果遇到VoLTE高清通话部分Modem上报的号码格式会有差异有的是在CLIP前面直接跟一个空白段有的是带前缀的86。写解析逻辑时建议做一层正则清洗统一规范成86138xxxx格式方便后续判断是否命中白名单。3.3 核心服务配置与转发规则先把Modem这条链路走通再谈上层转发。项目的核心守护进程我用的是一套Python编写的常驻服务内部的流程框架可以简化成下面这个伪代码逻辑import serial import time import requests import re MODEM_PORT /dev/smd7 BAUD_RATE 115200 notify_url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyYOUR_KEY def send_at(cmd, wait2): ser.write((cmd \r).encode()) time.sleep(wait) return ser.read(ser.inWaiting()).decode(errorsignore) def push_message(content): requests.post(notify_url, json{ msgtype: text, text: {content: content} }) ser serial.Serial(MODEM_PORT, BAUD_RATE, timeout1) send_at(ATCMNI2,1,0,0,0) send_at(ATCLIP1) buffer while True: chunk ser.read(256).decode(errorsignore) if not chunk: continue buffer chunk if CMTI in buffer: idx re.search(r\CMTI: SM,(\d), buffer).group(1) raw send_at(fATCMGR{idx}) # 解析号码与正文 match re.search(r(\\d).*?\r\n(.*?)\r\n, raw, re.S) number match.group(1) text match.group(2) push_message(f【短信转发】来自 {number}: {text}) buffer if CLIP in buffer: match re.search(r\CLIP: (\\d), buffer) caller match.group(1) push_message(f【来电转发】{caller} 正在呼叫备用号码) buffer 这段不是完整源码但已经把核心链路的思想展示出来了监听串口、正则提取事件、推送Webhook。配置部分还应该有白名单规则、黑名单规则和通知渠道配置。我在真实项目里配置文件用的是JSON{ modem: { port: /dev/smd7, baud_rate: 115200 }, sms: { enable: true, blacklist: [8612345678901] }, call: { enable: true, notify_list: [8613900000000] }, notify_channels: [ { type: wecom_group_bot, webhook: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxxx } ] }这里要特别说明配置里的notify_list字段。来电转发不能搞“所有号码都通知”否则广告推销电话会把你主用设备震到没电。我在规则里设计了白名单和黑名单两层过滤白名单命中的号码来电后不仅通知还可以自动触发“短信回复”名单外的号码只通知不处理黑名单号码直接静默挂断。3.4 用systemd把转发服务变成系统服务程序能跑起来只是第一步能长期稳定跑才是关键。手机上的守护进程最怕的是自己被系统杀掉。除了在系统设置里把程序加入“电池优化白名单”之外更稳妥的办法是做成系统服务。在root过的手机上我用的是类systemd的方案。类原生系统上实际上没有systemd但我可以把启动脚本放进/etc/init.d/或者使用SuperSU的启动脚本机制。更直接的方式是写一个init.d脚本#!/system/bin/sh # service.sh nohup python3 /data/local/tmp/sms_forwarder.py /data/local/tmp/forwarder.log 21 把这个脚本放到/system/etc/init.d/下并赋予执行权限重启后就会自动拉起。如果是Magisk环境也可以用Magisk的service.sh钩子这个更干净不受SELinux限制。这里我给一个额外经验日志输出必须加上“时间戳 事件类型”的结构化格式方便事后排查。我看过很多人的转发程序跑着跑着就“消失”了一问才发现连日志都没写出了问题根本不知道怎么定位。4. 典型故障排查与实用经验部署一个基于Modem的通信网关说句大实话从一开始到现在我踩过的坑比写过的代码还多。这节我把高频问题整理成速查表再加几条真正实用的经验。4.1 SIM卡没有被识别故障现象运行ATCPIN?返回ERROR或者ATCSQ返回CSQ: 99,99。排查顺序归成三步确认SIM卡物理接触旧手机卡槽氧化是高频问题拿出SIM卡用橡皮擦把金手指擦一遍重新插。确认卡没有欠费停机和PIN码锁定如果Modem串口返回CPIN: SIM PIN说明需要输入PIN码用ATCPIN1234解锁。确认射频天线通道没问题ATCSQ返回第二个字段为99代表没有信号需要检查天线或者所处位置的信号强度。经验很多“SIM卡不识别”的假象其实是卡槽弹片问题而不是软件问题。排查顺序从物理到逻辑别一上来就改代码。4.2 收到短信但程序不推送故障现象串口能看到CMTI但企业微信始终没消息。大概率是ATCMGR读取短信后解析正则不匹配。不同厂商Modem返回的短信头格式略有差异有的短信头里没有存储区标识双引号比如直接返回CMGR: 3,REC UNREAD,86138...。正则写得太死就会漏掉。还有一类的坑是短信内容含中文时Modem返回的可能是PDU模式编码一串十六进制字符而不仅仅是纯文本。遇到这种情况需要先把PDU解码成Unicode再转发。我会把解析短信的模块从“处理文本模式”升级成“同时处理PDU模式”这样才能覆盖所有Modem的返回。4.3 来电状态误判故障现象人在外面手机经常推送一些“呼入中”的通知但实际上并没有人打电话。这个我排查过很久最后定位到是状态机的“去抖动”没做好。某些运营商网络或者某些Modem驱动在信号切换、即使网络信令波动时也会上报一次RING但这个RING闪现之后立刻又消失。解决办法是收到RING后不立即推送通知而是等待1.5秒窗口确认两个条件都成立再推送1.5秒内没有出现ATH挂断指令对应的NO CARRIER事件且出现了CLIP带的主叫号码。这个“可信来电”校验逻辑能过滤掉至少80%的幽灵来电。4.4 手机长时间运行后系统卡死、转发静默故障现象网关手机跑了两三天服务还在但已经不转发任何东西了。这个问题本质是Modem串口长期被占用后驱动或硬件出现假死。我的处理策略是两层保底第一层在Python守护进程里做串口心跳检测每隔5分钟发一次AT如果连续3次没有返回OK就主动重试重新打开串口并重发初始化指令。第二层更极端的情况是Modem彻底假死、串口打不开需要在shell层定时执行reboot。我用的是“每12小时-24小时重启一次系统”的粗暴方案实测下来一台旧手机每周固定重启一两次对全天转发成功率的影响可以忽略不计系统状态反而更稳。把“稳定”理解成“服务经常自动拉起”而不是“进程永不退出”运维心态会好很多。4.5 通知渠道限流与消息风暴企业微信群机器人有频率限制一分钟内最多发20条消息。如果短信风暴来袭——比如某平台一下子发5条验证码——一次性推送5条是没问题的但如果是某个时段积压了50条直接打满频率限制。我在项目里加的防风暴策略是增加滑动窗口计数器超过阈值后把多条短信合并成一条摘要推送。比如“你收到6条新短信前3条来自A、B、C其余3条已省略”。这样既不会漏通知也不会触发限流。这条经验对于任何把网关接入即时通讯机器人的人来说都通用。4.6 来电转发的接听策略选择最后聊一条关于来电转发的核心取舍程序要不要真的去“接起”那个来电。我的观点是默认不接只做通知。原因是程序接起电话对方会听到一段“无人说话”的空白体验反而更奇怪。我实际在用的策略是响铃超过15秒且主叫号码在白名单内程序才发送ATH挂断然后立刻推送一条通知“某号码来电超过15秒未接听可能重要请回电”。这种做法不产生实际通话费用也避免了“对方听到我这边有声音但不说话”的诡异局面。如果你确实需要“一键回拨”那再考虑集成一个语音API把呼叫转出去那是另一个层面的故事了。5. 项目扩展方向与个人实践经验分享折腾完这套系统之后我最大的感觉是它能做的事其实远不止“短信转发”和“来电转发”这两个字面能力。我现在在这个基础框架之上又接了几件有价值的事用同一台网关监听银行余额变动短信触发自动记账逻辑用这台手机接收某个IoT设备的报警短信再将报警信息转成Webhook调用触发家里摄像头的录像标记甚至在出差的时候用它接收快递柜取件码再自动转发到我随身设备上。这些能力和短信转发共用同一套事件源扩展成本非常低。这个项目的巧妙之处就在这把Modem事件抽成统一的消息流所有后续处理都是“插拔式”的。分享两个个人觉得含金量比较高的经验第一个经验配上“反向指令通道”。很多教程止步于“手机→主设备”的单向转发但实际更香的是“主设备→手机→其他设备”的反向链路。比如我人在外面给网关手机发一条特定格式的短信“PING”程序收到后自动回复当前信号强度、IP地址和运行时长。这就是一台私有的、不依赖任何云平台的监控终端非常可靠因为底层是短信通道网络断了它都照样工作。第二个经验善用“多渠道叠加”。不要把企业微信群机器人当唯一转发通道我现在的配置是“企业微信为主、邮件为备份”。企业微信断网或者Token失效时邮件通道作为兜底。别看这事简单真等到主通道挂掉而备用通道能救急的时候你会回来感谢这条经验。如果下一步想把这事做得更完善可以再往这些方向延伸对接一个本地大模型让短信内容经过语义分析再决定是否打扰你把网关手机接入Home Assistant实现“来电即触发回家模式”或者把短信内容做结构化解析后入库生成个人提醒事项。这些都是顺理成章的扩展开源项目的好处就是你想怎么改就怎么改。我实际跑这套系统已经有几个月了最大的体会是稳定不靠花哨的设计靠的是把每一层不牢靠的地方都补齐PlusPlan。Modem会假死就加心跳和自动重启消息会重复就加去重通知渠道会限流就加合并。一层一层查漏补缺之后这东西才真正变成了“放在那儿不动它就很安心”的基础设施。如果你也想搭一套属于自己的短信和来电转发网关我建议先从一个最小链路开始跑通——串口可以收发AT指令、企业微信能收到第一条推送——再逐步往上加规则和容错这样你会对这套系统里里外外都心里有数。