现在网上关于硬件通讯的文章有很多,写法也都一样,我这里也只是记录一下我在开发过程中使用的方法。
在与硬件进行通讯之前,我们需要熟悉硬件通讯协议,我之前用到的是485单工通信协议,由报文头+数据+校验位组成。硬件方给我们的协议一般都是以他们需要处理的数据为准,大多是16进制的数据,我们在处理中有时需要转换成十进制或者将十进制转换成16进制,这个需要根据项目实际需要去处理。
了解了硬件协议后,我们需要知道硬件的设备号和波特率,这是硬件通讯实现的最重要的两个参数,这两个都是由硬件方提供的,有了这两个参数,我们就可以进行通讯啦。
与硬件实现链接的方法都是用C++去实现的,因为C++实现硬件间的通讯是最方便的,可以直接掉用很多底层的接口,并且用C++是可以提高效率的。在Android Studio 里我们使用NDK实现与C++的调用,以前使用Eclipse时还需要手写C文件,要提前学习各种格式,要注意很多地方。但是现在Android Studio可以很方便的帮我们创建这些文件和方法,只需要我们在船舰Projrct时勾选上支持C++即可。
C++实现连接的方法基本都是一样,直接上代码:
JNIEXPORT jobject JNICALL Java_android_serialport_SerialPort_open
(JNIEnv *env, jobject thiz, jstring path, jint baudrate) {
int fd;
speed_t speed;
jobject mFileDescriptor;
/* Check arguments */
{
speed = getBaudrate(baudrate);
if (speed == -1) {
LOGE("Invalid baudrate");
return NULL;
}
}
/* Opening device */
{
jboolean iscopy;
const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);
LOGD("Opening serial port %s", path_utf);
fd = open(path_utf, O_RDWR);
LOGD("open() fd = %d", fd);
(*env)->ReleaseStringUTFChars(env, path, path_utf);
if (fd == -1) {
/* Throw an exception */
LOGE("Cannot open port");
return NULL;
}
}
/* Configure device */
{
struct termios cfg;
LOGD("Configuring serial port");
if (tcgetattr(fd, &cfg)) {
LOGE("tcgetattr() failed");
close(fd);
return NULL;
}
cfmakeraw(&cfg);
cfsetispeed(&cfg, speed);
cfsetospeed(&cfg, speed);
if (tcsetattr(fd, TCSANOW, &cfg)) {
LOGE("tcsetattr() failed");
close(fd);
return NULL;
}
}
/* Create a corresponding file descriptor */
{
jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");
jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V");
jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);
(*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint) fd);
}
return mFileDescriptor;
}
关于JNI相关的语法可以直接百度,再学习一点C语言的写法,实现NDK与C的互相调用就很简单了。
上面方法返回的就是我们需要的FileDescriptor ,我们后面的操作都是基于它。
public SerialPort(File device, int baudrate) throws SecurityException, IOException {
if (!device.canRead() || !device.canWrite()) {
try {
Process su;
su = Runtime.getRuntime().exec("su");
String cmd = "chmod 777 " + device.getAbsolutePath();
su.getOutputStream().write(cmd.getBytes());
su.getOutputStream().flush();
int waitFor = su.waitFor();
boolean canRead = device.canRead();
boolean canWrite = device.canWrite();
if (waitFor != 0 || !canRead || !canWrite) {
throw new SecurityException();
}
} catch (Exception e) {
BaseUtil.saveErrorMsgToLocal(e.getMessage(), "seriallibrary");
e.printStackTrace();
}
}
mFd = open(device.getAbsolutePath(), baudrate);
if (mFd == null) {
throw new IOException();
}
mFileInputStream = new FileInputStream(mFd);
mFileOutputStream = new FileOutputStream(mFd);
}
通过上面的操作,我们就已经实现了和硬件的连接,剩下的就只有数据的传输了。
public byte[] sendData(byte[] data) {
synchronized (this) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
BaseUtil.saveErrorMsgToLocal(e.getMessage(), "hardwarelibrary");
e.printStackTrace();
}
int available;
try {
//清空之前没有读完的信息
if ((available = mInputStream.available()) > 0) {
byte[] bytes = new byte[available];
mInputStream.read(bytes);
}
//发送数据
mOutputStream.write(data);
mOutputStream.flush();
//启动超时计时器
mTimerTask = new MyTimerTask();
mTimer.schedule(mTimerTask, TIMEOUT);
isTimeout = false;
byte[] head = new byte[RESPONSE_HEAD_LEN];
while (!isTimeout) {
available = mInputStream.available();
if (available >= RESPONSE_HEAD_LEN) {
mInputStream.read(head);
int lastDataLen = head[RESPONSE_DATA_LEN_INDEX] + RESPONSE_CHECKSUM_LEN;
byte[] lastData = new byte[lastDataLen];
while (!isTimeout) {
available = mInputStream.available();
if (available >= lastDataLen) {
mInputStream.read(lastData);
byte[] result = new byte[RESPONSE_HEAD_LEN + lastDataLen];
System.arraycopy(head, 0, result, 0, RESPONSE_HEAD_LEN);
System.arraycopy(lastData, 0, result, RESPONSE_HEAD_LEN, lastDataLen);
return result;
}
Thread.sleep(10);
}
}
Thread.sleep(10);
}
return null;
} catch (Exception e) {
BaseUtil.saveErrorMsgToLocal(e.getMessage(), "hardwarelibrary");
e.printStackTrace();
} finally {
if (mTimerTask != null) {
mTimerTask.cancel();
}
}
return null;
}
}
上面就是i实现的数据的发送与读取,串口通信一般都是一发一收,由InputStream和OutputStream去实现数据的传输。
if ((available = mInputStream.available()) > 0) {
这里多了一步判断,避免上一次的数据还没有读完就再次进行数据传输。
mOutputStream.write(data);
mOutputStream.flush();
除了上面正常的数据发送接收外,还有几种情况也很常见:
1、限时读取指定长度的数据
2、限时读取指定标志位数据
以上两种情况在我们的日常开发中也是很常见的,之前在做身份证模块的时候,发现读取时间不一样,接收到的数据长度就会不一样,这个时候就得考虑定长数据去解析了。
public synchronized byte[] readData(int length, int timeout) throws Exception {
byte[] data = new byte[length];
long startTime = System.currentTimeMillis();
while (mInputStream.available() < length) {
if ((System.currentTimeMillis() - startTime) > timeout) {
throw new Exception("读取超时");
}
Thread.sleep(20);
}
mInputStream.read(data, 0, length);
return data;
}
第一种情况实现起来也很简单,主要是解决数据长度时间的判断.下面是第二种情况。
public byte[] readData(byte delimiter, int maxLen, int timeout) throws Exception {
byte[] data = new byte[maxLen];
int readedLen = 0;
long startTime = System.currentTimeMillis();
while (true) {
if(mSerialPort.getInputStream().available() <= 0) {
Thread.sleep(20);
}
if((System.currentTimeMillis() - startTime) > timeout) {
throw new Exception("读取超时");
}
mSerialPort.getInputStream().read(data, readedLen, 1);
if(delimiter == data[readedLen]) {
break;
}
readedLen ++;
if (readedLen == maxLen) {
throw new Exception("超过限定长度");
}
}
return data;
}
第二种情况就多了对指定字符的处理,读取到标志位,就结束本次数据处理。