最近在做一个短信猫发送短信的功能,在这过程中也熟悉了一些Java串口通讯的知识。
将短信猫或者其他的一些串口设备连接在电脑上,从设备管理器里就可以看到该外部设备了
因为我是用USB转串口,所以还需要再电脑上安装一个驱动,将设备连到电脑上之后打开驱动精灵,就会自动识别出需要下载的驱动,安装一下就好了。
在Windows上串口的名字一般是COM*,在Linux上串口的名字是/dev/tty*或/dev/ttyUSB*
设备连上以后开始写代码了。
用短信猫发送短信大致的思路就是通过程序向串口发送AT指令,串口将指令数据包传送给外部设备,之后设备收到指令数据包就会做出相应的处理。串口的字符默认是ASCII码,而短信猫发送中文短信一般是要将中文转为Unicode编码,这可能是短信运营商那边要求的字符集格式。
Java要做串口通讯还是挺麻烦的,JDK中自带串口类库已经很过时了。我是用的是第三方类库RXTXcomm.jar,这个jar包无法从maven仓库中下载,只能自己到网上找,
这里给上官方的下载地址:
http://rxtx.qbang.org/wiki/index.php/Download
将RXTXcomm.jar放到项目中,并且将rxtxSerial.dll和rxtxParallel.dll文件放入jdk的bin目录下。如果是Linux系统的话就又有一个坑了,我试过将librxtxSerial.so文件放到jdk的bin目录下,运行时候报出异常java.lang.UnsatisfiedLinkError: no rxtxSerial in java.library.path thrown while loading gnu.io.RXTXCommDriver
,说明上一步的dll或者so文件没有放入正确的位置。
在程序里面用System.getProperty("java.library.path")
打印出来发现Java在Linux上的默认调用动态库路径目录是在/usr/lib64/下面。
因为我的项目是maven工程,为了让RXTXcomm.jar打包的时候不会被漏掉,还需要在pom文件中添加代码:
在
节点中添加如下代码
<dependency>
<groupId>gnugroupId>
<artifactId>ioartifactId>
<version>2.2version>
<scope>systemscope>
<systemPath>${project.basedir}/src/main/resources/lib/RXTXcomm.jarsystemPath>
dependency>
在
节点中添加如下代码(这是打war包的配置,如果项目要打jar包的话这个配置是不行的,请自行百度jar包的配置):
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-war-pluginartifactId>
<configuration>
<webResources>
<resource>
<directory>${project.basedir}/src/main/resources/libdirectory>
<targetPath>WEB-INF/lib/targetPath>
<includes>
<include>**/*.jarinclude>
includes>
resource>
webResources>
configuration>
plugin>
准备工作完成以后开始编写代码了
串口操作工具类
@Slf4j
public class SerialPortUtils {
private SerialPort serialPort; // RS232串口对象
private InputStream inputStream; // 输入流
private OutputStream outputStream; // 输出流
/**串口开关标记*/
private boolean portOpenTag = false;
/**
* 打开串口
* @param portName 串口名称
*/
public void openPort(String portName) throws IOException {
if (!portOpenTag){
try {
CommPortIdentifier commPortId = CommPortIdentifier.getPortIdentifier(portName);
//open:(串口占用的程序名字(随意命名),阻塞时等待的毫秒数)
serialPort = (SerialPort) commPortId.open("czx",2000);
serialPort.setRTS(true); //打开RTS
//初始化COM口的传输参数,波特率,数据位,停止位...。
serialPort.setSerialPortParams(115200, SerialPort.DATABITS_8,
SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
} catch (NoSuchPortException e) {
log.error("串口号错误,错误的串口号:{}", portName);
} catch (PortInUseException e) {
log.error("串口已被占用", e);
} catch (UnsupportedCommOperationException e) {
log.error("不受支持的串口参数", e);
}
inputStream = serialPort.getInputStream();
outputStream = serialPort.getOutputStream();
portOpenTag = true;
}
else {
//一个串口操作工具只能操作一个串口,如果想要操作多个串口就创建多个对象
throw new RuntimeException("该串口操作对象已经打开了一个串口");
}
}
/**
* 向串口写入字节数组
* @param data
* @throws IOException
*/
public void writeByteData(byte[] data) throws IOException {
outputStream.write(data);
outputStream.flush();
}
/**
* 向串口写入文本数据
* @param data
*/
public void writeTextData(String data) throws IOException {
//串口通信要求的字符集是ASCII
byte[] bytes = data.getBytes(StandardCharsets.US_ASCII);
outputStream.write(bytes);
outputStream.flush();
}
/**
* 从串口读取字节数组
* @return
*/
public byte[] readByteData() throws IOException {
// 通过输入流对象的available方法获取数组字节长度
int i = inputStream.available();
if (i < 0){
return new byte[0];
}
byte[] readBuffer = new byte[i];
int readed = inputStream.read(readBuffer);
return readBuffer;
}
/**
* 从串口读取文本信息
* @return
*/
public String readTextData() throws IOException {
byte[] bytes = readByteData();
return new String(bytes, StandardCharsets.US_ASCII);
}
public void closePort(){
//将串口开关状态设为false
portOpenTag = false;
try {
if (outputStream != null){
outputStream.close();
}
if (inputStream != null){
inputStream.close();
}
if (serialPort != null){
serialPort.close();
}
} catch (IOException e) {
log.error("串口关闭异常", e);
}
}
/**
* 获取该工具对象是否已经打开了一个串口
* @return
*/
public boolean isPortOpen() {
return portOpenTag;
}
}
AT指令枚举类(有些厂家的设备可能指令会不一样,请以厂家的说明文档为准)
public enum AtCommand {
//指令名称
reset_data("at&f\r"), //恢复出厂设置
close_echo("ate0\r"), //关闭回显
save_parameters("at&w\r"), //保存参数
set_sms_code("at+cscs=\"ucs2\"\r"), //设定短信编码
set_unicode_encoding("at+cmgf=0\r"), //设定Unicode编码
pdu_length("at+cmgs=%d\r"), //pdu编码长度
terminator("\u001A"); //指令结束符号
blank("at\r"); //空指令,用来发送心跳用的
//指令码
private String commandCode;
AtCommand(String commandCode){
this.commandCode = commandCode;
}
/**
* 获取指令码
* @return
*/
public String getCommandCode(){
return commandCode;
}
}
PDU协议工具类,负责将短信内容构造成PDU编码,不同的设备厂家可能协议也不一样,具体请以厂家说明数为准
这里提供一个pud编码转换的网站
http://www.twit88.com/home/utility/sms-pdu-encode-decode?tdsourcetag=s_pctim_aiomsg
@Slf4j
public class ChkjProtocolUtil {
/**设备通信协议前缀*/
private static final String CHKJ_PRE = "0031000B81";
/**设备通信协议附加码*/
private static final String EXTRA_CODE = "0008A7";
/**
* 构造协议中的pdu短报文编码
* @param telephoneNum
* @param content
* @return
*/
public static String pduShortCode(String telephoneNum, String content){
//短报文pdu编码=前缀+电话(按协议转为小端)+附加码+消息长度(16进制)+消息(16进制)+结束符
return CHKJ_PRE + convertTelephone(telephoneNum) + EXTRA_CODE +
shortContentUnicodeLengthHex(content) + content2Hex(content) + AtCommand.terminator.getCommandCode();
}
/**
* 构造协议中的pdu长报文编码
* @param telephoneNum
* @param content
* @param page 报文总共几页
* @param limit 当前是第几页
* @return
*/
public static String pduLongCode(String telephoneNum, String content, int page, int limit){
String pageHex = convertDec2Hex(page);
String limitHex = convertDec2Hex(limit);
//长报文编码=协议前缀+手机号(小端)+ 附加码+消息长度(16进制)+"05000323"+ 报文总页数(16进制)+ 当前页数(16进制)+消息(16进制)+结束符
String result = CHKJ_PRE + convertTelephone(telephoneNum) + EXTRA_CODE + longContentUnicodeLengthHex(content) +
"05000323" + pageHex + limitHex + content2Hex(content) + AtCommand.terminator.getCommandCode();
return result;
}
/**
* 将手机号码转为短信猫规定的格式(最后面+F,然后两个两个字符对换)
* @return
*/
public static String convertTelephone(String telephoneNum){
char[] chars = (telephoneNum.concat("F")).toCharArray();
for (int i = 0; i < chars.length; i+=2){
char c = chars[i];
chars[i] = chars[i+1];
chars[i+1] = c;
}
return new String(chars);
}
/**
* 将消息内容转为16进制字符串
* @param content
* @return
*/
public static String content2Hex(String content){
try {
StringBuilder result = new StringBuilder();
byte[] contentBytes = content.getBytes("Unicode");
/*
Java中字符串转为Unicode编码只有有个坑,前两个字节永远是FF,FE,
这两个字节是为了标识大端还是小端,与我们业务无关,所以要把这两个字节去掉
*/
for (int i = 2; i < contentBytes.length; i++){
String hex = String.format("%x", contentBytes[i]);
if ((contentBytes[i] & 0xff) < 0x10){
result.append(0);
}
result.append(String.format("%x", contentBytes[i]));
}
return result.toString().toUpperCase();
} catch (UnsupportedEncodingException e) {
log.info("该计算机不支持Unicode编码", e);
return null;
}
}
/**
* 将消息内容转为Unicode编码
* 这里要注意的是,Java中的字符串转Unicode编码有个坑,任何字符串转为Unicode编码的字节数组以后,
* 前两位字节永远是FF,FE,这两个字节是为了标识大端还是小端,与我们业务无关,所以要把这两个字节去掉
* @return
*/
public static byte[] content2Unicode(String content){
try {
byte[] unicodeBytes = content.getBytes("Unicode");
byte[] result = new byte[unicodeBytes.length -2];
System.arraycopy(unicodeBytes, 2, result, 0, result.length);
return result;
} catch (UnsupportedEncodingException e) {
log.info("该计算机不支持Unicode字符集", e);
return null;
}
}
/**
* 根据消息内容,获取这个内容转为pud编码以后的长度
* 按照设备说明书上的算法,pud编码长度=pdu编码去掉最开始的00和最后的0x1A后的长度除以2
* @param pduCode 消息内容的16进制字符串
* @return
*/
public static int pduLength(String pduCode){
return (pduCode.length() - 3) >>> 1;
}
/**
* 获取短消息内容转为Unicode编码后的长度的16进制字符串
* @param shortContent 消息字符串
* @return
*/
public static String shortContentUnicodeLengthHex(String shortContent){
//短消息内容的unicode长度的16进制,因为一个字符转为unicode编码是2个字节,我就直接乘2
return convertDec2Hex(shortContent.length() << 1);
}
/**
* 获取长消息内容的Unicode长度的16进制字符串
* @param content
* @return
*/
public static String longContentUnicodeLengthHex(String content){
//协议中规定,长消息的内容长度为unicode长度+6
return convertDec2Hex((content.length() << 1) + 6);
}
/**
* 10进制转为16进制
* @param decNum 10进制数字
* @return 16进制字符串
*/
public static String convertDec2Hex(int decNum){
String result = String.format("%x", decNum);
//Java中10进制转为16进制有个坑,小于15的数转为16进制后前面不会补0,需要自己添上去
if (result.length() < 2){
return (0+ result).toUpperCase();
}else {
return result.toUpperCase();
}
}
}
测试代码
public class Demo {
public static void main(String[] args) throws NoSuchPortException, IOException, InterruptedException {
SerialPortUtils serialPortUtils = new SerialPortUtils();
serialPortUtils.openPort("COM3");
//恢复出厂设置
String atf = AtCommand.reset_data.getCommandCode();
sendAt(serialPortUtils, atf);
//关闭回显
String ate0 = AtCommand.close_echo.getCommandCode();
sendAt(serialPortUtils, ate0);
//保存参数
String atw = AtCommand.save_parameters.getCommandCode();
sendAt(serialPortUtils, atw);
//设定短信编码
String cscs = AtCommand.set_sms_code.getCommandCode();
sendAt(serialPortUtils, cscs);
//先把pdu协议内容构建出来
String content = "【啦啦啦】我们都是神枪手,每一个子弹射穿一个滴人,我们都是飞行军,哪怕那山高谁又深";
String pduCode = ChkjProtocolUtil.pduLongCode("这里填手机号", content, 2, 1);
//pud编码长度
int pduLength = ChkjProtocolUtil.pduLength(pduCode);
String cmgs = String.format(AtCommand.pdu_length.getCommandCode(), pduLength);
sendAt(serialPortUtils, cmgs);
//发短信
sendAt(serialPortUtils, pduCode);
//先把pdu协议内容构建出来
String content2 = "没有枪没有炮,自由那敌人人送山前,没有枪没有炮,敌人给我们造";
String pduCode2 = ChkjProtocolUtil.pduLongCode("这里填手机号", content2, 2, 2);
//pud编码长度
int pduLength2 = ChkjProtocolUtil.pduLength(pduCode2);
String cmgs2 = String.format(AtCommand.pdu_length.getCommandCode(), pduLength2);
sendAt(serialPortUtils, cmgs2);
//发短信
sendAt(serialPortUtils, pduCode2);
serialPortUtils.closePort();
}
private static boolean sendAt(SerialPortUtils serialPortUtils, String atCommand) throws IOException, InterruptedException {
serialPortUtils.writeTextData(atCommand);
String atResponse = "";
//pdu设备的处理速度比较慢,所以每隔300毫秒读取一下
for (int i = 0; i < 100; i++){
Thread.sleep(300);
atResponse += serialPortUtils.readTextData();
System.out.println(atResponse);
if (atResponse.contains("OK")){
return true;
}
else if (atResponse.contains(">")){
return true;
}
else if (atResponse.contains("ERROR")){
return false;
}
}
return false;
}
}