Android NDK 实现串口工具

这篇博客介绍了如何利用Android NDK实现一个串口工具,包括在SerialPort.c中处理串口通信,配置CMakeLists.txt和build.gradle文件,创建SerialPort.java和SerialPortFinder.java来管理和查找串口,以及设计SerialToolActivity.java和activity_serial_tool.xml布局。提供了源码下载链接和相关参考资料。

实现效果

在这里插入图片描述

SerialPort.c

将 SerialPort.c 放入 src/main/cpp 目录中。

#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <jni.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)

/* 
* Class: android_serialport_SerialPort 
* Method: open 
* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor; 
*/
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_test_serialtool_SerialPort_open
        (JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags)
{
    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_test_serialtool_SerialPort_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);
}

CMakeLists.txt

CmakeLists.txt 与 SerialPort.c 在同一目录下。

cmake_minimum_required(VERSION 3.4.1)

add_library(
        serial_port
        SHARED
        SerialPort.c
)

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log )

target_link_libraries(
        serial_port
        ${log-lib}
)

build.gradle

在 android {} 中添加:

//    支持ndk,加载cmake文件
externalNativeBuild {
     cmake {
         path file('src/main/cpp/CMakeLists.txt')
     }
 }

SerialPort.java

SerialPort.java 的包名需与 SerialPort.c 中声明的一致。

package com.test.serialtool;

import android.util.Log;

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.InputStreamReader;

/**
 * 该类的包名需与生成so时设置的包名相同
 */
public class SerialPort {

    static {
        System.loadLibrary("serial_port");
    }

    public native void close();

    public native FileDescriptor open(String var1, int var2, int var3);

    private File mDevice = null;
    private int mBaudrate;
    private FileDescriptor mFd = null;
    private boolean isConnected = false;
    private FileInputStream mFileInputStream = null;
    private FileOutputStream mFileOutputStream = null;

    public FileInputStream getInputStream() {
        return this.mFileInputStream;
    }

    public FileOutputStream getOutputStream() {
        return this.mFileOutputStream;
    }

    public SerialPort(File device, int baudrate) throws SecurityException, IOException {
        if (device == null) {
            Log.e("SerialPort", "device not null");
            throw new IOException();
        } else {
            if (!device.canRead() || !device.canWrite()) {
                try {
                    Process 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();
                    }

                    this.mDevice = device;
                    this.mBaudrate = baudrate;
                } catch (Exception var5) {
                    var5.printStackTrace();
                    throw new SecurityException();
                }
            }

        }
    }

    public boolean openPort(int flags) throws IOException {
        if (this.isConnected) {
            return this.isConnected;
        } else if (this.mDevice != null) {
            this.mFd = this.open(this.mDevice.getAbsolutePath(), this.mBaudrate, flags);
            if (this.mFd == null) {
                Log.e("SerialPort", "native open returns null");
                throw new IOException();
            } else {
                this.isConnected = true;
                this.mFileInputStream = new FileInputStream(this.mFd);
                this.mFileOutputStream = new FileOutputStream(this.mFd);
                return true;
            }
        } else {
            return false;
        }
    }

    public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {
        if (device == null) {
            Log.e("SerialPort", "device not null");
            throw new IOException();
        } else {
            this.mDevice = device;
            this.mBaudrate = baudrate;
            if (!device.canRead() || !device.canWrite()) {
                try {
                    Process 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 var6) {
                    var6.printStackTrace();
                    throw new SecurityException();
                }
            }

            this.mFd = this.open(device.getAbsolutePath(), baudrate, flags);
            if (this.mFd == null) {
                Log.e("SerialPort", "native open returns null");
                throw new IOException();
            } else {
                this.isConnected = true;
                this.mFileInputStream = new FileInputStream(this.mFd);
                this.mFileOutputStream = new FileOutputStream(this.mFd);
            }
        }
    }

    public void write(byte[] buffer, int offset, int count) throws IOException {
        if (this.isConnected && this.mFileOutputStream != null) {
            this.mFileOutputStream.write(buffer, offset, count);
        }
    }

    public boolean isConnected() {
        return this.isConnected;
    }

    public int read(byte[] buffer, int offset, int count) throws IOException {
        return this.isConnected && this.mFileInputStream != null ? this.mFileInputStream.read(buffer, offset, count) : 0;
    }

    public void writeLine(String text) throws IOException, IllegalAccessException {
        this.write(text + "\r\n");
    }

    public void write(String text) throws IOException, IllegalAccessException {
        if (this.isConnected && this.mFileOutputStream != null) {
            this.mFileOutputStream.write(text.getBytes());
        }
    }

    public int getAvailableByte() throws IOException {
        return this.isConnected && this.mFileInputStream != null ? this.mFileInputStream.available() : 0;
    }

    public String readExisting() throws IOException, IllegalAccessException {
        if (this.isConnected && this.mFileInputStream != null) {
            int count = this.mFileInputStream.available();
            char[] buffer = new char[count];
            this.read((char[])buffer, 0, count);
            return new String(buffer, 0, count);
        } else {
            return null;
        }
    }

    public int read(char[] buffer, int offset, int count) throws IOException, IllegalAccessException {
        if (this.isConnected && this.mFileInputStream != null) {
            if (offset >= 0 && count >= 0 && offset < buffer.length && offset + count <= buffer.length) {
                BufferedReader in = new BufferedReader(new InputStreamReader(this.mFileInputStream));
                return in.read(buffer, offset, count);
            } else {
                throw new IndexOutOfBoundsException();
            }
        } else {
            return 0;
        }
    }
}

