简介:串口短信猫.rar 面向需要在工业自动化、远程监控与报警系统中集成短信收发能力的开发者,尤其适合刚接触 GSM 模块、希望快速完成串口通信验证的编程人员。压缩包整体约 17.96MB,内含配置说明、测试源码、调试方法与测试程序等资料,源码部分覆盖 C++、Python、Java 等多种语言示例,演示串口初始化、AT 命令发送、设备响应读取及短信处理逻辑,可帮助读者对照搭建自己的收发系统。资源围绕 RS232 或 USB 串口连接展开,涉及波特率 9600、数据位 8、停止位 1、无校验等常见参数设置,并通过检查网络状态、读取 SIM 卡信息等 AT 命令完成基本功能验证。常见问题文档汇总通信异常与命令执行失败等故障现象及解决思路,测试程序则支持模拟收发,便于提前发现潜在问题。目前已有 149 人学习,适合作为串口短信猫开发与调试的入门参考。
1. 串口短信猫.rar 里到底装了什么:从 AT 指令到批量收发的完整链路
很多人第一次拿到「串口短信猫.rar」这种压缩包,第一反应是解压找 exe,双击,然后发现要么报错、要么界面简陋得让人怀疑人生。我当年也这样,折腾一下午没发出去一条短信,最后才发现问题根本不在软件,而在于我没搞懂串口短信猫的本质——它就是一个通过 RS232 或 USB 转串口芯片挂到电脑上的 GSM 模块,所有收发短信的动作,底层都是往串口写 AT 指令。压缩包里通常包含三类东西:串口调试工具或自研上位机、AT 指令测试脚本、以及一份可能写得不太全的说明文档。真正值钱的不是那个界面,而是它背后那套「串口参数 + AT 指令 + 编码格式」的组合拳。
这篇文章面向的是需要把短信收发能力集成进自己系统的开发者,比如做设备告警、验证码下发、批量通知的场景。我会把串口短信猫从硬件识别、串口配置、AT 指令交互、PDU 编码、批量任务调度到常见翻车点,按我实际落地过的路径讲一遍。你不需要那个 rar 里的具体软件,照着这套逻辑,用任意串口助手加几十行代码就能跑通。读完你至少能判断:这东西能不能接进你的业务、要调哪些参数、坑会出在哪。
2. 串口短信猫的硬件识别与串口参数配置:先让电脑认得出它
2.1 串口短信猫的三种常见形态与驱动差异
市面上能见到的串口短信猫,物理形态基本逃不出三类。第一类是纯串口版,DB9 接口,直接插主板原生 COM 口,工控机上最常见,优点是稳定、不挑系统,缺点是现在笔记本基本没这口。第二类是 USB 转串口版,内部用 CH340、CP2102、PL2303 这类芯片,插上后系统多出一个虚拟 COM 口,这是绝大多数人手里的形态。第三类是 USB 直接枚举成 Modem 设备,走的是 CDC ACM 或者厂商私有驱动,设备管理器里显示为调制解调器而不是 COM 口。
这三类的区别直接决定你后面怎么打开端口。纯串口和 USB 转串口,你拿到的是一个 COM 号,用串口库直接 open 就行。USB Modem 形态则可能需要走系统 API 或者特定驱动暴露的接口。我一般会先在设备管理器里确认它到底以什么身份出现,再决定用哪套代码。如果是 CH340 这类芯片,Windows 10 以后基本免驱,Linux 下内核也自带 ch341 驱动,插上就能看到 /dev/ttyUSB0。
提示:同一台机器插多个 USB 转串口设备时,COM 号可能每次开机都变。生产环境建议用芯片唯一 ID 在 udev 或系统层面绑定固定名称,别在代码里写死 COM3。
2.2 用命令行和串口助手确认端口可用
在写任何业务代码之前,先用最笨的办法确认硬件是活的。Windows 下可以用模式命令看端口列表,Linux 下直接列 tty 设备。下面这段 bash 在 Linux 和 macOS 上都能跑,用来观察插拔前后设备节点的变化。
# 插拔前先记录当前串口设备 ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null # 插入短信猫后再执行一次,对比多出来的节点 ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null # 查看该设备的驱动和 VID/PID 信息 udevadm info -a -n /dev/ttyUSB0 | grep -E "idVendor|idProduct|driver"这段命令的逻辑是:先建立基线,再对比增量,最后确认驱动绑定情况。参数上,ttyUSB 通常是 USB 转串口芯片,ttyACM 是 CDC ACM 类设备。如果插上后什么都没多出来,先换线、换口,再查驱动,别急着怀疑模块坏了。确认端口存在后,用串口助手以 9600、8N1、无流控打开,发一条AT,正常应该回OK。这一步是整个链路的地基,地基没打牢,后面所有代码都是白写。
2.3 串口参数里最容易被忽略的三个设置
串口参数看起来就波特率、数据位、停止位、校验位、流控这几项,但实际踩坑最多的往往是下面三个。第一是波特率,短信猫常见的有 9600、115200、57600,部分模块支持自动波特率,但自动协商经常在冷启动时失败,我一般手动固定成模块手册标称值。第二是流控,很多模块默认要硬件流控 RTS/CTS,你代码里没开,发长指令就会丢字符,表现为偶尔成功偶尔失败,非常玄学。第三是换行符,AT 指令结尾必须是\r,有些模块还要求\r\n,只发\n或者不发换行,模块根本不搭理你。
我习惯在代码里把这三个参数做成可配置项,而不是写死。因为不同批次、不同厂家的模块,这三项都可能不一样。下面是一个 Python 串口初始化的片段,用 pyserial,把关键参数都暴露出来。
import serial # 串口短信猫初始化,参数按模块手册调整 ser = serial.Serial( port="/dev/ttyUSB0", # 端口,Windows 下换成 COM3 这类 baudrate=9600, # 波特率,常见 9600/115200 bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=2, # 读超时,避免 readline 永久阻塞 rtscts=False, # 硬件流控,模块要求时才开 dsrdtr=False ) # 发一条最基本的 AT,确认链路 ser.write(b"AT\r") resp = ser.read(64) print(resp) # 期望看到 b'\r\nOK\r\n'逻辑说明:timeout 必须设,否则模块不回数据时你的线程就挂死在那里。rtscts 默认关,但如果发现发长指令丢字符,第一个要试的就是把它打开。write 的内容结尾必须是\r,这是 AT 指令的硬性约定。read 的长度给 64 字节是经验值,够读回 OK 或错误码,正式代码里应该按行读并做超时重试。
3. 用 AT 指令跑通短信收发:从文本模式到 PDU 模式
3.1 文本模式发短信的最小指令序列
短信猫刚上手,建议先用文本模式(Text Mode)发一条纯英文短信,把链路跑通。文本模式的指令序列很短,但每一步的返回都要检查,不能发完就不管。下面这段 Python 演示了完整流程,包括设置短信中心号码、设置文本模式、发短信。
import serial import time ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=2) def send_at(cmd, wait=1): ser.write((cmd + "\r").encode()) time.sleep(wait) return ser.read(256).decode(errors="ignore") # 1. 确认模块在线 print(send_at("AT")) # 2. 查询短信中心号码,确认 SIM 卡已注册 print(send_at("AT+CSCA?")) # 3. 设置为文本模式 print(send_at("AT+CMGF=1")) # 4. 指定目标号码,等待模块返回 '>' 提示 print(send_at('AT+CMGS="13800138000"', wait=1)) # 5. 发送内容,结尾用 Ctrl+Z (0x1A) 表示结束 ser.write(b"Hello from serial modem\x1a") time.sleep(3) print(ser.read(256).decode(errors="ignore"))逻辑说明:AT+CMGF=1切文本模式,AT+CMGS进入发送状态,模块回>后你才能写正文,正文以0x1A结束。参数上,目标号码要用引号包起来,国内号码直接写 11 位。如果第 4 步没等到>,通常是短信中心号码没设对,或者 SIM 卡没注册上网络。文本模式只适合纯英文和数字,中文会乱码,这是它最大的局限。
3.2 PDU 模式为什么绕不开:中文短信的编码真相
文本模式发中文,收到的是一串问号或者乱码,因为文本模式默认走 GSM 7bit 编码,中文根本不在这个字符集里。要发中文,必须切 PDU 模式(AT+CMGF=0),自己把短信内容编码成 PDU 串。PDU 的本质是把短信中心号码、目标号码、编码方式、时间戳、正文全部打包成十六进制字符串。中文用 UCS2 编码,每个字符两字节。
很多人卡在 PDU 编码上,是因为网上抄来的代码只处理了英文,中文一进去就错。下面这段 Python 用现成的编码逻辑,把中文短信转成 PDU,重点是 UCS2 部分。
def phone_to_pdu(phone): # 目标号码转 PDU 半字节交换格式 pdu = "91" + "%02X" % len(phone) for i in range(0, len(phone), 2): if i + 1 < len(phone): pdu += phone[i+1] + phone[i] else: pdu += "F" + phone[i] return pdu def text_to_ucs2(text): # 中文按 UCS2 编码,每字符两字节 return text.encode("utf-16-be").hex().upper() def build_pdu(phone, text, sca="+8613800100500"): sca_pdu = "00" # 用 SIM 卡内短信中心,这里置 00 first = "11" # 11 表示有状态报告+UCS2 编码 tp_len = len(text_to_ucs2(text)) // 2 pdu = sca_pdu + first + "00" + phone_to_pdu(phone) + "00" + \ "%02X" % tp_len + text_to_ucs2(text) return pdu pdu_str = build_pdu("13800138000", "设备告警:温度过高") print(pdu_str) # 发送时用 AT+CMGS=<长度>,长度是 PDU 串去掉短信中心部分后的字节数逻辑说明:phone_to_pdu做的是半字节交换,这是 PDU 格式的硬性要求。text_to_ucs2用 utf-16-be 得到大端 UCS2,正好对应 PDU 里的中文编码。first字节里的11表示同时要状态报告和用 UCS2,如果你不需要状态报告,改成01。发送时AT+CMGS=后面的长度参数是 PDU 串总长度减去短信中心号码部分的字节数,算错这个长度,模块直接回 ERROR。这块是血泪经验,长度差一个字节都不行。
3.3 收短信:主动查询和自动上报两种模式
收短信有两种玩法。第一种是主动查询,用AT+CMGL="ALL"列出所有短信,适合定时轮询的场景。第二种是自动上报,用AT+CNMI配置新短信到达时直接通过串口推给你,适合实时性要求高的场景。我一般先用主动查询把逻辑跑通,再切自动上报。
主动查询的返回是 PDU 串,需要自己解析。解析比编码更烦,因为要处理短信中心号码长度、时间戳、编码标志位。下面这段代码演示怎么从AT+CMGL的返回里提取正文。
def parse_pdu(pdu): # 简化解析:跳过短信中心,定位正文 idx = 0 sca_len = int(pdu[idx:idx+2], 16) idx += 2 + sca_len * 2 idx += 2 # 跳过 first octet phone_len = int(pdu[idx:idx+2], 16) idx += 2 phone_bytes = (phone_len + 1) // 2 * 2 idx += phone_bytes idx += 2 # 跳过协议标识 dcs = int(pdu[idx:idx+2], 16) idx += 2 idx += 14 # 跳过时间戳 7 字节 text_len = int(pdu[idx:idx+2], 16) idx += 2 body = pdu[idx:idx + text_len * 2] if dcs == 0x08: # UCS2 return bytes.fromhex(body).decode("utf-16-be") return bytes.fromhex(body).decode("ascii", errors="ignore") # 假设从 AT+CMGL 返回里拿到一行 PDU print(parse_pdu("0791..."))逻辑说明:解析的关键是每一步的偏移量都要算准,短信中心长度、号码长度、时间戳都是变长的。dcs 字段决定编码方式,0x08 是 UCS2,0x00 是 GSM 7bit。这段代码做了简化,实际生产里要处理长短信拼接(UDH),那是另一个坑。自动上报模式用AT+CNMI=2,1,0,0,0,新短信会以+CMTI形式通知你,你再根据索引去读,比轮询省资源。
4. 批量短信任务的调度与限速:别把模块当服务器用
4.1 串口短信猫的吞吐边界在哪里
很多人以为短信猫能像 HTTP 接口一样每秒发几十条,实际完全不是。一条短信从发指令到模块返回+CMGS确认,正常要 3 到 8 秒,取决于网络注册状态和短信中心响应速度。也就是说,单模块单卡的理论上限就是每分钟十几条。你硬压任务进去,模块的发送队列会溢出,表现为大量 ERROR 或者干脆不响应。
我实测过的边界是:连续发送时,每条之间至少留 5 秒间隔,模块能稳定工作几小时不出错。如果间隔压到 2 秒,前几十条可能正常,后面就开始随机失败。这个数字不是绝对的,和模块型号、信号强度、运营商都有关,但量级上不会差太多。所以批量任务的第一原则是限速,第二原则是失败重试,第三原则是别指望单模块扛高并发。
4.2 用队列加令牌桶做发送限速
限速的实现方式,我一般用队列加令牌桶。任务进队列,消费者按固定速率取令牌,拿到令牌才发。下面这段 Python 用简单的 sleep 加队列模拟,生产里可以换成 Redis 队列加定时任务。
import queue import threading import time task_q = queue.Queue() RATE_LIMIT = 5 # 每条间隔 5 秒 def worker(ser): while True: phone, text = task_q.get() try: # 这里调用前面封装好的发送函数 send_sms(ser, phone, text) print(f"sent to {phone}") except Exception as e: print(f"failed {phone}: {e}") task_q.put((phone, text)) # 简单重试,生产要加次数上限 finally: task_q.task_done() time.sleep(RATE_LIMIT) # 启动消费者 t = threading.Thread(target=worker, args=(ser,), daemon=True) t.start() # 生产任务 for i in range(100): task_q.put(("1380013800%d" % i, "告警通知"))逻辑说明:RATE_LIMIT 是核心参数,按模块实际能力调,别贪快。重试逻辑这里写得简单,生产环境必须加最大重试次数和失败落库,否则一条坏号码会无限循环。队列的好处是解耦,你的业务系统只管往里塞,发送节奏由消费者控制。如果任务量真的很大,正确做法是加模块加卡,而不是压榨单模块。
4.3 多模块并行时的端口隔离与任务分配
当你有多个短信猫时,任务分配和端口隔离就变得重要。每个模块独立一个串口、独立一个消费者线程,任务按模块健康状态分发。健康状态可以用定期发AT探测,连续失败就摘除该模块。下面是一个简单的模块池管理思路。
class ModemPool: def __init__(self, ports): self.modems = [] for p in ports: ser = serial.Serial(p, 9600, timeout=2) self.modems.append({"ser": ser, "healthy": True, "port": p}) def get_healthy(self): # 返回当前健康模块,轮询分配 for m in self.modems: if m["healthy"]: yield m def health_check(self): for m in self.modems: try: m["ser"].write(b"AT\r") resp = m["ser"].read(32) m["healthy"] = b"OK" in resp except Exception: m["healthy"] = False逻辑说明:健康检查要定期跑,比如每 30 秒一次,别太频繁,否则干扰正常发送。get_healthy 用生成器做轮询,简单但够用。生产里还要考虑模块重启、SIM 卡欠费、信号丢失这些情况,健康检查只能覆盖串口层面的问题,网络层面的要靠AT+CREG?查注册状态。
5. 串口短信猫避坑清单:那些让我加班到凌晨的故障
5.1 发中文乱码:编码标志位和长度算错
现象:英文短信正常,中文发出去对方收到乱码或者半截。原因通常是两个,一是AT+CMGF没切到 0,还在文本模式;二是 PDU 里 first octet 的编码标志没设成 UCS2,或者正文长度按字符数算而不是字节数。解决:确认AT+CMGF=0,first octet 用11或01,长度用len(text.encode("utf-16-be")) // 2。这个坑我踩过不止一次,每次都是长度差一两个字节。
5.2 模块偶尔不响应:流控和电源是元凶
现象:发指令十次里有一两次没返回,重启又好了。原因多半是硬件流控没开,模块缓冲区满了丢字符;或者 USB 供电不足,模块在发射瞬间电流拉高导致掉线。解决:先开 rtscts 试,不行就换带独立供电的 USB Hub,再不行用万用表量模块供电电压,发射时别低于 4.5V。这个问题的玄学程度很高,因为表现随机,但根因基本就这两个。
5.3 短信堆积在 SIM 卡:存储满导致新短信丢失
现象:收短信收着收着就收不到了,查AT+CPMS?发现 SIM 卡存储满了。原因是你读了短信但没删,模块默认存在 SIM 卡里,容量通常只有几十条。解决:每次读完短信后执行AT+CMGD=<索引>删除,或者配置AT+CPMS把存储切到模块内存。我一般读完就删,避免堆积。这个坑在长时间运行的系统里特别常见,前期测试量小发现不了。
5.4 目标号码格式错误:+86 和本地号码混用
现象:发送返回 ERROR,或者对方收不到。原因:号码格式不统一,有的带 +86,有的不带,PDU 编码时长度算错。解决:统一成不带国家码的本地格式,或者统一带 +86 并在编码时正确处理。PDU 里号码长度字段是号码位数,不是字节数,带 +86 时位数要算对。这个坑的隐蔽性在于,同一个号码有时能发有时不能,其实是格式在不同代码路径里不一致。
5.5 串口被占用:多个进程抢同一个端口
现象:程序启动报 Permission denied 或者串口打开失败。原因:上一个进程没正常退出,端口没释放,或者你开了两个程序同时读同一个口。解决:确保单进程独占,退出时显式 close,Linux 下可以用lsof /dev/ttyUSB0查占用。Windows 下更麻烦,有时候要拔插设备才能释放。生产环境建议用守护进程管理,别手动起多个实例。
6. 把短信猫接进业务系统的进阶做法:状态机与可观测性
前面讲的都是单次收发,真正上线要解决的是「长期稳定运行」和「出问题能查」。我的做法是给短信猫封装一个状态机,把空闲、发送中、等待确认、失败重试这几个状态显式管理起来,再配上日志和指标。状态机的核心是每个状态有超时,超时就回退到空闲并记录,避免卡死。
from enum import Enum class State(Enum): IDLE = 0 SENDING = 1 WAIT_ACK = 2 RETRY = 3 class SmsModem: def __init__(self, ser): self.ser = ser self.state = State.IDLE self.retry = 0 def tick(self): if self.state == State.IDLE: # 取任务,切 SENDING pass elif self.state == State.SENDING: # 写 AT+CMGS,切 WAIT_ACK,设超时 pass elif self.state == State.WAIT_ACK: # 读返回,成功回 IDLE,失败切 RETRY pass elif self.state == State.RETRY: if self.retry < 3: self.retry += 1 self.state = State.SENDING else: self.state = State.IDLE self.retry = 0逻辑说明:状态机的好处是每个环节都有明确的超时和转移条件,不会因为某一步没返回就永久挂起。retry 上限设 3 次,超过就放弃并记录,避免坏任务拖垮整个队列。tick 方法由定时器驱动,比如每 500 毫秒跑一次。
可观测性方面,我至少会记录四类信息:每条短信的发送时间、目标号码、状态、耗时。这些落到日志或者时序数据库里,出问题时能快速定位是模块问题、网络问题还是号码问题。下面这张表是我实际用的关键指标,你可以按需增减。
| 指标名 | 含义 | 正常范围 | 异常时先查 |
|---|---|---|---|
| send_duration | 单条发送耗时 | 3 到 8 秒 | 超过 15 秒查信号注册 |
| success_rate | 发送成功率 | 95% 以上 | 低于 90% 查 SIM 卡和余额 |
| at_timeout_count | AT 超时次数 | 接近 0 | 升高查流控和供电 |
| sim_storage_used | SIM 卡短信占用 | 低于 80% | 满了就删旧短信 |
最后说一个我自己的习惯:任何短信猫项目,上线前先跑 24 小时压测,用真实号码发几百条,观察成功率曲线和模块温度。压测能暴露 90% 的坑,剩下的 10% 靠日志和状态机兜底。这套东西不复杂,但需要耐心调参数。希望帮到你。
本文还有配套的精品资源,点击获取