1. 程式人生 > >利用JNI寫的安卓串列埠讀寫框架

利用JNI寫的安卓串列埠讀寫框架

C程式碼如下:

#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);
}
JAVA部分:

記得開一個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:

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();

	}
}
CommonUtil:
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可以不必放入安卓工程中,只要編譯好的庫資料夾放進去就行。