Android 通过USB与PLC设备通信(USB转串口)

经朋友介绍接的一个外包,要求用USB和PLC设备通信,于是乎就有了本文。内容不深,权当做个记录整理一下当时的思路。

一、解决思路

1. 首先,PLC设备通常都是用串口进行通讯,走的Modbus协议。这部分在学校的时候有接触过,不是难点。

2. 关键在于移动控制端,采用的是智能POS,用来控制PLC设备,并且进行交易收款。在开放的外围接口中,只有USB可以使用,因此需要在外部加一个USB转串口(此处用的是USB转485模块)

3. 控制端的USB是mini口,还需要USB转OTG线,设备才能识别到USB串口。(没错,记住这个OTG线,这里会有一个问题)。

问题:由于OTG线和普通USB线引脚接法不一样(区别在与OTG线的接口引脚把ID引脚和GND连接,这样设备才能识别到OTG外设),这就导致重电和通信无法进行。有问过做硬件的同时,似乎无法同时进行。不过网络上好像有可以边充电边通信的线卖。具体还没去试过,希望大神们能够指点一二。

二、USB通信

USB通信部分参考了GitHub上的一个项目usb-serial-for-android 进行开发的,该项目除了可以配置USB串口以外,还可以配置不同的串口模式,例如CH340、CP21xx系列的。站在巨人的肩上进行改造,就是方便~

整合上述资料,封装了一个类UsbManager,用于管理Usb:例如枚举USB串口设备、打开设备、写数据、度数据、设置设备参数等。

public abstract class USBManager {

    private static final String TAG = USBManager.class.getSimpleName();
    private static USBManager mInstance = null;

    public static USBManager getInstance(Context context){
        if(mInstance == null)
            mInstance = new USBManagerImpl(context);
        return mInstance;
    }

    /**
     * 搜索USB设备,USB一对多时使用
     *
     * @return
     *        搜索到的所有USB设备
     */
    public abstract List<USBDevice> listUsbDevices();
    /**
     * 搜索USB设备,USB一对一时使用
     *
     * @return
     *      &emsp;&emsp;搜索到的所有USB设备
     */
    public abstract List<UsbSerialPort> listUsbPort();
    /**
     * 用默认配置打开USB设备进行通信(波特率:115200、数据位:8、停止位:1、校验位:无)<BR/>
     *
     * @param port  要打开的USB设备
     * @return  是否打开成功</BR>
     *      &emsp;&emsp;true: 打开成功</BR>
     *      &emsp;&emsp;false: 打开失败</BR>
     */
    public abstract boolean openUsbPort(UsbSerialPort port);
    /**
     * 用自定义配置打开USB设备进行通信
     * @param port  要打开的USB设备
     * @param param 使用的参数
     * @return  是否打开成功</BR>
     *      &emsp;&emsp;true: 打开成功</BR>
     *      &emsp;&emsp;false: 打开失败</BR>
     */
    public abstract boolean openUsbPort(UsbSerialPort port, USBParams param);
    /**
     * 销毁USB设备端口(在应用退出前应及时销毁)
     */
    public abstract void destoryPort();
    /**
     * 向USB串口写数据
     *
     * @param data 数据
     * @param timeout 超时时间
     * @return 实际写成功的数据长度(int)
     */
    protected abstract int write(byte[] data, int timeout);
    /**
     * 从USB串口读数据
     *
     * @param data 数据
     * @param timeout 超时时间
     * @return 实际读取的数据长度(int)
     */
    protected abstract int read(byte[] data, int timeout);

    public abstract void setDTR(boolean value) throws IOException;
    public abstract void setRTS(boolean value) throws IOException;

    @Deprecated
    public abstract void setOnInputListener(SerialInputOutputManager.Listener listener);
    @Deprecated
    public abstract void writeDataToUsb(byte[] data);
}

由于要控制的PLC设备不可能只有一个,故还有考虑一对多的情况:listUsbDevices() 就是用于一对多的情况,其返回的USBDevice,封装了设备的所有操作,包括打开设备、配置设备参数、写数据、读数据等。在一对多的时候,只要调用该接口,便可以根据USBDevice来操作指定的USB串口设备。如下是两个方法的实现,不同点只是listUsbDevice()在获取到设备后,在封装一层USBDevice,然后返回。USBDevice中所有有关设备操作的接口和USBManager中的接口是一致的。

