Java串口通信

最近在做一个短信猫发送短信的功能,在这过程中也熟悉了一些Java串口通讯的知识。

硬件准备

将短信猫或者其他的一些串口设备连接在电脑上,从设备管理器里就可以看到该外部设备了
Java串口通信_第1张图片
因为我是用USB转串口,所以还需要再电脑上安装一个驱动,将设备连到电脑上之后打开驱动精灵,就会自动识别出需要下载的驱动,安装一下就好了。
在Windows上串口的名字一般是COM*,在Linux上串口的名字是/dev/tty*或/dev/ttyUSB*
设备连上以后开始写代码了。

编程思路

用短信猫发送短信大致的思路就是通过程序向串口发送AT指令,串口将指令数据包传送给外部设备,之后设备收到指令数据包就会做出相应的处理。串口的字符默认是ASCII码,而短信猫发送中文短信一般是要将中文转为Unicode编码,这可能是短信运营商那边要求的字符集格式。

准备jar包

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;
    }
}

你可能感兴趣的:(串口通讯)