一、导入
在我们的认识中Android系统是手机系统,它的物理接口一般只有usb host接口(之前的手机还有耳机接口,不过近几年取消了)。但其实安卓支持各种各样的工业接口,如HDMI、usb、网口、串口等等。本文将来说一下在安卓下开发串口通信。
二、串口是什么?
串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口 (Serial Interface)是指数据一位一位地顺序传送。其特点是通信线路简单,只要一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大降低了成本,特别适用于远距离通信,但传送速度较慢。
三.安卓串口Demo开发
这里使用的是谷歌开源的一个项目https://github.com/cepr/android-serialport-api 我这里以AS项目为例做一个简单的demo。
1、下载该项目,找到android-serialport-api文件夹中project文件夹里的libs文件夹。我们后面需要使用这个libs文件夹里的so库。 如图所示:
2、创建AS工程,在mian目录下创建jniLibs目录,并将刚刚找到的libs文件夹中的三个文件拷贝进去。
3、在app下的build.gradle添加如下配置,用以适配不同架构。
ndk {
//选择要添加的对应cpu类型的.so库。
abiFilters 'armeabi', 'armeabi-v7a', 'x86'
}
上面我们添加了三中类型的libserial_port.so库,所以build.gradle添加配置也是这三个。
4、创建包名:android_serialport_api,并把SerialPort.java拷贝进去,SerialPort.java已经封装了串口的打开、配置、读写和关闭串口的方法,我们直接调用即可。
以下是串口操作类SerialPort.java
package android_serialport_api;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class SerialPort {
private static final String TAG = "SerialPort";
public FileDescriptor mFd;
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;
public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {
mFd = open(device.getAbsolutePath(), baudrate, flags);
if (mFd == null) {
Log.e(TAG, "native open returns null");
throw new IOException();
}
mFileInputStream = new FileInputStream(mFd);
mFileOutputStream = new FileOutputStream(mFd);
}
// Getters and setters
public InputStream getInputStream() {
return mFileInputStream;
}
public OutputStream getOutputStream() {
return mFileOutputStream;
}
// JNI(调用java本地接口,实现串口的打开和关闭)
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
static {//加载jni下的C文件库
Log.d(TAG, "本地库加载中");
System.loadLibrary("serial_port");
}
}
以及使用串口的工具类SerialPortUtils.java
package android_serialport_api;
import android.util.Log;
import android.widget.Toast;
import com.example.androidserialport.DIYToast;
import com.example.androidserialport.Data;
import com.example.androidserialport.MainActivity;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
/**
* 串口监听工具
* Created by sam on 2018/3/31.
*/
public class SerialPortUtils {
private final String TAG = "SerialPortUtils";
private String path = "/dev/ttyS1";//串口名,安卓下一般为ttys1,ttys2... 而windows下则为com1,com2,com3... 我们平时用的usb口其实也是一个串口,所以用usb转232转换器就可以在电脑上使用串口了
private int baudrate = 9600;//波特率
public boolean serialPortStatus = false; //是否打开串口标志
public String data_;
public boolean threadStatus; //线程状态,为了安全终止线程
public SerialPort serialPort = null;
public InputStream inputStream = null;
public OutputStream outputStream = null;
/**
* 打开串口
* @return serialPort串口对象
*/
public SerialPort openSerialPort(){
try {
serialPort = new SerialPort(new File(path),baudrate,0);
this.serialPortStatus = true;
threadStatus = false; //线程状态
//获取打开的串口中的输入输出流,以便于串口数据的收发
inputStream = serialPort.getInputStream();
outputStream = serialPort.getOutputStream();
new ReadThread().start(); //开始线程监控是否有数据要接收
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "openSerialPort: 打开串口异常:" + e.toString());
DIYToast.ShowToast(MainActivity.context,"openSerialPort: 打开串口异常:" + e.toString());
return serialPort;
}
Log.d(TAG, "openSerialPort: 打开串口");
DIYToast.ShowToast(MainActivity.context,"openSerialPort: 打开串口");
return serialPort;
}
/**
* 关闭串口
*/
public void closeSerialPort(){
try {
inputStream.close();
outputStream.close();
this.serialPortStatus = false;
this.threadStatus = true; //线程状态
serialPort.close();
} catch (Exception e) {
Log.e(TAG, "closeSerialPort: 关闭串口异常:"+e.toString());
DIYToast.ShowToast(MainActivity.context,"closeSerialPort: 关闭串口异常:"+e.toString());
return;
}
Log.d(TAG, "closeSerialPort: 关闭串口成功");
DIYToast.ShowToast(MainActivity.context,"closeSerialPort: 关闭串口成功");
}
/**
* 发送串口指令
* @param data
*/
public void sendSerialPort(byte[] data){
//Log.d(TAG, "sendSerialPort: 发送数据");
try {
if (data.length > 0) {
outputStream.write(data);
//注意:有些工控机上需要以\n或者\r来表示数据发送完毕,所以在write完毕后要多输出个\r,这个看个人情况
//outputStream.write('\n');
//outputStream.write('\r'+'\n');
outputStream.flush();
//Log.d(TAG, "sendSerialPort: 串口数据发送成功");
}
} catch (IOException e) {
Log.e(TAG, "sendSerialPort: 串口数据发送失败:"+e.toString());
}
}
/**
* 因为这里是阻塞的,所以单开一线程来读数据
*/
private class ReadThread extends Thread {
@Override
public void run() {
super.run();
//判断进程是否在运行,更安全的结束进程
while (!threadStatus){
//Log.d(TAG, "监听打卡...");
//64 1024
byte[] buffer = new byte[16];
int size; //读取数据的大小
try {
size = inputStream.read(buffer);
if (size > 0){
Log.d(TAG, "接收到数据:" + new String(buffer));
Data.test=String.valueOf(new String(buffer));
//注意,多数情况下使用串口的单片机都是使用十六井控进行输送,所以这里可能还需要把buffer转换为十六进制
}
} catch (IOException e) {
Log.e(TAG, "run: 数据读取异常:" +e.toString());
}
}
}
}
/**
* Utility class to convert a byte array to a hexadecimal string.
*
* @param bytes Bytes to convert
* @return String, containing hexadecimal representation.
*/
public static String ByteArrayToHexString(byte[] bytes) {
final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
char[] hexChars = new char[bytes.length * 2];
int v;
for ( int j = 0; j < bytes.length; j++ ) {
v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
/**
* 将byte[]转为各种进制的字符串
* @param bytes byte[]
* @param radix 基数可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
* @return 转换后的字符串
*/
public static String binary(byte[] bytes, int radix){
return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数
}
}
我将通过串口的工具类SerialPortUtils来实现安卓串口通信具体功能。以下是我的主界面代码以及布局。
主界面代码
package com.example.androidserialport;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android_serialport_api.SerialPortUtils;
public class MainActivity extends AppCompatActivity {
private Button btn_open,btn_close,btn_send;
private TextView tv1;
private SerialPortUtils S;
public static Context context;
private EditText et_number;
private Handler handler = new Handler();
private Runnable runnable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context=this;
btn_open = findViewById(R.id.btn_open);
btn_close = findViewById(R.id.btn_close);
btn_send = findViewById(R.id.btn_send);
tv1 = findViewById(R.id.textView);
et_number = findViewById(R.id.et_number);
S = new SerialPortUtils();
//打开串口
btn_open.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
S.openSerialPort();
}
});
//关闭串口
btn_close.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
S.closeSerialPort();
}
});
//发送数据
btn_send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int number = Integer.parseInt(et_number.getText().toString());
byte[] bytes = {(byte) number};
S.sendSerialPort(bytes);
}
});
//接收数据
runnable = new Runnable() {
@Override
public void run() {
tv1.setText(Data.test);
handler.postDelayed(runnable,1000);
}
};
handler.post(runnable);
}
}
简单的布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="打开串口"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.179"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.704" />
<Button
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关闭串口"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.803"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.704" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="55dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.367"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.23" />
<EditText
android:id="@+id/et_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.529"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.232" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="接收数据"
android:textSize="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.389" />
</androidx.constraintlayout.widget.ConstraintLayout>
到这里基本就结束了,成功实现了安卓下的串口通信。贴张图吧。
这里使用的是虚拟串口,具体方法将在下一篇文章中讲解。
最后直接附上所需so库。