/**
     * 搜索USB设备, USB一对多时使用
     *
     * @return
     *      &emsp;&emsp;搜索到的所有USB设备
     */
    @Override
    public List<USBDevice> listUsbDevices(){
        List<UsbSerialDriver> drivers =
                UsbSerialProber.getDefaultProber().findAllDrivers(mUsbManager);
        if(drivers.size() > 0){
            UsbDevice device = drivers.get(0).getDevice();
            if(!mUsbManager.hasPermission(device)){
                String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
                PendingIntent i = PendingIntent.getBroadcast(mContext, 0,
                        new Intent(ACTION_USB_PERMISSION), 0);
                mUsbManager.requestPermission(device, i);
            }
        }
        final List<USBDevice> result = new ArrayList<>();
        int index = 1;
        for (final UsbSerialDriver driver : drivers) {
            List<USBDevice> list = new ArrayList<>();
            final List<UsbSerialPort> ports = driver.getPorts();
            for (UsbSerialPort port : ports) {
                list.add(new USBDevice(mContext, "COM"+(index++), port));
            }
            Log.d(TAG, String.format("usb serial port %s: %s port%s",
                    driver, Integer.valueOf(ports.size()), ports.size() == 1 ? "" : "s"));
            result.addAll(list);
            list = null;
        }
        drivers = null;
        return result;
    }

    /**
     * 搜索USB设备,USB一对一时使用
     *
     * @return
     *      &emsp;&emsp;搜索到的所有USB设备
     */
    @Override
    public List<UsbSerialPort> listUsbPort(){
        List<UsbSerialDriver> drivers =
                UsbSerialProber.getDefaultProber().findAllDrivers(mUsbManager);
        if(drivers.size() > 0){
            UsbDevice device = drivers.get(0).getDevice();
            if(!mUsbManager.hasPermission(device)){
                String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
                PendingIntent i = PendingIntent.getBroadcast(mContext, 0,
                        new Intent(ACTION_USB_PERMISSION), 0);
                mUsbManager.requestPermission(device, i);
            }
        }
        final List<UsbSerialPort> result = new ArrayList<UsbSerialPort>();
        for (final UsbSerialDriver driver : drivers) {
            final List<UsbSerialPort> ports = driver.getPorts();
            Log.d(TAG, String.format("usb serial port %s: %s port%s",
                    driver, Integer.valueOf(ports.size()), ports.size() == 1 ? "" : "s"));
            result.addAll(ports);
        }
        drivers = null;
        return result;
    }

其他几个接口就不贴代码了,不外乎打开设备,数据读写,和普通的设备操作差不多,可以直接参考源码。

三、Modbus协议封装

对于Modbus协议的介绍,可以参考这一篇博客,讲的很详细:Modbus学习总结 。

这里只简单介绍下读/写帧格式:

1. 写数据帧:

目标地址功能码寄存器高地址寄存器低地址数据CRC
1字节1字节1字节1字节n字节2字节

         例如:01 06 01 91 00 01 18 1B

 

 

 

2. 写数据帧:

发送:

目标地址功能码寄存器高地址寄存器低地址数据长度CRC
1字节1字节1字节1字节2字节2字节

 

        例如:01 03 00 10 00 04 CRC

 

 

应答:

目标地址功能码数据长度数据CRC
1字节1字节1字节n字节2字节

        例如:01 03 08 01 32 01 98 03 FF 00 4E CRC

 

 

注:这里的功能码,06-写寄存器操作;03-读单个寄存器操作

对于Modbus的读写,也封装了单独的类进行维护:ModbusTransfer。其中实现了sendCmdToPlc()和readDataFromPlc()两种接口。其中需要传入USBDevice的是用于设备一对多的情况。