SerialPortFinder.java

SerialPortFinder.java 用来查找设备支持的所有串口。

import android.util.Log;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.Iterator;
import java.util.Vector;

/**
 * 获取设备支持的所有串口
 */
public class SerialPortFinder {
    public class Driver {
        public Driver(String name, String root) {
            mDriverName = name;
            mDeviceRoot = root;
        }
        private String mDriverName;
        private String mDeviceRoot;
        Vector<File> mDevices = null;
        public Vector<File> getDevices() {
            if (mDevices == null) {
                mDevices = new Vector<File>();
                File dev = new File("/dev");
                File[] files = dev.listFiles();
                if (files != null) {
                    int i;
                    for (i=0; i<files.length; i++) {
                        if (files[i].getAbsolutePath().startsWith(mDeviceRoot)) {
                            Log.d(TAG, "Found new device: " + files[i]);
                            mDevices.add(files[i]);
                        }
                    }
                }
            }
            return mDevices;
        }
        public String getName() {
            return mDriverName;
        }
    }

    private static final String TAG = "SerialPort";

    private Vector<Driver> mDrivers = null;

    Vector<Driver> getDrivers() throws IOException {
        if (mDrivers == null) {
            mDrivers = new Vector<Driver>();
            LineNumberReader r = new LineNumberReader(new FileReader("/proc/tty/drivers"));
            String l;
            while((l = r.readLine()) != null) {
                // Issue 3:
                // Since driver name may contain spaces, we do not extract driver name with split()
                String drivername = l.substring(0, 0x15).trim();
                String[] w = l.split(" +");
                if ((w.length >= 5) && (w[w.length-1].equals("serial"))) {
                    Log.d(TAG, "Found new driver " + drivername + " on " + w[w.length-4]);
                    mDrivers.add(new Driver(drivername, w[w.length-4]));
                }
            }
            r.close();
        }
        return mDrivers;
    }

