短信猫设备调试
最近收到一个需要应用 JAVA 程序实现控制短信猫发送短信的需求,由于我长期从事的是软件开发工作,对于硬件交互相关知识了解较少,因此在调试短信猫的过程中遇到了诸多问题,在此记录,或能帮助在日后的开发中能少走些许弯路。
简介
所谓短信猫,其实是一种用来收发短信的设备,他和我们用的手机一样,需要手机SIM卡的支持,在需要收发短信的时候,在短信猫里面插入一张我们平时用的手机卡,插上电源,通过(USB或者串口、网口)数据线和电脑相连,在电脑的应用管理软件中就可以实现短信收发的功能。
下图是我调试时所使用的短信猫设备:

我们可以从设备中获取到一些信息,如产品型号参数与服务商、电源接口类型(DB9)、信号线接口、设备SIM卡槽、指示灯等信息。通过访问设备服务商官网,也能够获取到更多资料及相关的服务支持。
在正式开始测试前,需要进行以下内容的确认:
- 插入可用于短信发送的 SIM 卡。
- 连接电源和天线(确保电源指示灯及信号灯正常)。
- 下载任意的串口调试工具,如
SSCOM(Windows)、minicom(Linux)。 - 根据设备官网指引,判断是否需要配置调试所需的相关驱动(检查设备连接的主机是否存在相关的待更新驱动)。
- 准备必要的转接线(用于将短信猫设备连接到调试的电脑主机上)。
确认以上信息后,观察设备上的电源及ACT指示灯是否正常,电源指示灯为红色常亮,ACT灯为绿色闪烁或绿色常亮。
指令
将设备接入到主机后,可以通过串口工具观察接入的可用串口,并结合指令以确定具体的通信串口(注意确定波特率等参数)。
常用 AT 指令
| 指令 | 功能 | 取值 | 响应说明 | 备注 |
|---|---|---|---|---|
| AT | 检查设备是否响应 | 仅查询 | ||
| AT+CMGF | 设置信息格式 | 0-PDU模式,1-文本模式 | ||
| AT+PIN | 检查SIM卡状态 | |||
| AT+CSQ | 查询信号质量 | |||
| AT+CREG | 查询注册状态 | |||
| AT+CSCA | 查看短信中心 | AT+CSCA=“+8613334262200” | ||
| AT+CMGS | 文本模式内容发送 | AT+CMGS=“13327362277” |
设备检查
在正式进行短信发送前,需要确定通信状态、信号质量、卡注册状态等是否正常,这里列出部分常用的状态检测指令如下:
-
AT+CREG?命令用于查询移动站在网络中的注册状态。它可以帮助用户了解设备是否已经成功连接到蜂窝网络以及连接的具体情况。当发送该指令后,短信猫设备响应内容格式如下:+CREG: <n>,<stat>[,<lac>][,<ci>][,<AcT>][,<CiM>]其中,
<n>表示通过AT+CREG=<n>设置的网络注册通知级别。<stat>表示网络注册状态。其余的可选参数<lac>、<ci>、<AcT>、<CiM>则分别表示位置区码、小区识别码、接入技术和小区多时隙等级。<stat>的取值及含义:0-未注册(设备正在查找网络),1-已注册(处于本地网络),2-未注册(但设备不再查找网络),3-注册登记被拒绝,4-未知状态,5-已注册(处于漫游状态)。 -
AT+CSCA?命令可用于查询短信中心设置(同样地,该命令也用于设置短信中心号码,如AT+CSCA="+8613334262200")。在手机设备上,通常可以在
设置-关于手机-状态信息-网络-短信中心号码中查看。 -
AT+CSQ?命令可用于查询信号质量,该命令的响应格式为+CSQ: <rssi>,<ber>:-
<rssi>表示接收信号强度指示,是一个0到31之间的整数,以及99表示未知。数字越大,信号越强。 -
<ber>表示比特误码率,这是一个0到7之间的整数,或者99表示未知。数字越小,误码率越低,即信号质量越好。
-
-
AT+QCFG可以用来读取或设置多个配置选项,这些选项可能包括但不限于网络设置、频段选择、功率等级、PIN码管理等。具体的配置项取决于所使用的 Quectel 模块型号和支持的功能。如:在通过
AT+CREG?查询到设备的网络注册状态异常时,可以尝试使用AT+QCFG="nwscanmode",0,1来设置模块在网络搜索时优先选择 LTE 网络。
信息格式
文本模式
使用指令 AT+CMGF 可以指定信息发送模式,当信息发送模式为文本模式时,短信猫支持发送基于 GSM 7-bit 默认字符集的字符内容,这种编码仅允许发送基于 ASCII 字符集的信息,因此只支持英文字母及相关字符;也可以指定 UCS2 编码,以支持发送中文内容。
短信猫在发送和接收消息时,可以使用不同的字符编码方式来处理信息内容,这些编码方式决定来每个字符占用多少比特位以及能够表示的字符集范围:
-
7-bit
7-bit 编码是 GSM 标准中默认使用的编码方式,主要用于发送基于 ASCII 字符集的信息,它代表了每个字符的实际占用为 7 个比特位。
-
8-bit
8-bit 编码则意味着每个字符的实际占用为一个字节,即 8 个比特位,这种编码方式可以用来发送二进制数据,或是发送混合了不同字符集内容的数据。但这种编码方式不是特别常见,并且对于大多数文本信息来说,其处理效率较低,因此在实际应用中使用较少。
-
16-bit(UCS2)
UCS2 是指 Unicode 字符集的 2 字节版本(即 16 个比特位),它支持全球几乎所有语言字符,因此,这种编码方式是短信猫等设备中应用最广泛的一种。
一个简单的将字符串内容进行 UCS2 编码的 JAVA 代码示例如下:
public static String stringToUcs2Hex(String input) {
// 将字符串转换为UTF-16BE(即大端序的UCS2)字节数组
byte[] bytes = input.getBytes(StandardCharsets.UTF_16BE);
StringBuilder hexBuilder = new StringBuilder();
for (byte b : bytes) {
// 格式化输出为两位十六进制数
hexBuilder.append(String.format("%02X", b));
}
return hexBuilder.toString();
}
在文本模式下,发送消息示例如下:
// 设置短信中心号码(SMSC),这取决于SIM卡的移动运营商
AT+CSCA="+8613800755500"\r\n
OK
// 设置短信模式为文本模式
AT+CMGF=1\r\n
OK
// 设置字符编码,英文可以用GSM 7-bit编码,中文则应使用UCS-2编码
AT+CSCS="GSM"\r\n
OK
// 或者
AT+CSCS="UCS2"\r\n
OK
// 准备发送短信,指定接收者的手机号码
AT+CMGS="+8613000000000"\r\n
>
// 输入短信内容(GSM 7-bit编码时直接输入内容即可)
Hello World!
// 或者(UCS2编码时则需要先将内容转换为UCS2编码内容进行发送)
004F00A000EF00BC00D000C200CE00C200A1(你好,世界!)
// 结束输入并发送短信,Ctrl+Z 表示消息结束
^Z
+CMGS: 42
OK
PDU 模式
当指定信息发送模式为 PDU 模式时,同样需要进行额外的 PDU 编码处理。
PDU 编码是一种用来发送和接收SMS信息(短消息)的模式,相当于一个数据包,它不仅支持中文短信,也能发送英文短信,PDU模式被所有的手机所支持,可以使用任何的字符集,这也是手机的默认编码方式。
一段经过 PDU 编码后的数据示例:0891683133242602F011000D91683125260599F10008AA086D4B8BD56D88606F。
为便于观察,现将该示例数据使用空格拆分成不同的段:
08 91 683133242602F0 11 00 0D 91 683125260599F1 00 08 AA 08 6D4B8BD56D88606F
一般的 PDU 编码由以下十三项组成:
- 短信中心地址长度:对应于示例中的
08,计算为字符串91683133242602F0的字节长度,即 16 / 2 = 8(08)。 - 短信中心号码类型:对应于示例中的
91,91 为TON/NPI(手机等设备),81 为小灵通等。 - 短信中心号码,此一项与第2项一起,将与第1项保持一致:对应于示例中的
683133242602F0,为短信中心反向编码结果,还原则为8613334262200F,其中字符“F”是当手机号为奇数时的补充位。 - 文件头字节:对应于示例中的
11,指正常地发送短信。 - 信息类型:对应于示例中的
00,为常用取值。 - 被叫号码长度:对应于示例中的
0D,即字符串683125260599F1去掉F后的长度。 - 被叫号码类型,与第2项取值范围相同:对应于示例中的
91。 - 被叫号码,内容长度与第6项保持一致:对应于示例中的
683125260599F1,还原则为8613526250991F。 - 协议标识(TP-PID):对应于示例中的
00,为常用取值。 - 数据编码方案:对应于示例中的
08。 - 有效期(TP-VP):对应于示例中的
AA,还原十进制为 170,对应为 4 天。 - 用户数据长度:对应于示例中的
08,计算为字符串6D4B8BD56D88606F的字节长度。 - 用户数据内容,长度与第12项保持一致:对应于示例中的
6D4B8BD56D88606F,16进制表示的Unicode码,补充标识后为“\u6D4B\u8BD5\u6D88\u606F”,转码后得到结果:“测试消息”。
根据以上规则,现补充一段用于进行 PDU 编码的 JAVA 代码示例如下:
public static JSONObject createPduMessage(String sender, String receiver, String message) {
if (!sender.startsWith("86") || !receiver.startsWith("86")) {
return null;
}
sender = changeMobile(sender.toCharArray());
receiver = changeMobile(receiver.toCharArray());
sender = "0891" + sender;
receiver = "11000D91" + receiver;
String msgHex = str2Hex(message);
String lengthHex = Integer.toHexString(msgHex.length() / 2);
if (lengthHex.length() < 2) {
lengthHex = "0" + lengthHex;
}
String rcvStr = receiver + "0008AA" + lengthHex + msgHex;
String pduStr = sender + rcvStr;
// 最终PDU消息长度(去除源地址)
int code = rcvStr.length() / 2;
JSONObject resultObj = new JSONObject();
resultObj.put("pduStr", pduStr);
resultObj.put("code", code);
return resultObj;
}
/**
* 获取手机号反向编码:将手机号补充为偶数位,并调换手机号字符数组中的字符位置
* 示例:8613334262200 --> 8613334262200F --> 683133242602F0
*/
private static String changeMobile(char[] charArr) {
if (charArr.length % 2 != 0) {
charArr = Arrays.copyOf(charArr, charArr.length + 1);
charArr[charArr.length - 1] = 'F';
}
for (int i = 0; i < charArr.length - 1; i += 2) {
char temp = charArr[i];
charArr[i] = charArr[i + 1];
charArr[i + 1] = temp;
}
return String.valueOf(charArr);
}
/**
* 获取HEX格式的Unicode字符串
* 示例:消息 => \u6d88\u606f => 6d88606f
*/
private static String str2Hex(String str) {
StringBuilder unicode = new StringBuilder();
for (char c : str.toCharArray()) {
char[] chars = Character.toChars(c);
for (char ch : chars) {
unicode.append(String.format("%04x", (int) ch));
}
}
return unicode.toString().toUpperCase();
}
在 JAVA 中应用
短信猫支持使用串口进行指令交互,因此想要将其与 JAVA 软件程序结合起来,可以使用 jSerialComm 等串行端口通信的依赖库,它封装了很多简单易用的 API,且兼容多种操作系统。
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.11.0</version>
</dependency>
一段使用 jSerialComm 建立串口连接并进行指令交互的 JAVA 示例代码如下:
private String sendCommandForResp(String command) {
SerialPort serialPort = SerialPort.getCommPort(PORT_NAME);
serialPort.setComPortParameters(BAUD_RATE, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY);
serialPort.setComPortTimeouts(SerialPort.TIMEOUT_WRITE_BLOCKING, 0, 0);
if (!serialPort.openPort()) {
log.error("Serial port open failed!");
return null;
}
String resp = null;
try {
serialPort.getOutputStream().write(command.getBytes(StandardCharsets.US_ASCII));
serialPort.getOutputStream().flush(); // 确保所有字节都被写出
Thread.sleep(500); // 等待回应
StringBuilder responseBuilder = new StringBuilder();
byte[] buffer = new byte[1024];
int numRead;
// 持续读取直到没有更多数据
while (serialPort.bytesAvailable() > 0) {
numRead = serialPort.readBytes(buffer, buffer.length);
responseBuilder.append(new String(buffer, 0, numRead, StandardCharsets.US_ASCII));
}
resp = responseBuilder.toString().trim();
} catch (IOException | InterruptedException e) {
log.error(e.getMessage());
} finally {
serialPort.closePort();
}
}
Minicom
参考
著作権声明
本記事のリンク:https://www.chinmoku.cc/dev/java/others/smscat/
本博客中的所有内容,包括但不限于文字、图片、音频、视频、图表和其他可视化材料,均受版权法保护。未经本博客所有者书面授权许可,禁止在任何媒体、网站、社交平台或其他渠道上复制、传播、修改、发布、展示或以任何其他方式使用此博客中的任何内容。