经朋友介绍接的一个外包,要求用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通信部分参考了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 listUsbDevices();
/**
* 搜索USB设备,USB一对一时使用
*
* @return
* 搜索到的所有USB设备
*/
public abstract List listUsbPort();
/**
* 用默认配置打开USB设备进行通信(波特率:115200、数据位:8、停止位:1、校验位:无)
*
* @param port 要打开的USB设备
* @return 是否打开成功
* true: 打开成功
* false: 打开失败
*/
public abstract boolean openUsbPort(UsbSerialPort port);
/**
* 用自定义配置打开USB设备进行通信
* @param port 要打开的USB设备
* @param param 使用的参数
* @return 是否打开成功
* true: 打开成功
* false: 打开失败
*/
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
* 搜索到的所有USB设备
*/
@Override
public List listUsbDevices(){
List 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 result = new ArrayList<>();
int index = 1;
for (final UsbSerialDriver driver : drivers) {
List list = new ArrayList<>();
final List 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
* 搜索到的所有USB设备
*/
@Override
public List listUsbPort(){
List 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 result = new ArrayList();
for (final UsbSerialDriver driver : drivers) {
final List 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学习总结 。
这里只简单介绍下读/写帧格式:
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主设备
//添加了这个action,在系统检测到有USB设备接入时会启动这个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机设置的数据长度相等 。
瞎叨叨这么多,重要的还是上源码。哈哈哈,欢迎大神指出不足的地方。