@Override
    public byte[] sendCmdToPlc(int desAds, byte[] dataAds, byte[] data){
        synchronized (STATIC){
            Log.d(TAG, "sendCmdToPlc: des="+ desAds+ " dataAds="+ StringUtil.byte2HexStr(dataAds)+
                    " data="+ StringUtil.byte2HexStr(data));
            if(desAds < 0 || dataAds == null || data == null){
                Log.e(TAG, "sendCmdToPlc: params is error");
                return null;
            }
            int index = 0;
            byte[] param = new byte[1+1+2+data.length+2];

            param[index++] = (byte) desAds;//目标地址
            param[index++] = 6;//写数据指令
            System.arraycopy(dataAds, 0, param, index, dataAds.length);//数据寄存器地址
            index += 2;
            if(data.length > 0){//数据
                System.arraycopy(data, 0, param, index, data.length);
                index += data.length;
            }
            byte[] crc = crc_16(param, param.length - 2);//CRC
            System.arraycopy(crc, 0, param, index, 2);
            Log.d(TAG, "发送写指令:"+ StringUtil.byte2HexStr(param));
            int ret = USBManager.getInstance(mContext).write(param, 1000);
            if(ret <= 0){
                Log.e(TAG, "sendCmdToPlc: 写指令发送不成功!");
                return null;
            }
            //这里向设备发送成功后会有应答,并且应答数据和发送的指令一致
            byte[] temp = new byte[8];
            byte[] recv = new byte[param.length];
            long start = System.currentTimeMillis();
            int recvLength = 0;
            while (true){

                long currTime = System.currentTimeMillis();
                if(currTime-start >= WRITE_TIMEOUT) {
                    Log.e(TAG, "sendCmdToPlc: time out");
                    break;
                }
                ret = USBManager.getInstance(mContext).read(temp, 300);
                if(ret >= 0){
                    if(ret > recv.length){
                        Log.e(TAG, "sendCmdToPlc: the receive data is too long, please check the read length");
                        return null;
                    }
                    System.arraycopy(temp, 0, recv, recvLength, ret);
                    recvLength += ret;
                    if(D) Log.i(TAG, "sendCmdToPlc: totleLength="+ recvLength+ " param="+ StringUtil.byte2HexStr(recv));
                    if(recvLength == recv.length){//数据接受完毕
                        recvLength = 0;
                        Log.d(TAG, "接收数据: "+ StringUtil.byte2HexStr(recv));
                        crc = crc_16(recv, recv.length-2);
                        //进行crc校验,看是否和发送时一致
                        if(crc[0]!=param[param.length-2] || crc[1]!=param[param.length-1]){
                            Log.e(TAG, "sendCmdToPlc: crc check is error, real crc="+ StringUtil.byte2HexStr(crc));
                            return null;
                        }
                        if(!Arrays.equals(param, recv)){
                            Log.e(TAG, "sendCmdToPlc: reveive data is error");
                            return null;
                        }
                        return recv;
                    }
                }
            }
            return null;
        }
    }

    @Override
    public byte[] readDataFromPlc(int desAds, byte[] dataAds, int dataLength, int timeout){
        synchronized (STATIC){
            Log.d(TAG, "sendCmdToPlc: des="+ desAds+ " dataAds="+ StringUtil.byte2HexStr(dataAds)+
                    " dataLength="+ dataLength);
            if(desAds < 0 || dataAds == null || dataLength < 1){
                Log.e(TAG, "sendCmdToPlc: params is error");
                return null;
            }
            long startTime = System.currentTimeMillis();
            int index = 0;
            byte[] param = new byte[1+1+2+2+2];

            param[index++] = (byte) desAds;//目标地址
            param[index++] = 3;//读数据指令
            System.arraycopy(dataAds, 0, param, index, dataAds.length);//要读取的寄存器地址
            index += 2;
            param[index++] = (byte) ((dataLength>>8)&0xFF00);//数据长度
            param[index++] = (byte) (dataLength&0x00FF);
            byte[] crc = crc_16(param, param.length - 2);//CRC
            System.arraycopy(crc, 0, param, index, 2);
            Log.d(TAG, "发送读指令:"+ StringUtil.byte2HexStr(param));
            int ret = USBManager.getInstance(mContext).write(param, 1000);
            if(ret <= 0){
                Log.e(TAG, "readDataFromPlc: 读指令发送不成功!");
                return null;
            }
            param = null;
            int totleLength = 0;
            byte[] data = new byte[8];
            param = new byte[16];
            boolean isCheckLength = false;
            while(true){//超时控制
                long currTime = System.currentTimeMillis();
                if(currTime-startTime >= timeout) {
                    Log.e(TAG, "readDataFromPlc: time out");
                    break;
                }
                ret = USBManager.getInstance(mContext).read(data, 300);
                if(ret >= 0){
                    if(ret > param.length){
                        Log.e(TAG, "readDataFromPlc: the receive data is too long, please check the read length");
                        return null;
                    }
                    System.arraycopy(data, 0, param, totleLength, ret);
                    totleLength += ret;
                    if(D) Log.i(TAG, "readDataFromPlc: totleLength="+ totleLength+ " param="+ StringUtil.byte2HexStr(param));
                    if(!isCheckLength && totleLength >= 3){
                        isCheckLength = true;
                        //读取到数据长度字节
                        byte[] temp = new byte[totleLength];
                        System.arraycopy(param, 0, temp, 0, totleLength);
                        param = new byte[1+1+1+temp[2]+ 2];
                        System.arraycopy(temp, 0, param, 0, totleLength);
                        temp = null;
                        Log.i(TAG, "readDataFromPlc: after check length param="+ StringUtil.byte2HexStr(param));
                    }
                    if(totleLength == param.length){//数据接受完毕
                        totleLength = 0;
                        Log.d(TAG, "接收数据: "+ StringUtil.byte2HexStr(param));
                        crc = crc_16(param, param.length-2);
                        if(crc[0]!=param[param.length-2] || crc[1]!=param[param.length-1]){
                            Log.e(TAG, "readDataFromPlc: crc check is error, real crc="+ StringUtil.byte2HexStr(crc));
                            return null;
                        }
                        if(param[0] != desAds || param[1] != 3){
                            Log.e(TAG, "readDataFromPlc: reveive data is error");
                            return null;
                        }
                        int len = param[2];
                        data = null;
                        data = new byte[len];
                        System.arraycopy(param, 3, data, 0, len);
                        return data;
                    }
                }
            }
            return null;
        }
    }

