智能家居虽然不是一个新的名词,也一直不温不火,但是并不代表它没有潜力,尤其是AI+的时代,赋予了更强大的生命力和巨大的潜力,而Android系统得益于其开放性,常常被当成智能机器人、智能硬件、智能终端等上位机的操作系统,于是为了更好地实现上位机和底层核心板的通信,“串口”应运而生,早在16年的时候我就实现过串口,不过那时候项目进度紧急用的是第三方的开源库,后面在深入了解了串口之和Linux之后发现自己写也很简单。NDK实战系列相关文章链接:
嵌入式系统或传感器网络的很多应用和测试都需要通过PC机与嵌入式设备或传感器节点进行通信。其中,最常用的接口就是RS-232串口和并口(鉴于USB接口的复杂性以及不需要很大的数据传输量,USB接口用在这里还是显得过于奢侈)。
关于串口更多的基本知识请参阅Android NDK——实战演练之App端通过串口通信完成实时控制单片机上LED灯的颜色及灯光动画特效
串口通信本质上就是IO操作,一般是以16进制进行数据传输的。
在智能硬件的核心开发板中可以直接用VGA (HDIM)接口(转USB)或者从开发板跳线(连到USB 转换器),这两种是串口的常见物理形象,串口只需要使用到两条线路,一条负责接收一条负责发送。
转USB 只是为了更好的测试,因为电脑无法直接识别识别串口,所以转为USB就可以在PC中检测到,再通过xShell的工具就可以捕获串口的输入输出,当然也可以使用其他转换线,比如USB转RS232串口线。
Android是基于Linux的,而Linux系统是基于文件的,在Linux中一切都是文件,“串口”也不例外,只不过串口是定制化Android设备上的一种特殊的设备文件。
Linux中串口一般是是配置到驱动上使用的。
可以通过查找驱动的配置清单文件/proc/tty/drivers(drivers是文件不是文件夹):
因为是要找串口,所以只需要关注最后一栏描述有serial字样的dev设备文件,可能需要自己去尝试或者硬件工程师直接告诉你,找到之后中间栏就是描述这个类型驱动设备对应的路径,再进入到dev文件夹下
根据驱动的配置文件找到的类型,进一步筛选即可。(我的板子是**/dev/ttySAC2**)
由硬件工程师提供的,当然也可以通过一些串口网络调试工具进行测试,常见的波特率有115200、9600等,波特率必须和底层硬件一致才可以进行通信。
串口通信本质上就是IO操作,一般是以16进制进行数据传输的,串口通信中最重要的两个参数就是路径和波特率。
串口操作管理角色主要封装了三个功能模块:
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.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 串口核心管理类
* @author cmo
*/
public class SerialPortManager {
private List<PortDataInterface> observers;
private Executor executor;
/**
* 缓存指令的阻塞队列,避免因为并发产生类似“粘包”的问题(插入操作比较频繁时使用LinkedBlockingQueue,查询适合使用ArrayBlockingQueue)
*/
private LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<>();
/**
*输入流(从底层读取数据),可由打开串口后返回的fd进行创建
*/
private FileInputStream mFileInputStream;
/**
* 输出流(写入流,向底层发送数据)
*/
private FileOutputStream mFileOutputStream;
static {
System.loadLibrary("native-lib");
}
static class Holder{
private static final SerialPortManager INSTANCE=new SerialPortManager();
}
private SerialPortManager() {
observers = new ArrayList<>();
executor = Executors.newSingleThreadExecutor();
}
public static SerialPortManager getInstance() {
return Holder.INSTANCE;
}
/**
* 真正去发送指令
*/
private Runnable taskCenterRunnable=new Runnable() {
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
byte[] content=null;
try {
content=queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(content!=null){
try {
mFileOutputStream.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
};
/**
* 采用阻塞队列缓存发送的指令码
* @param command
*/
public void putCommand(byte[] command) {
try {
queue.put(command);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 注册观察者
* @param portDataInterface
*/
public void regist(PortDataInterface portDataInterface) {
if (portDataInterface != null) {
observers.add(portDataInterface);
}
}
public void unregist (PortDataInterface portDataInterface) {
if (portDataInterface != null) {
observers.remove(portDataInterface);
}
}
/**
* 通知观察者
* @param content
*/
public void notifyAll(byte[] content) {
for (PortDataInterface portDataInterface : observers) {
portDataInterface.onDataReceived(content);
}
}
/**
* 打开串口
* @param path 串口路径
* @param baudRate 波特率
*/
public void openSerialPort(String path, int baudRate) {
File file = new File(path);
Helper.chmod777(file);
FileDescriptor fd=open(path, baudRate);
mFileInputStream = new FileInputStream(fd);
mFileOutputStream = new FileOutputStream(fd);
if (fd != null) {
/**
* 开启接收串口数据的线程{@link SerialPortReadThread }
*/
startReadThread();
//通过线程池执行随时发送指令到串口
executor.execute(taskCenterRunnable);
}else{
throw new RuntimeException("CrazyMo:Can't open the port!!");
}
}
/**
* 启动接收串口的线程
*/
private void startReadThread() {
SerialPortReadThread serialPortReadThread = new SerialPortReadThread(mFileInputStream) {
@Override
public void onReceived(byte[] readBytes) {
/**
* TODO 根据协议栈去解析得到的数据,并根据具体业务进行分发,可以通过不同的接口返回,此次是为了通用,我不做分发逻辑了
*
* 因为不同的项目,可能分为不同的业务,但是底层串口的所有指令,都是经一个串口上报的,每一种业务只对它对应的指令感兴趣,
* 所以需要进行“业务的发分发”,这里我使用观察者模式实现,Activity作为观察者,流作为主题
*/
Log.d("CrazyMo", "onReceived: "+Helper.bytesToHex(readBytes));
// 通知观察者 也可以自己重构定义多个观察者,一个业务对应一个观察者
SerialPortManager.this.notifyAll(readBytes);
}
};
serialPortReadThread.start();
}
public static class Helper {
/**
* 获取Root权限仅针对特定的设备
* @param file
* @return
*/
public static boolean chmod777(File file) {
if (null == file || !file.exists()) {
return false;
}
try {
//只是获取ROOT权限,如果没有root的设备是不会成功的
Process su = Runtime.getRuntime().exec("/system/bin/su");
// 修改文件属性为 [可读 可写 可执行]
String cmd = "chmod 777 " + file.getAbsolutePath() + "\n" + "exit\n";
su.getOutputStream().write(cmd.getBytes());
if (0 == su.waitFor() && file.canRead() && file.canWrite() && file.canExecute()) {
return true;
}
} catch (IOException | InterruptedException e) {
// 没有ROOT权限
e.printStackTrace();
}
return false;
}
public static String bytesToHex(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if(hex.length() < 2){
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
}
/**
* 本质就是打开指定路径下的File并设置串口属性生成Linux下的句柄,再反射生成Java层的句柄
* @param path
* @param baudRate
* @return
*/
public native FileDescriptor open(String path, int baudRate);
}
/**
* 开启一个线程负责接收底层串口发送的数据
* @author cmo
*/
public abstract class ReceiveDataThread extends Thread {
private InputStream mInputStream;
/**
* 获取到的数据缓存到byte[]中
*/
private byte[] mReadBuffer;
public ReceiveDataThread(InputStream mInputStream) {
this.mInputStream = mInputStream;
this.mReadBuffer = new byte[1024];
}
public boolean isInterrupted=false;
public void setInterrupted(boolean interrupted) {
isInterrupted = interrupted;
}
@Override
public void run() {
while (!isInterrupted) {
try {
int size = mInputStream.read(mReadBuffer);
if (size<=0) {
return;
}
byte[] readBytes = new byte[size];
System.arraycopy(mReadBuffer, 0, readBytes, 0, size);
onReceived(readBytes);
} catch (IOException e) {
e.printStackTrace();
return;
}
}
}
/**
* TODO 提供给应用层的数据回调接口
* @param readBytes
*/
public abstract void onReceived(byte[] readBytes) ;
}
回调给应用层接收串口中的数据,比如需要在Activi中接收则需要实现此接口。
/**
* @author cmo
*/
public interface PortDataReceived{
/**
* 数据接收
* @param bytes 接收到的数据16进制数据
*/
void onDataReceived(byte[] bytes);
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "android/log.h"
static const char *TAG="CrazyMo";
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
extern "C"
JNIEXPORT jobject JNICALL
Java_com_crazymo_serialprot_SerialPortManager_open(JNIEnv *env, jobject instance,
jstring path_, jint baudRate) {
const char *path = env->GetStringUTFChars(path_, 0);
jobject mFileDescriptor;
speed_t speed = B115200;
int fd = open(path, O_RDWR);
if (fd == -1)
{
LOGE("打开失败");
return NULL;
}
struct termios cfg;
//获取串口属性
if (tcgetattr(fd, &cfg))
{
close(fd);
return NULL;
}
/**
* 将串口设置为原始模式并让fd(对串口可度可写),在原始模式下所有的输入数据以字节为单位进行处理
* 且终端不可回显,所有特定的终端输入/输出模式不可用
*/
cfmakeraw(&cfg);
//设置串口读取波特率
cfsetispeed(&cfg, speed);
//设置串口写入波特率
cfsetospeed(&cfg, speed);
/**
* TCSANOW:不等数据传输完毕就立即改变属性。
TCSADRAIN:等待所有数据传输结束才改变属性。
TCSAFLUSH:清空输入输出缓冲区才改变属性。
注意:当进行多重修改时,应当在这个函数之后再次调用 tcgetattr() 来检测是否所有修改都成功实现。
*/
if (tcsetattr(fd, TCSANOW, &cfg))
{
close(fd);
LOGE("设置属性失败");
return NULL;
}
//根据Linux的文件句柄去反射创建一个Java的文件句柄
jclass cFileDescriptor = env->FindClass( "java/io/FileDescriptor");
jmethodID iFileDescriptor = env->GetMethodID( cFileDescriptor, "" , "()V");
jfieldID descriptorID = env->GetFieldID(cFileDescriptor, "descriptor", "I");
mFileDescriptor = env->NewObject(cFileDescriptor, iFileDescriptor);
env->SetIntField( mFileDescriptor, descriptorID, (jint)fd);
env->ReleaseStringUTFChars(path_, path);
return mFileDescriptor;
}
public class MainActivity extends AppCompatActivity implements PortDataReceived {
private static final String SERIAL_PATH = "/dev/ttySAC2";
private static final int BAUD_RATE = 115200;
EditText edit_content;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit_content = findViewById(R.id.edit_content);
}
@Override
protected void onDestroy() {
super.onDestroy();
SerialPortManager.getInstance().unregist(this);
}
/**
* 打开串口
*/
public void open(View view) {
SerialPortManager.getInstance().openSerialPort(SERIAL_PATH, BAUD_RATE);
SerialPortManager.getInstance().regist(this);
}
/**
* 发送数据
*/
public void send(View view) {
String command = edit_content.getText().toString().trim();
if (TextUtils.isEmpty(command)) {
return;
}
byte[] sendContentBytes = command.getBytes();
SerialPortManager.getInstance().putCommand(sendContentBytes);
}
@Override
public void onDataReceived(byte[] bytes) {
Log.e("cmo", " 收到串口数据---->" + new String(bytes));
}
}