利用JNI寫的安卓串列埠讀寫框架
阿新 • • 發佈:2019-02-16
C程式碼如下:
JAVA部分:#include <termios.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <jni.h> //#include "SerialPort.h" #include "android/log.h" static const char *TAG="serial_port"; #define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args) #define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args) #define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args) static speed_t getBaudrate(jint baudrate) { switch(baudrate) { case 0: return B0; case 50: return B50; case 75: return B75; case 110: return B110; case 134: return B134; case 150: return B150; case 200: return B200; case 300: return B300; case 600: return B600; case 1200: return B1200; case 1800: return B1800; case 2400: return B2400; case 4800: return B4800; case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; case 57600: return B57600; case 115200: return B115200; case 230400: return B230400; case 460800: return B460800; case 500000: return B500000; case 576000: return B576000; case 921600: return B921600; case 1000000: return B1000000; case 1152000: return B1152000; case 1500000: return B1500000; case 2000000: return B2000000; case 2500000: return B2500000; case 3000000: return B3000000; case 3500000: return B3500000; case 4000000: return B4000000; default: return -1; } } /* * Class: android_serialport_SerialPort * Method: open * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor; */ JNIEXPORT jobject JNICALL Java_com_cjz_jniserialtest_SerialUtil_open (JNIEnv *env, jclass thiz, jstring path, jint baudRate) { int fd; speed_t speed; jobject mFileDescriptor; /* Check arguments */ { speed = getBaudrate(baudRate); if (speed == -1) { /* TODO: throw an exception */ // LOGE("Invalid baudrate"); return NULL; } } /* Opening device */ { jboolean iscopy; const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy); // LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR | flags); fd = open(path_utf, O_RDWR/* | flags*/); // LOGD("open() fd = %d", fd); (*env)->ReleaseStringUTFChars(env, path, path_utf); if (fd == -1) { /* Throw an exception */ // LOGE("Cannot open port"); /* TODO: throw an exception */ return NULL; } } /* Configure device */ { struct termios cfg; // LOGD("Configuring serial port"); if (tcgetattr(fd, &cfg)) { // LOGE("tcgetattr() failed"); close(fd); /* TODO: throw an exception */ return NULL; } cfmakeraw(&cfg); cfsetispeed(&cfg, speed); cfsetospeed(&cfg, speed); if (tcsetattr(fd, TCSANOW, &cfg)) { // LOGE("tcsetattr() failed"); close(fd); /* TODO: throw an exception */ 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; } /* * Class: cedric_serial_SerialPort * Method: close * Signature: ()V */ JNIEXPORT void JNICALL Java_com_cjz_jniserialtest_SerialUtil_close(JNIEnv *env, jobject thiz) { jclass SerialPortClass = (*env)->GetObjectClass(env, thiz); jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor"); jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;"); jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I"); jobject mFd = (*env)->GetObjectField(env, thiz, mFdID); jint descriptor = (*env)->GetIntField(env, mFd, descriptorID); // LOGD("close(fd = %d)", descriptor); close(descriptor); }
記得開一個com.cjz.jniserialtest的包去放這些類,否則JNI無法聯絡到類和C程式碼介面,導致出錯
SerialUtil:
package com.cjz.jniserialtest; import java.io.BufferedReader; 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.InputStreamReader; import java.io.OutputStream; import java.util.LinkedList; import android.util.Log; import com.hanlion.cardofclass.utils.CommUtils; import com.hanlion.cardofclass.utils.LogUtils; /** * @author 陳杰柱 * @version 串列埠讀寫框架 1.0 * * 當觀察者模式開啟時,最好不要使用其他單獨方法,否則容易因為 * 爭奪資料的原因導致出錯 ***/ public class SerialUtil { private FileDescriptor mFd; private FileInputStream mFileInputStream; private FileOutputStream mFileOutputStream; private ThreadTimeCount threadTimeCount; private Thread threadReadData; private boolean observerMode = false; private static final String TAG = "SerialPortMsg"; /**波特率列表類**/ public class BaudRateList { public static final int B50 = 50; public static final int B75 = 75; public static final int B110 = 110; public static final int B134 = 134; public static final int B150 = 150; public static final int B200 = 200; public static final int B300 = 300; public static final int B600 = 600; public static final int B1200 = 1200; public static final int B1800 = 1800; public static final int B2400 = 2400; public static final int B4800 = 4800; public static final int B9600 = 9600; public static final int B19200 = 19200; public static final int B38400 = 38400; public static final int B57600 = 57600; public static final int B115200 = 115200; public static final int B230400 = 230400; public static final int B460800 = 460800; public static final int B500000 = 500000; public static final int B576000 = 576000; public static final int B921600 = 921600; public static final int B1000000 = 1000000; public static final int B1152000 = 1152000; public static final int B1500000 = 1500000; public static final int B2000000 = 2000000; public static final int B2500000 = 2500000; public static final int B3000000 = 3000000; public static final int B3500000 = 3500000; public static final int B4000000 = 4000000; } /**開啟對應視窗內IO裝置,支援USB轉串列埠 * @param path 裝置檔案位置(inux把所有裝置視為一個一維檔案,不過按照圖靈機概念本來計算機就是“紙帶+讀寫頭”) * @param baudRate 波特率設定,**/ public native static synchronized FileDescriptor open(String path, int baudRate); /**關閉對應視窗內IO裝置**/ public native static synchronized FileDescriptor close(); static { System.loadLibrary("JNISerialCtrl"); } /**開啟對應視窗內IO裝置,支援USB轉串列埠 * @param path 裝置檔案位置(Linux把所有裝置視為一個一維檔案,不過按照圖靈機概念本來計算機就是“紙帶+讀寫頭”) * @param baudRate 波特率設定,**/ public SerialUtil(String devicePath, int baudrate) { /* Check access permission */ File device = new File(devicePath); if(!device.exists()) return ; if (!device.canRead() || !device.canWrite()) { try { /* Missing read/write permission, trying to chmod the file */ Process su; su = Runtime.getRuntime().exec(CommUtils.getSuPath()); String cmd = "chmod 666 " + device.getAbsolutePath() + "\n" + "exit\n"; su.getOutputStream().write(cmd.getBytes()); if ((su.waitFor() != 0) || !device.canRead() || !device.canWrite()) { LogUtils.logE("串列埠開啟失敗"); } } catch (Exception e) { e.printStackTrace(); } } mFd = open(device.getAbsolutePath(), baudrate); if (mFd == null) { Log.e(TAG, "native open returns null"); //throw new IOException(); return ; } mFileInputStream = new FileInputStream(mFd); mFileOutputStream = new FileOutputStream(mFd); } /**獲取裝置輸入流(主動捕獲模式)**/ public InputStream getInputStream() { return mFileInputStream; } /**獲取裝置輸出流(主動捕獲模式)**/ public OutputStream getOutputStream() { return mFileOutputStream; } /**獲取Reader,以文字方式讀取(當回車時結束)(主動捕獲模式,不關閉)**/ public BufferedReader getBufferedReader() { try { return new BufferedReader(new InputStreamReader(getInputStream())); } catch (Exception e) { return null; } } /**以Byte連結串列方式讀取(主動捕獲模式) * @param spiltTimeLengthMS 隔多久沒收到資料就視為本次接收結束,由使用者自定義**/ public LinkedList<Byte> getByteLinkedList(long spiltTimeLengthMS) { final LinkedList<Byte> linkedListDataPool = new LinkedList<Byte>(); threadTimeCount = new ThreadTimeCount(); threadTimeCount.setEndTimeConut(spiltTimeLengthMS); threadReadData = new Thread(new Runnable() { private byte temp; @Override public void run() { //加鎖,以防函式返回空值 synchronized (linkedListDataPool) { try { //如果read返回值為-1或者isInterrupted()接收到結束訊號則跳出迴圈 while( ((temp = (byte) (getInputStream().read() & 0xFF) != -1) && !threadReadData.isInterrupted()) { linkedListDataPool.add(temp); //超過某個毫秒數沒輸入,就將連結串列丟擲到變成陣列,然後清空,再接收 threadTimeCount.stillInputing(); } } catch (IOException e) { e.printStackTrace(); } } //Log.i(TAG, "collect Byte Finished"); } }); threadTimeCount.threadBabySitting(threadReadData); threadTimeCount.start(); synchronized (linkedListDataPool) { return linkedListDataPool; } } /**以StringBuffer連結串列方式讀取(主動捕獲模式) * @param spiltTimeLengthMS 隔多久沒收到資料就視為本次接收結束,由使用者自定義**/ public StringBuffer getStringBuffer(long spiltTimeLengthMS) { StringBuffer buffer = new StringBuffer(); for(byte temp : getByteLinkedList(spiltTimeLengthMS)) buffer.append((char)temp); return buffer; } /**以位元組陣列方式讀取(主動捕獲模式) * @param spiltTimeLengthMS 隔多久沒收到資料就視為本次接收結束,由使用者自定義**/ public byte[] getByteArray(long spiltTimeLengthMS) { LinkedList<Byte> bytes = getByteLinkedList(spiltTimeLengthMS); int position = 0; byte byteArray[] = new byte[bytes.size()]; for(byte temp : bytes) byteArray[position++] = temp; return byteArray; } /**以位元組陣列方式輸出到串列埠(主動捕獲模式)**/ public void outputByteArray(byte[] data) throws IOException { getOutputStream().write(data); getOutputStream().flush(); } /**以字串方式輸出到串列埠(主動捕獲模式)**/ public void outputString(String data) throws IOException { outputByteArray(data.getBytes()); } /**觀察者模式,通過回撥,檢測到資料就呼叫使用者自定義函式,被動捕獲模式 * 強烈推薦使用觀察者模式**/ public abstract class SerialObserver { private LinkedList<Byte> linkedListSerialData = new LinkedList<Byte>(); private long spiltTimeLengthMS; private long serialDataSize = 0; /** @param spiltTimeLengthMS 隔多久沒收到資料就視為本次接收結束,由使用者自定義**/ public SerialObserver(long spiltTimeLengthMS) { observerMode = true; SerialObserver.this.spiltTimeLengthMS = spiltTimeLengthMS; //資料採集執行緒: new Thread(new Runnable() { @Override public void run() { byte temp = 0; try { if(getInputStream() == null){ Log.e("SerialUtil", "讀取視窗識別失敗"); return; } while( ((temp = (byte) getInputStream().read()) != -1) && observerMode) { synchronized (linkedListSerialData) { linkedListSerialData.add(temp); serialDataSize ++ ; } } } catch (IOException e) { e.printStackTrace(); } } }).start(); //停頓檢查執行緒: new Thread(new Runnable() { @SuppressWarnings("unchecked") @Override public void run() { while(observerMode) { try { long oldSerialDataSize = serialDataSize; Thread.sleep(SerialObserver.this.spiltTimeLengthMS); if(oldSerialDataSize == serialDataSize) { synchronized (linkedListSerialData) { serialData((LinkedList<Byte>)linkedListSerialData.clone()); } linkedListSerialData.clear(); serialDataSize = 0; } } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } public void turnOffObserverMode(){observerMode = false;} public void turnOnObserverMode(){observerMode = true;} public abstract void serialData(LinkedList<Byte> data); } }
ThreadTimeCount:
CommonUtil:package com.cjz.jniserialtest; public class ThreadTimeCount extends Thread { /**迴圈控制開關**/ private boolean runSwitch = true; /**控制多少毫秒沒資料流入就代表這次資料傳入結束,預設100**/ private long endTimeCountMs = 100; private long inputTimes; private Thread threadIntoMe = null; @Override public void run() { while(runSwitch) { long oldUpdateCount = inputTimes; try { Thread.sleep(endTimeCountMs); } catch (InterruptedException e) { e.printStackTrace(); } if(inputTimes == oldUpdateCount) { if(this.threadIntoMe != null) this.threadIntoMe.interrupt(); close(); } } } @Override public void interrupt() { super.interrupt(); close(); } /**告訴執行緒其實我還在寫資料**/ public void stillInputing() { inputTimes ++; } public void close() { runSwitch = false; } /**設定多少毫秒沒收到資料就代表已完成一次接收 * @param timeCount 毫米級時間**/ public void setEndTimeConut(long timeCount) { this.endTimeCountMs = timeCount; } /**執行緒託管**/ public void threadBabySitting(Thread thread) { this.threadIntoMe = thread; if(this.threadIntoMe != null) this.threadIntoMe.start(); } }
public class CommUtils {
private static Activity currentActivity = null;
private static String suPath = "/system/xbin/su";
public static String getSuPath() {
return suPath;
}
public static void setSuPath(String suPath) {
CommUtils.suPath = suPath;
}
}
使用方法示例(擷取自己工程的一部分):
通過繼承抽象類SerialUtil.SerialObserver,splitTimeLengthMS代表超過多少毫秒沒有新位元組發過來,就當是接受完了一份資料,預設是100ms的間隔時間。然後內容會以Byte連結串列的形式回撥給serialData方法,這裡寫自己要用的程式碼即可。例子程式碼的是我自己安卓工程的一個內部類,把收到的卡號資料(小端)整理到一個long變數中,然後給打卡卡號處理方法進一步處理。就像流水線一樣。
public NormalModeController(ModeActivity act) {
//其他過程...
...
...
...
class SerialReader extends SerialUtil.SerialObserver {
public SerialReader(SerialUtil serialUtil, long spiltTimeLengthMS) {
serialUtil.super(spiltTimeLengthMS);
}
@Override
public void serialData(LinkedList<Byte> data) {
long cardNumber = 0;
String strCardNumber = null;
if(data == null) return;
if(data.size() < 4) return;
LogUtils.logI("serialData:" + String.format("%X", (data.get(0) & 0xFF)) + "," +
String.format("%X", (data.get(1) & 0xFF)) + "," +
String.format("%X", (data.get(2) & 0xFF)) + "," +
String.format("%X", (data.get(3) & 0xFF))
);
//方法1:
/*cardNumber = cardNumber | ((data.get(3) & 0xFF) << 24);
cardNumber = cardNumber | ((data.get(2) & 0xFF) << 16);
cardNumber = cardNumber | ((data.get(1) & 0xFF) << 8);
cardNumber = cardNumber | ((data.get(0) & 0xFF) << 0);*/
//方法2:抽象成了一個迴圈搞定(注意是小端資料格式,所以要反向迴圈。因為超過了4位元組,怕影響符號位,所以用long來儲存位移數)
for(int i = data.size() - 1; i >= 0; i--){
cardNumber = cardNumber | (data.get(i) & 0xFF);
if(i > 0) cardNumber = cardNumber << 8;
}
if(cardNumber == 0) return ;
//將其定長為10位整數:
strCardNumber = String.format("%010d", cardNumber);
Log.i("Content:", strCardNumber);
//把卡號傳送到主執行緒
Message msg = new Message();
msg.what = NormalModeControllerConstant.SEND_CARD_MSG;
msg.obj = strCardNumber;
handler.sendMessage(msg);
}
}
//建立SerialUtil物件,傳入串列埠裝置號,還有波特率。
SerialUtil serialUtil = new SerialUtil("/dev/ttyS3", SerialUtil.BaudRateList.B9600);
//建立繼承抽象類後實現了serialData方法的SerialReader類物件,傳入serialUtil打通上下文,設50ms為分割時間
SerialReader serialReader = new SerialReader(serialUtil, 50);
//開啟觀察者模式(回撥資料模式)
serialReader.turnOnObserverMode();
}
注意Gradle的App指令碼要加點程式碼(abiFilters可以自己增刪指令集型別)
Android.mk內容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := JNISerialCtrl
LOCAL_SRC_FILES := SerialCtrl.c
include $(BUILD_SHARED_LIBRARY)
找到你放SerialCtrl.c的資料夾,然後在裡面輸入ndk-build,把得到的各種如以armeabi為名的so資料夾,複製到libs資料夾即可。但SerialCtrl.c可以不必放入安卓工程中,只要編譯好的庫資料夾放進去就行。