高通410随身WiFi变身短信转发器:Debian系统Python脚本实战
发布时间:2026/9/28 9:35:12来源:尧图网络
如果你手头有一台高通410方案的随身WiFi而且跑的是Debian系统那恭喜你手里其实不是一台普通的热点设备而是一台小服务器。最近我就拿它干了一件事让设备自己把收到的短信转发出去。不刷机、不改内核、不折腾Bootloader就在系统里跑一个Python脚本搞定短信转发功能。这个需求的场景其实很常见把随身WiFi扔在老家、车里或者店里当常驻热点SIM卡收到的验证码、到账通知、设备告警短信不希望等到下次人过去才能看到。传统做法要么单独放一台安卓手机专门转发要么花钱买工业短信猫。而这台随身WiFi本身就带基带、带Linux系统只要把短信读出来再推送出去问题就解决了。这篇文章把我从零开始实测的过程、踩过的坑、最后稳定运行的方案完整写出来。如果你手里也是高通410方案的Debian随身WiFi或者类似方案的Linux开发板这篇可以直接当操作手册用。1. 这活儿为什么非得高通410不可随身WiFi里的隐藏Debian1.1 随身WiFi那么多为什么偏偏是高通410市面上几十块的随身WiFi大多用的是低成本方案芯片算力弱、内存小、系统封闭有的连Shell都进不去更不用说在里面跑Python。而高通410MSM8916本质上是早年安卓手机淘汰下来的SoC四核A53内存普遍512MB或1GB起步跑精简Debian系统绰绰有余。关键是这类设备很多出厂就是Linux系统有些明确就是Debian。这就意味着它自带包管理、自带Python能力我可以直接把它当一台无头Linux服务器用。相比其他方案这是一条系统级可编程的随身WiFi不是单纯的热点。我手头这台是常见的USB棒子形态一个USB口负责供电和数据侧面插SIM卡系统里能看到独立的串口设备。开机进系统以后它就是一台带4G基带的小电脑。1.2 不用刷机到底指什么先把边界说清楚这篇文章说的不刷机指设备当前已经跑着可用的Debian系统不需要重刷固件。如果你的设备目前是安卓系统想换到Debian那是另一个话题本文不展开。对于已经是Debian的设备完整的改装链路是SSH进去、装软件、写脚本、配自启。全程不碰分区、不碰bootloader、不碰内核。这也是我标题里强调不用刷机的原因。这里有一个很常见的误解有人觉得随身WiFi想干这种事必须刷机必须找第三方固件。实际很多原厂/商家预装系统已经开放了SSH和root权限只是大多数人不知道能进去或者进去以后不知道该干什么。1.3 几条路线横向对比我整理一下常见的短信转发方案方便你判断自己该走哪条路方案成本功耗部署难度稳定性单独一台安卓手机转发高高中等高但占地方USB 4G上网卡 电脑/软路由中等中等依赖主机中高工业短信猫GSM Modem贵低简单高高通410随身WiFi Python低极低中等高实测稳定对比下来用高通410随身WiFi是最划算的。它本身就是为了长期通电、低功耗运行设计的而Debian系统让它具备了完整的可编程性。一个几十块的设备同时干着热点和短信转发两件事。2. 进系统先别急着写脚本把Debian环境收拾到能用2.1 SSH连接与第一轮自检拿到设备后的第一件事是确认自己能进系统。这类带Debian的随身WiFi通常默认开启SSHroot密码一般在机身标签、包装盒或者商家给的说明里。如果商家没给试着用root/admin、root/123456这类常见组合或者查一下官方文档。连接方式有两种一是用网线连设备的LAN口直接SSH到管理IP二是电脑连接设备发出的WiFi再通过管理IP登录。开机后在终端执行ssh root192.168.1.1登录进去以后先确认系统底子cat /etc/os-release uname -a which python3 ls /dev/ttyUSB* 2/dev/null || ls /dev/ttyACM* 2/dev/null我这边输出了Debian版本信息Python 3 已经自带了串口设备也认到了。看到这一长串路径心里就有底了这是一个可以随便折腾的系统。第一次进来建议顺手看一眼磁盘空间和内存df -h free -m如果根分区只剩几十MB建议先清理一下。装几个Python包再写点脚本空间占用不大但也不能太抠。2.2 包管理与软件源让apt先把依赖装顺Debian的包管理是apt网上很多教程动不动就用pip我实际用下来发现python3-serial和python3-requests在Debian源里就有直接用apt装反而省心不会污染系统Python环境apt update apt install -y python3-serial python3-requests如果apt速度很慢大概率是默认源的问题。我习惯先把源备份再换成方便拉取的国内镜像源。具体地址按你网络环境实测来这里只说方法cp /etc/apt/sources.list /etc/apt/sources.list.bak sed -i s|http://deb.debian.org|https://mirrors.aliyun.com|g /etc/apt/sources.list apt update注意Debian 12 可能有.sources格式的源文件路径在/etc/apt/sources.list.d/改之前先ls看一下再动手。还有一点提醒apt update/download会消耗SIM卡的流量如果设备用的流量卡套餐很小最好先切换到包月流量大的卡上操作完再换回正式用的卡省得软件没装完流量先跑没了。2.3 顺手把休眠和USB挂起关掉这一步很关键也是最容易被忽略的。嵌入式Debian默认开了各种省电策略我一开始脚本跑着跑着发现/dev/ttyUSB2凭空消失了整条短信通道直接断掉。后来查内核日志是USB autosuspend把基带的USB接口挂起了。先检查当前状态cat /sys/bus/usb/devices/*/power/control如果有输出是auto说明USB设备处于自动挂起模式。临时解决办法是写入onecho on /sys/bus/usb/devices/2-1/power/control但这是临时的重启就没了。我做了一个systemd服务来持久化顺便把系统睡眠也禁掉。创建/etc/systemd/system/fix-usb-power.service[Unit] DescriptionDisable USB autosuspend for modem Aftermulti-user.target [Service] Typeoneshot ExecStart/bin/bash -c for ctl in /sys/bus/usb/devices/*/power/control; do echo on $ctl 2/dev/null || true; done RemainAfterExityes [Install] WantedBymulti-user.target然后启用systemctl daemon-reload systemctl enable --now fix-usb-power.service系统层面的休眠也顺手关了避免设备长时间没人用就进入睡眠状态systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target2.4 固定管理IP的风险与操作如果设备是通过网络SSH访问的固定管理IP还是有必要的。不然DHCP租约一变你就不知道该连哪个IP了。但这里要提醒一句动手之前一定先ip a看清楚哪个是LAN口、哪个是WAN口改错网络配置会导致直接断连只能恢复出厂设置。我这边管理口是eth0配置方式是在/etc/network/interfaces.d/lan里写auto eth0 iface eth0 inet static address 192.168.1.1 netmask 255.255.255.0如果系统用的是NetworkManager也可以nmcli connection modify Wired connection 1 ipv4.method manual ipv4.addresses 192.168.1.1/24 nmcli connection up Wired connection 1关键原则先备份原配置改完别急着重启网络先确认新地址能访问万一断了你还有备份能救。3. 短信从哪来AT指令串口和ModemManager两种读法3.1 先理解AT指令和串口设备的关系手机模块本质上都是通过AT指令控制的。在这台随身WiFi的Debian系统里基带芯片通过USB转串口暴露出来通常是/dev/ttyUSB0到/dev/ttyUSB3好几个设备。每个tty设备分工不同有的是拨号用的有的是日志用的短信一般落在其中某一个上。先找出哪个串口能发ATls /dev/ttyUSB*然后逐个测试用minicom或者直接重定向都行echo -e AT\r /dev/ttyUSB2 cat /dev/ttyUSB2能看到OK就说明这个口通了。我这边短信功能在ttyUSB2拨号在别的口。常用的短信相关AT指令如下指令作用ATCPIN?查询SIM卡状态ATCMGF1设置短信为文本模式ATCMGF0设置短信为PDU模式ATCMGLREC UNREAD列出未读短信ATCMGLALL列出全部短信ATCMGRindex读取指定编号短信ATCMGDindex删除指定编号短信ATCSQ查看信号强度验证卡是否就绪echo -e ATCPIN?\r /dev/ttyUSB2返回CPIN: READY就说明SIM卡状态正常。3.2 ModemManager一条命令也能读但我最终弃了Debian系统里如果装了ModemManager可以直接用mmcli读短信mmcli -m 0 --messaging-list-sms mmcli -m 0 --messaging-read 1这条路线的好处是不用自己拼AT指令坏处是ModemManager会强势占用串口设备。我试过好几次ModemManager服务一开Python里pyserial打开同一串口就直接报device busy两条路互斥。如果只是临时看短信mmcli确实方便。但要做自动化转发脚本我最终还是选择直接在Python里操作AT串口把ModemManager停掉并禁用了systemctl stop ModemManager systemctl disable ModemManager理由很简单脚本要长期稳定运行不能依赖一个抢占串口的系统服务。宁可自己多写几行AT指令把控制权抓在自己手里。3.3 中文短信和PDU模式才是绕不过去的坎文本模式ATCMGF1下纯英文数字短信直接可读但中文短信大概率乱码因为短信在空口传输时不走明文而是经过编码打包。短信内容的编码主要有三种7-bit编码英文短信默认压缩率最高8-bit编码数据短信用UCS2编码16位Unicode中文短信走这个在PDU模式ATCMGF0下ATCMGR返回的是一长串十六进制字符里面包含发件人号码、编码方式、时间戳和短信内容。UCS2部分每两个十六进制字符对应一个Unicode码点解码相对容易7-bit编码是位级别的压缩解包要按bit流处理。最稳妥的策略是文本模式先跑能读出明文就用明文发现读出来是乱码再为这一条切换到PDU模式单独读取并解码。后面脚本就是这么设计的。4. Python转发脚本完整实现轮询、解析、去重、推送一条龙4.1 为什么设计成轮询而不是监听AT指令接口没有主动推送短信的能力模块不会主动说我收到新短信了。只能让程序每隔几秒主动去问一次有没有未读短信。轮询间隔我实测下来10到15秒最合适。太短1到2秒会让串口一直忙碌偶尔出现响应超时太长30秒以上会让短信通知延迟明显。验证码类短信等10秒完全能接受。整体流程是打开串口循环查询未读短信解析发件人、时间、内容通过邮件或Webhook推出去转发成功的短信在基带里删除失败则保留下个周期继续重试4.2 串口通信封装与AT指令执行pyserial是Python访问串口的标准库直接构造AT指令字符串发过去再读回响应。注意AT指令末尾必须有\r很多新手上手就卡在只发\n。import serial import time SERIAL_PORT /dev/ttyUSB2 BAUD_RATE 115200 def send_at(ser, command, timeout5): ser.reset_input_buffer() ser.write((command \r).encode()) time.sleep(0.3) resp b end_time time.time() timeout while time.time() end_time: chunk ser.read(ser.in_waiting or 1) if not chunk: continue resp chunk if bOK in resp or bERROR in resp: break return resp.decode(errorsreplace)打开串口时注意加timeout不设timeout会导致读操作永久阻塞ser serial.Serial(SERIAL_PORT, BAUD_RATE, timeout2)查询未读短信def get_unread_sms(ser): resp send_at(ser, ATCMGLREC UNREAD, timeout8) messages [] current_index None current_phone None current_time None current_text [] for line in resp.splitlines(): if line.startswith(CMGL:): parts line.split(,) if len(parts) 4: current_index parts[0].split(:)[1].strip() current_phone parts[1].strip().strip() current_time parts[3].strip().strip() current_text [] elif current_index is not None: if line.startswith(ATCMGL): continue if line.strip() OK: msgs .join(current_text) messages.append({ index: current_index, phone: current_phone, time: current_time, text: msgs.strip() }) current_index None else: current_text.append(line.rstrip()) return messages注意不同模块的CMGL返回字段格式略有差异有的带日期有的不带解析时做好容错。4.3 中文乱码回退PDU解码与字符集判断文本模式下如果短信内容是中文经常返回乱码或者空串。我采用的办法是先判断文本是否像正常文本不像就切到PDU模式重新读取这条短信。def looks_readable(s): if not s: return False readable 0 for ch in s: if 32 ord(ch) 127 or ord(ch) 0x2E80: readable 1 return readable / len(s) 0.8对于PDU模式重点实现UCS2解码。短信PDU由SMSC部分和TPDU部分组成先去SMSC长度字节跳过SMSC然后从TPDU里找TP-DCS编码方式和TP-UD。我用一个简洁点的实现def decode_pdu_ucs2(pdu_hex): if not pdu_hex: return pdu pdu_hex.upper() idx 0 smsc_len int(pdu[0:2], 16) idx 2 smsc_len * 2 # 跳过TP-MTI第一个字节和TP-OA发件人号码这里假设前面结构固定 first_octet pdu[idx:idx 2] idx 2 # 发件人号码长度 addr_len int(pdu[idx:idx 2], 16) idx 2 # 如果号码是奇数个补F idx ((addr_len 1) // 2) * 2 # TP-PID / TP-DCS idx 2 dcs pdu[idx:idx 2] idx 2 # TP-SCTS 时间戳7字节 idx 14 # TP-UDL udl int(pdu[idx:idx 2], 16) idx 2 if dcs in (08, 18, 48): raw pdu[idx:idx udl * 2] return bytes.fromhex(raw).decode(utf-16-be, errorsreplace) return 如果内容是英文短信走的是7-bit编码文本模式下通常已经能直接读了PDU回退主要用于中文所以这里先保证UCS2正确。真遇到7-bit PDU建议文档搜一下GSM 7-bit解包算法按位读取就行不复杂但容易写错正文不展开了。4.4 转发渠道邮件和Webhook转发方式我做了两个渠道邮件和企业微信机器人。邮件适合必须收到的重要短信Webhook适合日常告警推送。邮件用smtplib核心函数import smtplib from email.mime.text import MIMEText from email.header import Header SMTP_HOST smtp.qq.com SMTP_PORT 465 SMTP_USER your_emailqq.com SMTP_AUTH_CODE your_smtp_auth_code TO_ADDR receiverexample.com def send_mail(subject, body): msg MIMEText(body, plain, utf-8) msg[Subject] Header(subject, utf-8) msg[From] SMTP_USER msg[To] TO_ADDR server smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout10) server.login(SMTP_USER, SMTP_AUTH_CODE) server.sendmail(SMTP_USER, [TO_ADDR], msg.as_string()) server.quit()注意很多运营商封锁了25端口的SMTP所以这里推荐465或587端口实际测试稳定。企业微信机器人更轻量一条POST请求就能推消息import requests WEBHOOK_URL https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxxx def send_wecom(text): data {msgtype: text, text: {content: text}} requests.post(WEBHOOK_URL, jsondata, timeout10)Webhook的好处是推送快、不依赖自己邮箱的发送频率限制适合做高频告警。4.5 删除策略与防重复短信转发后要不要删除我一开始也纠结过。删除的好处是基带里不堆积CMGL返回的速度快坏处是转发失败后短信没了。我的策略很明确只有转发成功才删除。for msg in get_unread_sms(ser): try: send_mail(新短信, f来自: {msg[phone]}\n时间: {msg[time]}\n内容: {msg[text]}) send_wecom(f新短信\n来自: {msg[phone]}\n时间: {msg[time]}\n内容: {msg[text]}) except Exception as e: log(f转发失败: {e}, 保留短信index{msg[index]}) else: send_at(ser, fATCMGD{msg[index]}) log(f已转发并删除 index{msg[index]})有些模块在CMGL之后不会自动清除未读标记如果一直不删同一个未读短信会被反复取到。所以对于已经成功转发的内容删除是最干脆的防重复方案。4.6 完整脚本与systemd自启部署把所有模块合并成最终的脚本放在/opt/sms_forward/sms_forward.pyimport serial import time import smtplib from email.mime.text import MIMEText from email.header import Header import requests import os SERIAL_PORT /dev/ttyUSB2 BAUD_RATE 115200 POLL_INTERVAL 10 SMTP_HOST smtp.qq.com SMTP_PORT 465 SMTP_USER your_emailqq.com SMTP_AUTH_CODE your_smtp_auth_code TO_ADDR receiverexample.com WEBHOOK_URL https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxxx def send_at(ser, command, timeout5): ser.reset_input_buffer() ser.write((command \r).encode()) time.sleep(0.3) resp b end_time time.time() timeout while time.time() end_time: chunk ser.read(ser.in_waiting or 1) if not chunk: continue resp chunk if bOK in resp or bERROR in resp: break return resp.decode(errorsreplace) def get_unread_sms(ser): resp send_at(ser, ATCMGLREC UNREAD, timeout8) messages [] current_index None current_phone None current_time None current_text [] for line in resp.splitlines(): if line.startswith(CMGL:): parts line.split(,) if len(parts) 4: current_index parts[0].split(:)[1].strip() current_phone parts[1].strip().strip() current_time parts[3].strip().strip() current_text [] elif current_index is not None: if line.startswith(ATCMGL): continue if line.strip() OK: text .join(current_text).strip() messages.append({ index: current_index, phone: current_phone, time: current_time, text: text }) current_index None else: current_text.append(line.rstrip()) return messages def looks_readable(s): if not s: return False readable 0 for ch in s: if 32 ord(ch) 127 or ord(ch) 0x2E80: readable 1 return readable / len(s) 0.8 def send_mail(subject, body): msg MIMEText(body, plain, utf-8) msg[Subject] Header(subject, utf-8) msg[From] SMTP_USER msg[To] TO_ADDR server smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout10) server.login(SMTP_USER, SMTP_AUTH_CODE) server.sendmail(SMTP_USER, [TO_ADDR], msg.as_string()) server.quit() def send_wecom(text): data {msgtype: text, text: {content: text}} requests.post(WEBHOOK_URL, jsondata, timeout10) def log(msg): print(f{time.strftime(%Y-%m-%d %H:%M:%S)} {msg}, flushTrue) def main(): # 切文本模式 ser serial.Serial(SERIAL_PORT, BAUD_RATE, timeout2) send_at(ser, ATCMGF1) log(短信转发服务已启动) while True: try: msgs get_unread_sms(ser) for msg in msgs: text msg[text] if not looks_readable(text): # 尝试切PDU模式读取当前index send_at(ser, ATCMGF0) pdu_resp send_at(ser, fATCMGR{msg[index]}, timeout5) decoded extract_pdu_content(pdu_resp) if decoded: text decoded send_at(ser, ATCMGF1) body f来自: {msg[phone]}\n时间: {msg[time]}\n内容: {text} log(f收到短信 index{msg[index]} phone{msg[phone]}) try: send_mail(短信转发, body) send_wecom(body) except Exception as e: log(f转发失败: {e}) else: send_at(ser, fATCMGD{msg[index]}) log(f已转发并删除 index{msg[index]}) except Exception as e: log(f主循环异常: {e}) try: ser.close() except Exception: pass time.sleep(5) try: ser serial.Serial(SERIAL_PORT, BAUD_RATE, timeout2) send_at(ser, ATCMGF1) except Exception as ser_err: log(f重连串口失败: {ser_err}) time.sleep(POLL_INTERVAL) if __name__ __main__: main()上面的extract_pdu_content函数建议优先使用4.3小节里的decode_pdu_ucs2逻辑从ATCMGR返回中提取PDU纯数据部分根据DCS判断编码后解码。这里我只演示UCS2分支完整逻辑按自己模块返回格式微调。部署systemd服务创建/etc/systemd/system/sms-forward.service[Unit] DescriptionSMS Forward Service Afternetwork.target [Service] ExecStart/usr/bin/python3 /opt/sms_forward/sms_forward.py Restartalways RestartSec10 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target启动并设置开机自启systemctl daemon-reload systemctl enable --now sms-forward journalctl -u sms-forward -f日志里能看到轮询和转发记录运行状态一眼可见。5. 实测中的坑与治本方案让我折腾到凌晨三点的五个问题5.1 中文短信乱码比想象中顽固我一开始天真地以为文本模式下中文也能读结果收到的验证码短信全是乱码。后来查了模块返回的原始数据确认短信在PDU里是按UCS2编码文本模式有时候能转出来有时候直接失败。解决方式只有一条路PDU模式原样取数据然后按UCS2解码。这也是为什么脚本里专门留了一个文本读到乱码就回退PDU的路径。实际测下来中文验证码短信、银行通知全部能正确解码。另一个小坑是PDU里时间戳的编码PDU的时间戳每个字节的高四位和低四位的顺序是反的比如真实月份09在PDU里是90。解析时如果发现时间不对记得交换高低位。5.2 同一个未读短信被反复转发第一次跑通脚本后我收到了好几条重复的转发通知。原因是这个模块在ATCMGL之后并不会自动把短信标记为已读下次查未读短信还能查到同一条。我的处理方法是转完就删。这个方案简单粗暴但有效。如果你更谨慎一点可以先把状态存数据库转发失败再捞回来但实际使用中先推送成功再删除的风险已经足够低。真怕删错可以在删除前把完整内容写一份到日志文件里。5.3 ModemManager抢占串口导致脚本刚启动就报错脚本写好后第一次用systemd启动日志里直接报[Errno 16] Device or resource busy。检查发现系统里的ModemManager自动把ttyUSB2抢占了。处理方式就是第3章提到的停掉并禁用ModemManagersystemctl stop ModemManager systemctl disable ModemManager如果设备里完全没有ModemManager反而是好事少一个系统服务干扰。5.4 USB串口不定时消失日志疯狂报错稳定运行一段时间后我发现日志里开始出现打不开串口的错误。检查发现内核把USB设备挂起了挂起之后串口设备直接消失。这个问题第2章的fix-usb-power.service能彻底解决。再加上禁止系统睡眠设备可以7x24小时无人工干预运行。调试的时候记得用dmesg | grep usb查一下设备断连的原因很大概率都是autosuspend。另外如果脚本捕捉到串口异常后能自动重连整个系统会更抗造。我在主循环里加了异常重连逻辑实测串口恢复后脚本能自己重新连上。5.5 AT指令偶尔卡死不响应有一个玄学问题连续跑几天后偶尔模块对AT指令不响应读取短信超时。最开始我以为是程序死了后来发现是AT指令响应一直没有结束符串口读超时后程序会继续跑但模块内部可能还在忙。处理办法是在send_at里加严格的超时控制超时后下一条指令前先复位输入缓冲区。串口被占死的最坏情况下重启一次Python进程都能恢复。systemd里配置了Restartalways所以进程崩溃后10秒内会自动拉起来。5.6 连续运行三天的实测数据最后汇报一下我这边的实测结果。设备是UFI003同款高通410方案Debian系统移动卡连续运行三天指标数值收短信条数57条成功转发条数57条转发成功率100%平均延迟10~15秒内存占用约38MBCPU占用基本为0转发延迟主要受轮询间隔影响脚本本身几乎不消耗资源。设备该当热点还当热点短信转发是后台跑着的小服务。有一点要特别提醒短信转发依赖设备当前有没有可用的网络出口。如果设备断网了或者SIM卡流量被停掉邮件和Webhook都可能发不出去。所以正式使用前最好用一张已经开通短信功能、且网络状态正常的SIM卡。SMTP发信也建议用465端口避免运营商对通用邮件端口做限制。整体折腾下来最大的感受是高通410这东西的Debian系统可玩性比想象中高太多。一个几十块的随身WiFi同时干着热点、短信转发两件事功耗还低。不刷机、不破坏原系统就用Python在用户态把所有事办完了。希望这篇实测记录能帮你少走几个弯路特别是PDU解码和USB挂起这两个坑提前知道能省下不少排查时间。
网站建设高端定制企业官网