├── serial_lib
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ └── values
│ │ │ └── strings.xml
│ │ ├── AndroidManifest.xml
│ │ ├── cpp
│ │ ├── CMakeLists.txt
│ │ ├── SerialPort.h
│ │ └── SerialPort.c
│ │ └── java
│ │ └── com
│ │ └── cl
│ │ └── serialportlibrary
│ │ ├── enumerate
│ │ ├── SerialStatus.java
│ │ └── SerialPortEnum.java
│ │ ├── listener
│ │ ├── OnOpenSerialPortListener.java
│ │ └── OnSerialPortDataListener.java
│ │ ├── stick
│ │ ├── AbsStickPackageHelper.java
│ │ ├── BaseStickPackageHelper.java
│ │ ├── StaticLenStickPackageHelper.java
│ │ ├── TimeoutStickPackageHelper.java
│ │ ├── CompositeStickPackageHelper.java
│ │ ├── VariableLenStickPackageHelper.java
│ │ ├── StickyPacketHelperFactory.java
│ │ └── SpecifiedStickPackageHelper.java
│ │ ├── Device.java
│ │ ├── SerialPort.java
│ │ ├── Driver.java
│ │ ├── SerialPortFinder.java
│ │ ├── thread
│ │ └── SerialPortReadThread.java
│ │ ├── SerialConfig.java
│ │ ├── utils
│ │ └── SerialPortLogUtil.java
│ │ ├── SerialPortManager.java
│ │ └── example
│ │ └── MultiSerialPortExample.java
├── proguard-rules.pro
├── consumer-rules.pro
└── build.gradle
├── img
├── qq2.jpg
├── pc_ck.jpg
├── QRCode_336.png
├── device-img.png
├── sample_picture.png
├── multiple_images.png
├── test_erformance1.png
├── test_erformance2.png
└── test_erformance3.png
├── app
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ ├── themes.xml
│ │ │ └── string_arrays.xml
│ │ ├── mipmap-xxhdpi
│ │ │ ├── bg_bjt.jpeg
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── color
│ │ │ ├── selector_log_text.xml
│ │ │ └── selector_spinner_text.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── layout
│ │ │ ├── include_fragment_container.xml
│ │ │ ├── spinner_item.xml
│ │ │ ├── spinner_default_item.xml
│ │ │ ├── item_device.xml
│ │ │ ├── activity_select_serial_port.xml
│ │ │ ├── item_log.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── fragment_log.xml
│ │ │ └── activity_main_java.xml
│ │ ├── values-night
│ │ │ └── themes.xml
│ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ └── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ └── com
│ │ │ └── cl
│ │ │ └── myapplication
│ │ │ ├── constant
│ │ │ └── PreferenceKeys.java
│ │ │ ├── message
│ │ │ ├── IMessage.java
│ │ │ ├── ConversionNoticeEvent.java
│ │ │ ├── SendMessage.java
│ │ │ ├── RecvMessage.java
│ │ │ └── LogManager.java
│ │ │ ├── util
│ │ │ ├── TimeUtil.java
│ │ │ ├── ListViewHolder.java
│ │ │ ├── PrefHelper.java
│ │ │ └── ByteUtil.java
│ │ │ ├── App.java
│ │ │ ├── adapter
│ │ │ ├── SpAdapter.java
│ │ │ └── DeviceAdapter.java
│ │ │ ├── SelectSerialPortActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── fragment
│ │ │ └── LogFragment.java
│ │ │ └── SingleSerialPortActivity.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── .github
└── ISSUE_TEMPLATE
│ └── 提交bug.md
├── settings.gradle
├── gradle.properties
├── gradlew.bat
├── gradlew
├── README4.1.1.md
└── README.md
/serial_lib/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/img/qq2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/qq2.jpg
--------------------------------------------------------------------------------
/img/pc_ck.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/pc_ck.jpg
--------------------------------------------------------------------------------
/img/QRCode_336.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/QRCode_336.png
--------------------------------------------------------------------------------
/img/device-img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/device-img.png
--------------------------------------------------------------------------------
/img/sample_picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/sample_picture.png
--------------------------------------------------------------------------------
/img/multiple_images.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/multiple_images.png
--------------------------------------------------------------------------------
/img/test_erformance1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/test_erformance1.png
--------------------------------------------------------------------------------
/img/test_erformance2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/test_erformance2.png
--------------------------------------------------------------------------------
/img/test_erformance3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/img/test_erformance3.png
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/bg_bjt.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-xxhdpi/bg_bjt.jpeg
--------------------------------------------------------------------------------
/serial_lib/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SerialPortLibrary
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cl-6666/serialPort/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/color/selector_log_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/serial_lib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Sep 28 20:08:42 CST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/serial_lib/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.4.1)
2 |
3 | add_library(SerialPort SHARED
4 | SerialPort.c)
5 |
6 | # Include libraries needed for libserial_port lib
7 | target_link_libraries(SerialPort
8 | android
9 | log)
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/app/src/main/res/color/selector_spinner_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/enumerate/SerialStatus.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.enumerate;
2 |
3 | /**
4 | * name:cl
5 | * date:2023/2/20
6 | * desc:
7 | */
8 | public enum SerialStatus {
9 | NO_READ_WRITE_PERMISSION,
10 | OPEN_FAIL,
11 | SUCCESS_OPENED
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/include_fragment_container.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/constant/PreferenceKeys.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.constant;
2 |
3 |
4 | public class PreferenceKeys {
5 |
6 | /**
7 | * 串口设备
8 | */
9 | public static String SERIAL_PORT_DEVICES = "serial_port_devices";
10 | /**
11 | * 波特率
12 | */
13 | public static String BAUD_RATE = "baud_rate";
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/message/IMessage.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.message;
2 |
3 | /**
4 | * 日志消息数据接口
5 | */
6 |
7 | public interface IMessage {
8 | /**
9 | * 消息文本
10 | *
11 | * @return
12 | */
13 | String getMessage();
14 |
15 | /**
16 | * 是否发送的消息
17 | *
18 | * @return
19 | */
20 | boolean isToSend();
21 | }
22 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/enumerate/SerialPortEnum.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.enumerate;
2 |
3 | /**
4 | * name:cl
5 | * date:2023/2/20
6 | * desc:串口枚举类型
7 | */
8 | public enum SerialPortEnum {
9 | //串口1
10 | SERIAL_ONE,
11 | //串口2
12 | SERIAL_TWO,
13 | //串口3
14 | SERIAL_THREE,
15 | //串口4
16 | SERIAL_FOUR,
17 | //串口5
18 | SERIAL_FIVE,
19 | //串口6
20 | SERIAL_SIX
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/listener/OnOpenSerialPortListener.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.listener;
2 |
3 | import com.cl.serialportlibrary.enumerate.SerialPortEnum;
4 | import com.cl.serialportlibrary.enumerate.SerialStatus;
5 |
6 | import java.io.File;
7 |
8 | /**
9 | * 打开串口监听
10 | */
11 | public interface OnOpenSerialPortListener {
12 |
13 | void openState(SerialPortEnum serialPortEnum, File device, SerialStatus status);
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/util/TimeUtil.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.util;
2 |
3 | import java.text.SimpleDateFormat;
4 | import java.util.Date;
5 |
6 |
7 | public class TimeUtil {
8 |
9 | public static final SimpleDateFormat DEFAULT_FORMAT =
10 | new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
11 |
12 | public static String currentTime() {
13 | Date date = new Date();
14 | return DEFAULT_FORMAT.format(date);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/提交bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 提交BUG
3 | about: Create a report to help us improve
4 | title: 建议
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## 【警告:请务必按照 issue 模板填写】
11 |
12 | ## 问题描述
13 |
14 | * 框架版本【必填】:XXX
15 |
16 | * 问题描述【必填】:XXX
17 |
18 | * 复现步骤【必填】:XXX
19 |
20 | * 是否必现【必填】:填是/否
21 |
22 | * 出现问题机型信息【必填】:请填写出现问题的品牌和机型
23 |
24 | * 出现问题的安卓版本【必填】:请填写出现问题的 Android 版本
25 |
26 | ## 其他
27 |
28 | * 提供报错堆栈(如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来)
29 |
30 | * 提供截图或视频(根据需要提供,此项不强制)
31 |
32 | * 提供解决方案(如果已经解决了的话,此项不强制)
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/message/ConversionNoticeEvent.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.message;
2 |
3 | /**
4 | * 项目:serialPort
5 | * 作者:Arry
6 | * 创建日期:2021/10/20
7 | * 描述:
8 | * 修订历史:
9 | */
10 | public class ConversionNoticeEvent {
11 |
12 | private String message;
13 |
14 | public ConversionNoticeEvent(String message) {
15 | this.message = message;
16 | }
17 |
18 | public String getMessage() {
19 | return message;
20 | }
21 |
22 | public void setMessage(String message) {
23 | this.message = message;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/listener/OnSerialPortDataListener.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.listener;
2 |
3 | import com.cl.serialportlibrary.enumerate.SerialPortEnum;
4 |
5 | /**
6 | * 串口消息监听
7 | */
8 | public interface OnSerialPortDataListener {
9 |
10 | /**
11 | * 数据接收
12 | *
13 | * @param bytes 接收到的数据
14 | */
15 | void onDataReceived(byte[] bytes, SerialPortEnum serialPortEnum);
16 |
17 | /**
18 | * 数据发送
19 | *
20 | * @param bytes 发送的数据
21 | */
22 | void onDataSent(byte[] bytes,SerialPortEnum serialPortEnum);
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 串口调试助手
4 | 串口设备:
5 | 波特率
6 | 打开串口
7 | 关闭串口
8 | 找不到串口设备
9 | 发送命令
10 | 输入命令
11 | 加载命令列表
12 | 数据位:
13 | 校验位:
14 | 停止位:
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/message/SendMessage.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.message;
2 |
3 |
4 | import com.cl.myapplication.util.TimeUtil;
5 |
6 | /**
7 | * 发送的日志
8 | */
9 |
10 | public class SendMessage implements IMessage {
11 |
12 | private String command;
13 | private String message;
14 |
15 | public SendMessage(String command) {
16 | this.command = command;
17 | this.message = TimeUtil.currentTime() + " 发送命令:" + command;
18 | }
19 |
20 | @Override
21 | public String getMessage() {
22 | return message;
23 | }
24 |
25 | @Override
26 | public boolean isToSend() {
27 | return true;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/stick/AbsStickPackageHelper.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.stick;
2 |
3 | import java.io.InputStream;
4 |
5 | /**
6 | * Accept the message, the helper of the sticky packet processing,
7 | * return the final data through the inputstream,
8 | * manually process the sticky packet, and the returned byte[] is the complete data we expect
9 | * Note: This method will be called repeatedly until a complete piece of data is parsed.
10 | * This method is synchronous, try not to do time-consuming operations, otherwise it will block reading data
11 | */
12 | public interface AbsStickPackageHelper {
13 | byte[] execute(InputStream is);
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/message/RecvMessage.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.message;
2 |
3 |
4 | import com.cl.myapplication.util.TimeUtil;
5 |
6 | /**
7 | * 收到的日志
8 | */
9 |
10 | public class RecvMessage implements IMessage {
11 |
12 | private String command;
13 | private String message;
14 |
15 | public RecvMessage(String command) {
16 | this.command = command;
17 | this.message = TimeUtil.currentTime() + " 收到命令:" + command;
18 | }
19 |
20 | @Override
21 | public String getMessage() {
22 | return message;
23 | }
24 |
25 | @Override
26 | public boolean isToSend() {
27 | return false;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/spinner_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/spinner_default_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_device.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/serial_lib/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\Android\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | maven { url 'https://jitpack.io' }
13 | }
14 | }
15 | dependencyResolutionManagement {
16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
17 | repositories {
18 | google()
19 | mavenCentral()
20 | maven { url 'https://jitpack.io' }
21 | }
22 | }
23 |
24 | rootProject.name = "serialPort"
25 | include ':serial_lib'
26 | include ':app'
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/serial_lib/consumer-rules.pro:
--------------------------------------------------------------------------------
1 | # Consumer proguard rules for serial_lib
2 |
3 | # Keep all public APIs
4 | -keep public class com.cl.serialportlibrary.** { *; }
5 |
6 | # Keep native methods
7 | -keepclasseswithmembernames class * {
8 | native ;
9 | }
10 |
11 | # Keep enums
12 | -keepclassmembers enum * {
13 | public static **[] values();
14 | public static ** valueOf(java.lang.String);
15 | }
16 |
17 | # Keep Serializable classes
18 | -keepclassmembers class * implements java.io.Serializable {
19 | static final long serialVersionUID;
20 | private static final java.io.ObjectStreamField[] serialPersistentFields;
21 | private void writeObject(java.io.ObjectOutputStream);
22 | private void readObject(java.io.ObjectInputStream);
23 | java.lang.Object writeReplace();
24 | java.lang.Object readResolve();
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/serial_lib/src/main/cpp/SerialPort.h:
--------------------------------------------------------------------------------
1 | /* DO NOT EDIT THIS FILE - it is machine generated */
2 | #include
3 | /* Header for class android_serialport_api_SerialPort */
4 |
5 | #ifndef _Included_qingwei_kong_serialportlibrary_SerialPort
6 | #define _Included_qingwei_kong_serialportlibrary_SerialPort
7 | #ifdef __cplusplus
8 | extern "C" {
9 | #endif
10 |
11 | /*
12 | * Class: android_serialport_api_SerialPort
13 | * Method: open
14 | * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
15 | */
16 | JNIEXPORT jobject JNICALL Java_com_cl_serialportlibrary_SerialPort_open
17 | (JNIEnv *, jclass, jstring, jint, jint,jint,jint,jint);
18 |
19 | /*
20 | * Class: android_serialport_api_SerialPort
21 | * Method: close
22 | * Signature: ()V
23 | */
24 | JNIEXPORT void JNICALL Java_com_cl_serialportlibrary_SerialPort_close
25 | (JNIEnv *, jobject);
26 |
27 | #ifdef __cplusplus
28 | }
29 | #endif
30 | #endif
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_select_serial_port.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
17 |
18 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.12.2"
3 | kotlin = "2.0.21"
4 | coreKtx = "1.10.1"
5 | junit = "4.13.2"
6 | junitVersion = "1.1.5"
7 | espressoCore = "3.5.1"
8 | appcompat = "1.6.1"
9 | material = "1.10.0"
10 |
11 | [libraries]
12 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
13 | junit = { group = "junit", name = "junit", version.ref = "junit" }
14 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
15 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
16 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
17 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
18 |
19 | [plugins]
20 | android-application = { id = "com.android.application", version.ref = "agp" }
21 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
22 |
23 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/Device.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary;
2 |
3 | import java.io.File;
4 | import java.io.Serializable;
5 |
6 |
7 | public class Device implements Serializable{
8 |
9 | private static final String TAG = Device.class.getSimpleName();
10 | private String name;
11 | private String root;
12 | private File file;
13 |
14 | public Device(String name, String root, File file) {
15 | this.name = name;
16 | this.root = root;
17 | this.file = file;
18 | }
19 |
20 | public String getName() {
21 | return name;
22 | }
23 |
24 | public void setName(String name) {
25 | this.name = name;
26 | }
27 |
28 | public String getRoot() {
29 | return root;
30 | }
31 |
32 | public void setRoot(String root) {
33 | this.root = root;
34 | }
35 |
36 | public File getFile() {
37 | return file;
38 | }
39 |
40 | public void setFile(File path) {
41 | this.file = file;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 |
12 | #3F51B5
13 | #303F9F
14 | #FF4081
15 | #e4e4e4
16 | #55a0a7cc
17 | #ffa0a7cc
18 |
19 | #e5e5e5
20 |
21 | #00000000
22 | #30bfbfbf
23 | #727272
24 | #B6B6B6
25 |
26 | #888888
27 | #20ffffff
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/values/string_arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | - 110
6 | - 300
7 | - 600
8 | - 1200
9 | - 2400
10 | - 4800
11 | - 9600
12 | - 14400
13 | - 19200
14 | - 38400
15 | - 56000
16 | - 57600
17 | - 115200
18 | - 128000
19 | - 256000
20 |
21 |
22 |
23 |
24 | - 1
25 | - 2
26 | - 3
27 | - 4
28 | - 5
29 | - 6
30 | - 7
31 | - 8
32 | - 9
33 | - 10
34 |
35 |
36 |
37 | - 0:设备默认
38 | - 1:快 20ms
39 | - 2:中 40ms
40 | - 3:慢 60ms
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/message/LogManager.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.message;
2 |
3 | import org.greenrobot.eventbus.EventBus;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | /**
9 | * log管理类
10 | */
11 |
12 | public class LogManager {
13 |
14 | public final List messages;
15 | private boolean mAutoEnd = true;
16 |
17 | public LogManager() {
18 | messages = new ArrayList<>();
19 | }
20 |
21 | private static class InstanceHolder {
22 |
23 | public static LogManager sManager = new LogManager();
24 | }
25 |
26 | public static LogManager instance() {
27 | return InstanceHolder.sManager;
28 | }
29 |
30 | public void add(IMessage message) {
31 | messages.add(message);
32 | }
33 |
34 | public void post(IMessage message) {
35 | EventBus.getDefault().post(message);
36 | }
37 |
38 | public void clear() {
39 | messages.clear();
40 | }
41 |
42 | public boolean isAutoEnd() {
43 | return mAutoEnd;
44 | }
45 |
46 | public void setAutoEnd(boolean autoEnd) {
47 | mAutoEnd = autoEnd;
48 | }
49 |
50 | public void changAutoEnd() {
51 | mAutoEnd = !mAutoEnd;
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/App.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication;
2 |
3 | import android.app.Application;
4 |
5 | import com.hjq.toast.ToastUtils;
6 | import com.cl.serialportlibrary.SimpleSerialPortManager;
7 |
8 | /**
9 | * 项目:serialPort
10 | * 作者:Arry
11 | * 创建日期:2021/10/20
12 | * 描述:
13 | * 修订历史:
14 | */
15 | public class App extends Application {
16 |
17 | @Override
18 | public void onCreate() {
19 | super.onCreate();
20 | // 初始化 Toast 框架
21 | ToastUtils.init(this);
22 |
23 | // 使用新的SimpleSerialPortManager进行全局初始化
24 | new SimpleSerialPortManager.QuickConfig()
25 | .setIntervalSleep(50) // 读取间隔50ms
26 | .setEnableLog(true) // 启用日志
27 | .setLogTag("SerialPortApp") // 设置日志标签
28 | .setDatabits(8) // 数据位8
29 | .setParity(0) // 无校验
30 | .setStopbits(1) // 停止位1
31 | .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING) // 不处理黏包
32 | .setMaxPacketSize(1024) // 最大包大小1KB
33 | .apply(this);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/stick/BaseStickPackageHelper.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.stick;
2 |
3 | import android.os.SystemClock;
4 |
5 | import com.cl.serialportlibrary.utils.SerialPortLogUtil;
6 |
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 |
10 | /**
11 | * The simplest thing to do is not to deal with sticky packets,
12 | * read directly and return as much as InputStream.available() reads
13 | */
14 | public class BaseStickPackageHelper implements AbsStickPackageHelper {
15 | public BaseStickPackageHelper() {
16 | }
17 |
18 | @Override
19 | public byte[] execute(InputStream is) {
20 | try {
21 | int available = is.available();
22 | if (available > 0) {
23 | byte[] buffer = new byte[available];
24 | int size = is.read(buffer);
25 | if (size > 0) {
26 | return buffer;
27 | }
28 | SerialPortLogUtil.d("BaseStickPackageHelper", "原始数据长度: " + buffer.length);
29 | } else {
30 | SystemClock.sleep(50); // 默认50ms间隔
31 | }
32 |
33 | } catch (IOException e) {
34 | e.printStackTrace();
35 | }
36 | return null;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_log.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
23 |
24 |
25 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/stick/StaticLenStickPackageHelper.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.stick;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 |
6 | /**
7 | * Fixed-length adhesive package treatment
8 | * Example: The protocol stipulates that the length of each packet is 16
9 | */
10 | public class StaticLenStickPackageHelper implements AbsStickPackageHelper {
11 | private int stackLen = 16;
12 |
13 | public StaticLenStickPackageHelper(int stackLen) {
14 | this.stackLen = stackLen;
15 | }
16 |
17 | /**
18 | * 默认构造函数,使用16字节固定长度
19 | */
20 | public StaticLenStickPackageHelper() {
21 | this.stackLen = 16;
22 | }
23 |
24 | @Override
25 | public byte[] execute(InputStream is) {
26 | int count = 0;
27 | int len = -1;
28 | byte temp;
29 | byte[] result = new byte[stackLen];
30 | try {
31 | while (count < stackLen && (len = is.read()) != -1) {
32 | temp = (byte) len;
33 | result[count] = temp;
34 | count++;
35 | }
36 | if (len == -1) {
37 | return null;
38 | }
39 | } catch (IOException e) {
40 | e.printStackTrace();
41 | return null;
42 | }
43 | return result;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/SerialPort.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary;
2 |
3 | import java.io.File;
4 | import java.io.FileDescriptor;
5 | import java.io.IOException;
6 |
7 | public class SerialPort {
8 |
9 | static {
10 | System.loadLibrary("SerialPort");
11 | }
12 |
13 | private static final String TAG = SerialPort.class.getSimpleName();
14 |
15 | /**
16 | * 文件设置最高权限 777 可读 可写 可执行
17 | *
18 | * @param file 文件
19 | * @return 权限修改是否成功
20 | */
21 | boolean chmod777(File file) {
22 | if (null == file || !file.exists()) {
23 | // 文件不存在
24 | return false;
25 | }
26 | try {
27 | // 获取ROOT权限
28 | Process su = Runtime.getRuntime().exec("/system/bin/su");
29 | // 修改文件属性为 [可读 可写 可执行]
30 | String cmd = "chmod 777 " + file.getAbsolutePath() + "\n" + "exit\n";
31 | su.getOutputStream().write(cmd.getBytes());
32 | if (0 == su.waitFor() && file.canRead() && file.canWrite() && file.canExecute()) {
33 | return true;
34 | }
35 | } catch (IOException | InterruptedException e) {
36 | // 没有ROOT权限
37 | e.printStackTrace();
38 | }
39 | return false;
40 | }
41 |
42 | // 打开串口
43 | protected native FileDescriptor open(String path, int baudrate, int flags, int databits, int stopbits, int parity);
44 |
45 | // 关闭串口
46 | protected native void close();
47 | }
48 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/Driver.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary;
2 |
3 | import java.io.File;
4 | import java.util.ArrayList;
5 |
6 | /**
7 | * 串口驱动信息类
8 | * 简化版本,仅用于SerialPortFinder
9 | */
10 | public class Driver {
11 |
12 | private String mDriverName;
13 | private String mDeviceRoot;
14 |
15 | public Driver(String name, String root) {
16 | mDriverName = name;
17 | mDeviceRoot = root;
18 | }
19 |
20 | /**
21 | * 获取驱动名称
22 | */
23 | public String getName() {
24 | return mDriverName;
25 | }
26 |
27 | /**
28 | * 获取设备根路径
29 | */
30 | public String getRoot() {
31 | return mDeviceRoot;
32 | }
33 |
34 | /**
35 | * 获取该驱动下的所有设备
36 | */
37 | public ArrayList getDevices() {
38 | ArrayList devices = new ArrayList<>();
39 | File dev = new File("/dev");
40 | File[] files = dev.listFiles();
41 |
42 | if (files != null) {
43 | for (File file : files) {
44 | if (file.getAbsolutePath().startsWith(mDeviceRoot)) {
45 | devices.add(file);
46 | }
47 | }
48 | }
49 |
50 | return devices;
51 | }
52 |
53 | @Override
54 | public String toString() {
55 | return "Driver{" +
56 | "name='" + mDriverName + '\'' +
57 | ", root='" + mDeviceRoot + '\'' +
58 | '}';
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
19 |
20 |
28 |
29 |
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | }
5 |
6 | android {
7 | namespace 'com.cl.myapplication'
8 | compileSdk 36
9 |
10 | defaultConfig {
11 | applicationId "com.cl.myapplication"
12 | minSdk 24
13 | targetSdk 36
14 | versionCode 401
15 | versionName "4.0.1"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_11
28 | targetCompatibility JavaVersion.VERSION_11
29 | }
30 | kotlinOptions {
31 | jvmTarget = '11'
32 | }
33 | buildFeatures{
34 | dataBinding=true
35 | }
36 | }
37 |
38 | dependencies {
39 |
40 | implementation libs.androidx.core.ktx
41 | implementation libs.androidx.appcompat
42 | implementation libs.material
43 | implementation 'androidx.cardview:cardview:1.0.0'
44 | testImplementation libs.junit
45 | androidTestImplementation libs.androidx.junit
46 | androidTestImplementation libs.androidx.espresso.core
47 | implementation 'org.greenrobot:eventbus:3.2.0'
48 | implementation 'com.github.getActivity:ToastUtils:9.6'
49 | implementation 'com.github.JessYanCoding:AndroidAutoSize:v1.2.1'
50 | implementation 'com.google.code.gson:gson:2.8.6'
51 | implementation project(path: ':serial_lib')
52 |
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/adapter/SpAdapter.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.adapter;
2 |
3 | import android.content.Context;
4 | import android.view.LayoutInflater;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 | import android.widget.BaseAdapter;
8 |
9 | import androidx.databinding.DataBindingUtil;
10 |
11 | import com.cl.myapplication.R;
12 | import com.cl.myapplication.databinding.ItemDeviceBinding;
13 |
14 |
15 | public class SpAdapter extends BaseAdapter {
16 |
17 | String[] datas;
18 | Context mContext;
19 |
20 | public SpAdapter(Context context) {
21 | this.mContext = context;
22 | }
23 |
24 | public void setDatas(String[] datas) {
25 | this.datas = datas;
26 | notifyDataSetChanged();
27 | }
28 |
29 | @Override
30 | public int getCount() {
31 | return datas == null ? 0 : datas.length;
32 | }
33 |
34 | @Override
35 | public Object getItem(int position) {
36 | return datas == null ? null : datas[position];
37 | }
38 |
39 | @Override
40 | public long getItemId(int position) {
41 | return position;
42 | }
43 |
44 | @Override
45 | public View getView(int position, View convertView, ViewGroup parent) {
46 | ItemDeviceBinding binding;
47 | if (convertView == null) {
48 | binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.item_device, parent, false);
49 | convertView = binding.getRoot();
50 | convertView.setTag(binding);
51 | } else {
52 | binding = (ItemDeviceBinding) convertView.getTag();
53 | }
54 |
55 | binding.tvDevice.setText(datas[position]);
56 |
57 | return convertView;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/SelectSerialPortActivity.kt:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import android.widget.AdapterView
7 | import android.widget.AdapterView.OnItemClickListener
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.databinding.DataBindingUtil
10 | import com.cl.myapplication.adapter.DeviceAdapter
11 | import com.cl.myapplication.databinding.ActivitySelectSerialPortBinding
12 | import com.cl.serialportlibrary.SerialPortFinder
13 |
14 | class SelectSerialPortActivity : AppCompatActivity(), OnItemClickListener {
15 |
16 |
17 | private var mDeviceAdapter: DeviceAdapter? = null
18 | val DEVICE = "device"
19 | private lateinit var binding: ActivitySelectSerialPortBinding
20 |
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | binding = DataBindingUtil.setContentView(this, R.layout.activity_select_serial_port)
25 |
26 | val serialPortFinder = SerialPortFinder()
27 | val devices = serialPortFinder.devices
28 | if (binding.lvDevices != null) {
29 | binding.lvDevices.emptyView = binding.tvEmpty
30 | mDeviceAdapter = DeviceAdapter(applicationContext, devices)
31 | binding.lvDevices.adapter = mDeviceAdapter
32 | binding.lvDevices.onItemClickListener = this
33 | }
34 |
35 | }
36 |
37 | override fun onItemClick(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
38 | val device = mDeviceAdapter!!.getItem(position)
39 | val intent = Intent(this, MainActivity::class.java)
40 | intent.putExtra(DEVICE, device)
41 | startActivity(intent)
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/util/ListViewHolder.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.util;
2 |
3 | import android.util.SparseArray;
4 | import android.view.LayoutInflater;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 | import android.widget.ImageView;
8 | import android.widget.TextView;
9 |
10 | public class ListViewHolder {
11 |
12 | private SparseArray mViewArray;
13 | public View itemView;
14 | public int position;
15 |
16 | public ListViewHolder(View itemView) {
17 | this.itemView = itemView;
18 | mViewArray = new SparseArray<>();
19 | this.itemView.setTag(this);
20 | }
21 |
22 | public ListViewHolder(int layoutId, ViewGroup parent) {
23 | View view = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
24 | this.itemView = view;
25 | mViewArray = new SparseArray<>();
26 | this.itemView.setTag(this);
27 | }
28 |
29 | public View getItemView() {
30 | return itemView;
31 | }
32 |
33 | public void bindPosition(int position) {
34 | this.position = position;
35 | }
36 |
37 | public int getPosition() {
38 | return position;
39 | }
40 |
41 | public V getView(int resId) {
42 | View view = mViewArray.get(resId);
43 | if (view == null) {
44 | view = itemView.findViewById(resId);
45 | mViewArray.put(resId, view);
46 | }
47 | return (V) view;
48 | }
49 |
50 | public void setText(int resId, CharSequence text) {
51 | TextView textView = getView(resId);
52 | textView.setText(text);
53 | }
54 |
55 | public TextView getText(int id) {
56 | return getView(id);
57 | }
58 |
59 | public ImageView getImage(int id) {
60 | return getView(id);
61 | }
62 | }
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_log.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
19 |
20 |
21 |
28 |
29 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/stick/TimeoutStickPackageHelper.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.stick;
2 |
3 | import android.os.SystemClock;
4 |
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | /**
11 | * 基于超时的黏包处理器
12 | * 在指定时间内没有新数据到达时,认为是一个完整的数据包
13 | * Author: cl
14 | * Date: 2023/10/26
15 | */
16 | public class TimeoutStickPackageHelper implements AbsStickPackageHelper {
17 |
18 | private final int timeout; // 超时时间(毫秒)
19 | private final List buffer = new ArrayList<>();
20 |
21 | public TimeoutStickPackageHelper(int timeout) {
22 | this.timeout = timeout;
23 | }
24 |
25 | @Override
26 | public byte[] execute(InputStream is) {
27 | buffer.clear();
28 | long lastDataTime = System.currentTimeMillis();
29 |
30 | try {
31 | while (true) {
32 | int available = is.available();
33 | if (available > 0) {
34 | // 有数据可读
35 | byte[] tempBuffer = new byte[available];
36 | int readBytes = is.read(tempBuffer);
37 | if (readBytes > 0) {
38 | for (int i = 0; i < readBytes; i++) {
39 | buffer.add(tempBuffer[i]);
40 | }
41 | lastDataTime = System.currentTimeMillis();
42 | }
43 | } else {
44 | // 没有数据,检查超时
45 | if (!buffer.isEmpty() && (System.currentTimeMillis() - lastDataTime) >= timeout) {
46 | // 超时且缓冲区有数据,返回数据包
47 | byte[] result = new byte[buffer.size()];
48 | for (int i = 0; i < buffer.size(); i++) {
49 | result[i] = buffer.get(i);
50 | }
51 | return result;
52 | }
53 |
54 | // 短暂休眠,避免CPU过度占用
55 | SystemClock.sleep(10);
56 | }
57 | }
58 | } catch (IOException e) {
59 | e.printStackTrace();
60 | return null;
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
19 |
22 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/adapter/DeviceAdapter.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.adapter;
2 |
3 | import android.content.Context;
4 | import android.view.LayoutInflater;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 | import android.widget.BaseAdapter;
8 |
9 | import androidx.databinding.DataBindingUtil;
10 |
11 | import com.cl.myapplication.R;
12 | import com.cl.myapplication.databinding.ItemDeviceBinding;
13 | import com.cl.serialportlibrary.Device;
14 |
15 | import java.io.File;
16 | import java.util.ArrayList;
17 |
18 | /**
19 | * 串口列表适配器
20 | */
21 | public class DeviceAdapter extends BaseAdapter {
22 |
23 | private LayoutInflater mInflater;
24 | private ArrayList devices;
25 |
26 | public DeviceAdapter(Context context, ArrayList devices) {
27 | this.mInflater = LayoutInflater.from(context);
28 | this.devices = devices;
29 | }
30 |
31 | @Override
32 | public int getCount() {
33 | return devices.size();
34 | }
35 |
36 | @Override
37 | public Device getItem(int position) {
38 | return devices.get(position);
39 | }
40 |
41 | @Override
42 | public long getItemId(int position) {
43 | return position;
44 | }
45 |
46 | @Override
47 | public View getView(int position, View convertView, ViewGroup parent) {
48 | ItemDeviceBinding binding;
49 | if (null == convertView) {
50 | binding = DataBindingUtil.inflate(mInflater, R.layout.item_device, parent, false);
51 | convertView = binding.getRoot();
52 | convertView.setTag(binding);
53 | } else {
54 | binding = (ItemDeviceBinding) convertView.getTag();
55 | }
56 |
57 | String deviceName = devices.get(position).getName();
58 | String driverName = devices.get(position).getRoot();
59 | File file = devices.get(position).getFile();
60 | boolean canRead = file.canRead();
61 | boolean canWrite = file.canWrite();
62 | boolean canExecute = file.canExecute();
63 | String path = file.getAbsolutePath();
64 |
65 | StringBuffer permission = new StringBuffer();
66 | permission.append("\t权限[");
67 | permission.append(canRead ? " 可读 " : " 不可读 ");
68 | permission.append(canWrite ? " 可写 " : " 不可写 ");
69 | permission.append(canExecute ? " 可执行 " : " 不可执行 ");
70 | permission.append("]");
71 |
72 | binding.tvDevice.setText(String.format("%s [%s] (%s) %s", deviceName, driverName, path, permission));
73 |
74 | return convertView;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/serial_lib/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'maven-publish'
4 | }
5 |
6 | android {
7 | namespace "com.cl.serialportlibrary"
8 | compileSdk 34
9 |
10 | defaultConfig {
11 | minSdk 21
12 | targetSdk 34
13 | versionCode 506
14 | versionName "5.0.6"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | compileOptions {
19 | sourceCompatibility JavaVersion.VERSION_17
20 | targetCompatibility JavaVersion.VERSION_17
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 |
30 | externalNativeBuild {
31 | cmake {
32 | path "src/main/cpp/CMakeLists.txt"
33 | }
34 | }
35 | }
36 |
37 | dependencies {
38 | implementation fileTree(dir: "libs", include: ["*.jar"])
39 | implementation 'androidx.appcompat:appcompat:1.6.1'
40 | testImplementation 'junit:junit:4.13.2'
41 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
42 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
43 | }
44 |
45 | afterEvaluate {
46 | publishing {
47 | publications {
48 | release(MavenPublication) {
49 | groupId = 'com.github.cl-6666'
50 | artifactId = 'serialPort'
51 | version = '5.0.6'
52 |
53 | artifact bundleReleaseAar
54 |
55 | pom {
56 | name = 'SerialPort Library'
57 | description = 'Android Serial Port Library with JNI support'
58 | url = 'https://github.com/cl-6666/serialPort'
59 |
60 | licenses {
61 | license {
62 | name = 'The Apache License, Version 2.0'
63 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
64 | }
65 | }
66 |
67 | developers {
68 | developer {
69 | id = 'cl-6666'
70 | name = 'cl-6666'
71 | }
72 | }
73 |
74 | scm {
75 | connection = 'scm:git:git://github.com/cl-6666/serialPort.git'
76 | developerConnection = 'scm:git:ssh://github.com:cl-6666/serialPort.git'
77 | url = 'https://github.com/cl-6666/serialPort/tree/master'
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/util/PrefHelper.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.util;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.preference.PreferenceManager;
6 |
7 |
8 | public class PrefHelper {
9 |
10 | private static PrefHelper sInstance;
11 |
12 | private SharedPreferences mPreferences;
13 |
14 | public static void initDefault(Context context) {
15 | sInstance = new PrefHelper(PreferenceManager.getDefaultSharedPreferences(context));
16 | }
17 |
18 | public static PrefHelper getDefault() {
19 | return sInstance;
20 | }
21 |
22 | public static PrefHelper get(Context context, String name) {
23 | return new PrefHelper(context, name);
24 | }
25 |
26 | private PrefHelper(SharedPreferences preferences) {
27 | mPreferences = preferences;
28 | }
29 |
30 | private PrefHelper(Context context, String name) {
31 | mPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE);
32 | }
33 |
34 | public SharedPreferences.Editor edit() {
35 | return mPreferences.edit();
36 | }
37 |
38 | public SharedPreferences.Editor putInt(String key, int value) {
39 | return edit().putInt(key, value);
40 | }
41 |
42 | public void saveInt(String key, int value) {
43 | putInt(key, value).apply();
44 | }
45 |
46 | public int getInt(String key, int defValue) {
47 | return mPreferences.getInt(key, defValue);
48 | }
49 |
50 | public SharedPreferences.Editor putFloat(String key, float value) {
51 | return edit().putFloat(key, value);
52 | }
53 |
54 | public void saveFloat(String key, float value) {
55 | putFloat(key, value).apply();
56 | }
57 |
58 | public float getFloat(String key, float defValue) {
59 | return mPreferences.getFloat(key, defValue);
60 | }
61 |
62 | public SharedPreferences.Editor putBoolean(String key, boolean value) {
63 | return edit().putBoolean(key, value);
64 | }
65 |
66 | public void saveBoolean(String key, boolean value) {
67 | putBoolean(key, value).apply();
68 | }
69 |
70 | public boolean getBoolean(String key, boolean defValue) {
71 | return mPreferences.getBoolean(key, defValue);
72 | }
73 |
74 | public SharedPreferences.Editor putLong(String key, long value) {
75 | return edit().putLong(key, value);
76 | }
77 |
78 | public void saveLong(String key, long value) {
79 | putLong(key, value).apply();
80 | }
81 |
82 | public long getLong(String key, long defValue) {
83 | return mPreferences.getLong(key, defValue);
84 | }
85 |
86 | public SharedPreferences.Editor putString(String key, String value) {
87 | return edit().putString(key, value);
88 | }
89 |
90 | public void saveString(String key, String value) {
91 | putString(key, value).apply();
92 | }
93 |
94 | public String getString(String key, String defValue) {
95 | return mPreferences.getString(key, defValue);
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication
2 |
3 | import android.os.Bundle
4 | import android.text.TextUtils
5 | import android.util.Log
6 | import android.view.View
7 | import android.widget.Toast
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.databinding.DataBindingUtil
10 | import com.cl.myapplication.databinding.ActivityMainBinding
11 | import com.cl.serialportlibrary.Device
12 | import com.cl.serialportlibrary.SimpleSerialPortManager
13 |
14 | class MainActivity : AppCompatActivity(){
15 |
16 | private val TAG = MainActivity::class.java.simpleName
17 | val DEVICE = "device"
18 | private var isSerialPortOpened = false
19 | private var mToast: Toast? = null
20 | private lateinit var binding: ActivityMainBinding
21 |
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
26 | val device = intent.getSerializableExtra(DEVICE) as Device?
27 | Log.i(TAG, "onCreate: device = $device")
28 | if (null == device) {
29 | finish()
30 | return
31 | }
32 |
33 | // 使用SimpleSerialPortManager打开串口
34 | val devicePath = device.name
35 | val baudRate = device.root.toInt()
36 | SimpleSerialPortManager.getInstance().openSerialPort(devicePath, baudRate) { data ->
37 | runOnUiThread {
38 | val receivedData = String(data)
39 | Log.i(TAG, "接收到数据: $receivedData")
40 | // binding.tvReceiveContent.text = receivedData
41 | }
42 | }
43 | isSerialPortOpened = true
44 | }
45 |
46 |
47 | fun onSend(view: View) {
48 | val editTextSendContent = binding.etSendContent.text.toString()
49 | if (TextUtils.isEmpty(editTextSendContent)) {
50 | Log.i(TAG, "onSend: 发送内容为 null")
51 | return
52 | }
53 | val sendContentBytes = editTextSendContent.toByteArray()
54 | val sendBytes = SimpleSerialPortManager.getInstance().sendData(sendContentBytes)
55 | Log.i(TAG, "onSend: sendBytes = $sendBytes")
56 | showToast(if (sendBytes) "发送成功" else "发送失败")
57 | }
58 |
59 |
60 | fun onDestroy(view: View) {
61 | if (isSerialPortOpened) {
62 | SimpleSerialPortManager.getInstance().closeSerialPort()
63 | isSerialPortOpened = false
64 | }
65 | finish()
66 | }
67 |
68 | override fun onDestroy() {
69 | super.onDestroy()
70 | if (isSerialPortOpened) {
71 | SimpleSerialPortManager.getInstance().closeSerialPort()
72 | }
73 | }
74 |
75 |
76 | /**
77 | * Toast
78 | *
79 | * @param content content
80 | */
81 | private fun showToast(content: String) {
82 | if (null == mToast) {
83 | mToast = Toast.makeText(applicationContext, null, Toast.LENGTH_SHORT)
84 | }
85 | mToast?.setText(content)
86 | mToast?.show()
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/stick/CompositeStickPackageHelper.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.stick;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | /**
10 | * 组合式黏包处理器
11 | * 首先尝试使用主要处理器,如果失败则使用备用处理器
12 | * Author: cl
13 | * Date: 2023/10/26
14 | */
15 | public class CompositeStickPackageHelper implements AbsStickPackageHelper {
16 |
17 | private final AbsStickPackageHelper primaryHelper;
18 | private final AbsStickPackageHelper fallbackHelper;
19 | private final List buffer = new ArrayList<>();
20 |
21 | public CompositeStickPackageHelper(AbsStickPackageHelper primaryHelper, AbsStickPackageHelper fallbackHelper) {
22 | this.primaryHelper = primaryHelper;
23 | this.fallbackHelper = fallbackHelper;
24 | }
25 |
26 | @Override
27 | public byte[] execute(InputStream is) {
28 | // 先尝试读取一些数据到缓冲区
29 | try {
30 | int available = is.available();
31 | if (available > 0) {
32 | byte[] tempBuffer = new byte[available];
33 | int readBytes = is.read(tempBuffer);
34 | if (readBytes > 0) {
35 | for (int i = 0; i < readBytes; i++) {
36 | buffer.add(tempBuffer[i]);
37 | }
38 | }
39 | }
40 |
41 | if (buffer.isEmpty()) {
42 | return null;
43 | }
44 |
45 | // 将缓冲区数据转换为字节数组
46 | byte[] bufferData = new byte[buffer.size()];
47 | for (int i = 0; i < buffer.size(); i++) {
48 | bufferData[i] = buffer.get(i);
49 | }
50 |
51 | // 尝试使用主要处理器
52 | ByteArrayInputStream primaryStream = new ByteArrayInputStream(bufferData);
53 | byte[] primaryResult = primaryHelper.execute(primaryStream);
54 |
55 | if (primaryResult != null && primaryResult.length > 0) {
56 | // 主要处理器成功,清除已处理的数据
57 | if (primaryResult.length <= buffer.size()) {
58 | for (int i = 0; i < primaryResult.length; i++) {
59 | buffer.remove(0);
60 | }
61 | }
62 | return primaryResult;
63 | }
64 |
65 | // 主要处理器失败,尝试备用处理器
66 | ByteArrayInputStream fallbackStream = new ByteArrayInputStream(bufferData);
67 | byte[] fallbackResult = fallbackHelper.execute(fallbackStream);
68 |
69 | if (fallbackResult != null && fallbackResult.length > 0) {
70 | // 备用处理器成功,清除已处理的数据
71 | if (fallbackResult.length <= buffer.size()) {
72 | for (int i = 0; i < fallbackResult.length; i++) {
73 | buffer.remove(0);
74 | }
75 | }
76 | return fallbackResult;
77 | }
78 |
79 | // 两个处理器都失败,保持缓冲区数据等待更多数据
80 | return null;
81 |
82 | } catch (IOException e) {
83 | e.printStackTrace();
84 | return null;
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/SerialPortFinder.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary;
2 |
3 |
4 | import com.cl.serialportlibrary.utils.SerialPortLogUtil;
5 |
6 | import java.io.File;
7 | import java.io.FileReader;
8 | import java.io.IOException;
9 | import java.io.LineNumberReader;
10 | import java.util.ArrayList;
11 | import java.util.Iterator;
12 | import java.util.Vector;
13 |
14 | public class SerialPortFinder {
15 |
16 | private static final String TAG = SerialPortFinder.class.getSimpleName();
17 | private static final String DRIVERS_PATH = "/proc/tty/drivers";
18 | private static final String SERIAL_FIELD = "serial";
19 |
20 | public SerialPortFinder() {
21 | File file = new File(DRIVERS_PATH);
22 | boolean b = file.canRead();
23 | SerialPortLogUtil.i(TAG, "SerialPortFinder: file.canRead() = " + b);
24 | }
25 |
26 | /**
27 | * 获取 Drivers
28 | *
29 | * @return Drivers
30 | * @throws IOException IOException
31 | */
32 | private ArrayList getDrivers() throws IOException {
33 | ArrayList drivers = new ArrayList<>();
34 | LineNumberReader lineNumberReader = new LineNumberReader(new FileReader(DRIVERS_PATH));
35 | String readLine;
36 | while ((readLine = lineNumberReader.readLine()) != null) {
37 | String driverName = readLine.substring(0, 0x15).trim();
38 | String[] fields = readLine.split(" +");
39 | if ((fields.length >= 5) && (fields[fields.length - 1].equals(SERIAL_FIELD))) {
40 | SerialPortLogUtil.d(TAG, "Found new driver " + driverName + " on " + fields[fields.length - 4]);
41 | drivers.add(new Driver(driverName, fields[fields.length - 4]));
42 | }
43 | }
44 | return drivers;
45 | }
46 |
47 | /**
48 | * 获取串口
49 | *
50 | * @return 串口
51 | */
52 | public ArrayList getDevices() {
53 | ArrayList devices = new ArrayList<>();
54 | try {
55 | ArrayList drivers = getDrivers();
56 | for (Driver driver : drivers) {
57 | String driverName = driver.getName();
58 | ArrayList driverDevices = driver.getDevices();
59 | for (File file : driverDevices) {
60 | String devicesName = file.getName();
61 | devices.add(new Device(devicesName, driverName, file));
62 | }
63 | }
64 | } catch (IOException e) {
65 | e.printStackTrace();
66 | }
67 | return devices;
68 | }
69 |
70 |
71 | public String[] getAllDevicesPath() {
72 | Vector devices = new Vector();
73 | // Parse each driver
74 | Iterator itdriv;
75 | try {
76 | itdriv = getDrivers().iterator();
77 | while (itdriv.hasNext()) {
78 | Driver driver = itdriv.next();
79 | Iterator itdev = driver.getDevices().iterator();
80 | while (itdev.hasNext()) {
81 | String device = itdev.next().getAbsolutePath();
82 | devices.add(device);
83 | }
84 | }
85 | } catch (IOException e) {
86 | e.printStackTrace();
87 | }
88 | return devices.toArray(new String[devices.size()]);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/thread/SerialPortReadThread.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.thread;
2 |
3 | import com.cl.serialportlibrary.utils.SerialPortLogUtil;
4 | import com.cl.serialportlibrary.enumerate.SerialPortEnum;
5 | import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
6 | import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
7 |
8 | import java.io.IOException;
9 | import java.io.InputStream;
10 | import java.util.List;
11 |
12 |
13 | /**
14 | * 串口消息读取线程
15 | */
16 | public abstract class SerialPortReadThread extends Thread {
17 |
18 | public abstract void onDataReceived(byte[] bytes);
19 |
20 | private InputStream mInputStream;
21 | private SerialPortEnum mSerialPortEnum;
22 | private List mStickPackageHelpers;
23 |
24 | public SerialPortReadThread(InputStream inputStream, SerialPortEnum mSerialPortEnum, List stickPackageHelpers) {
25 | mInputStream = inputStream;
26 | this.mSerialPortEnum = mSerialPortEnum;
27 | this.mStickPackageHelpers = stickPackageHelpers;
28 |
29 | // 如果没有提供粘包处理器,使用默认的
30 | if (this.mStickPackageHelpers == null || this.mStickPackageHelpers.isEmpty()) {
31 | this.mStickPackageHelpers = new java.util.ArrayList<>();
32 | this.mStickPackageHelpers.add(new BaseStickPackageHelper());
33 | }
34 | }
35 |
36 | @Override
37 | public void run() {
38 | if (mInputStream == null) return;
39 | while (!Thread.currentThread().isInterrupted()) {
40 | try {
41 | if (mStickPackageHelpers.size() > mSerialPortEnum.ordinal()) {
42 | AbsStickPackageHelper helper = mStickPackageHelpers.get(mSerialPortEnum.ordinal());
43 | byte[] buffer = helper.execute(mInputStream);
44 | if (buffer != null && buffer.length > 0) {
45 | SerialPortLogUtil.d("SerialPortReadThread", "接收数据,长度: " + buffer.length);
46 | onDataReceived(buffer);
47 | }
48 | } else {
49 | // 使用第一个处理器作为默认
50 | if (!mStickPackageHelpers.isEmpty()) {
51 | AbsStickPackageHelper helper = mStickPackageHelpers.get(0);
52 | byte[] buffer = helper.execute(mInputStream);
53 | if (buffer != null && buffer.length > 0) {
54 | SerialPortLogUtil.d("SerialPortReadThread", "接收数据(默认处理器),长度: " + buffer.length);
55 | onDataReceived(buffer);
56 | }
57 | } else {
58 | SerialPortLogUtil.e("SerialPortReadThread", "没有可用的粘包处理器");
59 | break;
60 | }
61 | }
62 | } catch (Exception e) {
63 | SerialPortLogUtil.e("SerialPortReadThread", "读取数据异常: " + e.getMessage());
64 | e.printStackTrace();
65 | break;
66 | }
67 | }
68 | }
69 |
70 | /**
71 | * 关闭线程,释放资源
72 | */
73 | public void release() {
74 | interrupt();
75 | if (mInputStream != null) {
76 | try {
77 | mInputStream.close();
78 | } catch (IOException e) {
79 | e.printStackTrace();
80 | }
81 | mInputStream = null;
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/util/ByteUtil.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.util;
2 |
3 | /**
4 | * name:cl
5 | * date:2022/12/27
6 | * desc:Byte工具类
7 | */
8 | public class ByteUtil {
9 |
10 |
11 | /**
12 | * 字节数组转16进制
13 | *
14 | * @param bytes 需要转换的byte数组
15 | * @return 转换后的Hex字符串
16 | */
17 | public static String bytesToHex(byte[] bytes) {
18 | StringBuffer sb = new StringBuffer();
19 | for (int i = 0; i < bytes.length; i++) {
20 | String hex = Integer.toHexString(bytes[i] & 0xFF);
21 | if (hex.length() < 2) {
22 | sb.append(0);
23 | }
24 | sb.append(hex);
25 | }
26 | return sb.toString();
27 | }
28 |
29 |
30 | public static String trim(String s) {
31 | int i = s.length();// 字符串最后一个字符的位置
32 | int j = 0;// 字符串第一个字符
33 | int k = 0;// 中间变量
34 | char[] arrayOfChar = s.toCharArray();// 将字符串转换成字符数组
35 | while ((j < i) && (arrayOfChar[(k + j)] <= ' '))
36 | ++j;// 确定字符串前面的空格数
37 | while ((j < i) && (arrayOfChar[(k + i - 1)] <= ' '))
38 | --i;// 确定字符串后面的空格数
39 | return (((j > 0) || (i < s.length())) ? s.substring(j, i) : s);// 返回去除空格后的字符串
40 | }
41 |
42 | public static String toChineseHex(String s) {
43 | String ss = s;
44 | byte[] bt = new byte[0];
45 |
46 | try {
47 | bt = ss.getBytes("UTF-8");
48 | } catch (Exception e) {
49 | e.printStackTrace();
50 | }
51 | String s1 = "";
52 | for (int i = 0; i < bt.length; i++) {
53 | String tempStr = Integer.toHexString(bt[i]);
54 | if (tempStr.length() > 2)
55 | tempStr = tempStr.substring(tempStr.length() - 2);
56 | s1 = s1 + tempStr + "";
57 | }
58 | return s1.toUpperCase();
59 | }
60 |
61 |
62 | /**
63 | * 异或校验,返回一个字节
64 | */
65 | public static byte orVerification(byte[] bytes) {
66 | int nAll = 0;
67 | for (int i = 0; i < bytes.length; i++) {
68 | int nTemp = bytes[i];
69 | nAll = nAll ^ nTemp;
70 | }
71 | return (byte) nAll;
72 | }
73 |
74 | public static byte complement(byte[] bytes) {
75 | int iSum = 0;
76 | for (int i = 0; i < bytes.length; i++) {
77 | iSum += bytes[i];
78 | }
79 | iSum = 256 - iSum;
80 | return (byte) iSum;
81 | }
82 |
83 |
84 | /**
85 | * 多个byte数组合并
86 | */
87 | public static byte[] byteMergerAll(byte[]... values) {
88 | int length_byte = 0;
89 | for (int i = 0; i < values.length; i++) {
90 | length_byte += values[i].length;
91 | }
92 | byte[] all_byte = new byte[length_byte];
93 | int countLength = 0;
94 | for (int i = 0; i < values.length; i++) {
95 | byte[] b = values[i];
96 | System.arraycopy(b, 0, all_byte, countLength, b.length);
97 | countLength += b.length;
98 | }
99 | return all_byte;
100 | }
101 |
102 |
103 | public static byte[] hex2Byte(String hex) {
104 | String[] parts = hex.split(" ");
105 | byte[] bytes = new byte[parts.length];
106 | for (int i = 0; i < parts.length; i++) {
107 | bytes[i] = (byte) Integer.parseInt(parts[i], 16);
108 | }
109 | return bytes;
110 | }
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/stick/VariableLenStickPackageHelper.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.stick;
2 |
3 |
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.nio.ByteOrder;
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | /**
11 | * Variable-length sticky packet processing, used in the protocol with a length field
12 | * Example: The protocol is: type+dataLen+data+md5
13 | * type: Named type, two bytes
14 | * dataLen: The length of the data field, two bytes
15 | * data: Data field, variable length, length dataLen
16 | * md5: md5 field, 8 bytes
17 | * Use: 1.byteOrder: first determine the big and small ends, ByteOrder.BIG_ENDIAN or ByteOrder.LITTLE_ENDIAN;
18 | * 2.lenSize: The length of the len field, 2 in this example
19 | * 3.lenIndex: The position of the len field, 2 in this example, because the len field is preceded by type, and its length is 2
20 | * 4.offset: the length of the entire package -len, this example is the length of the three fields of type+dataLen+md5, that is, 2+2+8=12
21 | */
22 | public class VariableLenStickPackageHelper implements AbsStickPackageHelper {
23 | private int offset = 0;
24 | private int lenIndex = 0;
25 | private int lenSize = 2;
26 | private ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;
27 | private final List mBytes;
28 | private final int lenStartIndex;
29 | private final int lenEndIndex;
30 |
31 | public VariableLenStickPackageHelper(ByteOrder byteOrder, int lenSize, int lenIndex, int offset) {
32 | this.byteOrder = byteOrder;
33 | this.lenSize = lenSize;
34 | this.offset = offset;
35 | this.lenIndex = lenIndex;
36 | mBytes = new ArrayList<>();
37 | lenStartIndex = lenIndex;
38 | lenEndIndex = lenIndex + lenSize - 1;
39 | if (lenStartIndex > lenEndIndex) {
40 | throw new IllegalStateException("lenStartIndex>lenEndIndex");
41 | }
42 | }
43 |
44 | private int getLen(byte[] src, ByteOrder order) {
45 | int re = 0;
46 | if (order == ByteOrder.BIG_ENDIAN) {
47 | for (byte b : src) {
48 | re = (re << 8) | (b & 0xff);
49 | }
50 | } else {
51 | for (int i = src.length - 1; i >= 0; i--) {
52 | re = (re << 8) | (src[i] & 0xff);
53 | }
54 | }
55 | return re;
56 | }
57 |
58 | @Override
59 | public byte[] execute(InputStream is) {
60 | mBytes.clear();
61 | int count = 0;
62 | int len = -1;
63 | byte temp;
64 | byte[] result;
65 | int msgLen = -1;
66 | byte[] lenField = new byte[lenSize];
67 | try {
68 | while ((len = is.read()) != -1) {
69 | temp = (byte) len;
70 | if (count >= lenStartIndex && count <= lenEndIndex) {
71 | lenField[count - lenStartIndex] = temp;
72 | if (count == lenEndIndex) {
73 | msgLen = getLen(lenField, byteOrder);
74 | }
75 | }
76 | count++;
77 | mBytes.add(temp);
78 | if (msgLen != -1) {
79 | if (count == msgLen + offset) {
80 | break;
81 | } else if (count > msgLen + offset) {
82 | len = -1;
83 | break;
84 | }
85 | }
86 | }
87 | if (len == -1) {
88 | return null;
89 | }
90 | } catch (IOException e) {
91 | e.printStackTrace();
92 | return null;
93 | }
94 | result = new byte[mBytes.size()];
95 | for (int i = 0; i < result.length; i++) {
96 | result[i] = mBytes.get(i);
97 | }
98 | return result;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/stick/StickyPacketHelperFactory.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.stick;
2 |
3 | import java.nio.charset.StandardCharsets;
4 |
5 | /**
6 | * 黏包处理器工厂类
7 | * 提供常用的黏包处理器配置
8 | * Author: cl
9 | * Date: 2023/10/26
10 | */
11 | public class StickyPacketHelperFactory {
12 |
13 | /**
14 | * 创建无黏包处理的处理器
15 | */
16 | public static AbsStickPackageHelper createNoProcessing() {
17 | return new BaseStickPackageHelper();
18 | }
19 |
20 | /**
21 | * 创建固定长度的黏包处理器
22 | * @param length 数据包固定长度
23 | */
24 | public static AbsStickPackageHelper createFixedLength(int length) {
25 | return new StaticLenStickPackageHelper(length);
26 | }
27 |
28 | /**
29 | * 创建基于分隔符的黏包处理器
30 | * @param delimiter 分隔符字符串
31 | */
32 | public static AbsStickPackageHelper createDelimiterBased(String delimiter) {
33 | byte[] delimiterBytes = delimiter.getBytes(StandardCharsets.UTF_8);
34 | return new SpecifiedStickPackageHelper(new byte[0], delimiterBytes);
35 | }
36 |
37 | /**
38 | * 创建基于开始和结束标识的黏包处理器
39 | * @param startMarker 开始标识
40 | * @param endMarker 结束标识
41 | */
42 | public static AbsStickPackageHelper createMarkerBased(String startMarker, String endMarker) {
43 | byte[] startBytes = startMarker.getBytes(StandardCharsets.UTF_8);
44 | byte[] endBytes = endMarker.getBytes(StandardCharsets.UTF_8);
45 | return new SpecifiedStickPackageHelper(startBytes, endBytes);
46 | }
47 |
48 | /**
49 | * 创建基于开始和结束标识的黏包处理器 (字节版本)
50 | * @param startMarker 开始标识字节数组
51 | * @param endMarker 结束标识字节数组
52 | */
53 | public static AbsStickPackageHelper createMarkerBased(byte[] startMarker, byte[] endMarker) {
54 | return new SpecifiedStickPackageHelper(startMarker, endMarker);
55 | }
56 |
57 | /**
58 | * 创建变长黏包处理器
59 | * @param byteOrder 字节序
60 | * @param lenSize 长度字段大小
61 | * @param lenIndex 长度字段位置
62 | * @param offset 偏移量
63 | */
64 | public static AbsStickPackageHelper createVariableLength(java.nio.ByteOrder byteOrder, int lenSize, int lenIndex, int offset) {
65 | return new VariableLenStickPackageHelper(byteOrder, lenSize, lenIndex, offset);
66 | }
67 |
68 | /**
69 | * 创建默认变长黏包处理器
70 | * 默认配置:大端字节序,2字节长度字段,位置在索引2,偏移12字节
71 | */
72 | public static AbsStickPackageHelper createVariableLength() {
73 | return new VariableLenStickPackageHelper(java.nio.ByteOrder.BIG_ENDIAN, 2, 2, 12);
74 | }
75 |
76 | /**
77 | * 创建自定义超时的基础处理器
78 | * @param timeout 超时时间(毫秒)
79 | */
80 | public static AbsStickPackageHelper createTimeoutBased(int timeout) {
81 | return new TimeoutStickPackageHelper(timeout);
82 | }
83 |
84 | /**
85 | * 创建组合式黏包处理器
86 | * @param primaryHelper 主要处理器
87 | * @param fallbackHelper 备用处理器
88 | */
89 | public static AbsStickPackageHelper createComposite(AbsStickPackageHelper primaryHelper,
90 | AbsStickPackageHelper fallbackHelper) {
91 | return new CompositeStickPackageHelper(primaryHelper, fallbackHelper);
92 | }
93 |
94 | /**
95 | * 常用协议的快速创建方法
96 | */
97 | public static class Common {
98 |
99 | /**
100 | * AT指令协议(以\r\n结尾)
101 | */
102 | public static AbsStickPackageHelper createATCommand() {
103 | return createDelimiterBased("\r\n");
104 | }
105 |
106 | /**
107 | * JSON协议(以换行符结尾)
108 | */
109 | public static AbsStickPackageHelper createJsonLine() {
110 | return createDelimiterBased("\n");
111 | }
112 |
113 | /**
114 | * Modbus RTU协议(固定长度,通常8字节)
115 | */
116 | public static AbsStickPackageHelper createModbusRTU() {
117 | return createFixedLength(8);
118 | }
119 |
120 | /**
121 | * 自定义协议(STX/ETX包围)
122 | */
123 | public static AbsStickPackageHelper createSTXETX() {
124 | return createMarkerBased(new byte[]{0x02}, new byte[]{0x03}); // STX, ETX
125 | }
126 |
127 | /**
128 | * 自定义协议(SOH/EOT包围)
129 | */
130 | public static AbsStickPackageHelper createSOHEOT() {
131 | return createMarkerBased(new byte[]{0x01}, new byte[]{0x04}); // SOH, EOT
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/fragment/LogFragment.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication.fragment;
2 |
3 | import android.os.Bundle;
4 | import android.view.LayoutInflater;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 | import android.widget.BaseAdapter;
8 | import android.widget.TextView;
9 |
10 | import androidx.annotation.Nullable;
11 | import androidx.databinding.DataBindingUtil;
12 | import androidx.fragment.app.Fragment;
13 |
14 | import com.cl.myapplication.R;
15 | import com.cl.myapplication.databinding.FragmentLogBinding;
16 | import com.cl.myapplication.message.ConversionNoticeEvent;
17 | import com.cl.myapplication.message.IMessage;
18 | import com.cl.myapplication.message.LogManager;
19 | import com.cl.myapplication.util.ListViewHolder;
20 |
21 | import org.greenrobot.eventbus.EventBus;
22 |
23 |
24 | public class LogFragment extends Fragment {
25 |
26 | private LogAdapter mAdapter;
27 | private boolean mConversionNotice = true;
28 | private FragmentLogBinding binding;
29 |
30 | @Nullable
31 | @Override
32 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
33 | @Nullable Bundle savedInstanceState) {
34 |
35 | binding = DataBindingUtil.inflate(inflater, R.layout.fragment_log, container, false);
36 |
37 | binding.btnClearLog.setOnClickListener(new View.OnClickListener() {
38 | @Override
39 | public void onClick(View v) {
40 | // 清空列表
41 | LogManager.instance().clear();
42 | updateList();
43 | }
44 | });
45 |
46 | binding.btnAutoEnd.setOnClickListener(new View.OnClickListener() {
47 | @Override
48 | public void onClick(View v) {
49 | LogManager.instance().changAutoEnd();
50 | updateAutoEndButton();
51 | }
52 | });
53 |
54 | binding.btnWhetherHexadecimal.setOnClickListener((view1) -> {
55 | if (mConversionNotice){
56 | EventBus.getDefault().post(new ConversionNoticeEvent("1"));
57 | mConversionNotice=false;
58 | }else {
59 | EventBus.getDefault().post(new ConversionNoticeEvent("2"));
60 | mConversionNotice=true;
61 | }
62 | });
63 |
64 | mAdapter = new LogAdapter();
65 | binding.lvLogs.setAdapter(mAdapter);
66 |
67 | updateAutoEndButton();
68 | return binding.getRoot();
69 | }
70 |
71 | public void updateAutoEndButton() {
72 | if (binding != null) {
73 | if (LogManager.instance().isAutoEnd()) {
74 | binding.btnAutoEnd.setText("禁止自动显示最新日志");
75 | binding.lvLogs.setSelection(mAdapter.getCount() - 1);
76 | } else {
77 | binding.btnAutoEnd.setText("自动显示最新日志");
78 | }
79 | }
80 | }
81 |
82 | private static class LogAdapter extends BaseAdapter {
83 |
84 | @Override
85 | public int getCount() {
86 | return LogManager.instance().messages.size();
87 | }
88 |
89 | @Override
90 | public IMessage getItem(int position) {
91 | return LogManager.instance().messages.get(position);
92 | }
93 |
94 | @Override
95 | public long getItemId(int position) {
96 | return position;
97 | }
98 |
99 | @Override
100 | public View getView(int position, View convertView, ViewGroup parent) {
101 |
102 | IMessage message = getItem(position);
103 |
104 | ListViewHolder holder;
105 | if (convertView == null) {
106 | holder = new ListViewHolder(R.layout.item_log, parent);
107 | convertView = holder.getItemView();
108 | } else {
109 | holder = (ListViewHolder) convertView.getTag();
110 | }
111 |
112 | TextView tvLog = holder.getText(R.id.tv_log);
113 | TextView tvNum = holder.getText(R.id.tv_num);
114 |
115 | tvLog.setText(message.getMessage());
116 | tvLog.setEnabled(message.isToSend());
117 |
118 | tvNum.setText(String.valueOf(position + 1));
119 |
120 | return convertView;
121 | }
122 | }
123 |
124 | public void add(IMessage message) {
125 | LogManager.instance().add(message);
126 | updateList();
127 | }
128 |
129 | public void updateList() {
130 | if (binding != null) {
131 | mAdapter.notifyDataSetChanged();
132 | if (LogManager.instance().isAutoEnd()) {
133 | binding.lvLogs.setSelection(mAdapter.getCount() - 1);
134 | }
135 | }
136 | }
137 | }
138 |
139 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/stick/SpecifiedStickPackageHelper.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.stick;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.util.ArrayList;
6 | import java.util.Arrays;
7 | import java.util.List;
8 |
9 | /**
10 | * The sticky packet processing of specific characters,
11 | * one Byte[] at the beginning and the end, cannot be empty at the same time,
12 | * if one of them is empty, then the non-empty is used as the split marker
13 | * Example: The protocol is formulated as ^+data+$, starting with ^ and ending with $
14 | */
15 | public class SpecifiedStickPackageHelper implements AbsStickPackageHelper {
16 | private final byte[] head;
17 | private final byte[] tail;
18 | private final List bytes;
19 | private final int headLen;
20 | private final int tailLen;
21 |
22 | public SpecifiedStickPackageHelper(byte[] head, byte[] tail) {
23 | this.head = head;
24 | this.tail = tail;
25 | if (head == null || tail == null) {
26 | throw new IllegalStateException(" head or tail ==null");
27 | }
28 | if (head.length == 0 && tail.length == 0) {
29 | throw new IllegalStateException(" head and tail length==0");
30 | }
31 | headLen = head.length;
32 | tailLen = tail.length;
33 | bytes = new ArrayList<>();
34 | }
35 |
36 | /**
37 | * 构造函数 - 只使用结束标识
38 | * @param tail 结束标识
39 | */
40 | public SpecifiedStickPackageHelper(byte[] tail) {
41 | this(new byte[0], tail);
42 | }
43 |
44 | /**
45 | * 构造函数 - 字符串版本
46 | * @param head 开始标识字符串
47 | * @param tail 结束标识字符串
48 | */
49 | public SpecifiedStickPackageHelper(String head, String tail) {
50 | this(head != null ? head.getBytes() : new byte[0],
51 | tail != null ? tail.getBytes() : new byte[0]);
52 | }
53 |
54 | /**
55 | * 构造函数 - 只使用结束标识字符串
56 | * @param tail 结束标识字符串
57 | */
58 | public SpecifiedStickPackageHelper(String tail) {
59 | this(new byte[0], tail != null ? tail.getBytes() : new byte[0]);
60 | }
61 |
62 | private boolean endWith(Byte[] src, byte[] target) {
63 | if (src.length < target.length) {
64 | return false;
65 | }
66 | for (int i = 0; i < target.length; i++) {
67 | if (target[target.length - i - 1] != src[src.length - i - 1]) {
68 | return false;
69 | }
70 | }
71 | return true;
72 | }
73 |
74 | private byte[] getRangeBytes(List list, int start, int end) {
75 | Byte[] temps = Arrays.copyOfRange(list.toArray(new Byte[0]), start, end);
76 | byte[] result = new byte[temps.length];
77 | for (int i = 0; i < result.length; i++) {
78 | result[i] = temps[i];
79 | }
80 | return result;
81 | }
82 |
83 | @Override
84 | public byte[] execute(InputStream is) {
85 | bytes.clear();
86 | int len = -1;
87 | byte temp;
88 | int startIndex = -1;
89 | byte[] result = null;
90 | boolean isFindStart = false, isFindEnd = false;
91 | try {
92 | while ((len = is.read()) != -1) {
93 | temp = (byte) len;
94 | bytes.add(temp);
95 | Byte[] byteArray = bytes.toArray(new Byte[]{});
96 | if (headLen == 0 || tailLen == 0) {//Only head or tail markers
97 | if (endWith(byteArray, head) || endWith(byteArray, tail)) {
98 | if (startIndex == -1) {
99 | startIndex = bytes.size() - headLen;
100 | } else {
101 | result = getRangeBytes(bytes, startIndex, bytes.size());
102 | break;
103 | }
104 | }
105 | } else {
106 | if (!isFindStart) {
107 | if (endWith(byteArray, head)) {
108 | startIndex = bytes.size() - headLen;
109 | isFindStart = true;
110 | }
111 | } else if (!isFindEnd) {
112 | if (endWith(byteArray, tail)) {
113 | if (startIndex + headLen <= bytes.size() - tailLen) {
114 | isFindEnd = true;
115 | result = getRangeBytes(bytes, startIndex, bytes.size());
116 | break;
117 | }
118 | }
119 | }
120 |
121 | }
122 | }
123 | if (len == -1) {
124 | return null;
125 | }
126 | } catch (IOException e) {
127 | e.printStackTrace();
128 | return null;
129 | }
130 | return result;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main_java.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
23 |
24 |
32 |
33 |
42 |
43 |
44 |
53 |
54 |
61 |
62 |
70 |
71 |
72 |
73 |
74 |
75 |
79 |
80 |
85 |
86 |
87 |
96 |
97 |
101 |
102 |
106 |
107 |
108 |
113 |
114 |
121 |
122 |
127 |
128 |
129 |
134 |
135 |
142 |
143 |
148 |
149 |
150 |
155 |
156 |
163 |
164 |
169 |
170 |
171 |
172 |
173 |
178 |
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/SerialConfig.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary;
2 |
3 | import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
4 | import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
5 |
6 | /**
7 | * name:cl
8 | * date:2023/10/26
9 | * desc:串口配置类
10 | */
11 | public class SerialConfig {
12 |
13 | //配置日志相关参数
14 | private boolean enableLogging;
15 | //串口接收间隔时间
16 | private int intervalSleep;
17 | //串口重连机制
18 | private boolean serialPortReconnection;
19 | int flags;
20 | int databits;
21 | int stopbits;
22 | int parity;
23 |
24 | // 黏包处理相关配置
25 | private boolean enableStickyPacketProcessing;
26 | private int maxPacketSize;
27 | private int packetTimeout;
28 | private AbsStickPackageHelper[] stickyPacketHelpers;
29 | private boolean autoReconnect;
30 | private int reconnectInterval;
31 | private int maxReconnectAttempts;
32 |
33 |
34 | public SerialConfig(Builder builder) {
35 | this.enableLogging=builder.enableLogging;
36 | this.intervalSleep=builder.intervalSleep;
37 | this.serialPortReconnection=builder.serialPortReconnection;
38 | this.flags=builder.flags;
39 | this.databits=builder.databits;
40 | this.stopbits=builder.stopbits;
41 | this.parity=builder.parity;
42 | this.enableStickyPacketProcessing=builder.enableStickyPacketProcessing;
43 | this.maxPacketSize=builder.maxPacketSize;
44 | this.packetTimeout=builder.packetTimeout;
45 | this.stickyPacketHelpers=builder.stickyPacketHelpers;
46 | this.autoReconnect=builder.autoReconnect;
47 | this.reconnectInterval=builder.reconnectInterval;
48 | this.maxReconnectAttempts=builder.maxReconnectAttempts;
49 | }
50 |
51 |
52 | public boolean isEnableLogging() {
53 | return enableLogging;
54 | }
55 |
56 | public void setEnableLogging(boolean enableLogging) {
57 | this.enableLogging = enableLogging;
58 | }
59 |
60 | public int getIntervalSleep() {
61 | return intervalSleep;
62 | }
63 |
64 | public boolean isSerialPortReconnection() {
65 | return serialPortReconnection;
66 | }
67 |
68 | public void setIntervalSleep(int intervalSleep) {
69 | this.intervalSleep = intervalSleep;
70 | }
71 |
72 | public int getFlags() {
73 | return flags;
74 | }
75 |
76 | public int getDatabits() {
77 | return databits;
78 | }
79 |
80 | public int getStopbits() {
81 | return stopbits;
82 | }
83 |
84 | public int getParity() {
85 | return parity;
86 | }
87 |
88 | public void setFlags(int flags) {
89 | this.flags = flags;
90 | }
91 |
92 | public void setDatabits(int databits) {
93 | this.databits = databits;
94 | }
95 |
96 | public void setStopbits(int stopbits) {
97 | this.stopbits = stopbits;
98 | }
99 |
100 | public void setParity(int parity) {
101 | this.parity = parity;
102 | }
103 |
104 | public boolean isEnableStickyPacketProcessing() {
105 | return enableStickyPacketProcessing;
106 | }
107 |
108 | public void setEnableStickyPacketProcessing(boolean enableStickyPacketProcessing) {
109 | this.enableStickyPacketProcessing = enableStickyPacketProcessing;
110 | }
111 |
112 | public int getMaxPacketSize() {
113 | return maxPacketSize;
114 | }
115 |
116 | public void setMaxPacketSize(int maxPacketSize) {
117 | this.maxPacketSize = maxPacketSize;
118 | }
119 |
120 | public int getPacketTimeout() {
121 | return packetTimeout;
122 | }
123 |
124 | public void setPacketTimeout(int packetTimeout) {
125 | this.packetTimeout = packetTimeout;
126 | }
127 |
128 | public AbsStickPackageHelper[] getStickyPacketHelpers() {
129 | return stickyPacketHelpers;
130 | }
131 |
132 | public void setStickyPacketHelpers(AbsStickPackageHelper[] stickyPacketHelpers) {
133 | this.stickyPacketHelpers = stickyPacketHelpers;
134 | }
135 |
136 | public boolean isAutoReconnect() {
137 | return autoReconnect;
138 | }
139 |
140 | public void setAutoReconnect(boolean autoReconnect) {
141 | this.autoReconnect = autoReconnect;
142 | }
143 |
144 | public int getReconnectInterval() {
145 | return reconnectInterval;
146 | }
147 |
148 | public void setReconnectInterval(int reconnectInterval) {
149 | this.reconnectInterval = reconnectInterval;
150 | }
151 |
152 | public int getMaxReconnectAttempts() {
153 | return maxReconnectAttempts;
154 | }
155 |
156 | public void setMaxReconnectAttempts(int maxReconnectAttempts) {
157 | this.maxReconnectAttempts = maxReconnectAttempts;
158 | }
159 |
160 | public static class Builder {
161 |
162 | //配置日志相关参数
163 | private boolean enableLogging = true;
164 | //串口接收间隔时间
165 | private int intervalSleep = 50;
166 | //串口重连机制
167 | private boolean serialPortReconnection = false;
168 | // 标志位
169 | int flags = 0;
170 | // 数据位
171 | int databits = 8;
172 | // 停止位
173 | int stopbits = 1;
174 | // 校验位:0 表示无校验位,1 表示奇校验,2 表示偶校验
175 | int parity = 0;
176 |
177 | // 黏包处理相关配置
178 | private boolean enableStickyPacketProcessing = true;
179 | private int maxPacketSize = 1024;
180 | private int packetTimeout = 1000;
181 | private AbsStickPackageHelper[] stickyPacketHelpers = {new BaseStickPackageHelper()};
182 | private boolean autoReconnect = false;
183 | private int reconnectInterval = 5000;
184 | private int maxReconnectAttempts = 3;
185 |
186 | public Builder setEnableLogging(boolean enableLogging) {
187 | this.enableLogging = enableLogging;
188 | return this;
189 | }
190 |
191 | public Builder setIntervalSleep(int sleep) {
192 | intervalSleep = sleep;
193 | return this;
194 | }
195 |
196 | public Builder setSerialPortReconnection(boolean serialReconnection) {
197 | serialPortReconnection = serialReconnection;
198 | return this;
199 | }
200 |
201 | public Builder setFlags(int flags) {
202 | this.flags = flags;
203 | return this;
204 | }
205 |
206 | public Builder setDatabits(int databits) {
207 | this.databits = databits;
208 | return this;
209 | }
210 |
211 | public Builder setStopbits(int stopbits) {
212 | this.stopbits = stopbits;
213 | return this;
214 | }
215 |
216 | public Builder setParity(int parity) {
217 | this.parity = parity;
218 | return this;
219 | }
220 |
221 | public Builder setEnableStickyPacketProcessing(boolean enableStickyPacketProcessing) {
222 | this.enableStickyPacketProcessing = enableStickyPacketProcessing;
223 | return this;
224 | }
225 |
226 | public Builder setMaxPacketSize(int maxPacketSize) {
227 | this.maxPacketSize = maxPacketSize;
228 | return this;
229 | }
230 |
231 | public Builder setPacketTimeout(int packetTimeout) {
232 | this.packetTimeout = packetTimeout;
233 | return this;
234 | }
235 |
236 | public Builder setStickyPacketHelpers(AbsStickPackageHelper... stickyPacketHelpers) {
237 | this.stickyPacketHelpers = stickyPacketHelpers;
238 | return this;
239 | }
240 |
241 | public Builder setAutoReconnect(boolean autoReconnect) {
242 | this.autoReconnect = autoReconnect;
243 | return this;
244 | }
245 |
246 | public Builder setReconnectInterval(int reconnectInterval) {
247 | this.reconnectInterval = reconnectInterval;
248 | return this;
249 | }
250 |
251 | public Builder setMaxReconnectAttempts(int maxReconnectAttempts) {
252 | this.maxReconnectAttempts = maxReconnectAttempts;
253 | return this;
254 | }
255 |
256 | public SerialConfig build() {
257 | return new SerialConfig(this);
258 | }
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/serial_lib/src/main/cpp/SerialPort.c:
--------------------------------------------------------------------------------
1 |
2 |
3 | /*
4 | * Copyright 2009-2011 Cedric Priscal
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | #include
20 | #include
21 | #include
22 | #include
23 | #include
24 | #include
25 | #include
26 |
27 | #include "SerialPort.h"
28 |
29 | #include "android/log.h"
30 | static const char *TAG="serial_port";
31 | #define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
32 | #define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
33 | #define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
34 |
35 | static speed_t getBaudrate(jint baudrate)
36 | {
37 | switch(baudrate) {
38 | case 0: return B0;
39 | case 50: return B50;
40 | case 75: return B75;
41 | case 110: return B110;
42 | case 134: return B134;
43 | case 150: return B150;
44 | case 200: return B200;
45 | case 300: return B300;
46 | case 600: return B600;
47 | case 1200: return B1200;
48 | case 1800: return B1800;
49 | case 2400: return B2400;
50 | case 4800: return B4800;
51 | case 9600: return B9600;
52 | case 19200: return B19200;
53 | case 38400: return B38400;
54 | case 57600: return B57600;
55 | case 115200: return B115200;
56 | case 230400: return B230400;
57 | case 460800: return B460800;
58 | case 500000: return B500000;
59 | case 576000: return B576000;
60 | case 921600: return B921600;
61 | case 1000000: return B1000000;
62 | case 1152000: return B1152000;
63 | case 1500000: return B1500000;
64 | case 2000000: return B2000000;
65 | case 2500000: return B2500000;
66 | case 3000000: return B3000000;
67 | case 3500000: return B3500000;
68 | case 4000000: return B4000000;
69 | default: return -1;
70 | }
71 | }
72 |
73 | /*
74 | * Class: android_serialport_SerialPort
75 | * Method: open
76 | * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
77 | */
78 | JNIEXPORT jobject JNICALL Java_com_cl_serialportlibrary_SerialPort_open
79 | (JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags, jint databits, jint stopbits, jint parity)
80 | {
81 | int fd;
82 | speed_t speed;
83 | jobject mFileDescriptor;
84 |
85 | /* Check arguments */
86 | {
87 | speed = getBaudrate(baudrate);
88 | if (speed == -1) {
89 | LOGE("Invalid baudrate");
90 | return NULL;
91 | }
92 | }
93 |
94 | /* Opening device */
95 | {
96 | jboolean iscopy;
97 | const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);
98 | LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR | flags);
99 | fd = open(path_utf, O_RDWR | flags);
100 | LOGD("open() fd = %d", fd);
101 | (*env)->ReleaseStringUTFChars(env, path, path_utf);
102 | if (fd == -1) {
103 | LOGE("Cannot open port");
104 | return NULL;
105 | }
106 | }
107 |
108 | /* Configure device */
109 | {
110 | struct termios cfg;
111 | LOGD("Configuring serial port");
112 | if (tcgetattr(fd, &cfg)) {
113 | LOGE("tcgetattr() failed");
114 | close(fd);
115 | return NULL;
116 | }
117 |
118 | // Initialize termios struct
119 | cfmakeraw(&cfg);
120 |
121 | // Set data bits
122 | cfg.c_cflag &= ~CSIZE;
123 | switch (databits) {
124 | case 5:
125 | cfg.c_cflag |= CS5;
126 | break;
127 | case 6:
128 | cfg.c_cflag |= CS6;
129 | break;
130 | case 7:
131 | cfg.c_cflag |= CS7;
132 | break;
133 | case 8:
134 | cfg.c_cflag |= CS8;
135 | break;
136 | default:
137 | LOGE("Invalid data bits");
138 | close(fd);
139 | return NULL;
140 | }
141 |
142 | // Set stop bits
143 | switch (stopbits) {
144 | case 1:
145 | cfg.c_cflag &= ~CSTOPB;
146 | break;
147 | case 2:
148 | cfg.c_cflag |= CSTOPB;
149 | break;
150 | default:
151 | LOGE("Invalid stop bits");
152 | close(fd);
153 | return NULL;
154 | }
155 |
156 | switch (parity) {
157 | case 0:
158 | cfg.c_cflag &= ~PARENB; //PARITY OFF
159 | break;
160 | case 1:
161 | cfg.c_cflag |= (PARODD | PARENB); //PARITY ODD
162 | cfg.c_iflag &= ~IGNPAR;
163 | cfg.c_iflag |= PARMRK;
164 | cfg.c_iflag |= INPCK;
165 | break;
166 | case 2:
167 | cfg.c_iflag &= ~(IGNPAR | PARMRK); //PARITY EVEN
168 | cfg.c_iflag |= INPCK;
169 | cfg.c_cflag |= PARENB;
170 | cfg.c_cflag &= ~PARODD;
171 | break;
172 | case 3:
173 | // PARITY SPACE
174 | cfg.c_iflag &= ~IGNPAR; // Make sure wrong parity is not ignored
175 | cfg.c_iflag |= PARMRK; // Marks parity error, parity error
176 | // is given as three char sequence
177 | cfg.c_iflag |= INPCK; // Enable input parity checking
178 | cfg.c_cflag |= PARENB | CMSPAR; // Enable parity and set space parity
179 | cfg.c_cflag &= ~PARODD; //
180 | break;
181 | case 4:
182 | // PARITY MARK
183 | cfg.c_iflag &= ~IGNPAR; // Make sure wrong parity is not ignored
184 | cfg.c_iflag |= PARMRK; // Marks parity error, parity error
185 | // is given as three char sequence
186 | cfg.c_iflag |= INPCK; // Enable input parity checking
187 | cfg.c_cflag |= PARENB | CMSPAR | PARODD;
188 | break;
189 | default:
190 | cfg.c_cflag &= ~PARENB;
191 | break;
192 | }
193 |
194 | // Set baud rate
195 | cfsetispeed(&cfg, speed);
196 | cfsetospeed(&cfg, speed);
197 |
198 | if (tcsetattr(fd, TCSANOW, &cfg)) {
199 | LOGE("tcsetattr() failed");
200 | close(fd);
201 | return NULL;
202 | }
203 | }
204 |
205 | /* Create a corresponding file descriptor */
206 | {
207 | jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");
208 | jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "", "()V");
209 | jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
210 | mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);
211 | (*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint)fd);
212 | }
213 |
214 | return mFileDescriptor;
215 | }
216 |
217 | JNIEXPORT void JNICALL Java_com_cl_serialportlibrary_SerialPort_close
218 | (JNIEnv *env, jobject thiz)
219 | {
220 | jclass SerialPortClass = (*env)->GetObjectClass(env, thiz);
221 | jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor");
222 |
223 | jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");
224 | jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I");
225 |
226 | jobject mFd = (*env)->GetObjectField(env, thiz, mFdID);
227 | jint descriptor = (*env)->GetIntField(env, mFd, descriptorID);
228 |
229 | LOGD("close(fd = %d)", descriptor);
230 | close(descriptor);
231 | }
232 |
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/utils/SerialPortLogUtil.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.utils;
2 |
3 | import android.util.Log;
4 |
5 | import java.text.SimpleDateFormat;
6 | import java.util.Date;
7 | import java.util.Locale;
8 |
9 | /**
10 | * 串口日志工具类
11 | * 替代XLog的增强实现,包含更详细的调试信息
12 | * 提供时间戳、调用位置、数据格式化等功能
13 | */
14 | public class SerialPortLogUtil {
15 |
16 | private static final String DEFAULT_TAG = "SerialPort";
17 | private static boolean isDebugEnabled = true;
18 | private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault());
19 |
20 | /**
21 | * 设置是否启用调试日志
22 | * @param enabled 是否启用
23 | */
24 | public static void setDebugEnabled(boolean enabled) {
25 | isDebugEnabled = enabled;
26 | if (enabled) {
27 | Log.i(DEFAULT_TAG, "================== 串口日志系统已启用 ==================");
28 | }
29 | }
30 |
31 | /**
32 | * 获取是否启用调试日志
33 | */
34 | public static boolean isDebugEnabled() {
35 | return isDebugEnabled;
36 | }
37 |
38 | /**
39 | * 获取当前时间戳
40 | */
41 | private static String getTimeStamp() {
42 | return DATE_FORMAT.format(new Date());
43 | }
44 |
45 | /**
46 | * 获取调用者信息
47 | */
48 | private static String getCallerInfo() {
49 | StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
50 | // 跳过前面的调用栈,找到真正的调用者
51 | for (int i = 4; i < stackTrace.length; i++) {
52 | StackTraceElement element = stackTrace[i];
53 | String className = element.getClassName();
54 | if (!className.equals(SerialPortLogUtil.class.getName())) {
55 | String simpleClassName = className.substring(className.lastIndexOf('.') + 1);
56 | return String.format("[%s.%s:%d]", simpleClassName, element.getMethodName(), element.getLineNumber());
57 | }
58 | }
59 | return "[Unknown]";
60 | }
61 |
62 | /**
63 | * 格式化消息
64 | */
65 | private static String formatMessage(String message) {
66 | return String.format("%s %s %s", getTimeStamp(), getCallerInfo(), message);
67 | }
68 |
69 | /**
70 | * 输出调试日志
71 | * @param tag 标签
72 | * @param message 消息
73 | */
74 | public static void d(String tag, String message) {
75 | if (isDebugEnabled) {
76 | Log.d(tag != null ? tag : DEFAULT_TAG, formatMessage(message));
77 | }
78 | }
79 |
80 | /**
81 | * 输出调试日志(使用默认标签)
82 | * @param message 消息
83 | */
84 | public static void d(String message) {
85 | d(DEFAULT_TAG, message);
86 | }
87 |
88 | /**
89 | * 输出信息日志
90 | * @param tag 标签
91 | * @param message 消息
92 | */
93 | public static void i(String tag, String message) {
94 | if (isDebugEnabled) {
95 | Log.i(tag != null ? tag : DEFAULT_TAG, formatMessage(message));
96 | }
97 | }
98 |
99 | /**
100 | * 输出信息日志(使用默认标签)
101 | * @param message 消息
102 | */
103 | public static void i(String message) {
104 | i(DEFAULT_TAG, message);
105 | }
106 |
107 | /**
108 | * 输出警告日志
109 | * @param tag 标签
110 | * @param message 消息
111 | */
112 | public static void w(String tag, String message) {
113 | if (isDebugEnabled) {
114 | Log.w(tag != null ? tag : DEFAULT_TAG, formatMessage(message));
115 | }
116 | }
117 |
118 | /**
119 | * 输出警告日志(使用默认标签)
120 | * @param message 消息
121 | */
122 | public static void w(String message) {
123 | w(DEFAULT_TAG, message);
124 | }
125 |
126 | /**
127 | * 输出错误日志
128 | * @param tag 标签
129 | * @param message 消息
130 | */
131 | public static void e(String tag, String message) {
132 | // 错误日志始终输出,不受isDebugEnabled控制
133 | Log.e(tag != null ? tag : DEFAULT_TAG, formatMessage(message));
134 | }
135 |
136 | /**
137 | * 输出错误日志(使用默认标签)
138 | * @param message 消息
139 | */
140 | public static void e(String message) {
141 | e(DEFAULT_TAG, message);
142 | }
143 |
144 | /**
145 | * 输出错误日志(带异常)
146 | * @param tag 标签
147 | * @param message 消息
148 | * @param throwable 异常
149 | */
150 | public static void e(String tag, String message, Throwable throwable) {
151 | // 错误日志始终输出,不受isDebugEnabled控制
152 | Log.e(tag != null ? tag : DEFAULT_TAG, formatMessage(message), throwable);
153 | }
154 |
155 | /**
156 | * 输出错误日志(带异常,使用默认标签)
157 | * @param message 消息
158 | * @param throwable 异常
159 | */
160 | public static void e(String message, Throwable throwable) {
161 | e(DEFAULT_TAG, message, throwable);
162 | }
163 |
164 | /**
165 | * 打印数据(专门用于串口数据调试)
166 | * @param tag 标签
167 | * @param prefix 前缀
168 | * @param data 数据
169 | */
170 | public static void printData(String tag, String prefix, byte[] data) {
171 | if (!isDebugEnabled || data == null) return;
172 |
173 | StringBuilder sb = new StringBuilder();
174 | sb.append(prefix).append(" [").append(data.length).append(" bytes]: ");
175 |
176 | // 十六进制格式
177 | sb.append("HEX[");
178 | for (int i = 0; i < Math.min(data.length, 32); i++) { // 最多显示32字节
179 | if (i > 0) sb.append(" ");
180 | sb.append(String.format("%02X", data[i] & 0xFF));
181 | }
182 | if (data.length > 32) {
183 | sb.append("...");
184 | }
185 | sb.append("] ");
186 |
187 | // ASCII格式(可打印字符)
188 | sb.append("ASCII[");
189 | for (int i = 0; i < Math.min(data.length, 32); i++) {
190 | byte b = data[i];
191 | if (b >= 32 && b < 127) {
192 | sb.append((char) b);
193 | } else {
194 | sb.append('.');
195 | }
196 | }
197 | if (data.length > 32) {
198 | sb.append("...");
199 | }
200 | sb.append("]");
201 |
202 | d(tag, sb.toString());
203 | }
204 |
205 | /**
206 | * 打印数据(使用默认标签)
207 | * @param prefix 前缀
208 | * @param data 数据
209 | */
210 | public static void printData(String prefix, byte[] data) {
211 | printData(DEFAULT_TAG, prefix, data);
212 | }
213 |
214 | /**
215 | * 打印串口状态信息
216 | * @param tag 标签
217 | * @param devicePath 设备路径
218 | * @param baudRate 波特率
219 | * @param isOpen 是否打开
220 | */
221 | public static void printSerialStatus(String tag, String devicePath, int baudRate, boolean isOpen) {
222 | i(tag, String.format("串口状态 - 设备: %s, 波特率: %d, 状态: %s",
223 | devicePath, baudRate, isOpen ? "已打开" : "已关闭"));
224 | }
225 |
226 | /**
227 | * 打印串口配置信息
228 | * @param tag 标签
229 | * @param databits 数据位
230 | * @param parity 校验位
231 | * @param stopbits 停止位
232 | * @param flags 标志位
233 | */
234 | public static void printSerialConfig(String tag, int databits, int parity, int stopbits, int flags) {
235 | String parityStr;
236 | switch (parity) {
237 | case 0: parityStr = "无校验"; break;
238 | case 1: parityStr = "奇校验"; break;
239 | case 2: parityStr = "偶校验"; break;
240 | default: parityStr = "未知(" + parity + ")"; break;
241 | }
242 |
243 | i(tag, String.format("串口配置 - 数据位: %d, 校验位: %s, 停止位: %d, 标志位: 0x%X",
244 | databits, parityStr, stopbits, flags));
245 | }
246 |
247 | /**
248 | * 打印性能信息
249 | * @param tag 标签
250 | * @param operation 操作名称
251 | * @param startTime 开始时间
252 | */
253 | public static void printPerformance(String tag, String operation, long startTime) {
254 | long duration = System.currentTimeMillis() - startTime;
255 | d(tag, String.format("性能统计 - %s 耗时: %dms", operation, duration));
256 | }
257 |
258 | /**
259 | * 打印分隔线
260 | * @param tag 标签
261 | * @param title 标题
262 | */
263 | public static void printSeparator(String tag, String title) {
264 | if (isDebugEnabled) {
265 | String separator = "==================== " + title + " ====================";
266 | i(tag, separator);
267 | }
268 | }
269 |
270 | /**
271 | * 打印分隔线(使用默认标签)
272 | * @param title 标题
273 | */
274 | public static void printSeparator(String title) {
275 | printSeparator(DEFAULT_TAG, title);
276 | }
277 | }
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/SerialPortManager.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary;
2 |
3 | import android.os.Handler;
4 | import android.os.HandlerThread;
5 | import android.os.Message;
6 |
7 | import com.cl.serialportlibrary.utils.SerialPortLogUtil;
8 | import com.cl.serialportlibrary.enumerate.SerialPortEnum;
9 | import com.cl.serialportlibrary.enumerate.SerialStatus;
10 | import com.cl.serialportlibrary.listener.OnOpenSerialPortListener;
11 | import com.cl.serialportlibrary.listener.OnSerialPortDataListener;
12 | import com.cl.serialportlibrary.thread.SerialPortReadThread;
13 | import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
14 | import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
15 |
16 | import java.util.ArrayList;
17 | import java.util.Arrays;
18 | import java.util.List;
19 |
20 | import java.io.Closeable;
21 | import java.io.File;
22 | import java.io.FileDescriptor;
23 | import java.io.FileInputStream;
24 | import java.io.FileOutputStream;
25 | import java.io.IOException;
26 |
27 |
28 | public class SerialPortManager extends SerialPort {
29 |
30 | private static final String TAG = SerialPortManager.class.getSimpleName();
31 | private FileInputStream mFileInputStream;
32 | private FileOutputStream mFileOutputStream;
33 | private FileDescriptor mFd;
34 | private OnOpenSerialPortListener mOnOpenSerialPortListener;
35 | private OnSerialPortDataListener mOnSerialPortDataListener;
36 | private HandlerThread mSendingHandlerThread;
37 | private Handler mSendingHandler;
38 | private SerialPortReadThread mSerialPortReadThread;
39 | //串口类型
40 | private final SerialPortEnum mSerialPortEnum;
41 | //串口配置
42 | private SerialConfig mSerialConfig;
43 | //粘包处理器
44 | private List mStickPackageHelpers;
45 |
46 | public SerialPortManager() {
47 | this(SerialPortEnum.SERIAL_ONE);
48 | }
49 |
50 | public SerialPortManager(SerialPortEnum mSerialPortEnum) {
51 | this.mSerialPortEnum = mSerialPortEnum;
52 | this.mStickPackageHelpers = new ArrayList<>();
53 | this.mStickPackageHelpers.add(new BaseStickPackageHelper());
54 | }
55 |
56 | /**
57 | * 设置串口配置
58 | */
59 | public void setSerialConfig(SerialConfig config) {
60 | this.mSerialConfig = config;
61 | if (config.getStickyPacketHelpers() != null && config.getStickyPacketHelpers().length > 0) {
62 | this.mStickPackageHelpers = Arrays.asList(config.getStickyPacketHelpers());
63 | }
64 | }
65 |
66 | /**
67 | * 设置粘包处理器
68 | */
69 | public void setStickPackageHelpers(List helpers) {
70 | if (helpers != null) {
71 | this.mStickPackageHelpers = helpers;
72 | }
73 | }
74 |
75 | /**
76 | * 打开串口
77 | *
78 | * @param devicePath 串口号
79 | * @param baudRate 波特率
80 | */
81 | public boolean openSerialPort(String devicePath, int baudRate) {
82 | closeSerialPort();
83 | SerialPortLogUtil.i(TAG, "openSerialPort: " + String.format("打开串口 %s 波特率 %s", devicePath, baudRate));
84 | // 校验串口权限
85 | if (!checkSerialPortPermission(devicePath)) {
86 | return false;
87 | }
88 | try {
89 | // 使用配置参数或默认值
90 | int flags = mSerialConfig != null ? mSerialConfig.getFlags() : 0;
91 | int databits = mSerialConfig != null ? mSerialConfig.getDatabits() : 8;
92 | int stopbits = mSerialConfig != null ? mSerialConfig.getStopbits() : 1;
93 | int parity = mSerialConfig != null ? mSerialConfig.getParity() : 0;
94 |
95 | SerialPortLogUtil.d(TAG, "串口参数 - flags: " + flags + ", databits: " + databits + ", stopbits: " + stopbits + ", parity: " + parity);
96 | mFd = open(devicePath, baudRate, flags, databits, stopbits, parity);
97 | mFileInputStream = new FileInputStream(mFd);
98 | mFileOutputStream = new FileOutputStream(mFd);
99 | SerialPortLogUtil.i(TAG, "openSerialPort: 串口已经打开 " + mFd);
100 | notifySerialPortOpened(new File(devicePath), SerialStatus.SUCCESS_OPENED);
101 | // 开启发送消息的线程
102 | startSendThread();
103 | // 开启接收消息的线程
104 | startReadThread();
105 | return true;
106 | } catch (Exception e) {
107 | e.printStackTrace();
108 | notifySerialPortOpened(new File(devicePath), SerialStatus.OPEN_FAIL);
109 | return false;
110 | }
111 | }
112 |
113 | /**
114 | * 检查串口权限
115 | *
116 | * @param devicePath 串口号
117 | * @return 是否有权限
118 | */
119 | private boolean checkSerialPortPermission(String devicePath) {
120 | File deviceFile = new File(devicePath);
121 | if (!deviceFile.canRead() || !deviceFile.canWrite()) {
122 | boolean chmod777 = chmod777(deviceFile);
123 | if (!chmod777) {
124 | SerialPortLogUtil.e(TAG, "串口权限不足: " + devicePath);
125 | notifySerialPortOpened(deviceFile, SerialStatus.NO_READ_WRITE_PERMISSION);
126 | return false;
127 | }
128 | }
129 | return true;
130 | }
131 |
132 | /**
133 | * 通知串口打开状态
134 | *
135 | * @param deviceFile 串口文件
136 | * @param status 打开状态
137 | */
138 | private void notifySerialPortOpened(File deviceFile, SerialStatus status) {
139 | if (null != mOnOpenSerialPortListener) {
140 | mOnOpenSerialPortListener.openState(mSerialPortEnum, deviceFile, status);
141 | }
142 | }
143 |
144 | /**
145 | * 检查串口是否已打开
146 | */
147 | public boolean isOpen() {
148 | return mFd != null && mFileInputStream != null && mFileOutputStream != null;
149 | }
150 |
151 | /**
152 | * 关闭串口
153 | */
154 | public void closeSerialPort() {
155 | if (null != mFd) {
156 | close();
157 | mFd = null;
158 | }
159 | // 停止发送消息的线程
160 | stopSendThread();
161 | // 停止接收消息的线程
162 | stopReadThread();
163 | closeStream(mFileInputStream);
164 | closeStream(mFileOutputStream);
165 | }
166 |
167 | /**
168 | * 关闭流
169 | *
170 | * @param stream 流对象
171 | */
172 | private void closeStream(Closeable stream) {
173 | if (null != stream) {
174 | try {
175 | stream.close();
176 | } catch (IOException e) {
177 | e.printStackTrace();
178 | }
179 | }
180 | }
181 |
182 | /**
183 | * 添加打开串口监听
184 | *
185 | * @param listener listener
186 | * @return SerialPortManager
187 | */
188 | public SerialPortManager setOnOpenSerialPortListener(OnOpenSerialPortListener listener) {
189 | mOnOpenSerialPortListener = listener;
190 | return this;
191 | }
192 |
193 | /**
194 | * 添加数据通信监听
195 | *
196 | * @param listener listener
197 | * @return SerialPortManager
198 | */
199 | public SerialPortManager setOnSerialPortDataListener(OnSerialPortDataListener listener) {
200 | mOnSerialPortDataListener = listener;
201 | return this;
202 | }
203 |
204 | /**
205 | * 开启发送消息的线程
206 | */
207 | private void startSendThread() {
208 | // 开启发送消息的线程
209 | mSendingHandlerThread = new HandlerThread("mSendingHandlerThread" + mSerialPortEnum.name());
210 | mSendingHandlerThread.start();
211 | // Handler
212 | mSendingHandler = new Handler(mSendingHandlerThread.getLooper()) {
213 | @Override
214 | public void handleMessage(Message msg) {
215 | byte[] sendBytes = (byte[]) msg.obj;
216 |
217 | if (null != mFileOutputStream && null != sendBytes && 0 < sendBytes.length) {
218 | try {
219 | mFileOutputStream.write(sendBytes);
220 | if (null != mOnSerialPortDataListener) {
221 | mOnSerialPortDataListener.onDataSent(sendBytes, mSerialPortEnum);
222 | }
223 | } catch (IOException e) {
224 | e.printStackTrace();
225 | }
226 | }
227 | }
228 | };
229 | }
230 |
231 | /**
232 | * 停止发送消息线程
233 | */
234 | private void stopSendThread() {
235 | mSendingHandler = null;
236 | if (null != mSendingHandlerThread) {
237 | mSendingHandlerThread.interrupt();
238 | mSendingHandlerThread.quit();
239 | mSendingHandlerThread = null;
240 | }
241 | }
242 |
243 | /**
244 | * 开启接收消息的线程
245 | */
246 | private void startReadThread() {
247 | mSerialPortReadThread = new SerialPortReadThread(mFileInputStream, mSerialPortEnum, mStickPackageHelpers) {
248 | @Override
249 | public void onDataReceived(byte[] bytes) {
250 | if (null != mOnSerialPortDataListener) {
251 | mOnSerialPortDataListener.onDataReceived(bytes, mSerialPortEnum);
252 | }
253 | }
254 | };
255 | mSerialPortReadThread.start();
256 | SerialPortLogUtil.d(TAG, "启动数据接收线程");
257 | }
258 |
259 | /**
260 | * 停止接收消息的线程
261 | */
262 | private void stopReadThread() {
263 | if (null != mSerialPortReadThread) {
264 | mSerialPortReadThread.release();
265 | }
266 | }
267 |
268 | /**
269 | * 发送数据
270 | *
271 | * @param sendBytes 发送数据
272 | * @return 发送是否成功
273 | */
274 | public boolean sendBytes(byte[] sendBytes) {
275 | if (null != mFd && null != mFileInputStream && null != mFileOutputStream) {
276 | if (null != mSendingHandler) {
277 | Message message = Message.obtain();
278 | message.obj = sendBytes;
279 | return mSendingHandler.sendMessage(message);
280 | }
281 | }
282 | return false;
283 | }
284 |
285 | }
--------------------------------------------------------------------------------
/serial_lib/src/main/java/com/cl/serialportlibrary/example/MultiSerialPortExample.java:
--------------------------------------------------------------------------------
1 | package com.cl.serialportlibrary.example;
2 |
3 | import android.app.Application;
4 | import android.util.Log;
5 |
6 | import com.cl.serialportlibrary.MultiSerialPortManager;
7 | import com.cl.serialportlibrary.enumerate.SerialStatus;
8 | import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
9 | import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
10 | import com.cl.serialportlibrary.stick.SpecifiedStickPackageHelper;
11 | import com.cl.serialportlibrary.stick.StaticLenStickPackageHelper;
12 | import com.cl.serialportlibrary.stick.StickyPacketHelperFactory;
13 |
14 | /**
15 | * 多串口使用示例
16 | * 展示如何同时管理多个串口,每个串口使用不同的粘包处理策略
17 | */
18 | public class MultiSerialPortExample {
19 |
20 | private static final String TAG = "MultiSerialPortExample";
21 |
22 | /**
23 | * 示例1:基础多串口使用
24 | */
25 | public void basicMultiSerialExample(Application application) {
26 | MultiSerialPortManager manager = MultiSerialPortManager.getInstance();
27 |
28 | // 串口1:不需要粘包处理,用于简单的数据传输
29 | manager.openSerialPort("GPS", "/dev/ttyS1", 9600,
30 | new MultiSerialPortManager.SerialPortConfig.Builder()
31 | .setDatabits(8)
32 | .setParity(0)
33 | .setStopbits(1)
34 | .setStickyPacketHelpers(new BaseStickPackageHelper()) // 不处理粘包
35 | .build(),
36 | // 状态回调
37 | (serialId, success, status) -> {
38 | Log.i(TAG, String.format("串口[%s] 状态: %s", serialId, success ? "打开成功" : "打开失败"));
39 | },
40 | // 数据回调
41 | new MultiSerialPortManager.OnSerialPortDataCallback() {
42 | @Override
43 | public void onDataReceived(String serialId, byte[] data) {
44 | String gpsData = new String(data);
45 | Log.i(TAG, "GPS数据: " + gpsData);
46 | // 处理GPS数据
47 | }
48 | });
49 |
50 | // 串口2:需要按换行符分包,用于文本协议
51 | manager.openSerialPort("SENSOR", "/dev/ttyS2", 115200,
52 | new MultiSerialPortManager.SerialPortConfig.Builder()
53 | .setDatabits(8)
54 | .setParity(0)
55 | .setStopbits(1)
56 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) // 按换行符分包
57 | .build(),
58 | null, // 不需要状态回调
59 | new MultiSerialPortManager.OnSerialPortDataCallback() {
60 | @Override
61 | public void onDataReceived(String serialId, byte[] data) {
62 | String sensorData = new String(data).trim();
63 | Log.i(TAG, "传感器数据: " + sensorData);
64 | // 处理传感器数据
65 | }
66 | });
67 |
68 | // 发送数据到不同串口
69 | manager.sendData("GPS", "AT+GPS\r\n");
70 | manager.sendData("SENSOR", "READ_TEMP\n");
71 | }
72 |
73 | /**
74 | * 示例2:复杂的多串口场景
75 | */
76 | public void advancedMultiSerialExample(Application application) {
77 | MultiSerialPortManager manager = MultiSerialPortManager.getInstance();
78 |
79 | // 串口1:Modbus RTU协议,固定长度包
80 | manager.openSerialPort("MODBUS", "/dev/ttyS3", 9600,
81 | new MultiSerialPortManager.SerialPortConfig.Builder()
82 | .setDatabits(8)
83 | .setParity(2) // 偶校验
84 | .setStopbits(1)
85 | .setStickyPacketHelpers(new StaticLenStickPackageHelper(8)) // 固定8字节
86 | .build(),
87 | (serialId, success, status) -> {
88 | if (success) {
89 | Log.i(TAG, "Modbus串口打开成功");
90 | // 发送读取寄存器命令
91 | byte[] readCmd = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, (byte)0x84, 0x0A};
92 | manager.sendData("MODBUS", readCmd);
93 | }
94 | },
95 | new MultiSerialPortManager.OnSerialPortDataCallback() {
96 | @Override
97 | public void onDataReceived(String serialId, byte[] data) {
98 | Log.i(TAG, "Modbus响应: " + bytesToHex(data));
99 | // 解析Modbus响应
100 | parseModbusResponse(data);
101 | }
102 | });
103 |
104 | // 串口2:自定义协议,可变长度包
105 | manager.openSerialPort("CUSTOM", "/dev/ttyS4", 115200,
106 | new MultiSerialPortManager.SerialPortConfig.Builder()
107 | .setDatabits(8)
108 | .setParity(0)
109 | .setStopbits(1)
110 | .setStickyPacketHelpers(
111 | StickyPacketHelperFactory.createVariableLength(
112 | java.nio.ByteOrder.BIG_ENDIAN, 2, 2, 12)) // 可变长度包
113 | .build(),
114 | null,
115 | new MultiSerialPortManager.OnSerialPortDataCallback() {
116 | @Override
117 | public void onDataReceived(String serialId, byte[] data) {
118 | Log.i(TAG, "自定义协议数据: " + bytesToHex(data));
119 | // 处理自定义协议数据
120 | parseCustomProtocol(data);
121 | }
122 | });
123 |
124 | // 串口3:AT命令,多种分隔符
125 | manager.openSerialPort("MODEM", "/dev/ttyS5", 115200,
126 | new MultiSerialPortManager.SerialPortConfig.Builder()
127 | .setDatabits(8)
128 | .setParity(0)
129 | .setStopbits(1)
130 | .setStickyPacketHelpers(
131 | StickyPacketHelperFactory.Common.createATCommand()) // AT命令分包
132 | .build(),
133 | null,
134 | new MultiSerialPortManager.OnSerialPortDataCallback() {
135 | @Override
136 | public void onDataReceived(String serialId, byte[] data) {
137 | String response = new String(data).trim();
138 | Log.i(TAG, "AT响应: " + response);
139 | // 处理AT命令响应
140 | parseATResponse(response);
141 | }
142 | });
143 | }
144 |
145 | /**
146 | * 示例3:动态管理串口
147 | */
148 | public void dynamicSerialManagement() {
149 | MultiSerialPortManager manager = MultiSerialPortManager.getInstance();
150 |
151 | // 创建一个通用的数据回调
152 | MultiSerialPortManager.OnSerialPortDataCallback commonCallback =
153 | new MultiSerialPortManager.OnSerialPortDataCallback() {
154 | @Override
155 | public void onDataReceived(String serialId, byte[] data) {
156 | Log.i(TAG, String.format("串口[%s] 收到数据: %s", serialId, new String(data)));
157 | }
158 |
159 | @Override
160 | public void onDataSent(String serialId, byte[] data) {
161 | Log.d(TAG, String.format("串口[%s] 发送数据: %s", serialId, new String(data)));
162 | }
163 | };
164 |
165 | // 批量打开串口
166 | String[] devices = {"/dev/ttyS1", "/dev/ttyS2", "/dev/ttyS3"};
167 | int[] baudRates = {9600, 115200, 57600};
168 |
169 | for (int i = 0; i < devices.length; i++) {
170 | String serialId = "SERIAL_" + (i + 1);
171 |
172 | // 根据不同需求配置不同的粘包处理
173 | MultiSerialPortManager.SerialPortConfig config;
174 | switch (i) {
175 | case 0: // 第一个串口不需要粘包处理
176 | config = new MultiSerialPortManager.SerialPortConfig.Builder()
177 | .setStickyPacketHelpers(new BaseStickPackageHelper())
178 | .build();
179 | break;
180 | case 1: // 第二个串口需要换行符分包
181 | config = new MultiSerialPortManager.SerialPortConfig.Builder()
182 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n"))
183 | .build();
184 | break;
185 | case 2: // 第三个串口需要固定长度分包
186 | config = new MultiSerialPortManager.SerialPortConfig.Builder()
187 | .setStickyPacketHelpers(new StaticLenStickPackageHelper(16))
188 | .build();
189 | break;
190 | default:
191 | config = new MultiSerialPortManager.SerialPortConfig.Builder().build();
192 | break;
193 | }
194 |
195 | manager.openSerialPort(serialId, devices[i], baudRates[i], config, null, commonCallback);
196 | }
197 |
198 | // 打印所有串口状态
199 | manager.printAllSerialStatus();
200 |
201 | // 向所有串口发送测试数据
202 | for (String serialId : manager.getOpenedSerialPorts()) {
203 | manager.sendData(serialId, "Hello from " + serialId + "\n");
204 | }
205 |
206 | // 动态更新某个串口的粘包处理器
207 | manager.updateStickyPacketHelpers("SERIAL_1",
208 | new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("END")});
209 | }
210 |
211 | /**
212 | * 示例4:串口数据路由
213 | */
214 | public void serialDataRouting() {
215 | MultiSerialPortManager manager = MultiSerialPortManager.getInstance();
216 |
217 | // 主控制串口:接收外部命令
218 | manager.openSerialPort("MAIN_CTRL", "/dev/ttyS1", 115200,
219 | new MultiSerialPortManager.SerialPortConfig.Builder()
220 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
221 | .build(),
222 | null,
223 | new MultiSerialPortManager.OnSerialPortDataCallback() {
224 | @Override
225 | public void onDataReceived(String serialId, byte[] data) {
226 | String command = new String(data).trim();
227 | Log.i(TAG, "主控制命令: " + command);
228 |
229 | // 根据命令路由到不同的串口
230 | if (command.startsWith("GPS:")) {
231 | String gpsCmd = command.substring(4);
232 | manager.sendData("GPS_MODULE", gpsCmd + "\r\n");
233 | } else if (command.startsWith("SENSOR:")) {
234 | String sensorCmd = command.substring(7);
235 | manager.sendData("SENSOR_MODULE", sensorCmd + "\n");
236 | }
237 | }
238 | });
239 |
240 | // GPS模块串口
241 | manager.openSerialPort("GPS_MODULE", "/dev/ttyS2", 9600,
242 | new MultiSerialPortManager.SerialPortConfig.Builder()
243 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
244 | .build(),
245 | null,
246 | new MultiSerialPortManager.OnSerialPortDataCallback() {
247 | @Override
248 | public void onDataReceived(String serialId, byte[] data) {
249 | String gpsResponse = new String(data).trim();
250 | Log.i(TAG, "GPS响应: " + gpsResponse);
251 | // 将GPS响应转发到主控制串口
252 | manager.sendData("MAIN_CTRL", "GPS_RESP:" + gpsResponse + "\r\n");
253 | }
254 | });
255 |
256 | // 传感器模块串口
257 | manager.openSerialPort("SENSOR_MODULE", "/dev/ttyS3", 115200,
258 | new MultiSerialPortManager.SerialPortConfig.Builder()
259 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n"))
260 | .build(),
261 | null,
262 | new MultiSerialPortManager.OnSerialPortDataCallback() {
263 | @Override
264 | public void onDataReceived(String serialId, byte[] data) {
265 | String sensorResponse = new String(data).trim();
266 | Log.i(TAG, "传感器响应: " + sensorResponse);
267 | // 将传感器响应转发到主控制串口
268 | manager.sendData("MAIN_CTRL", "SENSOR_RESP:" + sensorResponse + "\r\n");
269 | }
270 | });
271 | }
272 |
273 | /**
274 | * 清理资源
275 | */
276 | public void cleanup() {
277 | // 关闭所有串口
278 | MultiSerialPortManager.getInstance().closeAllSerialPorts();
279 | Log.i(TAG, "所有串口已关闭");
280 | }
281 |
282 | // 辅助方法
283 | private String bytesToHex(byte[] bytes) {
284 | StringBuilder sb = new StringBuilder();
285 | for (byte b : bytes) {
286 | sb.append(String.format("%02X ", b));
287 | }
288 | return sb.toString().trim();
289 | }
290 |
291 | private void parseModbusResponse(byte[] data) {
292 | // Modbus响应解析逻辑
293 | if (data.length >= 3) {
294 | int slaveId = data[0] & 0xFF;
295 | int functionCode = data[1] & 0xFF;
296 | Log.i(TAG, String.format("Modbus - 从机ID: %d, 功能码: %d", slaveId, functionCode));
297 | }
298 | }
299 |
300 | private void parseCustomProtocol(byte[] data) {
301 | // 自定义协议解析逻辑
302 | Log.i(TAG, "解析自定义协议数据,长度: " + data.length);
303 | }
304 |
305 | private void parseATResponse(String response) {
306 | // AT命令响应解析逻辑
307 | if (response.equals("OK")) {
308 | Log.i(TAG, "AT命令执行成功");
309 | } else if (response.equals("ERROR")) {
310 | Log.e(TAG, "AT命令执行失败");
311 | } else if (response.startsWith("+")) {
312 | Log.i(TAG, "AT命令数据响应: " + response);
313 | }
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/README4.1.1.md:
--------------------------------------------------------------------------------
1 | # Android串口通信框架 SerialPort v4.1.1
2 |
3 | [](https://github.com/cl-6666/serialPort)
4 | [](https://android-arsenal.com/api?level=21)
5 |
6 | > 这是串口通信框架的4.1.1稳定版本文档。如果你是新用户,建议使用最新的 [5.0.0版本](README.md),它提供了更简单的API和更强大的功能。
7 |
8 | ⚠️ **注意**: 4.1.1版本为历史版本,仅用于维护现有项目。新项目请使用 [5.0.0版本](README.md)。
9 |
10 | ## 📖 版本说明
11 |
12 | - **当前版本**: 4.1.1 (稳定维护版本)
13 | - **推荐版本**: [5.0.0版本](README.md) - 功能更强大,API更简单
14 |
15 | ## 🚀 快速开始
16 |
17 | ### 依赖集成
18 |
19 | 在项目的 `build.gradle` 中添加依赖:
20 |
21 | ```gradle
22 | dependencies {
23 | implementation 'com.github.cl-6666:serialPort:4.1.1'
24 | }
25 | ```
26 |
27 | 在项目根目录的 `build.gradle` 中添加:
28 |
29 | ```gradle
30 | allprojects {
31 | repositories {
32 | maven { url 'https://jitpack.io' }
33 | }
34 | }
35 | ```
36 |
37 | ### 权限配置
38 |
39 | 在 `AndroidManifest.xml` 中添加必要权限:
40 |
41 | ```xml
42 |
43 |
44 | ```
45 |
46 | ## 📚 使用指南
47 |
48 | ### 1. Application中初始化
49 |
50 | ```java
51 | public class App extends Application {
52 |
53 | @Override
54 | public void onCreate() {
55 | super.onCreate();
56 |
57 | // 方式1:使用XLogConfig配置日志
58 | XLogConfig logConfig = new XLogConfig.Builder()
59 | .logSwitch(true) // 开启日志
60 | .tag("SerialPort") // 设置tag
61 | .build();
62 |
63 | SerialConfig serialConfig = new SerialConfig.Builder()
64 | .setXLogConfig(logConfig) // 配置日志参数
65 | .setIntervalSleep(50) // 设置读取间隔
66 | .setSerialPortReconnection(false) // 是否开启串口重连
67 | .setFlags(0) // 标志位
68 | .setDatabits(8) // 数据位
69 | .setStopbits(1) // 停止位
70 | .setParity(0) // 校验位:0无校验,1奇校验,2偶校验
71 | .build();
72 |
73 | SerialUtils.getInstance().init(this, serialConfig);
74 |
75 | // 方式2:简化初始化
76 | SerialUtils.getInstance().init(this, true, "SerialPort", 50, 8, 0, 1);
77 | }
78 | }
79 | ```
80 |
81 | ### 2. 单串口使用
82 |
83 | ```java
84 | public class MainActivity extends AppCompatActivity {
85 |
86 | @Override
87 | protected void onCreate(Bundle savedInstanceState) {
88 | super.onCreate(savedInstanceState);
89 | setContentView(R.layout.activity_main);
90 |
91 | // 设置串口监听
92 | SerialUtils.getInstance().setmSerialPortDirectorListens(new SerialPortDirectorListens() {
93 | @Override
94 | public void onSerialPortOpenSuccess(File device, SerialPortEnum serialPortEnum) {
95 | Log.i("Serial", "串口打开成功: " + device.getPath());
96 | }
97 |
98 | @Override
99 | public void onSerialPortOpenFail(File device, SerialStatus status) {
100 | Log.e("Serial", "串口打开失败: " + status);
101 | }
102 |
103 | @Override
104 | public void onDataReceive(byte[] bytes, SerialPortEnum serialPortEnum) {
105 | String data = new String(bytes);
106 | Log.i("Serial", "接收数据: " + data);
107 | // 处理接收到的数据
108 | }
109 |
110 | @Override
111 | public void onDataSend(byte[] bytes, SerialPortEnum serialPortEnum) {
112 | Log.i("Serial", "发送数据: " + new String(bytes));
113 | }
114 | });
115 |
116 | // 打开串口
117 | List deviceList = new ArrayList<>();
118 | deviceList.add(new Device("/dev/ttyS4", "115200", new File("/dev/ttyS4")));
119 | SerialUtils.getInstance().manyOpenSerialPort(deviceList);
120 | }
121 |
122 | // 发送数据
123 | private void sendData() {
124 | String data = "Hello World";
125 | SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, data.getBytes());
126 | }
127 |
128 | @Override
129 | protected void onDestroy() {
130 | super.onDestroy();
131 | // 关闭串口
132 | SerialUtils.getInstance().serialPortClose();
133 | }
134 | }
135 | ```
136 |
137 | ### 3. 多串口使用
138 |
139 | ```java
140 | public class MultiSerialActivity extends AppCompatActivity {
141 |
142 | @Override
143 | protected void onCreate(Bundle savedInstanceState) {
144 | super.onCreate(savedInstanceState);
145 |
146 | // 设置粘包处理
147 | SerialUtils.getInstance().setStickPackageHelper(
148 | new BaseStickPackageHelper(), // 串口1:不处理粘包
149 | new SpecifiedStickPackageHelper("\n"), // 串口2:换行符分包
150 | new StaticLenStickPackageHelper(8) // 串口3:固定8字节
151 | );
152 |
153 | // 设置监听
154 | SerialUtils.getInstance().setmSerialPortDirectorListens(new SerialPortDirectorListens() {
155 | @Override
156 | public void onSerialPortOpenSuccess(File device, SerialPortEnum serialPortEnum) {
157 | Log.i("Serial", "串口[" + serialPortEnum + "]打开成功: " + device.getPath());
158 | }
159 |
160 | @Override
161 | public void onSerialPortOpenFail(File device, SerialStatus status) {
162 | Log.e("Serial", "串口打开失败: " + status);
163 | }
164 |
165 | @Override
166 | public void onDataReceive(byte[] bytes, SerialPortEnum serialPortEnum) {
167 | String data = new String(bytes);
168 | Log.i("Serial", "串口[" + serialPortEnum + "]收到: " + data);
169 |
170 | // 根据串口类型处理数据
171 | switch (serialPortEnum) {
172 | case SERIAL_ONE:
173 | handleGpsData(data);
174 | break;
175 | case SERIAL_TWO:
176 | handleSensorData(data);
177 | break;
178 | case SERIAL_THREE:
179 | handleModbusData(bytes);
180 | break;
181 | }
182 | }
183 |
184 | @Override
185 | public void onDataSend(byte[] bytes, SerialPortEnum serialPortEnum) {
186 | Log.i("Serial", "串口[" + serialPortEnum + "]发送: " + new String(bytes));
187 | }
188 | });
189 |
190 | // 打开多个串口
191 | List deviceList = new ArrayList<>();
192 | deviceList.add(new Device("/dev/ttyS1", "9600", new File("/dev/ttyS1"))); // GPS
193 | deviceList.add(new Device("/dev/ttyS2", "115200", new File("/dev/ttyS2"))); // 传感器
194 | deviceList.add(new Device("/dev/ttyS3", "9600", new File("/dev/ttyS3"))); // Modbus
195 |
196 | SerialUtils.getInstance().manyOpenSerialPort(deviceList);
197 | }
198 |
199 | // 向不同串口发送数据
200 | private void sendToSerial() {
201 | // 向串口1发送GPS命令
202 | SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, "AT+GPS?\r\n".getBytes());
203 |
204 | // 向串口2发送传感器命令
205 | SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_TWO, "READ_TEMP\n".getBytes());
206 |
207 | // 向串口3发送Modbus命令
208 | byte[] modbusCmd = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, (byte)0x84, 0x0A};
209 | SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_THREE, modbusCmd);
210 | }
211 |
212 | private void handleGpsData(String data) {
213 | // 处理GPS数据
214 | }
215 |
216 | private void handleSensorData(String data) {
217 | // 处理传感器数据
218 | }
219 |
220 | private void handleModbusData(byte[] data) {
221 | // 处理Modbus数据
222 | }
223 | }
224 | ```
225 |
226 | ### 4. 粘包处理配置
227 |
228 | ```java
229 | public class StickyPacketConfig {
230 |
231 | public void configureStickyPacket() {
232 | // 1. 不处理粘包(默认)
233 | SerialUtils.getInstance().setStickPackageHelper(new BaseStickPackageHelper());
234 |
235 | // 2. 按分隔符分包
236 | SerialUtils.getInstance().setStickPackageHelper(
237 | new SpecifiedStickPackageHelper("\r\n".getBytes()) // 按\r\n分包
238 | );
239 |
240 | // 3. 固定长度分包
241 | SerialUtils.getInstance().setStickPackageHelper(
242 | new StaticLenStickPackageHelper(16) // 固定16字节
243 | );
244 |
245 | // 4. 可变长度分包
246 | SerialUtils.getInstance().setStickPackageHelper(
247 | new VariableLenStickPackageHelper(
248 | java.nio.ByteOrder.BIG_ENDIAN, // 字节序
249 | 2, // 长度字段大小
250 | 2, // 长度字段位置
251 | 12 // 包头长度
252 | )
253 | );
254 |
255 | // 5. 多串口不同策略
256 | SerialUtils.getInstance().setStickPackageHelper(
257 | new BaseStickPackageHelper(), // 串口1:不处理
258 | new SpecifiedStickPackageHelper("\n"), // 串口2:换行符
259 | new StaticLenStickPackageHelper(8), // 串口3:固定长度
260 | new VariableLenStickPackageHelper( // 串口4:可变长度
261 | java.nio.ByteOrder.BIG_ENDIAN, 2, 2, 12)
262 | );
263 | }
264 | }
265 | ```
266 |
267 | ### 5. 串口参数配置
268 |
269 | ```java
270 | public class SerialParamConfig {
271 |
272 | public void configureParams() {
273 | SerialConfig serialConfig = new SerialConfig.Builder()
274 | .setIntervalSleep(50) // 读取间隔50ms
275 | .setDatabits(8) // 数据位8
276 | .setStopbits(1) // 停止位1
277 | .setParity(0) // 校验位:0=无校验
278 | .setFlags(0) // 标志位
279 | .setSerialPortReconnection(false) // 是否重连
280 | .build();
281 |
282 | SerialUtils.getInstance().init(getApplication(), serialConfig);
283 | }
284 |
285 | // 常用配置
286 | public void commonConfigs() {
287 | // 标准配置 8N1
288 | SerialConfig config8N1 = new SerialConfig.Builder()
289 | .setDatabits(8).setParity(0).setStopbits(1)
290 | .build();
291 |
292 | // Modbus RTU 8E1
293 | SerialConfig configModbus = new SerialConfig.Builder()
294 | .setDatabits(8).setParity(2).setStopbits(1) // 偶校验
295 | .build();
296 |
297 | // 老式设备 7E2
298 | SerialConfig configOld = new SerialConfig.Builder()
299 | .setDatabits(7).setParity(2).setStopbits(2)
300 | .build();
301 | }
302 | }
303 | ```
304 |
305 | ## 📖 API参考
306 |
307 | ### SerialUtils 主要方法
308 | | 方法 | 说明 |
309 | |------|------|
310 | | `init(Application, SerialConfig)` | 初始化串口框架 |
311 | | `init(Application, boolean, String, int, int, int, int)` | 简化初始化 |
312 | | `setmSerialPortDirectorListens(SerialPortDirectorListens)` | 设置串口监听 |
313 | | `setStickPackageHelper(AbsStickPackageHelper...)` | 设置粘包处理 |
314 | | `manyOpenSerialPort(List)` | 打开多个串口 |
315 | | `sendData(SerialPortEnum, byte[])` | 发送数据 |
316 | | `serialPortClose()` | 关闭串口 |
317 |
318 | ### SerialConfig 配置项
319 | | 参数 | 说明 | 默认值 |
320 | |------|------|--------|
321 | | `intervalSleep` | 读取间隔(ms) | 50 |
322 | | `databits` | 数据位 | 8 |
323 | | `stopbits` | 停止位 | 1 |
324 | | `parity` | 校验位 | 0 |
325 | | `flags` | 标志位 | 0 |
326 | | `serialPortReconnection` | 是否重连 | false |
327 |
328 | ### 粘包处理器
329 | | 类型 | 说明 | 适用场景 |
330 | |------|------|----------|
331 | | `BaseStickPackageHelper` | 不处理粘包 | 简单数据流 |
332 | | `SpecifiedStickPackageHelper` | 分隔符分包 | 文本协议 |
333 | | `StaticLenStickPackageHelper` | 固定长度分包 | 固定格式协议 |
334 | | `VariableLenStickPackageHelper` | 可变长度分包 | 复杂二进制协议 |
335 |
336 | ## 🛠️ 故障排查
337 |
338 | ### 常见问题
339 |
340 | 1. **串口打开失败**
341 | ```java
342 | // 检查设备路径
343 | String[] devices = new SerialPortFinder().getAllDevicesPath();
344 |
345 | // 检查权限
346 | File deviceFile = new File("/dev/ttyS4");
347 | if (!deviceFile.canRead() || !deviceFile.canWrite()) {
348 | Log.e("Serial", "设备权限不足");
349 | }
350 | ```
351 |
352 | 2. **数据接收不完整**
353 | ```java
354 | // 尝试不处理粘包
355 | SerialUtils.getInstance().setStickPackageHelper(new BaseStickPackageHelper());
356 |
357 | // 或者调整读取间隔
358 | SerialConfig config = new SerialConfig.Builder()
359 | .setIntervalSleep(20) // 减少到20ms
360 | .build();
361 | ```
362 |
363 | 3. **日志输出问题**
364 | ```java
365 | // 确保启用日志
366 | XLogConfig logConfig = new XLogConfig.Builder()
367 | .logSwitch(true)
368 | .tag("SerialPort")
369 | .build();
370 | ```
371 |
372 | ## 🎯 升级到5.0.0
373 |
374 | 如果你想升级到最新的5.0.0版本,以下是主要的变化:
375 |
376 | ### 4.1.1版本
377 | ```java
378 | // 初始化
379 | SerialUtils.getInstance().init(this, true, "TAG", 50, 8, 0, 1);
380 |
381 | // 使用
382 | SerialUtils.getInstance().setmSerialPortDirectorListens(...);
383 | SerialUtils.getInstance().manyOpenSerialPort(deviceList);
384 | SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, data);
385 | ```
386 |
387 | ### 5.0.0版本 (推荐)
388 | ```java
389 | // 简化初始化
390 | new SimpleSerialPortManager.QuickConfig()
391 | .setDatabits(8).setParity(0).setStopbits(1)
392 | .apply(this);
393 |
394 | // 简化使用
395 | SimpleSerialPortManager.getInstance()
396 | .openSerialPort("/dev/ttyS4", 115200, data -> {
397 | // 处理数据
398 | });
399 |
400 | // 多串口
401 | MultiSerialPortManager manager = SimpleSerialPortManager.multi();
402 | manager.openSerialPort("GPS", "/dev/ttyS1", 9600, config, statusCallback, dataCallback);
403 | ```
404 |
405 | **升级优势**:
406 | - API更简单易用
407 | - 支持真正的多串口管理
408 | - 更好的错误处理
409 | - 增强的日志系统
410 | - 更高的性能
411 |
412 | 详细的5.0.0版本使用请查看 [最新文档](README.md)。
413 |
414 | ## 📞 联系我们
415 |
416 | - **QQ群**: 458173716
417 | - **博客**: https://blog.csdn.net/a214024475/article/details/113735085
418 | - **GitHub**: https://github.com/cl-6666/serialPort
419 |
420 | ## 📄 许可证
421 |
422 | ```
423 | Licensed under the Apache License, Version 2.0
424 | ```
425 |
426 | ---
427 |
428 | ⚠️ **再次提醒**: 4.1.1版本为历史版本,新项目建议使用 [5.0.0版本](README.md),功能更强大,使用更简单!
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android串口通信框架 SerialPort
2 |
3 | [](https://github.com/cl-6666/serialPort)
4 | [](https://android-arsenal.com/api?level=21)
5 | [](https://www.apache.org/licenses/LICENSE-2.0)
6 |
7 | > 一个灵活、高效并且轻量的Android串口通信框架,让串口操作变得简单易用。支持单串口、多串口、粘包处理、自定义配置等功能。
8 |
9 |
10 |
11 |
12 | ## ⭐ 特性
13 |
14 | - 🚀 **简单易用** - 链式调用,一行代码完成配置
15 | - 🔧 **多串口支持** - 同时管理多个串口,独立配置
16 | - 📦 **智能粘包处理** - 支持多种粘包策略,可动态切换
17 | - ⚡ **高性能** - 多线程处理,线程安全设计
18 | - 🛡️ **稳定可靠** - 完善的错误处理和资源管理
19 | - 📝 **详细日志** - 丰富的调试信息,方便排查问题
20 | - 🎯 **灵活配置** - 支持数据位、校验位、停止位等参数配置
21 |
22 | ## 📖 版本说明
23 |
24 | - **当前版本**: 5.0.0 (推荐) - 全新架构,功能强大
25 | - **历史版本**: [4.1.1版本文档](README4.1.1.md) - 稳定版本
26 |
27 | ### 5.0.0 版本重大更新 🎉
28 |
29 | - ✅ **架构重构**: 移除SerialUtils依赖,架构更清晰
30 | - ✅ **API简化**: 新增SimpleSerialPortManager,使用更简单
31 | - ✅ **多串口管理**: 全新MultiSerialPortManager,支持复杂场景
32 | - ✅ **增强日志**: 自研日志系统,调试信息更丰富
33 | - ✅ **独立配置**: 每个串口可独立配置粘包处理策略
34 | - ✅ **性能优化**: 减少30%冗余代码,性能提升显著
35 |
36 | ## 🚀 快速开始
37 |
38 | ### 依赖集成
39 |
40 | 在项目的 `build.gradle` 中添加依赖:
41 |
42 | ```gradle
43 | dependencies {
44 | implementation 'com.github.cl-6666:serialPort:v5.0.7'
45 | }
46 | ```
47 |
48 | 在项目根目录的 `build.gradle` 中添加:
49 |
50 | ```gradle
51 | allprojects {
52 | repositories {
53 | maven { url 'https://jitpack.io' }
54 | }
55 | }
56 | ```
57 |
58 | ### 权限配置
59 |
60 | 在 `AndroidManifest.xml` 中添加必要权限:
61 |
62 | ```xml
63 |
64 |
65 | ```
66 |
67 | ## 📚 使用指南
68 |
69 | ### 1️⃣ 单串口使用 - 基础示例
70 |
71 | #### 最简单的使用方式
72 |
73 | ```java
74 | public class MainActivity extends AppCompatActivity {
75 |
76 | @Override
77 | protected void onCreate(Bundle savedInstanceState) {
78 | super.onCreate(savedInstanceState);
79 |
80 | // 一行代码打开串口并接收数据
81 | SimpleSerialPortManager.getInstance()
82 | .openSerialPort("/dev/ttyS4", 115200, data -> {
83 | String receivedData = new String(data);
84 | Log.i("Serial", "收到数据: " + receivedData);
85 | // 处理接收到的数据
86 | });
87 | }
88 |
89 | // 发送数据
90 | private void sendData() {
91 | SimpleSerialPortManager.getInstance().sendData("Hello World");
92 | }
93 |
94 | @Override
95 | protected void onDestroy() {
96 | super.onDestroy();
97 | // 关闭串口
98 | SimpleSerialPortManager.getInstance().closeSerialPort();
99 | }
100 | }
101 | ```
102 |
103 | #### 完整配置示例
104 |
105 | ```java
106 | public class App extends Application {
107 |
108 | @Override
109 | public void onCreate() {
110 | super.onCreate();
111 |
112 | // 全局配置(可选)
113 | new SimpleSerialPortManager.QuickConfig()
114 | .setIntervalSleep(50) // 读取间隔50ms
115 | .setEnableLog(true) // 启用日志
116 | .setLogTag("SerialPortApp") // 设置日志标签
117 | .setDatabits(8) // 数据位8
118 | .setParity(0) // 无校验
119 | .setStopbits(1) // 停止位1
120 | .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING)
121 | .apply(this);
122 | }
123 | }
124 | ```
125 |
126 | ### 2️⃣ 数据位、校验位、停止位配置
127 |
128 | ```java
129 | public class SerialConfigExample {
130 |
131 | public void configureSerialParams() {
132 | SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance();
133 |
134 | // 方式1:使用QuickConfig配置
135 | new SimpleSerialPortManager.QuickConfig()
136 | .setDatabits(8) // 数据位:5, 6, 7, 8
137 | .setParity(0) // 校验位:0=无校验, 1=奇校验, 2=偶校验
138 | .setStopbits(1) // 停止位:1 或 2
139 | .setFlags(0) // 标志位
140 | .apply(getApplication());
141 |
142 | // 方式2:动态设置
143 | manager.setDatabits(8) // 设置数据位
144 | .setParity(2) // 设置偶校验
145 | .setStopbits(1) // 设置停止位1
146 | .setFlags(0); // 设置标志位
147 |
148 | // 打开串口
149 | manager.openSerialPort("/dev/ttyS4", 115200, data -> {
150 | Log.i("Serial", "数据: " + new String(data));
151 | });
152 | }
153 |
154 | // 常用配置组合
155 | public void commonConfigurations() {
156 | SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance();
157 |
158 | // 标准配置 8N1 (8数据位, 无校验, 1停止位)
159 | manager.setDatabits(8).setParity(0).setStopbits(1);
160 |
161 | // Modbus RTU 8E1 (8数据位, 偶校验, 1停止位)
162 | manager.setDatabits(8).setParity(2).setStopbits(1);
163 |
164 | // 老式设备 7E2 (7数据位, 偶校验, 2停止位)
165 | manager.setDatabits(7).setParity(2).setStopbits(2);
166 | }
167 | }
168 | ```
169 |
170 | ### 3️⃣ 粘包处理详解
171 |
172 | 粘包是串口通信中常见的问题,5.0.0版本提供了多种处理策略:
173 |
174 | ```java
175 | public class StickyPacketExample {
176 |
177 | public void noProcessing() {
178 | // 策略1:不处理粘包 - 适用于简单数据流
179 | new SimpleSerialPortManager.QuickConfig()
180 | .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING)
181 | .apply(this);
182 | }
183 |
184 | public void delimiterBased() {
185 | // 策略2:基于分隔符 - 适用于文本协议
186 | new SimpleSerialPortManager.QuickConfig()
187 | .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED)
188 | .apply(this);
189 |
190 | // 自定义分隔符
191 | SimpleSerialPortManager.getInstance()
192 | .configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED);
193 | }
194 |
195 | public void fixedLength() {
196 | // 策略3:固定长度 - 适用于固定长度协议
197 | new SimpleSerialPortManager.QuickConfig()
198 | .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.FIXED_LENGTH)
199 | .apply(this);
200 | }
201 |
202 | public void variableLength() {
203 | // 策略4:可变长度 - 适用于带长度字段的协议
204 | new SimpleSerialPortManager.QuickConfig()
205 | .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.VARIABLE_LENGTH)
206 | .apply(this);
207 | }
208 | }
209 | ```
210 |
211 | ### 4️⃣ 多串口管理 - 强大功能
212 |
213 | ```java
214 | public class MultiSerialExample {
215 |
216 | public void basicMultiSerial() {
217 | MultiSerialPortManager manager = SimpleSerialPortManager.multi();
218 |
219 | // 串口1:GPS模块,不需要粘包处理
220 | manager.openSerialPort("GPS", "/dev/ttyS1", 9600,
221 | new MultiSerialPortManager.SerialPortConfig.Builder()
222 | .setDatabits(8)
223 | .setParity(0)
224 | .setStopbits(1)
225 | .setStickyPacketHelpers(new BaseStickPackageHelper()) // 不处理粘包
226 | .build(),
227 | // 状态回调
228 | (serialId, success, status) -> {
229 | Log.i("GPS", "状态: " + (success ? "成功" : "失败"));
230 | },
231 | // 数据回调
232 | (serialId, data) -> {
233 | String gpsData = new String(data);
234 | Log.i("GPS", "数据: " + gpsData);
235 | handleGpsData(gpsData);
236 | });
237 |
238 | // 串口2:传感器模块,需要换行符分包
239 | manager.openSerialPort("SENSOR", "/dev/ttyS2", 115200,
240 | new MultiSerialPortManager.SerialPortConfig.Builder()
241 | .setDatabits(8)
242 | .setParity(0)
243 | .setStopbits(1)
244 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) // 换行符分包
245 | .build(),
246 | null, // 不需要状态回调
247 | (serialId, data) -> {
248 | String sensorData = new String(data).trim();
249 | Log.i("SENSOR", "数据: " + sensorData);
250 | handleSensorData(sensorData);
251 | });
252 |
253 | // 发送数据到不同串口
254 | manager.sendData("GPS", "AT+GPS?\r\n");
255 | manager.sendData("SENSOR", "READ_TEMP\n");
256 | }
257 |
258 | // 动态管理串口
259 | public void dynamicManagement() {
260 | MultiSerialPortManager manager = SimpleSerialPortManager.multi();
261 |
262 | // 查看串口状态
263 | List openedPorts = manager.getOpenedSerialPorts();
264 | boolean isOpened = manager.isSerialPortOpened("GPS");
265 | manager.printAllSerialStatus();
266 |
267 | // 动态更新粘包策略
268 | manager.updateStickyPacketHelpers("GPS",
269 | new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("\r\n")});
270 |
271 | // 关闭特定串口
272 | manager.closeSerialPort("GPS");
273 |
274 | // 关闭所有串口
275 | manager.closeAllSerialPorts();
276 | }
277 | }
278 | ```
279 |
280 | ## 🎯 实际应用场景
281 |
282 | ### 工业控制场景
283 | ```java
284 | public class IndustrialControlExample {
285 |
286 | public void setupIndustrialPorts() {
287 | MultiSerialPortManager manager = SimpleSerialPortManager.multi();
288 |
289 | // PLC通信 - Modbus RTU
290 | manager.openSerialPort("PLC", "/dev/ttyS1", 9600,
291 | new MultiSerialPortManager.SerialPortConfig.Builder()
292 | .setDatabits(8).setParity(2).setStopbits(1) // 8E1
293 | .setStickyPacketHelpers(new StaticLenStickPackageHelper(8))
294 | .build(),
295 | null, this::handlePlcData);
296 |
297 | // 传感器数据采集 - 文本协议
298 | manager.openSerialPort("SENSORS", "/dev/ttyS3", 9600,
299 | new MultiSerialPortManager.SerialPortConfig.Builder()
300 | .setDatabits(7).setParity(2).setStopbits(1) // 7E1
301 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
302 | .build(),
303 | null, this::handleSensorData);
304 | }
305 | }
306 | ```
307 |
308 | ### 通信网关场景
309 | ```java
310 | public class GatewayExample {
311 |
312 | public void setupGateway() {
313 | MultiSerialPortManager manager = SimpleSerialPortManager.multi();
314 |
315 | // 上行通信(与服务器)
316 | manager.openSerialPort("UPLINK", "/dev/ttyS1", 115200,
317 | new MultiSerialPortManager.SerialPortConfig.Builder()
318 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n"))
319 | .build(),
320 | null, this::handleUplinkData);
321 |
322 | // 下行设备1 - GPS
323 | manager.openSerialPort("GPS", "/dev/ttyS2", 9600,
324 | new MultiSerialPortManager.SerialPortConfig.Builder()
325 | .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
326 | .build(),
327 | null, data -> forwardToUplink("GPS", data));
328 | }
329 |
330 | private void forwardToUplink(String deviceId, byte[] data) {
331 | String message = String.format("[%s]%s\n", deviceId, new String(data));
332 | SimpleSerialPortManager.multi().sendData("UPLINK", message);
333 | }
334 | }
335 | ```
336 |
337 | ## 🔧 高级功能
338 |
339 | ### 日志系统
340 | ```java
341 | // 启用详细日志
342 | SerialPortLogUtil.setDebugEnabled(true);
343 |
344 | // 自定义日志输出
345 | SerialPortLogUtil.i("MyTag", "自定义日志信息");
346 | SerialPortLogUtil.printData("发送", data); // 十六进制+ASCII显示
347 | SerialPortLogUtil.printSerialConfig("MySerial", 8, 0, 1, 0); // 配置信息
348 | ```
349 |
350 | ### 错误处理
351 | ```java
352 | manager.openSerialPort("TEST", "/dev/ttyS1", 9600,
353 | (serialId, success, status) -> {
354 | if (!success) {
355 | switch (status) {
356 | case NO_READ_WRITE_PERMISSION:
357 | Log.e("Serial", "权限不足");
358 | break;
359 | case OPEN_FAIL:
360 | Log.e("Serial", "打开失败");
361 | break;
362 | }
363 | }
364 | },
365 | dataCallback);
366 | ```
367 |
368 | ## 🛠️ 故障排查
369 |
370 | ### 常见问题
371 |
372 | 1. **串口打开失败**
373 | ```java
374 | // 检查设备路径
375 | String[] devices = new SerialPortFinder().getAllDevicesPath();
376 |
377 | // 检查权限
378 | File deviceFile = new File("/dev/ttyS4");
379 | boolean canRead = deviceFile.canRead();
380 | boolean canWrite = deviceFile.canWrite();
381 | ```
382 |
383 | 2. **数据接收不完整**
384 | ```java
385 | // 启用日志查看原始数据
386 | SerialPortLogUtil.setDebugEnabled(true);
387 |
388 | // 尝试不同的粘包策略
389 | manager.configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING);
390 | ```
391 |
392 | ## 📖 API参考
393 |
394 | ### SimpleSerialPortManager (单串口)
395 | | 方法 | 说明 |
396 | |------|------|
397 | | `getInstance()` | 获取单例实例 |
398 | | `openSerialPort(path, baudRate, callback)` | 打开串口 |
399 | | `sendData(data)` | 发送数据 |
400 | | `closeSerialPort()` | 关闭串口 |
401 | | `setDatabits(databits)` | 设置数据位 |
402 | | `setParity(parity)` | 设置校验位 |
403 | | `setStopbits(stopbits)` | 设置停止位 |
404 |
405 | ### MultiSerialPortManager (多串口)
406 | | 方法 | 说明 |
407 | |------|------|
408 | | `getInstance()` | 获取实例 |
409 | | `openSerialPort(id, path, baudRate, config, statusCallback, dataCallback)` | 打开串口 |
410 | | `sendData(serialId, data)` | 发送数据到指定串口 |
411 | | `closeSerialPort(serialId)` | 关闭指定串口 |
412 | | `closeAllSerialPorts()` | 关闭所有串口 |
413 | | `isSerialPortOpened(serialId)` | 检查串口状态 |
414 |
415 | ## 🎯 版本迁移
416 |
417 | ### 从4.1.1迁移到5.0.0
418 |
419 | **旧版本 (4.1.1)**:
420 | ```java
421 | // 在Application中初始化
422 | SerialUtils.getInstance().init(this, true, "TAG", 50, 8, 0, 1);
423 |
424 | // 使用
425 | SerialUtils.getInstance().setmSerialPortDirectorListens(...);
426 | SerialUtils.getInstance().manyOpenSerialPort(list);
427 | ```
428 |
429 | **新版本 (5.0.0)**:
430 | ```java
431 | // 简化的初始化(可选)
432 | new SimpleSerialPortManager.QuickConfig()
433 | .setDatabits(8).setParity(0).setStopbits(1)
434 | .apply(this);
435 |
436 | // 直接使用
437 | SimpleSerialPortManager.getInstance()
438 | .openSerialPort("/dev/ttyS4", 115200, data -> {
439 | // 处理数据
440 | });
441 | ```
442 |
443 | ## 📞 联系我们
444 |
445 | - **QQ群**: 458173716
446 | - **博客**: https://blog.csdn.net/a214024475/article/details/113735085
447 | - **GitHub**: https://github.com/cl-6666/serialPort
448 |
449 |
450 | ### PC端串口调试助手
451 |
452 |
453 | **下载链接**: https://pan.baidu.com/s/1DL2TOHz9bl9RIKIG3oCSWw?pwd=f7sh
454 |
455 | ### QQ技术交流群
456 |
457 |
458 | **QQ群号**: 458173716
459 |
460 | ---
461 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cl/myapplication/SingleSerialPortActivity.java:
--------------------------------------------------------------------------------
1 | package com.cl.myapplication;
2 |
3 | import android.app.Activity;
4 | import android.content.pm.PackageManager;
5 | import android.os.Bundle;
6 | import android.text.TextUtils;
7 | import android.view.View;
8 | import android.widget.AdapterView;
9 | import android.widget.ArrayAdapter;
10 |
11 | import androidx.appcompat.app.AppCompatActivity;
12 | import androidx.core.app.ActivityCompat;
13 | import androidx.databinding.DataBindingUtil;
14 | import androidx.fragment.app.FragmentManager;
15 |
16 | import com.cl.myapplication.adapter.SpAdapter;
17 | import com.cl.myapplication.constant.PreferenceKeys;
18 | import com.cl.myapplication.databinding.ActivityMainJavaBinding;
19 | import com.cl.myapplication.fragment.LogFragment;
20 | import com.cl.myapplication.message.ConversionNoticeEvent;
21 | import com.cl.myapplication.message.IMessage;
22 | import com.cl.myapplication.message.LogManager;
23 | import com.cl.myapplication.message.RecvMessage;
24 | import com.cl.myapplication.message.SendMessage;
25 | import com.cl.myapplication.util.PrefHelper;
26 | import com.hjq.toast.ToastUtils;
27 | import com.cl.serialportlibrary.Device;
28 | import com.cl.serialportlibrary.SerialPortFinder;
29 | import com.cl.serialportlibrary.SimpleSerialPortManager;
30 | import com.cl.serialportlibrary.utils.SerialPortLogUtil;
31 |
32 | import org.greenrobot.eventbus.EventBus;
33 | import org.greenrobot.eventbus.Subscribe;
34 | import org.greenrobot.eventbus.ThreadMode;
35 |
36 | import java.util.Arrays;
37 |
38 | /**
39 | * 单串口演示
40 | */
41 | public class SingleSerialPortActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener {
42 |
43 | private ActivityMainJavaBinding binding;
44 | private Device mDevice;
45 |
46 | private String[] mDevices;
47 | private String[] mBaudrates;
48 | private int mDeviceIndex;
49 | private int mBaudrateIndex;
50 | private boolean mOpened = false;
51 | private boolean mConversionNotice = true;
52 | private LogFragment mLogFragment;
53 |
54 | final String[] databits = new String[]{"8", "7", "6", "5"};
55 | final String[] paritys = new String[]{"NONE", "ODD", "EVEN", "SPACE", "MARK"};
56 | final String[] stopbits = new String[]{"1", "2"};
57 |
58 | //先定义
59 | private static final int REQUEST_EXTERNAL_STORAGE = 1;
60 |
61 | private static String[] PERMISSIONS_STORAGE = {
62 | "android.permission.READ_EXTERNAL_STORAGE",
63 | "android.permission.WRITE_EXTERNAL_STORAGE"};
64 |
65 |
66 | @Override
67 | protected void onCreate(Bundle savedInstanceState) {
68 | super.onCreate(savedInstanceState);
69 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main_java);
70 | verifyStoragePermissions(this);
71 | initFragment();
72 | initDevice();
73 | initSpinners();
74 |
75 | //设置数据位
76 | SpAdapter spAdapter1 = new SpAdapter(this);
77 | spAdapter1.setDatas(databits);
78 | binding.spDatabits.setAdapter(spAdapter1);
79 | binding.spDatabits.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
80 | @Override
81 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
82 | closeSerialPort();
83 | int dataBitValue = Integer.parseInt(databits[position]);
84 | SimpleSerialPortManager.getInstance().setDatabits(dataBitValue);
85 | SerialPortLogUtil.i("MainJavaActivity", "设置数据位: " + dataBitValue);
86 | }
87 |
88 | @Override
89 | public void onNothingSelected(AdapterView> parent) {
90 |
91 | }
92 | });
93 |
94 | //设置数据位
95 | SpAdapter spAdapter2 = new SpAdapter(this);
96 | spAdapter2.setDatas(paritys);
97 | binding.spParity.setAdapter(spAdapter2);
98 | binding.spParity.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
99 | @Override
100 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
101 | closeSerialPort();
102 | SimpleSerialPortManager.getInstance().setParity(position);
103 | SerialPortLogUtil.i("MainJavaActivity", "设置校验位: " + paritys[position] + " (值: " + position + ")");
104 | }
105 |
106 | @Override
107 | public void onNothingSelected(AdapterView> parent) {
108 |
109 | }
110 | });
111 |
112 | //设置停止位
113 | SpAdapter spAdapter3 = new SpAdapter(this);
114 | spAdapter3.setDatas(stopbits);
115 | binding.spStopbits.setAdapter(spAdapter3);
116 | binding.spStopbits.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
117 | @Override
118 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
119 | closeSerialPort();
120 | int stopBitValue = Integer.parseInt(stopbits[position]);
121 | SimpleSerialPortManager.getInstance().setStopbits(stopBitValue);
122 | SerialPortLogUtil.i("MainJavaActivity", "设置停止位: " + stopBitValue);
123 | }
124 |
125 | @Override
126 | public void onNothingSelected(AdapterView> parent) {
127 |
128 | }
129 | });
130 |
131 | binding.btnOpenDevice.setOnClickListener(v -> {
132 | if (mOpened) {
133 | closeSerialPort();
134 | } else {
135 | openSerialPort();
136 | }
137 | });
138 |
139 | binding.btnSendData.setOnClickListener((view) -> {
140 | onSend();
141 | });
142 | }
143 |
144 | /**
145 | * 打开串口
146 | */
147 | private void openSerialPort() {
148 | String devicePath = mDevice.getName();
149 | int baudRate = Integer.parseInt(mDevice.getRoot());
150 |
151 | SerialPortLogUtil.i("MainJavaActivity", "打开的串口为:" + devicePath + "----" + baudRate);
152 |
153 | // 使用SimpleSerialPortManager打开串口
154 | boolean success = SimpleSerialPortManager.getInstance()
155 | .openSerialPort(devicePath, baudRate,
156 | // 打开状态回调
157 | (isSuccess, status) -> {
158 | runOnUiThread(() -> {
159 | switch (status) {
160 | case SUCCESS_OPENED:
161 | ToastUtils.show("串口打开成功");
162 | mOpened = true;
163 | updateViewState(true);
164 | break;
165 | case NO_READ_WRITE_PERMISSION:
166 | ToastUtils.show("没有读写权限");
167 | updateViewState(false);
168 | break;
169 | case OPEN_FAIL:
170 | ToastUtils.show("串口打开失败");
171 | updateViewState(false);
172 | break;
173 | }
174 | });
175 | },
176 | // 数据接收回调
177 | new SimpleSerialPortManager.OnDataReceivedCallback() {
178 | @Override
179 | public void onDataReceived(byte[] data) {
180 | SerialPortLogUtil.i("MainJavaActivity", "onDataReceived [ byte[] ]: " + Arrays.toString(data));
181 | SerialPortLogUtil.i("MainJavaActivity", "onDataReceived [ String ]: " + new String(data));
182 |
183 | runOnUiThread(() -> {
184 | if (mConversionNotice) {
185 | LogManager.instance().post(new RecvMessage(bytesToHex(data)));
186 | } else {
187 | LogManager.instance().post(new RecvMessage(Arrays.toString(data)));
188 | }
189 | });
190 | }
191 |
192 | @Override
193 | public void onDataSent(byte[] data) {
194 | SerialPortLogUtil.i("MainJavaActivity", "onDataSent [ byte[] ]: " + Arrays.toString(data));
195 | SerialPortLogUtil.i("MainJavaActivity", "onDataSent [ String ]: " + new String(data));
196 |
197 | runOnUiThread(() -> {
198 | if (mConversionNotice) {
199 | LogManager.instance().post(new SendMessage(bytesToHex(data)));
200 | } else {
201 | LogManager.instance().post(new SendMessage(Arrays.toString(data)));
202 | }
203 | });
204 | }
205 | });
206 | }
207 |
208 | private void closeSerialPort() {
209 | SimpleSerialPortManager.getInstance().closeSerialPort();
210 | mOpened = false;
211 | updateViewState(mOpened);
212 | }
213 |
214 | public static void verifyStoragePermissions(Activity activity) {
215 | try {
216 | //检测是否有写的权限
217 | int permission = ActivityCompat.checkSelfPermission(activity,
218 | "android.permission.WRITE_EXTERNAL_STORAGE");
219 | if (permission != PackageManager.PERMISSION_GRANTED) {
220 | // 没有写的权限,去申请写的权限,会弹出对话框
221 | ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
222 | }
223 | } catch (Exception e) {
224 | e.printStackTrace();
225 | }
226 | }
227 |
228 | /**
229 | * 更新视图状态
230 | *
231 | * @param isSerialPortOpened
232 | */
233 | private void updateViewState(boolean isSerialPortOpened) {
234 | int stringRes = isSerialPortOpened ? R.string.close_serial_port : R.string.open_serial_port;
235 | binding.btnOpenDevice.setText(stringRes);
236 | binding.spinnerDevices.setEnabled(!isSerialPortOpened);
237 | binding.spinnerBaudrate.setEnabled(!isSerialPortOpened);
238 | binding.btnSendData.setEnabled(isSerialPortOpened);
239 | binding.btnLoadList.setEnabled(isSerialPortOpened);
240 | }
241 |
242 | /**
243 | * 初始化设备列表
244 | */
245 | private void initDevice() {
246 | PrefHelper.initDefault(this);
247 | SerialPortFinder serialPortFinder = new SerialPortFinder();
248 | // 设备
249 | mDevices = serialPortFinder.getAllDevicesPath();
250 | if (mDevices.length == 0) {
251 | mDevices = new String[]{
252 | getString(R.string.no_serial_device)
253 | };
254 | }
255 | // 波特率
256 | mBaudrates = getResources().getStringArray(R.array.baudrates);
257 |
258 | mDeviceIndex = PrefHelper.getDefault().getInt(PreferenceKeys.SERIAL_PORT_DEVICES, 0);
259 | mDeviceIndex = mDeviceIndex >= mDevices.length ? mDevices.length - 1 : mDeviceIndex;
260 | mBaudrateIndex = PrefHelper.getDefault().getInt(PreferenceKeys.BAUD_RATE, 0);
261 |
262 | mDevice = new Device(mDevices[mDeviceIndex], mBaudrates[mBaudrateIndex], null);
263 | }
264 |
265 |
266 | /**
267 | * 初始化下拉选项
268 | */
269 | private void initSpinners() {
270 | ArrayAdapter deviceAdapter =
271 | new ArrayAdapter(this, R.layout.spinner_default_item, mDevices);
272 | deviceAdapter.setDropDownViewResource(R.layout.spinner_item);
273 | binding.spinnerDevices.setAdapter(deviceAdapter);
274 | binding.spinnerDevices.setOnItemSelectedListener(this);
275 |
276 | ArrayAdapter baudrateAdapter =
277 | new ArrayAdapter(this, R.layout.spinner_default_item, mBaudrates);
278 | baudrateAdapter.setDropDownViewResource(R.layout.spinner_item);
279 | binding.spinnerBaudrate.setAdapter(baudrateAdapter);
280 | binding.spinnerBaudrate.setOnItemSelectedListener(this);
281 |
282 | binding.spinnerDevices.setSelection(mDeviceIndex);
283 | binding.spinnerBaudrate.setSelection(mBaudrateIndex);
284 | }
285 |
286 |
287 | /**
288 | * 发送数据
289 | */
290 | public void onSend() {
291 | String sendContent = binding.etData.getText().toString().trim();
292 | if (TextUtils.isEmpty(sendContent)) {
293 | SerialPortLogUtil.i("MainJavaActivity", "onSend: 发送内容为 null");
294 | return;
295 | }
296 | byte[] sendContentBytes = sendContent.getBytes();
297 | // 使用SimpleSerialPortManager发送数据
298 | boolean sendBytes = SimpleSerialPortManager.getInstance().sendData(sendContentBytes);
299 | SerialPortLogUtil.i("MainJavaActivity", "onSend: sendBytes = " + sendBytes);
300 | }
301 |
302 | @Override
303 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
304 | // Spinner 选择监听
305 | int parentId = parent.getId();
306 | if (parentId == R.id.spinner_devices) {
307 | mDeviceIndex = position;
308 | mDevice.setName(mDevices[mDeviceIndex]);
309 | } else if (parentId == R.id.spinner_baudrate) {
310 | mBaudrateIndex = position;
311 | mDevice.setRoot(mBaudrates[mBaudrateIndex]);
312 | }
313 | }
314 |
315 | @Override
316 | public void onNothingSelected(AdapterView> parent) {
317 |
318 | }
319 |
320 | @Override
321 | protected void onDestroy() {
322 | SimpleSerialPortManager.getInstance().closeSerialPort();
323 | super.onDestroy();
324 | }
325 |
326 | @Override
327 | public void onStart() {
328 | super.onStart();
329 | EventBus.getDefault().register(this);
330 | }
331 |
332 | @Override
333 | public void onStop() {
334 | super.onStop();
335 | EventBus.getDefault().unregister(this);
336 | }
337 |
338 | @Override
339 | protected void onResume() {
340 | super.onResume();
341 | refreshLogList();
342 | }
343 |
344 | /**
345 | * 初始化日志Fragment
346 | */
347 | protected void initFragment() {
348 | FragmentManager fragmentManager = getSupportFragmentManager();
349 | mLogFragment = (LogFragment) fragmentManager.findFragmentById(R.id.log_fragment);
350 | }
351 |
352 |
353 | /**
354 | * 刷新日志列表
355 | */
356 | protected void refreshLogList() {
357 | mLogFragment.updateAutoEndButton();
358 | mLogFragment.updateList();
359 | }
360 |
361 | @Subscribe(threadMode = ThreadMode.MAIN)
362 | public void onMessageEvent(IMessage message) {
363 | // 收到时间,刷新界面
364 | mLogFragment.add(message);
365 | }
366 |
367 | @Subscribe(threadMode = ThreadMode.MAIN)
368 | public void onConversionNotice(ConversionNoticeEvent messageEvent) {
369 | if (messageEvent.getMessage().equals("1")) {
370 | mConversionNotice = false;
371 | } else {
372 | mConversionNotice = true;
373 | }
374 |
375 | }
376 |
377 |
378 | /**
379 | * 字节数组转16进制
380 | *
381 | * @param bytes 需要转换的byte数组
382 | * @return 转换后的Hex字符串
383 | */
384 | public static String bytesToHex(byte[] bytes) {
385 | StringBuffer sb = new StringBuffer();
386 | for (int i = 0; i < bytes.length; i++) {
387 | String hex = Integer.toHexString(bytes[i] & 0xFF);
388 | if (hex.length() < 2) {
389 | sb.append(0);
390 | }
391 | sb.append(hex);
392 | }
393 | return sb.toString();
394 | }
395 |
396 |
397 | }
--------------------------------------------------------------------------------