    public String[] getAllDevices() {
        Vector<String> devices = new Vector<String>();
        // Parse each driver
        Iterator<Driver> itdriv;
        try {
            itdriv = getDrivers().iterator();
            while(itdriv.hasNext()) {
                Driver driver = itdriv.next();
                Iterator<File> itdev = driver.getDevices().iterator();
                while(itdev.hasNext()) {
                    String device = itdev.next().getName();
                    String value = String.format("%s (%s)", device, driver.getName());
                    devices.add(value);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return devices.toArray(new String[devices.size()]);
    }

    public String[] getAllDevicesPath() {
        Vector<String> devices = new Vector<String>();
        // Parse each driver
        Iterator<Driver> itdriv;
        try {
            itdriv = getDrivers().iterator();
            while(itdriv.hasNext()) {
                Driver driver = itdriv.next();
                Iterator<File> itdev = driver.getDevices().iterator();
                while(itdev.hasNext()) {
                    String device = itdev.next().getAbsolutePath();
                    devices.add(device);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return devices.toArray(new String[devices.size()]);
    }
}

activity_serial_tool.xml

布局文件。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <!--打开/关闭串口的点击事件-->
        <variable
            name="onOpenClickListener"
            type="android.view.View.OnClickListener" />

        <!--刷新的开关事件-->
        <variable
            name="onRefreshCheckedChangeListener"
            type="android.widget.CompoundButton.OnCheckedChangeListener" />

        <!--清屏的开关事件-->
        <variable
            name="onClearCheckedChangeListener"
            type="android.widget.CompoundButton.OnCheckedChangeListener" />

        <!--保存的开关事件-->
        <variable
            name="onSaveCheckedChangeListener"
            type="android.widget.CompoundButton.OnCheckedChangeListener" />

        <!--选择常用命令的点击事件-->
        <variable
            name="onSelectCommandClickListener"
            type="android.view.View.OnClickListener" />

        <!--发送的点击事件-->
        <variable
            name="onSendClickListener"
            type="android.view.View.OnClickListener" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <LinearLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:orientation="horizontal">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="串口号:"/>

                    <Spinner
                        android:id="@+id/spinner_serial_port"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"></Spinner>
                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:gravity="center"
                    android:layout_marginTop="10dp">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="波特率:" />

                    <Spinner
                        android:id="@+id/spinner_baud_rate"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"></Spinner>
                </LinearLayout>
            </LinearLayout>

            <Button
                android:id="@+id/btn_open"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="打开"
                android:onClick="@{onOpenClickListener}"/>
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv_console"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:background="#e3e3e3"
                android:fadeScrollbars="false"
                android:scrollbars="vertical"
                android:gravity="bottom" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <CheckBox
                    android:id="@+id/cb_refresh"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:checked="true"
                    android:text="刷新"
                    android:onCheckedChanged="@{onRefreshCheckedChangeListener}"/>

                <CheckBox
                    android:id="@+id/cb_clear"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="清屏"
                    android:onCheckedChanged="@{onClearCheckedChangeListener}"/>

                <CheckBox
                    android:id="@+id/cb_hex"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Hex"/>

                <CheckBox
                    android:id="@+id/cb_save"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="保存"
                    android:onCheckedChanged="@{onSaveCheckedChangeListener}"/>
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:layout_marginTop="5dp">

                <EditText
                    android:id="@+id/et_command"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:layout_alignParentBottom="true" />

                <ImageView
                    android:id="@+id/iv_select_command"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:src="@drawable/ic_down"
                    android:onClick="@{onSelectCommandClickListener}"/>

                <Button
                    android:id="@+id/btn_send"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="发送"
                    android:onClick="@{onSendClickListener}"/>
            </LinearLayout>

        </LinearLayout>

    </LinearLayout>
</layout>

SerialToolActivity.java

public class SerialToolActivity extends BaseActivity<AndroidViewModel, ActivitySerialToolBinding> {

    @Override
    protected int getContentViewId() {
        return R.layout.activity_serial_tool;
    }

    private static String PREF_KEY_LAST_PATH = "serial_path";
    private static String PREF_KEY_LAST_BAUD_RATE = "serial_baud_rate";

    /**
     * ascii格式的常用命令
     */
    private File commandFile = null;

    /**
     * 十六进制的常用命令
     */
    private File hexCommandFile = null;

    private SerialPort port;
    private boolean isOpen;
    private Thread readThread;
    private Handler handler = new MyHandler();
    private File saveFile = null;

    /**
     * 常用命令
     */
    private CommandSet commands = null;

    /**
     * 常用十六进制字符串命令
     */
    private CommandSet hexCommands = null;

    /**
     * websocket重连线程
     */
    private ExecutorService reconnectService = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.setTitle("串口工具");

        // 初始化软件
        new RxPermissions(this)
                .request(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .subscribe(aBoolean -> {
                        if (aBoolean) {
                            init();
                        } else {
                            finish();
                        }
                });

        // 初始化界面
        initView();

        // 初始化事件
        initListener();
    }

    /**
     * 初始化
     */
    private void init() {
        try {
            commandFile = new File(DirectoryManager.MAIN.getPath(), "command.txt");
            hexCommandFile = new File(DirectoryManager.MAIN.getPath(), "hexCommand.txt");

            // 拷贝assets中的文件
            String[] files = getAssets().list("");
            for (String file : files) {
                if (file.equals(commandFile.getName())) {
                    DirectoryManager.copyAssets(this, file, DirectoryManager.MAIN.getAbsolutePath(), false);
                } else if (file.equals(hexCommandFile.getName())) {
                    DirectoryManager.copyAssets(this, file,  DirectoryManager.MAIN.getAbsolutePath(), false);
                }
            }

            FileUtil.notifySystemToScan(DirectoryManager.MAIN.getPath());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化界面
     */
    private void initView() {

        // 获取串口号
        String[] devices = new SerialPortFinder().getAllDevicesPath();
        ArrayAdapter<String> deviceAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, devices);
        dataBinding.spinnerSerialPort.setAdapter(deviceAdapter);
        int index = Arrays.asList(devices).indexOf(prefs.getString(PREF_KEY_LAST_PATH, ""));
        if (index > -1) {
            dataBinding.spinnerSerialPort.setSelection(index);
        }

        // 获取波特率
        String[] baudrates = getResources().getStringArray(R.array.serial_baud_rates);
        ArrayAdapter<String> baudrateAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, baudrates);
        dataBinding.spinnerBaudRate.setAdapter(baudrateAdapter);
        index = Arrays.asList(baudrates).indexOf(prefs.getString(PREF_KEY_LAST_BAUD_RATE, ""));
        if (index > -1) {
            dataBinding.spinnerBaudRate.setSelection(index);
        }

        dataBinding.tvConsole.setMovementMethod(ScrollingMovementMethod.getInstance());
    }

    private void initListener() {

        // 打开或关闭串口
        dataBinding.setOnOpenClickListener(v -> {
            if (isOpen == false) {
                open();
            }
            else {
                close();

                dataBinding.btnOpen.setText("打开");
                dataBinding.spinnerSerialPort.setEnabled(true);
                dataBinding.spinnerBaudRate.setEnabled(true);
                Toast.makeText(this, "串口已关闭!", Toast.LENGTH_SHORT).show();
            }
        });

        // 发送命令
        dataBinding.setOnSendClickListener(v -> {
            if (isOpen && port != null) {
                String command = dataBinding.etCommand.getText().toString().trim();
                if (TextUtils.isEmpty(command) == false) {
                    if (dataBinding.cbHex.isChecked() == false) {
                        try {
                            // 发送ASCII格式的命令
                            port.writeLine(command);
                            Toast.makeText(this, "发送成功!", Toast.LENGTH_SHORT).show();
                        } catch (Exception e) {
                            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
                        }
                    } else {
                        try {
                            // 发送Hex格式的命令
                            // 将十六进制字符串转为字节数组
                            byte[] data = Helper.hexStr2BytesWithBlank(command);
                            if (data != null) {
                                port.write(data, 0, data.length);
                                Toast.makeText(this, "发送成功!", Toast.LENGTH_SHORT).show();
                            }
                        } catch (Exception e) {
                            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
                        }
                    }
                }
            }
        });

        // 清屏
        dataBinding.setOnClearCheckedChangeListener((buttonView, isChecked) -> {
            if (isChecked) {
                dataBinding.tvConsole.setText("");
                dataBinding.cbClear.setChecked(false);
            }
        });

        // 勾选保存,创建新文件进行保存
        dataBinding.setOnSaveCheckedChangeListener((buttonView, isChecked) -> {

            // 每次勾选创建新的文件
            if (isChecked) {
                saveFile = new File(DirectoryManager.MAIN.getPath(), String.format("serial_%s.txt", Helper.getShortDate()));
                Toast.makeText(this, String.format("数据保存到:%s", saveFile.getAbsolutePath()), Toast.LENGTH_LONG).show();
            }
        });

        // 选择常用命令
        dataBinding.setOnSelectCommandClickListener(v -> {
            if (dataBinding.cbHex.isChecked()) {
                // 加载十六进制常用命令
                if (hexCommands == null) {
                    hexCommands = getCommandSet(hexCommandFile);
                }

                if (hexCommands == null) {
                    return;
                }

                List<String> comments = hexCommands.getComments();
                if (comments != null && comments.size() > 0) {
                    DialogUtil.showSingleSelectDialog(this, comments, (dialog, which) -> {
                        dialog.dismiss();
                        dataBinding.etCommand.setText(hexCommands.getCommand(which));
                    });
                }
            } else {
                // 加载ASCII常用命令
                if (commands == null) {
                    commands = getCommandSet(commandFile);
                }

                if (commands == null) {
                    return;
                }

                List<String> comments = commands.getComments();
                if (comments != null && comments.size() > 0) {
                    DialogUtil.showSingleSelectDialog(this, comments, (dialog, which) -> {
                        dialog.dismiss();
                        dataBinding.etCommand.setText(commands.getCommand(which));
                    });
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        close();
        super.onDestroy();
    }

    /**
     * 点击打开按钮
     */
    private void open() {
        dataBinding.tvConsole.setText("");

        String devicePort = dataBinding.spinnerSerialPort.getSelectedItem().toString();
        int baudrate = Integer.parseInt(dataBinding.spinnerBaudRate.getSelectedItem().toString());
        if (open(devicePort, baudrate)) {
            dataBinding.btnOpen.setText("关闭");
            dataBinding.spinnerSerialPort.setEnabled(false);
            dataBinding.spinnerBaudRate.setEnabled(false);
            Toast.makeText(this, "打开串口成功!", Toast.LENGTH_SHORT).show();

            readThread = new Thread(new ReadRunnable());
            readThread.start();

            prefs.edit().putString(PREF_KEY_LAST_PATH, devicePort)
                        .putString(PREF_KEY_LAST_BAUD_RATE, String.valueOf(baudrate)).commit();
        } else {
            Toast.makeText(this, "打开串口失败!", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * 打开串口
     */
    private boolean open(String devicePort, int baudrate) {
        if (port != null) {
            close();
        }
        try {
            port = new SerialPort(new File(devicePort), baudrate, 0);
            if (port != null) {
                isOpen = port.isConnected();
            }
        } catch (Exception e) {
            return false;
        }
        return isOpen;
    }

    /**
     * 关闭串口
     */
    private void close() {
        isOpen = false;
        if (port != null) {
            if (port.isConnected()) {
                port.close();
            }
            port = null;
        }
    }

    /**
     * 避免数据过多导致TextView无法刷新
     */
    private void appendMsg(String msg) {
        if (dataBinding.cbRefresh.isChecked() == false) {
            return;
        }

        int len = dataBinding.tvConsole.getText().length();
        if (len >= 3072) {
            int removeLength = 3072 / 3 * 2;
            CharSequence oldMsg = dataBinding.tvConsole.getText().subSequence(len - removeLength, len);
            dataBinding.tvConsole.setText(oldMsg);
        }
        msg = "\r\n" + msg;
        dataBinding.tvConsole.append(msg);
    }

    /**
     * 保存到文件
     */
    private void saveMsg(String msg) {
        if (dataBinding.cbSave.isChecked() == false) {
            return;
        }

        try {
            FileUtil.writeFile(saveFile, msg, true, true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取文件,获取命令集
     */
    public CommandSet getCommandSet(File commandFile) {
        CommandSet commands = new CommandSet();
        commands.load(commandFile);
        return commands;
    }

    /**
     * 显示接收的数据
     */
    private class MyHandler extends Handler {

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);

            switch (msg.what) {
                case 1:
                    // 显示到界面中
                    appendMsg(msg.obj.toString());

                    // 保存到文件
                    saveMsg(msg.obj.toString());
                    break;
            }
        }
    }

    /**
     * 接收数据
     */
    private class ReadRunnable implements Runnable {

        @Override
        public void run() {
            while (isOpen) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                byte[] temp = new byte[4095];
                try {
                    if (port == null) {
                        break;
                    }
                    int len = port.read(temp, 0, temp.length);

                    if (len > 0) {
                        String msg = null;
                        if (dataBinding.cbHex.isChecked()) {
                            msg = Helper.byte2HexStr(temp, 0, len);
                        } else {
                            msg = new String(temp, 0, len);
                        }
                        msg.trim();

                        handler.obtainMessage(1, msg).sendToTarget();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

源码下载

https://download.csdn.net/download/doris_d/13121730

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值