四、应用

首先需要在Manifest中配置扫描设备时显示的界面,并添加过滤条件:

        //添加这个配置,表示设备是USB主设备
        <uses-feature android:name="android.hardware.usb.host" />
        
        <activity
            android:name="test.usb.serialport.DeviceListActivity"
            android:label="@string/app_name"
            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            //添加了这个action,在系统检测到有USB设备接入时会启动这个Activity。
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>
            //添加设备过滤
            <meta-data
                android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>

在DeviceListActivity中显示扫描到的USB设备,并记录,当选中其中一个设备时进行参数设置等初始化操作,这里不多说。接下来介绍下验证通信可行的方式:

1. 连线说明:

将DB9延长线将两个USB转485模块连起来(485芯片和232芯片如果混用的话要加一个485转232模块), 一端的USB口连接电脑;一端的USB口通过USB转Mico线连接到POS机上.

2. Demo使用

进入主界面后,插入USB线,如果没有弹出设备搜索框,则点击”检测USB串口”主动检测USB设备(默认系统检测到USB设备插入后会自动弹出设备搜索对话框).

第一次使用应用并,点击USB设备后,会弹出USB授权对话框,此时必须允许: ”默认情况下使用该USB设备”; 

授权通过后,再次点击USB设备进入测试页面,如果打开成功,或有Toast提示:”open usb success”, 否则提示: “open usb fail”.

打开电脑的串口助手,按如下配置打开串口.

按如上操作,电脑端和POS端均正常打开串口后,可进行测试: 

**** 测试1, 发送写指令 *******

选择好数据地址和发送的数据后,点击: “发送写指令”按钮. 此时如果连接正常会在电脑的串口助手接受到POS机发送的数据.

                            

***** 读指令测试 ******

      

选择好要读取的数据地址和数据长度,并点击:”读取数据” 按钮,该测试会先发送读指令给电脑, 然后等待接收数据. 所以在串口助手接收到数据后需要发送一段数据给POS机,该数据所包含的数据长度必源码须与POS机设置的数据长度相等 。 

瞎叨叨这么多,重要的还是上源码。哈哈哈,欢迎大神指出不足的地方。

 

 

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值