1.調(diào)試工具ComAssistant 分析
Android 端調(diào)試工具ComAssistant 如圖妥箕,處于何人之手已不可考,找到的源碼是用eclipse 寫(xiě)的更舞。源碼見(jiàn)文末分享畦幢。
此串口調(diào)試工具,可以同時(shí)對(duì)四個(gè)串口讀寫(xiě)是四個(gè)獨(dú)立的線程缆蝉,選定串口路徑 ,Linux把每個(gè)硬件也看作是一個(gè)文件,所以都是“dev/ttyS1”這種的呛讲。
注意:官方提供的 demo 沒(méi)有N-8-1( N 不奇偶校驗(yàn)位 8 8個(gè)數(shù)據(jù)位 1 1個(gè)停止位)的設(shè)定禾怠。
第一次根據(jù)設(shè)備終端說(shuō)明或者自己嘗試連接電腦打開(kāi)調(diào)試助手 查看到底哪個(gè)口對(duì)應(yīng)哪個(gè)路徑。
2.源碼分析:
Eclipse版本的從哪個(gè)資源網(wǎng)站下載的忘記了贝搁,不過(guò)解壓看是2012年8月的吗氏,所以這里邊的api 適配到10(API等級(jí)10:Android 2.3.3-2.3.7 Gingerbread 姜餅),Eclipse項(xiàng)目結(jié)構(gòu):
從結(jié)構(gòu)中可以看出來(lái) 是把Android官方提供的android_serial_api 從項(xiàng)目包中獨(dú)立出來(lái)雷逆,此源碼唯一不好的是 GBK 編碼的 導(dǎo)入Android Studio中時(shí) 亂碼 要從新折騰弦讽。
SerialPortFinder與SerialPort分析:
SerialPortFinder就是遍歷獲取設(shè)備上所有devices以及對(duì)應(yīng)的path;
public class SerialPort {
private static final String TAG = "SerialPort";
/*
* Do not remove or rename the field mFd: it is used by native method close();
*/
private FileDescriptor mFd;
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;
public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {
/* Check access permission */
if (!device.canRead() || !device.canWrite()) {
try {
/* Missing read/write permission, trying to chmod the file */
Process su;
su = Runtime.getRuntime().exec("/system/bin/su");
String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"
+ "exit\n";
su.getOutputStream().write(cmd.getBytes());
if ((su.waitFor() != 0) || !device.canRead()
|| !device.canWrite()) {
throw new SecurityException();
}
} catch (Exception e) {
e.printStackTrace();
throw new SecurityException();
}
}
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
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
static {
System.loadLibrary("serial_port");
}
}
創(chuàng)建了打開(kāi)串口和關(guān)閉串口的本地方法,在jni中實(shí)現(xiàn)膀哲,給Java層調(diào)用往产。
主要是分析 SerialHelp和 Activity的實(shí)現(xiàn)邏輯,SerialHelper代碼:
public abstract class SerialHelper{
private SerialPort mSerialPort;
private OutputStream mOutputStream;
private InputStream mInputStream;
private ReadThread mReadThread;
private SendThread mSendThread;
private String sPort="/dev/s3c2410_serial0";
private int iBaudRate=9600;
private boolean _isOpen=false;
private byte[] _bLoopData=new byte[]{0x30};
private int iDelay=500;
//----------------------------------------------------
public SerialHelper(String sPort,int iBaudRate){
this.sPort = sPort;
this.iBaudRate=iBaudRate;
}
public SerialHelper(){
this("/dev/s3c2410_serial0",9600);
}
public SerialHelper(String sPort){
this(sPort,9600);
}
public SerialHelper(String sPort,String sBaudRate){
this(sPort,Integer.parseInt(sBaudRate));
}
//----------------------------------------------------
public void open() throws SecurityException, IOException,InvalidParameterException{
File device = new File(sPort);
//檢查訪問(wèn)權(quán)限某宪,如果沒(méi)有讀寫(xiě)權(quán)限仿村,進(jìn)行文件操作,修改文件訪問(wèn)權(quán)限
if (!device.canRead() || !device.canWrite()) {
try {
//通過(guò)掛在到linux的方式兴喂,修改文件的操作權(quán)限
Process su = Runtime.getRuntime().exec("/system/bin/su");
//一般的都是/system/bin/su路徑蔼囊,有的也是/system/xbin/su
String cmd = "chmod 777 " + device.getAbsolutePath() + "\n" + "exit\n";
su.getOutputStream().write(cmd.getBytes());
if ((su.waitFor() != 0) || !device.canRead() || !device.canWrite()) {
throw new SecurityException();
}
} catch (Exception e) {
e.printStackTrace();
throw new SecurityException();
}
}
mSerialPort = new SerialPort(new File(sPort), iBaudRate, 0);
mOutputStream = mSerialPort.getOutputStream();
mInputStream = mSerialPort.getInputStream();
mReadThread = new ReadThread();
mReadThread.start();
mSendThread = new SendThread();
mSendThread.setSuspendFlag();
mSendThread.start();
_isOpen=true;
}
//----------------------------------------------------
public void close(){
if (mReadThread != null)
mReadThread.interrupt();
if (mSerialPort != null) {
mSerialPort.close();
mSerialPort = null;
}
_isOpen=false;
}
//----------------------------------------------------
public void send(byte[] bOutArray){
try
{
mOutputStream.write(bOutArray);
} catch (IOException e)
{
e.printStackTrace();
}
}
//----------------------------------------------------
public void sendHex(String sHex){
byte[] bOutArray = MyFunc.HexToByteArr(sHex);
send(bOutArray);
}
//----------------------------------------------------
public void sendTxt(String sTxt){
byte[] bOutArray =sTxt.getBytes();
send(bOutArray);
}
//----------------------------------------------------
private class ReadThread extends Thread {
@Override
public void run() {
super.run();
while(!isInterrupted()) {
try
{
if (mInputStream == null) return;
byte[] buffer=new byte[512];
int size = mInputStream.read(buffer);
if (size > 0){
ComBean ComRecData = new ComBean(sPort,buffer,size);
onDataReceived(ComRecData);
}
try
{
Thread.sleep(50);//延時(shí)50ms
} catch (InterruptedException e)
{
e.printStackTrace();
}
} catch (Throwable e)
{
e.printStackTrace();
return;
}
}
}
}
//----------------------------------------------------
private class SendThread extends Thread{
public boolean suspendFlag = true;// 控制線程的執(zhí)行
@Override
public void run() {
super.run();
while(!isInterrupted()) {
synchronized (this)
{
while (suspendFlag)
{
try
{
wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
send(getbLoopData());
try
{
Thread.sleep(iDelay);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
//線程暫停
public void setSuspendFlag() {
this.suspendFlag = true;
}
//喚醒線程
public synchronized void setResume() {
this.suspendFlag = false;
notify();
}
}
//----------------------------------------------------
public int getBaudRate()
{
return iBaudRate;
}
public boolean setBaudRate(int iBaud)
{
if (_isOpen)
{
return false;
} else
{
iBaudRate = iBaud;
return true;
}
}
public boolean setBaudRate(String sBaud)
{
int iBaud = Integer.parseInt(sBaud);
return setBaudRate(iBaud);
}
//----------------------------------------------------
public String getPort()
{
return sPort;
}
public boolean setPort(String sPort)
{
if (_isOpen)
{
return false;
} else
{
this.sPort = sPort;
return true;
}
}
//----------------------------------------------------
public boolean isOpen()
{
return _isOpen;
}
//----------------------------------------------------
public byte[] getbLoopData()
{
return _bLoopData;
}
//----------------------------------------------------
public void setbLoopData(byte[] bLoopData)
{
this._bLoopData = bLoopData;
}
//----------------------------------------------------
public void setTxtLoopData(String sTxt){
this._bLoopData = sTxt.getBytes();
}
//----------------------------------------------------
public void setHexLoopData(String sHex){
this._bLoopData = MyFunc.HexToByteArr(sHex);
}
//----------------------------------------------------
public int getiDelay()
{
return iDelay;
}
//----------------------------------------------------
public void setiDelay(int iDelay)
{
this.iDelay = iDelay;
}
//----------------------------------------------------
public void startSend()
{
if (mSendThread != null)
{
mSendThread.setResume();
}
}
//----------------------------------------------------
public void stopSend()
{
if (mSendThread != null)
{
mSendThread.setSuspendFlag();
}
}
//----------------------------------------------------
protected abstract void onDataReceived(ComBean ComRecData);
}
除去一些get set方法 ,主要是 構(gòu)造方法 衣迷,打開(kāi)關(guān)閉方法 以及最后一行的abstract 方法onDataReceived()和一個(gè)讀的線程ReadThread 和一個(gè)發(fā)送命令線程SendThread 畏鼓;在ReadThread 在接收或者叫讀線程中 調(diào)用了onDataReceived()方法這樣在用的時(shí)候 可以直接實(shí)現(xiàn)調(diào)用。
SendThread 中 自動(dòng)發(fā) 的原理就是 執(zhí)行while語(yǔ)句發(fā)送命令 線程sleep()來(lái)間隔循環(huán),控制線程暫停和喚起用的是 wait()和notif()壶谒,所以就可以通過(guò)設(shè)定flag實(shí)現(xiàn)自動(dòng)發(fā)送云矫。
wait() 與 notify/notifyAll 方法必須在同步代碼塊(synchronized關(guān)鍵字)中使用.
由于 wait() 與 notify/notifyAll() 是放在同步代碼塊中的,因此線程在執(zhí)行它們時(shí)汗菜,肯定是進(jìn)入了臨界區(qū)中的让禀,即該線程肯定是獲得了鎖的。
當(dāng)線程執(zhí)行wait()時(shí)陨界,會(huì)把當(dāng)前的鎖釋放堆缘,然后讓出CPU,進(jìn)入等待狀態(tài)普碎。
當(dāng)執(zhí)行notify/notifyAll方法時(shí)吼肥,會(huì)喚醒一個(gè)處于等待該 對(duì)象鎖 的線程,然后繼續(xù)往下執(zhí)行麻车,直到執(zhí)行完退出對(duì)象鎖鎖住的區(qū)域(synchronized修飾的代碼塊)后再釋放鎖缀皱。
ReadThread 就簡(jiǎn)單了也是while()代碼塊 定時(shí)sleep循環(huán) 之后 讀到內(nèi)容之后封裝成實(shí)體對(duì)象調(diào)用抽象方法onDataReceived()傳遞到要實(shí)現(xiàn)的地方。
MyFunc是一些數(shù)據(jù)轉(zhuǎn)換的靜態(tài)方法动猬,如圖:
ComAssistantActivity的大致截圖 770行
ComAssistantActivity中 數(shù)據(jù)比較多啤斗,但是也不難捋順,從左側(cè)概要中可以看出來(lái)主要是一些事件處理和兩個(gè)繼承類:串口控制類SerialControl 繼承SerialHelper和刷新顯示線程DispQueueThread
如圖是Activity onCreate()是實(shí)例化四個(gè)串口控制SerialControl 對(duì)象以及刷新線程并啟動(dòng)赁咙。
//----------------------------------------------------串口控制類
private class SerialControl extends SerialHelper{
public SerialControl(){
}
@Override
protected void onDataReceived(final ComBean ComRecData)
{
//數(shù)據(jù)接收量大或接收時(shí)彈出軟鍵盤(pán)钮莲,界面會(huì)卡頓,可能和6410的顯示性能有關(guān)
//直接刷新顯示免钻,接收數(shù)據(jù)量大時(shí),卡頓明顯崔拥,但接收與顯示同步极舔。
//用線程定時(shí)刷新顯示可以獲得較流暢的顯示效果,但是接收數(shù)據(jù)速度快于顯示速度時(shí)链瓦,顯示會(huì)滯后拆魏。
//最終效果差不多-_-,線程定時(shí)刷新稍好一些慈俯。
DispQueue.AddQueue(ComRecData);//線程定時(shí)刷新顯示(推薦)
Log.e("TAG", MyFunc.ByteArrToHex(ComRecData.bRec));
/*
runOnUiThread(new Runnable()//直接刷新顯示
{
public void run()
{
DispRecData(ComRecData);
}
});*/
}
}
SerialControl 繼承SerialHelper渤刃,那么它的實(shí)例就可以對(duì)串口進(jìn)行讀寫(xiě)操作 并且 在onDataReceived()中實(shí)現(xiàn)對(duì)接收到的數(shù)據(jù)進(jìn)行處理。即添加到 刷新線程的 數(shù)據(jù)源隊(duì)列中:DispQueue是DispQueueThread 的實(shí)例贴膘。
//----------------------------------------------------刷新顯示線程
private class DispQueueThread extends Thread{
private Queue<ComBean> QueueList = new LinkedList<ComBean>();
@Override
public void run() {
super.run();
while(!isInterrupted()) {
final ComBean ComData;
while((ComData=QueueList.poll())!=null)
{
runOnUiThread(new Runnable()
{
public void run()
{
DispRecData(ComData);//更新界面
}
});
try
{
Thread.sleep(100);//顯示性能高的話卖子,可以把此數(shù)值調(diào)小。
} catch (Exception e)
{
e.printStackTrace();
}
break;
}
}
}
public synchronized void AddQueue(ComBean ComData){
QueueList.add(ComData);
}
}
其中QueueList做為接收到的數(shù)據(jù)存放隊(duì)列刑峡,LinkedList是有序的洋闽,為什么AddQueue要同步加鎖呢
public synchronized void AddQueue(ComBean ComData){
QueueList.add(ComData);
}
因?yàn)長(zhǎng)inkedList是線程不安全的,開(kāi)啟了四個(gè)串口控制對(duì)象如果同時(shí)add()會(huì)拋出ConcurrentModificationException異常氛琢。
while語(yǔ)句執(zhí)行的條件LinkedList.poll()方法的含義:找到并刪除表頭喊递,返回null或隊(duì)列中第一個(gè)對(duì)象随闪,還是用源碼來(lái)分析LinkedList
public E poll() {
return size == 0 ? null : removeFirst();
}
/**
* Removes the first object from this {@code LinkedList}.
*
* @return the removed object.
* @throws NoSuchElementException
* if this {@code LinkedList} is empty.
*/
public E removeFirst() {
return removeFirstImpl();
}
private E removeFirstImpl() {
Link<E> first = voidLink.next;
if (first != voidLink) {
Link<E> next = first.next;
voidLink.next = next;
next.previous = voidLink;
size--;
modCount++;
return first.data;
}
throw new NoSuchElementException();
}
3.項(xiàng)目實(shí)現(xiàn)
用該eclipse項(xiàng)目源碼 做嘗試移植了一份Android Studio 3.0 的項(xiàng)目阳似,幾番測(cè)試通過(guò)打的包也能用,同比可以遷移到自己項(xiàng)目铐伴。代碼分享文末撮奏;
在main 目錄下創(chuàng)建 jni 和jniLibs ,
0.把原Eclipse項(xiàng)目的android_serialport_api包復(fù)制到在main/java下当宴。
1.把原eclipse中的libs路徑下的三個(gè)平臺(tái)的serial_port.so同目錄復(fù)制到j(luò)niLibs下畜吊。
2.把原eclipse中的c .h 文件復(fù)制到j(luò)ni并重命名為android_serialport_api_SerialPort,或者使用Terminal命令生成C的頭文件自己在把代碼復(fù)制進(jìn)去(注意路徑對(duì)應(yīng)方法名户矢,這個(gè)1應(yīng)該是區(qū)分包名和下劃線:Java_android_1serialport_1api_SerialPort_open)
Terminal命令
①輸入cd app\src\main\java進(jìn)入源碼所在目錄
②輸入javah -jni android_serialport_api.SerialPort生成頭文件
③把生成的android_serialport_api_SerialPort.h復(fù)制到j(luò)ni下邊(沒(méi)有該目錄就右鍵 Moudle玲献,右鍵菜單中選擇 New -> Folder -> JNI Folder)
④右鍵 jni 文件夾,右鍵菜單中選擇New -> C/C++ Source File創(chuàng)建與 .h 文件同名的 .c 文件梯浪。
⑤把原Eclipse 的jni下對(duì)應(yīng)的.c .h文件代碼復(fù)制進(jìn)去
3.在build.gradle 的android節(jié)點(diǎn)中添加
sourceSets.main {
jniLibs.srcDir 'src/main/jniLibs'
jni.srcDirs = []
}
上圖的右側(cè)標(biāo)紅部分捌年,否則會(huì)提示
Flag android.useDeprecatedNdk is no longer supported and will be removed in the next version of Android Studio. Please switch to a supported build system.
這樣就直接可以用原項(xiàng)目編譯好的.so 注意前提是要在在 local.properties 添加 ndk 路徑:
#Sat Jan 20 10:09:24 CST 2018
ndk.dir=F\:\\sdk\\ndk-bundle
sdk.dir=F\:\\sdk
其下目錄有Eclipse 項(xiàng)目源碼和Android Studio 源碼 以及自己使用本機(jī)debug 密鑰打包的 Android調(diào)試工具和 PC 端調(diào)試工具,
github 地址 https://github.com/silencefun/ComTest
百度云鏈接: https://pan.baidu.com/s/1nw37xu5 密碼: qscc
如果覺(jué)得有幫助挂洛,請(qǐng)點(diǎn)個(gè)贊? ★礼预,謝謝。
Android 串口通信筆記2 調(diào)試工具分析 工具類實(shí)現(xiàn)分析虏劲、項(xiàng)目實(shí)現(xiàn)
Android 串口通信開(kāi)發(fā)筆記3:CMake 方式實(shí)現(xiàn)和 多對(duì)多的實(shí)現(xiàn)邏輯
Android 串口開(kāi)發(fā) 支持N-8-1(數(shù)據(jù)位停止位校驗(yàn)方式) 設(shè)定