Repository: cl-6666/serialPort
Branch: master
Commit: 7b7fafdd03f9
Files: 89
Total size: 322.0 KB
Directory structure:
gitextract_f57wyq09/
├── .github/
│ └── ISSUE_TEMPLATE/
│ └── 提交bug.md
├── .gitignore
├── README.md
├── README4.1.1.md
├── README_EN.md
├── app/
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── cl/
│ │ └── myapplication/
│ │ ├── App.java
│ │ ├── MainActivity.kt
│ │ ├── MultiSerialPortActivity.java
│ │ ├── SelectSerialPortActivity.kt
│ │ ├── SingleSerialPortActivity.java
│ │ ├── adapter/
│ │ │ ├── DeviceAdapter.java
│ │ │ └── SpAdapter.java
│ │ ├── constant/
│ │ │ └── PreferenceKeys.java
│ │ ├── fragment/
│ │ │ └── LogFragment.java
│ │ ├── message/
│ │ │ ├── ConversionNoticeEvent.java
│ │ │ ├── IMessage.java
│ │ │ ├── LogManager.java
│ │ │ ├── RecvMessage.java
│ │ │ └── SendMessage.java
│ │ └── util/
│ │ ├── ByteUtil.java
│ │ ├── ListViewHolder.java
│ │ ├── PrefHelper.java
│ │ └── TimeUtil.java
│ └── res/
│ ├── color/
│ │ ├── selector_log_text.xml
│ │ └── selector_spinner_text.xml
│ ├── drawable/
│ │ └── ic_launcher_background.xml
│ ├── drawable-v24/
│ │ └── ic_launcher_foreground.xml
│ ├── layout/
│ │ ├── activity_main.xml
│ │ ├── activity_main_java.xml
│ │ ├── activity_multi_serial.xml
│ │ ├── activity_multi_serial_new.xml
│ │ ├── activity_select_serial_port.xml
│ │ ├── fragment_log.xml
│ │ ├── include_fragment_container.xml
│ │ ├── item_device.xml
│ │ ├── item_log.xml
│ │ ├── spinner_default_item.xml
│ │ └── spinner_item.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── values/
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── string_arrays.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── values-night/
│ └── themes.xml
├── build.gradle
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── serial_lib/
│ ├── .gitignore
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── cpp/
│ │ ├── CMakeLists.txt
│ │ ├── SerialPort.c
│ │ └── SerialPort.h
│ ├── java/
│ │ └── com/
│ │ └── cl/
│ │ └── serialportlibrary/
│ │ ├── Device.java
│ │ ├── Driver.java
│ │ ├── MultiSerialPortManager.java
│ │ ├── SerialConfig.java
│ │ ├── SerialPort.java
│ │ ├── SerialPortFinder.java
│ │ ├── SerialPortManager.java
│ │ ├── SimpleSerialPortManager.java
│ │ ├── enumerate/
│ │ │ ├── SerialPortEnum.java
│ │ │ └── SerialStatus.java
│ │ ├── example/
│ │ │ └── MultiSerialPortExample.java
│ │ ├── listener/
│ │ │ ├── OnOpenSerialPortListener.java
│ │ │ └── OnSerialPortDataListener.java
│ │ ├── stick/
│ │ │ ├── AbsStickPackageHelper.java
│ │ │ ├── BaseStickPackageHelper.java
│ │ │ ├── CompositeStickPackageHelper.java
│ │ │ ├── SpecifiedStickPackageHelper.java
│ │ │ ├── StaticLenStickPackageHelper.java
│ │ │ ├── StickyPacketHelperFactory.java
│ │ │ ├── TimeoutStickPackageHelper.java
│ │ │ └── VariableLenStickPackageHelper.java
│ │ ├── thread/
│ │ │ └── SerialPortReadThread.java
│ │ └── utils/
│ │ └── SerialPortLogUtil.java
│ └── res/
│ └── values/
│ └── strings.xml
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/提交bug.md
================================================
---
name: 提交BUG
about: Create a report to help us improve
title: 建议
labels: ''
assignees: ''
---
## 【警告:请务必按照 issue 模板填写】
## 问题描述
* 框架版本【必填】:XXX
* 问题描述【必填】:XXX
* 复现步骤【必填】:XXX
* 是否必现【必填】:填是/否
* 出现问题机型信息【必填】:请填写出现问题的品牌和机型
* 出现问题的安卓版本【必填】:请填写出现问题的 Android 版本
## 其他
* 提供报错堆栈(如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来)
* 提供截图或视频(根据需要提供,此项不强制)
* 提供解决方案(如果已经解决了的话,此项不强制)
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
================================================
FILE: README.md
================================================
# Android串口通信框架 SerialPort
[中文](README.md) | [English](README_EN.md)
[](https://github.com/cl-6666/serialPort)
[](https://android-arsenal.com/api?level=21)
[](https://www.apache.org/licenses/LICENSE-2.0)
> 一个灵活、高效并且轻量的Android串口通信框架,让串口操作变得简单易用。支持单串口、多串口、粘包处理、自定义配置等功能。
## 📱 体验演示
想要快速体验串口通信框架的强大功能?直接下载演示 APK 安装到您的 Android 设备上试试吧!
### 📥 [点击下载演示 APK](https://www.pgyer.com/XNzY)
[](https://www.pgyer.com/XNzY)
**版本**: v5.0.8 | **大小**: ~7 MB | **API**: 21+ | **架构**: arm64-v8a, armeabi-v7a, x86, x86_64
### 演示 APK 功能
- ✅ 单串口通信演示
- ✅ 多串口管理演示
- ✅ 粘包处理策略切换
- ✅ 串口参数配置(数据位、校验位、停止位)
- ✅ 实时数据收发测试
- ✅ 十六进制/ASCII 数据显示
- ✅ 性能测试与统计
> **提示**: 演示 APK 需要在具有串口的 Android 设备上运行(如工控设备、开发板等)。如果您的设备没有串口,可以查看源码了解使用方法。
## ⭐ 特性
- 🚀 **简单易用** - 链式调用,一行代码完成配置
- 🔧 **多串口支持** - 同时管理多个串口,独立配置
- 📦 **智能粘包处理** - 支持多种粘包策略,可动态切换
- ⚡ **高性能** - 多线程处理,线程安全设计
- 🛡️ **稳定可靠** - 完善的错误处理和资源管理
- 📝 **详细日志** - 丰富的调试信息,方便排查问题
- 🎯 **灵活配置** - 支持数据位、校验位、停止位等参数配置
- ✨ **Google Play 认证** - 支持 16KB 页面对齐,完全符合 Google Play 上架要求
## 📖 版本说明
- **当前版本**: 5.0.8 (推荐) - 全新架构,功能强大,支持 Google Play 16KB 页面对齐
- **历史版本**: [4.1.1版本文档](README4.1.1.md) - 稳定版本
### 5.0.8 版本更新 🔥 (2025-12-25)
- ✅ **16KB 页面对齐**: 完全适配 Google Play 16KB 页面大小要求
- ✅ **Android 15 支持**: 兼容最新 Android 15 系统
- ✅ **原生库优化**: arm64-v8a 架构原生库已通过 Google Play 审核标准
- ✅ **向后兼容**: 完全兼容旧版本 Android 设备,无需修改代码
> **重要提示**: 从 2024 年开始,Google Play 要求所有 arm64-v8a 原生库必须支持 16KB 页面大小。5.0.8 版本已完全适配此要求,可放心上架 Google Play。
## 🚀 快速开始
### 依赖集成
在项目的 `build.gradle` 中添加依赖:
```gradle
dependencies {
implementation 'com.github.cl-6666:serialPort:v5.0.8'
}
```
在项目根目录的 `build.gradle` 中添加:
```gradle
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
```
### 权限配置
在 `AndroidManifest.xml` 中添加必要权限:
```xml
```
## 📚 使用指南
### 1️⃣ 单串口使用 - 基础示例
#### 最简单的使用方式
```java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 一行代码打开串口并接收数据
SimpleSerialPortManager.getInstance()
.openSerialPort("/dev/ttyS4", 115200, data -> {
String receivedData = new String(data);
Log.i("Serial", "收到数据: " + receivedData);
// 处理接收到的数据
});
}
// 发送数据
private void sendData() {
SimpleSerialPortManager.getInstance().sendData("Hello World");
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关闭串口
SimpleSerialPortManager.getInstance().closeSerialPort();
}
}
```
#### 完整配置示例
```java
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
// 全局配置(可选)
new SimpleSerialPortManager.QuickConfig()
.setIntervalSleep(50) // 读取间隔50ms
.setEnableLog(true) // 启用日志
.setLogTag("SerialPortApp") // 设置日志标签
.setDatabits(8) // 数据位8
.setParity(0) // 无校验
.setStopbits(1) // 停止位1
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING)
.apply(this);
}
}
```
### 2️⃣ 数据位、校验位、停止位配置
```java
public class SerialConfigExample {
public void configureSerialParams() {
SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance();
// 方式1:使用QuickConfig配置
new SimpleSerialPortManager.QuickConfig()
.setDatabits(8) // 数据位:5, 6, 7, 8
.setParity(0) // 校验位:0=无校验, 1=奇校验, 2=偶校验
.setStopbits(1) // 停止位:1 或 2
.setFlags(0) // 标志位
.apply(getApplication());
// 方式2:动态设置
manager.setDatabits(8) // 设置数据位
.setParity(2) // 设置偶校验
.setStopbits(1) // 设置停止位1
.setFlags(0); // 设置标志位
// 打开串口
manager.openSerialPort("/dev/ttyS4", 115200, data -> {
Log.i("Serial", "数据: " + new String(data));
});
}
// 常用配置组合
public void commonConfigurations() {
SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance();
// 标准配置 8N1 (8数据位, 无校验, 1停止位)
manager.setDatabits(8).setParity(0).setStopbits(1);
// Modbus RTU 8E1 (8数据位, 偶校验, 1停止位)
manager.setDatabits(8).setParity(2).setStopbits(1);
// 老式设备 7E2 (7数据位, 偶校验, 2停止位)
manager.setDatabits(7).setParity(2).setStopbits(2);
}
}
```
### 3️⃣ 粘包处理详解
粘包是串口通信中常见的问题,5.0.0版本提供了多种处理策略:
```java
public class StickyPacketExample {
public void noProcessing() {
// 策略1:不处理粘包 - 适用于简单数据流
new SimpleSerialPortManager.QuickConfig()
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING)
.apply(this);
}
public void delimiterBased() {
// 策略2:基于分隔符 - 适用于文本协议
new SimpleSerialPortManager.QuickConfig()
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED)
.apply(this);
// 自定义分隔符
SimpleSerialPortManager.getInstance()
.configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED);
}
public void fixedLength() {
// 策略3:固定长度 - 适用于固定长度协议
new SimpleSerialPortManager.QuickConfig()
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.FIXED_LENGTH)
.apply(this);
}
public void variableLength() {
// 策略4:可变长度 - 适用于带长度字段的协议
new SimpleSerialPortManager.QuickConfig()
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.VARIABLE_LENGTH)
.apply(this);
}
}
```
### 4️⃣ 多串口管理 - 强大功能
```java
public class MultiSerialExample {
public void basicMultiSerial() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// 串口1:GPS模块,不需要粘包处理
manager.openSerialPort("GPS", "/dev/ttyS1", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(0)
.setStopbits(1)
.setStickyPacketHelpers(new BaseStickPackageHelper()) // 不处理粘包
.build(),
// 状态回调
(serialId, success, status) -> {
Log.i("GPS", "状态: " + (success ? "成功" : "失败"));
},
// 数据回调
(serialId, data) -> {
String gpsData = new String(data);
Log.i("GPS", "数据: " + gpsData);
handleGpsData(gpsData);
});
// 串口2:传感器模块,需要换行符分包
manager.openSerialPort("SENSOR", "/dev/ttyS2", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(0)
.setStopbits(1)
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) // 换行符分包
.build(),
null, // 不需要状态回调
(serialId, data) -> {
String sensorData = new String(data).trim();
Log.i("SENSOR", "数据: " + sensorData);
handleSensorData(sensorData);
});
// 发送数据到不同串口
manager.sendData("GPS", "AT+GPS?\r\n");
manager.sendData("SENSOR", "READ_TEMP\n");
}
// 动态管理串口
public void dynamicManagement() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// 查看串口状态
List openedPorts = manager.getOpenedSerialPorts();
boolean isOpened = manager.isSerialPortOpened("GPS");
manager.printAllSerialStatus();
// 动态更新粘包策略
manager.updateStickyPacketHelpers("GPS",
new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("\r\n")});
// 关闭特定串口
manager.closeSerialPort("GPS");
// 关闭所有串口
manager.closeAllSerialPorts();
}
}
```
## 🎯 实际应用场景
### 工业控制场景
```java
public class IndustrialControlExample {
public void setupIndustrialPorts() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// PLC通信 - Modbus RTU
manager.openSerialPort("PLC", "/dev/ttyS1", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8).setParity(2).setStopbits(1) // 8E1
.setStickyPacketHelpers(new StaticLenStickPackageHelper(8))
.build(),
null, this::handlePlcData);
// 传感器数据采集 - 文本协议
manager.openSerialPort("SENSORS", "/dev/ttyS3", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(7).setParity(2).setStopbits(1) // 7E1
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
.build(),
null, this::handleSensorData);
}
}
```
### 通信网关场景
```java
public class GatewayExample {
public void setupGateway() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// 上行通信(与服务器)
manager.openSerialPort("UPLINK", "/dev/ttyS1", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n"))
.build(),
null, this::handleUplinkData);
// 下行设备1 - GPS
manager.openSerialPort("GPS", "/dev/ttyS2", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
.build(),
null, data -> forwardToUplink("GPS", data));
}
private void forwardToUplink(String deviceId, byte[] data) {
String message = String.format("[%s]%s\n", deviceId, new String(data));
SimpleSerialPortManager.multi().sendData("UPLINK", message);
}
}
```
## 🔧 高级功能
### 日志系统
```java
// 启用详细日志
SerialPortLogUtil.setDebugEnabled(true);
// 自定义日志输出
SerialPortLogUtil.i("MyTag", "自定义日志信息");
SerialPortLogUtil.printData("发送", data); // 十六进制+ASCII显示
SerialPortLogUtil.printSerialConfig("MySerial", 8, 0, 1, 0); // 配置信息
```
### 错误处理
```java
manager.openSerialPort("TEST", "/dev/ttyS1", 9600,
(serialId, success, status) -> {
if (!success) {
switch (status) {
case NO_READ_WRITE_PERMISSION:
Log.e("Serial", "权限不足");
break;
case OPEN_FAIL:
Log.e("Serial", "打开失败");
break;
}
}
},
dataCallback);
```
## 🛠️ 故障排查
### 常见问题
1. **串口打开失败**
```java
// 检查设备路径
String[] devices = new SerialPortFinder().getAllDevicesPath();
// 检查权限
File deviceFile = new File("/dev/ttyS4");
boolean canRead = deviceFile.canRead();
boolean canWrite = deviceFile.canWrite();
```
2. **数据接收不完整**
```java
// 启用日志查看原始数据
SerialPortLogUtil.setDebugEnabled(true);
// 尝试不同的粘包策略
manager.configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING);
```
## 📖 API参考
### SimpleSerialPortManager (单串口)
| 方法 | 说明 |
|------|------|
| `getInstance()` | 获取单例实例 |
| `openSerialPort(path, baudRate, callback)` | 打开串口 |
| `sendData(data)` | 发送数据 |
| `closeSerialPort()` | 关闭串口 |
| `setDatabits(databits)` | 设置数据位 |
| `setParity(parity)` | 设置校验位 |
| `setStopbits(stopbits)` | 设置停止位 |
### MultiSerialPortManager (多串口)
| 方法 | 说明 |
|------|------|
| `getInstance()` | 获取实例 |
| `openSerialPort(id, path, baudRate, config, statusCallback, dataCallback)` | 打开串口 |
| `sendData(serialId, data)` | 发送数据到指定串口 |
| `closeSerialPort(serialId)` | 关闭指定串口 |
| `closeAllSerialPorts()` | 关闭所有串口 |
| `isSerialPortOpened(serialId)` | 检查串口状态 |
## 🎯 版本迁移
### 从4.1.1迁移到5.0.0
**旧版本 (4.1.1)**:
```java
// 在Application中初始化
SerialUtils.getInstance().init(this, true, "TAG", 50, 8, 0, 1);
// 使用
SerialUtils.getInstance().setmSerialPortDirectorListens(...);
SerialUtils.getInstance().manyOpenSerialPort(list);
```
**新版本 (5.0.0)**:
```java
// 简化的初始化(可选)
new SimpleSerialPortManager.QuickConfig()
.setDatabits(8).setParity(0).setStopbits(1)
.apply(this);
// 直接使用
SimpleSerialPortManager.getInstance()
.openSerialPort("/dev/ttyS4", 115200, data -> {
// 处理数据
});
```
## 📞 联系我们
- **QQ群**: 458173716
- **博客**: https://blog.csdn.net/a214024475/article/details/113735085
- **GitHub**: https://github.com/cl-6666/serialPort
### PC端串口调试助手
**下载链接**: https://pan.baidu.com/s/1DL2TOHz9bl9RIKIG3oCSWw?pwd=f7sh
### QQ技术交流群
**QQ群号**: 458173716
## 🔬 技术说明
### 16KB 页面对齐适配 (v5.0.8)
从 2024 年开始,Google Play 要求所有使用原生库(.so 文件)的应用必须支持 16KB 页面大小,以适配最新的 Android 设备。本库已完全适配此要求。
#### 技术实现
我们在 CMake 构建配置中针对 arm64-v8a 架构添加了以下链接器标志:
```cmake
# CMakeLists.txt
if(ANDROID_ABI STREQUAL "arm64-v8a")
target_compile_options(SerialPort PRIVATE -fno-emulated-tls)
target_link_options(SerialPort PRIVATE
"LINKER:-z,max-page-size=16384"
"LINKER:-z,common-page-size=16384")
endif()
```
#### 兼容性说明
- ✅ **完全兼容**: 支持所有 Android 5.0+ (API 21+) 设备
- ✅ **无需修改**: 开发者无需修改任何代码,直接升级即可
- ✅ **性能优化**: 16KB 页面对齐可提升部分设备的内存管理效率
- ✅ **Google Play 认证**: 已通过 Google Play 的 16KB 页面对齐检测
#### 验证方法
使用 Android Studio 的 APK Analyzer 工具可以验证原生库是否支持 16KB 页面对齐:
1. 构建 APK 或 AAB 文件
2. 在 Android Studio 中选择 `Build` → `Analyze APK...`
3. 查看 `lib/arm64-v8a/libSerialPort.so` 的 `Alignment` 列
4. 显示 `16 KB` 表示已正确配置
#### 相关资源
- [Google Play 16KB 页面大小要求](https://developer.android.com/guide/practices/page-sizes)
- [CMake 链接器选项文档](https://cmake.org/cmake/help/latest/command/target_link_options.html)
---
================================================
FILE: README4.1.1.md
================================================
# Android串口通信框架 SerialPort v4.1.1
[](https://github.com/cl-6666/serialPort)
[](https://android-arsenal.com/api?level=21)
> 这是串口通信框架的4.1.1稳定版本文档。如果你是新用户,建议使用最新的 [5.0.0版本](README.md),它提供了更简单的API和更强大的功能。
⚠️ **注意**: 4.1.1版本为历史版本,仅用于维护现有项目。新项目请使用 [5.0.0版本](README.md)。
## 📖 版本说明
- **当前版本**: 4.1.1 (稳定维护版本)
- **推荐版本**: [5.0.0版本](README.md) - 功能更强大,API更简单
## 🚀 快速开始
### 依赖集成
在项目的 `build.gradle` 中添加依赖:
```gradle
dependencies {
implementation 'com.github.cl-6666:serialPort:4.1.1'
}
```
在项目根目录的 `build.gradle` 中添加:
```gradle
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
```
### 权限配置
在 `AndroidManifest.xml` 中添加必要权限:
```xml
```
## 📚 使用指南
### 1. Application中初始化
```java
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
// 方式1:使用XLogConfig配置日志
XLogConfig logConfig = new XLogConfig.Builder()
.logSwitch(true) // 开启日志
.tag("SerialPort") // 设置tag
.build();
SerialConfig serialConfig = new SerialConfig.Builder()
.setXLogConfig(logConfig) // 配置日志参数
.setIntervalSleep(50) // 设置读取间隔
.setSerialPortReconnection(false) // 是否开启串口重连
.setFlags(0) // 标志位
.setDatabits(8) // 数据位
.setStopbits(1) // 停止位
.setParity(0) // 校验位:0无校验,1奇校验,2偶校验
.build();
SerialUtils.getInstance().init(this, serialConfig);
// 方式2:简化初始化
SerialUtils.getInstance().init(this, true, "SerialPort", 50, 8, 0, 1);
}
}
```
### 2. 单串口使用
```java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 设置串口监听
SerialUtils.getInstance().setmSerialPortDirectorListens(new SerialPortDirectorListens() {
@Override
public void onSerialPortOpenSuccess(File device, SerialPortEnum serialPortEnum) {
Log.i("Serial", "串口打开成功: " + device.getPath());
}
@Override
public void onSerialPortOpenFail(File device, SerialStatus status) {
Log.e("Serial", "串口打开失败: " + status);
}
@Override
public void onDataReceive(byte[] bytes, SerialPortEnum serialPortEnum) {
String data = new String(bytes);
Log.i("Serial", "接收数据: " + data);
// 处理接收到的数据
}
@Override
public void onDataSend(byte[] bytes, SerialPortEnum serialPortEnum) {
Log.i("Serial", "发送数据: " + new String(bytes));
}
});
// 打开串口
List deviceList = new ArrayList<>();
deviceList.add(new Device("/dev/ttyS4", "115200", new File("/dev/ttyS4")));
SerialUtils.getInstance().manyOpenSerialPort(deviceList);
}
// 发送数据
private void sendData() {
String data = "Hello World";
SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, data.getBytes());
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关闭串口
SerialUtils.getInstance().serialPortClose();
}
}
```
### 3. 多串口使用
```java
public class MultiSerialActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置粘包处理
SerialUtils.getInstance().setStickPackageHelper(
new BaseStickPackageHelper(), // 串口1:不处理粘包
new SpecifiedStickPackageHelper("\n"), // 串口2:换行符分包
new StaticLenStickPackageHelper(8) // 串口3:固定8字节
);
// 设置监听
SerialUtils.getInstance().setmSerialPortDirectorListens(new SerialPortDirectorListens() {
@Override
public void onSerialPortOpenSuccess(File device, SerialPortEnum serialPortEnum) {
Log.i("Serial", "串口[" + serialPortEnum + "]打开成功: " + device.getPath());
}
@Override
public void onSerialPortOpenFail(File device, SerialStatus status) {
Log.e("Serial", "串口打开失败: " + status);
}
@Override
public void onDataReceive(byte[] bytes, SerialPortEnum serialPortEnum) {
String data = new String(bytes);
Log.i("Serial", "串口[" + serialPortEnum + "]收到: " + data);
// 根据串口类型处理数据
switch (serialPortEnum) {
case SERIAL_ONE:
handleGpsData(data);
break;
case SERIAL_TWO:
handleSensorData(data);
break;
case SERIAL_THREE:
handleModbusData(bytes);
break;
}
}
@Override
public void onDataSend(byte[] bytes, SerialPortEnum serialPortEnum) {
Log.i("Serial", "串口[" + serialPortEnum + "]发送: " + new String(bytes));
}
});
// 打开多个串口
List deviceList = new ArrayList<>();
deviceList.add(new Device("/dev/ttyS1", "9600", new File("/dev/ttyS1"))); // GPS
deviceList.add(new Device("/dev/ttyS2", "115200", new File("/dev/ttyS2"))); // 传感器
deviceList.add(new Device("/dev/ttyS3", "9600", new File("/dev/ttyS3"))); // Modbus
SerialUtils.getInstance().manyOpenSerialPort(deviceList);
}
// 向不同串口发送数据
private void sendToSerial() {
// 向串口1发送GPS命令
SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, "AT+GPS?\r\n".getBytes());
// 向串口2发送传感器命令
SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_TWO, "READ_TEMP\n".getBytes());
// 向串口3发送Modbus命令
byte[] modbusCmd = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, (byte)0x84, 0x0A};
SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_THREE, modbusCmd);
}
private void handleGpsData(String data) {
// 处理GPS数据
}
private void handleSensorData(String data) {
// 处理传感器数据
}
private void handleModbusData(byte[] data) {
// 处理Modbus数据
}
}
```
### 4. 粘包处理配置
```java
public class StickyPacketConfig {
public void configureStickyPacket() {
// 1. 不处理粘包(默认)
SerialUtils.getInstance().setStickPackageHelper(new BaseStickPackageHelper());
// 2. 按分隔符分包
SerialUtils.getInstance().setStickPackageHelper(
new SpecifiedStickPackageHelper("\r\n".getBytes()) // 按\r\n分包
);
// 3. 固定长度分包
SerialUtils.getInstance().setStickPackageHelper(
new StaticLenStickPackageHelper(16) // 固定16字节
);
// 4. 可变长度分包
SerialUtils.getInstance().setStickPackageHelper(
new VariableLenStickPackageHelper(
java.nio.ByteOrder.BIG_ENDIAN, // 字节序
2, // 长度字段大小
2, // 长度字段位置
12 // 包头长度
)
);
// 5. 多串口不同策略
SerialUtils.getInstance().setStickPackageHelper(
new BaseStickPackageHelper(), // 串口1:不处理
new SpecifiedStickPackageHelper("\n"), // 串口2:换行符
new StaticLenStickPackageHelper(8), // 串口3:固定长度
new VariableLenStickPackageHelper( // 串口4:可变长度
java.nio.ByteOrder.BIG_ENDIAN, 2, 2, 12)
);
}
}
```
### 5. 串口参数配置
```java
public class SerialParamConfig {
public void configureParams() {
SerialConfig serialConfig = new SerialConfig.Builder()
.setIntervalSleep(50) // 读取间隔50ms
.setDatabits(8) // 数据位8
.setStopbits(1) // 停止位1
.setParity(0) // 校验位:0=无校验
.setFlags(0) // 标志位
.setSerialPortReconnection(false) // 是否重连
.build();
SerialUtils.getInstance().init(getApplication(), serialConfig);
}
// 常用配置
public void commonConfigs() {
// 标准配置 8N1
SerialConfig config8N1 = new SerialConfig.Builder()
.setDatabits(8).setParity(0).setStopbits(1)
.build();
// Modbus RTU 8E1
SerialConfig configModbus = new SerialConfig.Builder()
.setDatabits(8).setParity(2).setStopbits(1) // 偶校验
.build();
// 老式设备 7E2
SerialConfig configOld = new SerialConfig.Builder()
.setDatabits(7).setParity(2).setStopbits(2)
.build();
}
}
```
## 📖 API参考
### SerialUtils 主要方法
| 方法 | 说明 |
|------|------|
| `init(Application, SerialConfig)` | 初始化串口框架 |
| `init(Application, boolean, String, int, int, int, int)` | 简化初始化 |
| `setmSerialPortDirectorListens(SerialPortDirectorListens)` | 设置串口监听 |
| `setStickPackageHelper(AbsStickPackageHelper...)` | 设置粘包处理 |
| `manyOpenSerialPort(List)` | 打开多个串口 |
| `sendData(SerialPortEnum, byte[])` | 发送数据 |
| `serialPortClose()` | 关闭串口 |
### SerialConfig 配置项
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `intervalSleep` | 读取间隔(ms) | 50 |
| `databits` | 数据位 | 8 |
| `stopbits` | 停止位 | 1 |
| `parity` | 校验位 | 0 |
| `flags` | 标志位 | 0 |
| `serialPortReconnection` | 是否重连 | false |
### 粘包处理器
| 类型 | 说明 | 适用场景 |
|------|------|----------|
| `BaseStickPackageHelper` | 不处理粘包 | 简单数据流 |
| `SpecifiedStickPackageHelper` | 分隔符分包 | 文本协议 |
| `StaticLenStickPackageHelper` | 固定长度分包 | 固定格式协议 |
| `VariableLenStickPackageHelper` | 可变长度分包 | 复杂二进制协议 |
## 🛠️ 故障排查
### 常见问题
1. **串口打开失败**
```java
// 检查设备路径
String[] devices = new SerialPortFinder().getAllDevicesPath();
// 检查权限
File deviceFile = new File("/dev/ttyS4");
if (!deviceFile.canRead() || !deviceFile.canWrite()) {
Log.e("Serial", "设备权限不足");
}
```
2. **数据接收不完整**
```java
// 尝试不处理粘包
SerialUtils.getInstance().setStickPackageHelper(new BaseStickPackageHelper());
// 或者调整读取间隔
SerialConfig config = new SerialConfig.Builder()
.setIntervalSleep(20) // 减少到20ms
.build();
```
3. **日志输出问题**
```java
// 确保启用日志
XLogConfig logConfig = new XLogConfig.Builder()
.logSwitch(true)
.tag("SerialPort")
.build();
```
## 🎯 升级到5.0.0
如果你想升级到最新的5.0.0版本,以下是主要的变化:
### 4.1.1版本
```java
// 初始化
SerialUtils.getInstance().init(this, true, "TAG", 50, 8, 0, 1);
// 使用
SerialUtils.getInstance().setmSerialPortDirectorListens(...);
SerialUtils.getInstance().manyOpenSerialPort(deviceList);
SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, data);
```
### 5.0.0版本 (推荐)
```java
// 简化初始化
new SimpleSerialPortManager.QuickConfig()
.setDatabits(8).setParity(0).setStopbits(1)
.apply(this);
// 简化使用
SimpleSerialPortManager.getInstance()
.openSerialPort("/dev/ttyS4", 115200, data -> {
// 处理数据
});
// 多串口
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
manager.openSerialPort("GPS", "/dev/ttyS1", 9600, config, statusCallback, dataCallback);
```
**升级优势**:
- API更简单易用
- 支持真正的多串口管理
- 更好的错误处理
- 增强的日志系统
- 更高的性能
详细的5.0.0版本使用请查看 [最新文档](README.md)。
## 📞 联系我们
- **QQ群**: 458173716
- **博客**: https://blog.csdn.net/a214024475/article/details/113735085
- **GitHub**: https://github.com/cl-6666/serialPort
## 📄 许可证
```
Licensed under the Apache License, Version 2.0
```
---
⚠️ **再次提醒**: 4.1.1版本为历史版本,新项目建议使用 [5.0.0版本](README.md),功能更强大,使用更简单!
================================================
FILE: README_EN.md
================================================
# Android Serial Communication Framework SerialPort
[English](README_EN.md) | [中文](README.md)
[](https://github.com/cl-6666/serialPort)
[](https://android-arsenal.com/api?level=21)
[](https://www.apache.org/licenses/LICENSE-2.0)
> A flexible, efficient, and lightweight Android serial communication framework that makes serial port operations simple. Supports single-port, multi-port, sticky-packet handling, custom configuration, and more.
## 📱 Demo APK
Want to try it quickly? Download the demo APK and install it on your Android device.
### 📥 [Download Demo APK](https://www.pgyer.com/XNzY)
[](https://www.pgyer.com/XNzY)
**Version**: v5.0.8 | **Size**: ~7 MB | **API**: 21+ | **ABIs**: arm64-v8a, armeabi-v7a, x86, x86_64
### Demo Features
- ✅ Single serial port demo
- ✅ Multi-serial management demo
- ✅ Sticky-packet strategy switching
- ✅ Serial params configuration (data bits, parity, stop bits)
- ✅ Real-time send/receive test
- ✅ Hex/ASCII display
- ✅ Performance test & statistics
> Note: the demo APK must run on an Android device with serial ports (industrial devices, development boards, etc.). If your device has no serial port, check the source code for usage.
## ⭐ Features
- 🚀 **Easy to use** - fluent APIs, configure with one line
- 🔧 **Multi-serial support** - manage multiple ports with independent configs
- 📦 **Smart sticky-packet handling** - multiple strategies, switch at runtime
- ⚡ **High performance** - multithreaded, thread-safe design
- 🛡️ **Stable & reliable** - solid error handling and resource management
- 📝 **Detailed logs** - rich debug information for troubleshooting
- 🎯 **Flexible config** - data bits, parity, stop bits, etc.
- ✨ **Google Play ready** - supports 16 KB page alignment and passes Play requirements
## 📖 Versions
- **Current**: 5.0.8 (recommended) - new architecture, powerful features, supports Google Play 16 KB page alignment
- **Legacy**: [4.1.1 docs](README4.1.1.md) - stable legacy version
### 5.0.8 Changes 🔥 (2025-12-25)
- ✅ **16 KB page alignment**: fully compatible with Google Play 16 KB page size requirements
- ✅ **Android 15 support**: compatible with Android 15
- ✅ **Native library optimization**: `arm64-v8a` native lib meets Google Play checks
- ✅ **Backward compatible**: works on older Android devices without code changes
> Important: since 2024, Google Play requires all `arm64-v8a` native libraries to support 16 KB page size. v5.0.8 fully meets this requirement.
### 5.0.0 Major Update 🎉
- ✅ **Architecture refactor**: removed `SerialUtils` dependency, clearer design
- ✅ **Simplified API**: introduced `SimpleSerialPortManager`, easier usage
- ✅ **Multi-serial management**: new `MultiSerialPortManager` for complex scenarios
- ✅ **Enhanced logging**: built-in logging system for better debugging
- ✅ **Independent config**: each port can use its own sticky-packet strategy
- ✅ **Performance improvements**: reduced ~30% redundant code
## 🚀 Quick Start
### Dependency
Add dependency in your module `build.gradle`:
```gradle
dependencies {
implementation 'com.github.cl-6666:serialPort:v5.0.8'
}
```
Add JitPack in the root `build.gradle`:
```gradle
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
```
### Permissions
Add required permissions in `AndroidManifest.xml`:
```xml
```
## 📚 Usage Guide
### 1️⃣ Single Port - Basic Example
#### Minimal usage
```java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Open serial port and receive data with one line
SimpleSerialPortManager.getInstance()
.openSerialPort("/dev/ttyS4", 115200, data -> {
String receivedData = new String(data);
Log.i("Serial", "Received: " + receivedData);
// Handle received data
});
}
// Send data
private void sendData() {
SimpleSerialPortManager.getInstance().sendData("Hello World");
}
@Override
protected void onDestroy() {
super.onDestroy();
// Close serial port
SimpleSerialPortManager.getInstance().closeSerialPort();
}
}
```
#### Full configuration example
```java
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
// Global config (optional)
new SimpleSerialPortManager.QuickConfig()
.setIntervalSleep(50) // Read interval: 50ms
.setEnableLog(true) // Enable logs
.setLogTag("SerialPortApp") // Log tag
.setDatabits(8) // Data bits: 8
.setParity(0) // Parity: none
.setStopbits(1) // Stop bits: 1
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING)
.apply(this);
}
}
```
### 2️⃣ Data Bits / Parity / Stop Bits
```java
public class SerialConfigExample {
public void configureSerialParams() {
SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance();
// Option 1: use QuickConfig
new SimpleSerialPortManager.QuickConfig()
.setDatabits(8) // Data bits: 5, 6, 7, 8
.setParity(0) // Parity: 0=none, 1=odd, 2=even
.setStopbits(1) // Stop bits: 1 or 2
.setFlags(0) // Flags
.apply(getApplication());
// Option 2: set dynamically
manager.setDatabits(8) // Set data bits
.setParity(2) // Set even parity
.setStopbits(1) // Set stop bits to 1
.setFlags(0); // Set flags
// Open serial port
manager.openSerialPort("/dev/ttyS4", 115200, data -> {
Log.i("Serial", "Data: " + new String(data));
});
}
// Common configurations
public void commonConfigurations() {
SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance();
// Standard 8N1 (8 data bits, no parity, 1 stop bit)
manager.setDatabits(8).setParity(0).setStopbits(1);
// Modbus RTU 8E1 (8 data bits, even parity, 1 stop bit)
manager.setDatabits(8).setParity(2).setStopbits(1);
// Legacy devices 7E2 (7 data bits, even parity, 2 stop bits)
manager.setDatabits(7).setParity(2).setStopbits(2);
}
}
```
### 3️⃣ Sticky-Packet Handling
Sticky packets are common in serial communication. Since v5.0.0, multiple strategies are provided:
```java
public class StickyPacketExample {
public void noProcessing() {
// Strategy 1: no processing - good for simple streams
new SimpleSerialPortManager.QuickConfig()
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING)
.apply(this);
}
public void delimiterBased() {
// Strategy 2: delimiter-based - good for text protocols
new SimpleSerialPortManager.QuickConfig()
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED)
.apply(this);
// Custom delimiter
SimpleSerialPortManager.getInstance()
.configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED);
}
public void fixedLength() {
// Strategy 3: fixed-length - good for fixed-length protocols
new SimpleSerialPortManager.QuickConfig()
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.FIXED_LENGTH)
.apply(this);
}
public void variableLength() {
// Strategy 4: variable-length - good for protocols with length fields
new SimpleSerialPortManager.QuickConfig()
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.VARIABLE_LENGTH)
.apply(this);
}
}
```
### 4️⃣ Multi-Serial Management
```java
public class MultiSerialExample {
public void basicMultiSerial() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// Port 1: GPS module, no sticky-packet processing
manager.openSerialPort("GPS", "/dev/ttyS1", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(0)
.setStopbits(1)
.setStickyPacketHelpers(new BaseStickPackageHelper()) // No processing
.build(),
// Status callback
(serialId, success, status) -> {
Log.i("GPS", "Status: " + (success ? "Success" : "Failed"));
},
// Data callback
(serialId, data) -> {
String gpsData = new String(data);
Log.i("GPS", "Data: " + gpsData);
handleGpsData(gpsData);
});
// Port 2: sensor module, split by newline
manager.openSerialPort("SENSOR", "/dev/ttyS2", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(0)
.setStopbits(1)
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) // Newline delimiter
.build(),
null, // No status callback needed
(serialId, data) -> {
String sensorData = new String(data).trim();
Log.i("SENSOR", "Data: " + sensorData);
handleSensorData(sensorData);
});
// Send to different ports
manager.sendData("GPS", "AT+GPS?\r\n");
manager.sendData("SENSOR", "READ_TEMP\n");
}
// Dynamic management
public void dynamicManagement() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// Query status
List openedPorts = manager.getOpenedSerialPorts();
boolean isOpened = manager.isSerialPortOpened("GPS");
manager.printAllSerialStatus();
// Update sticky-packet strategy dynamically
manager.updateStickyPacketHelpers("GPS",
new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("\r\n")});
// Close one port
manager.closeSerialPort("GPS");
// Close all ports
manager.closeAllSerialPorts();
}
}
```
## 🎯 Real-world Scenarios
### Industrial control
```java
public class IndustrialControlExample {
public void setupIndustrialPorts() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// PLC - Modbus RTU
manager.openSerialPort("PLC", "/dev/ttyS1", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8).setParity(2).setStopbits(1) // 8E1
.setStickyPacketHelpers(new StaticLenStickPackageHelper(8))
.build(),
null, this::handlePlcData);
// Sensor acquisition - text protocol
manager.openSerialPort("SENSORS", "/dev/ttyS3", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(7).setParity(2).setStopbits(1) // 7E1
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
.build(),
null, this::handleSensorData);
}
}
```
### Communication gateway
```java
public class GatewayExample {
public void setupGateway() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// Uplink (to server)
manager.openSerialPort("UPLINK", "/dev/ttyS1", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n"))
.build(),
null, this::handleUplinkData);
// Downlink device 1 - GPS
manager.openSerialPort("GPS", "/dev/ttyS2", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
.build(),
null, data -> forwardToUplink("GPS", data));
}
private void forwardToUplink(String deviceId, byte[] data) {
String message = String.format("[%s]%s\n", deviceId, new String(data));
SimpleSerialPortManager.multi().sendData("UPLINK", message);
}
}
```
## 🔧 Advanced
### Logging
```java
// Enable verbose logs
SerialPortLogUtil.setDebugEnabled(true);
// Custom output
SerialPortLogUtil.i("MyTag", "Custom log message");
SerialPortLogUtil.printData("Send", data); // Hex + ASCII
SerialPortLogUtil.printSerialConfig("MySerial", 8, 0, 1, 0); // Config info
```
### Error handling
```java
manager.openSerialPort("TEST", "/dev/ttyS1", 9600,
(serialId, success, status) -> {
if (!success) {
switch (status) {
case NO_READ_WRITE_PERMISSION:
Log.e("Serial", "No permission");
break;
case OPEN_FAIL:
Log.e("Serial", "Open failed");
break;
}
}
},
dataCallback);
```
## 🛠️ Troubleshooting
### Common issues
1. **Failed to open serial port**
```java
// Check device paths
String[] devices = new SerialPortFinder().getAllDevicesPath();
// Check permission
File deviceFile = new File("/dev/ttyS4");
boolean canRead = deviceFile.canRead();
boolean canWrite = deviceFile.canWrite();
```
2. **Incomplete received data**
```java
// Enable logs to inspect raw data
SerialPortLogUtil.setDebugEnabled(true);
// Try different sticky-packet strategies
manager.configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING);
```
## 📖 API Reference
### SimpleSerialPortManager (single port)
| Method | Description |
|------|------|
| `getInstance()` | Get singleton instance |
| `openSerialPort(path, baudRate, callback)` | Open serial port |
| `sendData(data)` | Send data |
| `closeSerialPort()` | Close serial port |
| `setDatabits(databits)` | Set data bits |
| `setParity(parity)` | Set parity |
| `setStopbits(stopbits)` | Set stop bits |
### MultiSerialPortManager (multi port)
| Method | Description |
|------|------|
| `getInstance()` | Get instance |
| `openSerialPort(id, path, baudRate, config, statusCallback, dataCallback)` | Open serial port |
| `sendData(serialId, data)` | Send data to a port |
| `closeSerialPort(serialId)` | Close a port |
| `closeAllSerialPorts()` | Close all ports |
| `isSerialPortOpened(serialId)` | Check port status |
## 🎯 Migration
### From 4.1.1 to 5.0.0
**Old (4.1.1)**:
```java
// Init in Application
SerialUtils.getInstance().init(this, true, "TAG", 50, 8, 0, 1);
// Usage
SerialUtils.getInstance().setmSerialPortDirectorListens(...);
SerialUtils.getInstance().manyOpenSerialPort(list);
```
**New (5.0.0)**:
```java
// Simplified init (optional)
new SimpleSerialPortManager.QuickConfig()
.setDatabits(8).setParity(0).setStopbits(1)
.apply(this);
// Direct usage
SimpleSerialPortManager.getInstance()
.openSerialPort("/dev/ttyS4", 115200, data -> {
// Handle data
});
```
## 📞 Contact
- **QQ group**: 458173716
- **Blog**: https://blog.csdn.net/a214024475/article/details/113735085
- **GitHub**: https://github.com/cl-6666/serialPort
### PC serial debugging assistant
**Download**: https://pan.baidu.com/s/1DL2TOHz9bl9RIKIG3oCSWw?pwd=f7sh
### QQ technical group
**Group ID**: 458173716
## 🔬 Technical Notes
### 16 KB Page Alignment (v5.0.8)
Since 2024, Google Play requires apps that ship native libraries (`.so`) to support 16 KB page size, to be compatible with newer Android devices. This library fully meets the requirement.
#### Implementation
In CMake configuration, the following linker flags are added for `arm64-v8a`:
```cmake
# CMakeLists.txt
if(ANDROID_ABI STREQUAL "arm64-v8a")
target_compile_options(SerialPort PRIVATE -fno-emulated-tls)
target_link_options(SerialPort PRIVATE
"LINKER:-z,max-page-size=16384"
"LINKER:-z,common-page-size=16384")
endif()
```
#### Compatibility
- ✅ **Fully compatible**: supports Android 5.0+ (API 21+)
- ✅ **No code change**: upgrade and use directly
- ✅ **Optimized**: 16 KB alignment can improve memory management on some devices
- ✅ **Google Play ready**: passes 16 KB alignment checks
#### Verification
You can verify alignment with Android Studio APK Analyzer:
1. Build an APK or AAB
2. In Android Studio: `Build` → `Analyze APK...`
3. Check the `Alignment` column for `lib/arm64-v8a/libSerialPort.so`
4. `16 KB` means it is configured correctly
#### Resources
- [Google Play 16 KB page size requirements](https://developer.android.com/guide/practices/page-sizes)
- [CMake `target_link_options` docs](https://cmake.org/cmake/help/latest/command/target_link_options.html)
---
================================================
FILE: app/build.gradle
================================================
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace 'com.cl.myapplication'
compileSdk 36
defaultConfig {
applicationId "com.cl.myapplication"
minSdk 24
targetSdk 36
versionCode 508
versionName "5.0.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '11'
}
buildFeatures{
dataBinding=true
}
}
dependencies {
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation libs.material
implementation 'androidx.cardview:cardview:1.0.0'
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'com.github.getActivity:ToastUtils:9.6'
implementation 'com.github.JessYanCoding:AndroidAutoSize:v1.2.1'
implementation 'com.google.code.gson:gson:2.8.6'
// implementation project(path: ':serial_lib')
implementation 'com.github.cl-6666:serialPort:v5.0.8'
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/cl/myapplication/App.java
================================================
package com.cl.myapplication;
import android.app.Application;
import com.hjq.toast.ToastUtils;
import com.cl.serialportlibrary.SimpleSerialPortManager;
/**
* 项目:serialPort
* 作者:Arry
* 创建日期:2021/10/20
* 描述:
* 修订历史:
*/
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化 Toast 框架
ToastUtils.init(this);
// 使用新的SimpleSerialPortManager进行全局初始化
new SimpleSerialPortManager.QuickConfig()
.setIntervalSleep(50) // 读取间隔50ms
.setEnableLog(true) // 启用日志
.setLogTag("SerialPortApp") // 设置日志标签
.setDatabits(8) // 数据位8
.setParity(0) // 无校验
.setStopbits(1) // 停止位1
.setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING) // 不处理黏包
.setMaxPacketSize(1024) // 最大包大小1KB
.apply(this);
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/MainActivity.kt
================================================
package com.cl.myapplication
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.cl.myapplication.databinding.ActivityMainBinding
import com.cl.serialportlibrary.Device
import com.cl.serialportlibrary.SimpleSerialPortManager
class MainActivity : AppCompatActivity(){
private val TAG = MainActivity::class.java.simpleName
val DEVICE = "device"
private var isSerialPortOpened = false
private var mToast: Toast? = null
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val device = intent.getSerializableExtra(DEVICE) as Device?
Log.i(TAG, "onCreate: device = $device")
if (null == device) {
finish()
return
}
// 使用SimpleSerialPortManager打开串口
val devicePath = device.name
val baudRate = device.root.toInt()
SimpleSerialPortManager.getInstance().openSerialPort(devicePath, baudRate) { data ->
runOnUiThread {
val receivedData = String(data)
Log.i(TAG, "接收到数据: $receivedData")
// binding.tvReceiveContent.text = receivedData
}
}
isSerialPortOpened = true
}
fun onSend(view: View) {
val editTextSendContent = binding.etSendContent.text.toString()
if (TextUtils.isEmpty(editTextSendContent)) {
Log.i(TAG, "onSend: 发送内容为 null")
return
}
val sendContentBytes = editTextSendContent.toByteArray()
val sendBytes = SimpleSerialPortManager.getInstance().sendData(sendContentBytes)
Log.i(TAG, "onSend: sendBytes = $sendBytes")
showToast(if (sendBytes) "发送成功" else "发送失败")
}
fun onDestroy(view: View) {
if (isSerialPortOpened) {
SimpleSerialPortManager.getInstance().closeSerialPort()
isSerialPortOpened = false
}
finish()
}
override fun onDestroy() {
super.onDestroy()
if (isSerialPortOpened) {
SimpleSerialPortManager.getInstance().closeSerialPort()
}
}
/**
* Toast
*
* @param content content
*/
private fun showToast(content: String) {
if (null == mToast) {
mToast = Toast.makeText(applicationContext, null, Toast.LENGTH_SHORT)
}
mToast?.setText(content)
mToast?.show()
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/MultiSerialPortActivity.java
================================================
package com.cl.myapplication;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import com.cl.myapplication.adapter.SpAdapter;
import com.cl.myapplication.databinding.ActivityMultiSerialBinding;
import com.cl.serialportlibrary.MultiSerialPortManager;
import com.cl.serialportlibrary.SerialPortFinder;
import com.cl.serialportlibrary.SimpleSerialPortManager;
import com.cl.serialportlibrary.enumerate.SerialStatus;
import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
import com.cl.serialportlibrary.stick.CompositeStickPackageHelper;
import com.cl.serialportlibrary.stick.SpecifiedStickPackageHelper;
import com.cl.serialportlibrary.stick.StaticLenStickPackageHelper;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* 多串口演示Activity
* 展示如何同时管理多个串口,每个串口使用不同的粘包策略
*/
public class MultiSerialPortActivity extends AppCompatActivity {
private static final String TAG = "MultiSerialPortActivity";
private ActivityMultiSerialBinding binding;
private TextView statusText;
private TextView dataText;
private final SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault());
// 串口设备列表
private String[] mDevices;
private String[] mBaudrates = {"9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"};
// 全局串口参数
private String[] mDatabits = {"8", "7", "6", "5"};
private String[] mParitys = {"NONE", "ODD", "EVEN", "SPACE", "MARK"};
private String[] mStopbits = {"1", "2"};
private int globalDatabits = 8;
private int globalParity = 0;
private int globalStopbits = 1;
// 各串口的配置
private SerialPortConfig gpsConfig = new SerialPortConfig();
private SerialPortConfig sensorConfig = new SerialPortConfig();
private SerialPortConfig modbusConfig = new SerialPortConfig();
private SerialPortConfig customConfig = new SerialPortConfig();
// 串口状态
private boolean gpsOpened = false;
private boolean sensorOpened = false;
private boolean modbusOpened = false;
private boolean customOpened = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_multi_serial);
initViews();
initDevices();
setupGlobalParamSpinners();
setupSpinners();
setupMultiSerial();
}
private void initViews() {
statusText = binding.tvStatus;
dataText = binding.tvData;
// 设置按钮点击事件
binding.btnRefreshPorts.setOnClickListener(v -> refreshSerialPorts());
binding.btnOpenAll.setOnClickListener(v -> openAllSerialPorts());
binding.btnCloseAll.setOnClickListener(v -> closeAllSerialPorts());
binding.btnSendTest.setOnClickListener(v -> sendTestData());
binding.btnClearLog.setOnClickListener(v -> clearLog());
// 各串口的开关按钮
binding.btnGpsToggle.setOnClickListener(v -> toggleGpsPort());
binding.btnSensorToggle.setOnClickListener(v -> toggleSensorPort());
binding.btnModbusToggle.setOnClickListener(v -> toggleModbusPort());
binding.btnCustomToggle.setOnClickListener(v -> toggleCustomPort());
}
private void setupMultiSerial() {
updateStatus("多串口管理器初始化完成");
}
/**
* 发送测试数据到所有串口
*/
private void sendTestData() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
int sentCount = 0;
// 向GPS发送AT命令
if (gpsOpened) {
boolean ok = manager.sendData("GPS", "AT+GPS?\r\n");
if (!ok) {
appendData("TX_FAIL [GPS] AT+GPS?\\r\\n");
}
if (ok) {
sentCount++;
}
}
// 向传感器发送读取命令
if (sensorOpened) {
boolean ok = manager.sendData("SENSOR", "READ_TEMP\n");
if (!ok) {
appendData("TX_FAIL [SENSOR] READ_TEMP\\n");
}
if (ok) {
sentCount++;
}
}
// 向Modbus设备发送读取寄存器命令
if (modbusOpened) {
byte[] modbusCmd = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, (byte)0x84, 0x0A};
boolean ok = manager.sendData("MODBUS", modbusCmd);
if (!ok) {
appendData("TX_FAIL [MODBUS] " + bytesToHex(modbusCmd));
}
if (ok) {
sentCount++;
}
}
// 向自定义协议设备发送命令
if (customOpened) {
String payload = "$$START$$GET_STATUS$$END$$";
boolean ok = manager.sendData("CUSTOM", payload);
if (!ok) {
appendData("TX_FAIL [CUSTOM] " + payload);
}
if (ok) {
sentCount++;
}
}
updateStatus("测试数据发送请求已提交到 " + sentCount + " 个串口");
}
/**
* 更新状态显示
*/
private void updateStatus(String message) {
appendLog(statusText, message, 20);
Log.i(TAG, message);
}
private void appendData(String message) {
appendLog(dataText, message, 200);
}
private void appendLog(TextView target, String message, int maxLines) {
if (target == null) {
return;
}
String time = timeFormatter.format(new Date());
String currentText = target.getText().toString();
String newText = time + " " + message + "\n" + currentText;
String[] lines = newText.split("\n");
if (lines.length > maxLines) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < maxLines; i++) {
sb.append(lines[i]).append("\n");
}
newText = sb.toString();
}
target.setText(newText);
}
private String sanitizeForSingleLine(String text) {
if (text == null) {
return "";
}
return text.replace("\r", "\\r").replace("\n", "\\n");
}
/**
* 字节数组转十六进制字符串
*/
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 清理资源
closeAllSerialPorts();
}
/**
* 演示动态更新粘包处理器
*/
public void updateStickyPacketExample(View view) {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// 动态更新GPS串口的粘包处理器,改为按回车换行分包
boolean success = manager.updateStickyPacketHelpers("GPS",
new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("\r\n")});
if (success) {
updateStatus("GPS串口粘包处理器已更新为\\r\\n分包");
} else {
updateStatus("GPS串口粘包处理器更新失败");
}
}
/**
* 展示串口数据路由功能
*/
public void serialRoutingExample() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
// 主控制串口:接收外部命令并路由到其他串口
manager.openSerialPort("MAIN_CTRL", "/dev/ttyS0", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
.build(),
null,
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String command = new String(data).trim();
Log.i(TAG, "主控制命令: " + command);
// 根据命令前缀路由到不同串口
if (command.startsWith("GPS:")) {
String gpsCmd = command.substring(4);
manager.sendData("GPS", gpsCmd + "\r\n");
updateStatus("路由命令到GPS: " + gpsCmd);
} else if (command.startsWith("SENSOR:")) {
String sensorCmd = command.substring(7);
manager.sendData("SENSOR", sensorCmd + "\n");
updateStatus("路由命令到传感器: " + sensorCmd);
} else if (command.startsWith("MODBUS:")) {
// 这里可以解析十六进制字符串并发送到Modbus
updateStatus("路由命令到Modbus: " + command);
}
}
});
updateStatus("串口路由功能已启用,可通过主控制串口发送命令");
}
/**
* 初始化设备列表
*/
private void initDevices() {
SerialPortFinder serialPortFinder = new SerialPortFinder();
mDevices = serialPortFinder.getAllDevicesPath();
if (mDevices.length == 0) {
mDevices = new String[]{"没有找到串口设备"};
}
// 初始化默认配置
gpsConfig.device = mDevices.length > 0 ? mDevices[0] : "";
gpsConfig.baudrate = "9600";
sensorConfig.device = mDevices.length > 1 ? mDevices[1] : (mDevices.length > 0 ? mDevices[0] : "");
sensorConfig.baudrate = "115200";
modbusConfig.device = mDevices.length > 2 ? mDevices[2] : (mDevices.length > 0 ? mDevices[0] : "");
modbusConfig.baudrate = "9600";
customConfig.device = mDevices.length > 3 ? mDevices[3] : (mDevices.length > 0 ? mDevices[0] : "");
customConfig.baudrate = "115200";
}
/**
* 设置全局参数下拉框
*/
private void setupGlobalParamSpinners() {
// 数据位配置
setupSpinner(binding.spinnerDatabits, mDatabits, 0, (position) -> {
globalDatabits = Integer.parseInt(mDatabits[position]);
updateStatus("全局数据位已设置为: " + globalDatabits);
closeAllOpenedPorts();
});
// 校验位配置
setupSpinner(binding.spinnerParity, mParitys, 0, (position) -> {
globalParity = position;
updateStatus("全局校验位已设置为: " + mParitys[position]);
closeAllOpenedPorts();
});
// 停止位配置
setupSpinner(binding.spinnerStopbits, mStopbits, 0, (position) -> {
globalStopbits = Integer.parseInt(mStopbits[position]);
updateStatus("全局停止位已设置为: " + globalStopbits);
closeAllOpenedPorts();
});
}
/**
* 关闭所有已打开的串口(参数变更时使用)
*/
private void closeAllOpenedPorts() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
boolean hasOpenPorts = false;
if (gpsOpened) {
manager.closeSerialPort("GPS");
gpsOpened = false;
hasOpenPorts = true;
}
if (sensorOpened) {
manager.closeSerialPort("SENSOR");
sensorOpened = false;
hasOpenPorts = true;
}
if (modbusOpened) {
manager.closeSerialPort("MODBUS");
modbusOpened = false;
hasOpenPorts = true;
}
if (customOpened) {
manager.closeSerialPort("CUSTOM");
customOpened = false;
hasOpenPorts = true;
}
if (hasOpenPorts) {
updateButtonStates();
updateStatus("参数变更,已关闭所有串口,请重新打开");
}
}
/**
* 设置下拉框
*/
private void setupSpinners() {
// GPS串口配置
setupSpinner(binding.spinnerGpsDevice, mDevices, 0, (position) -> {
gpsConfig.device = mDevices[position];
if (gpsOpened) {
updateStatus("GPS串口设备已更改,请重新打开串口");
closeGpsPort();
}
});
setupSpinner(binding.spinnerGpsBaudrate, mBaudrates, 0, (position) -> {
gpsConfig.baudrate = mBaudrates[position];
if (gpsOpened) {
updateStatus("GPS串口波特率已更改,请重新打开串口");
closeGpsPort();
}
});
// 传感器串口配置
setupSpinner(binding.spinnerSensorDevice, mDevices, mDevices.length > 1 ? 1 : 0, (position) -> {
sensorConfig.device = mDevices[position];
if (sensorOpened) {
updateStatus("传感器串口设备已更改,请重新打开串口");
closeSensorPort();
}
});
setupSpinner(binding.spinnerSensorBaudrate, mBaudrates, 4, (position) -> {
sensorConfig.baudrate = mBaudrates[position];
if (sensorOpened) {
updateStatus("传感器串口波特率已更改,请重新打开串口");
closeSensorPort();
}
});
// Modbus串口配置
setupSpinner(binding.spinnerModbusDevice, mDevices, mDevices.length > 2 ? 2 : 0, (position) -> {
modbusConfig.device = mDevices[position];
if (modbusOpened) {
updateStatus("Modbus串口设备已更改,请重新打开串口");
closeModbusPort();
}
});
setupSpinner(binding.spinnerModbusBaudrate, mBaudrates, 0, (position) -> {
modbusConfig.baudrate = mBaudrates[position];
if (modbusOpened) {
updateStatus("Modbus串口波特率已更改,请重新打开串口");
closeModbusPort();
}
});
// 自定义串口配置
setupSpinner(binding.spinnerCustomDevice, mDevices, mDevices.length > 3 ? 3 : 0, (position) -> {
customConfig.device = mDevices[position];
if (customOpened) {
updateStatus("自定义串口设备已更改,请重新打开串口");
closeCustomPort();
}
});
setupSpinner(binding.spinnerCustomBaudrate, mBaudrates, 4, (position) -> {
customConfig.baudrate = mBaudrates[position];
if (customOpened) {
updateStatus("自定义串口波特率已更改,请重新打开串口");
closeCustomPort();
}
});
}
/**
* 设置单个下拉框
*/
private void setupSpinner(Spinner spinner, String[] data, int defaultSelection, OnSpinnerItemSelectedListener listener) {
ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.spinner_default_item, data);
adapter.setDropDownViewResource(R.layout.spinner_item);
spinner.setAdapter(adapter);
spinner.setSelection(defaultSelection);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
listener.onItemSelected(position);
}
@Override
public void onNothingSelected(AdapterView> parent) {
}
});
}
/**
* 刷新串口列表
*/
private void refreshSerialPorts() {
initDevices();
setupSpinners();
updateStatus("串口列表已刷新,共找到 " + mDevices.length + " 个设备");
}
/**
* 打开所有串口
*/
private void openAllSerialPorts() {
updateStatus("开始打开所有串口...");
if (!gpsOpened) toggleGpsPort();
if (!sensorOpened) toggleSensorPort();
if (!modbusOpened) toggleModbusPort();
if (!customOpened) toggleCustomPort();
}
/**
* 关闭所有串口
*/
private void closeAllSerialPorts() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
manager.closeAllSerialPorts();
gpsOpened = false;
sensorOpened = false;
modbusOpened = false;
customOpened = false;
updateButtonStates();
updateStatus("所有串口已关闭");
}
/**
* GPS串口开关
*/
private void toggleGpsPort() {
if (gpsOpened) {
closeGpsPort();
} else {
openGpsPort();
}
}
private void openGpsPort() {
if (gpsConfig.device.equals("没有找到串口设备")) {
updateStatus("GPS串口:没有可用设备");
return;
}
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
int baudrate = Integer.parseInt(gpsConfig.baudrate);
manager.openSerialPort("GPS", gpsConfig.device, baudrate,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(globalDatabits)
.setParity(globalParity)
.setStopbits(globalStopbits)
.setStickyPacketHelpers(new BaseStickPackageHelper())
.build(),
(serialId, success, status) -> {
gpsOpened = success;
runOnUiThread(() -> {
updateButtonStates();
updateStatus(String.format("GPS串口[%s]: %s",
gpsConfig.device, success ? "打开成功" : "打开失败 - " + status));
});
},
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String text = sanitizeForSingleLine(new String(data));
String hex = bytesToHex(data);
Log.i(TAG, "GPS数据: " + text);
runOnUiThread(() -> appendData("RX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex));
}
@Override
public void onDataSent(String serialId, byte[] data) {
String text = sanitizeForSingleLine(new String(data));
String hex = bytesToHex(data);
runOnUiThread(() -> appendData("TX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex));
}
});
}
private void closeGpsPort() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
manager.closeSerialPort("GPS");
gpsOpened = false;
updateButtonStates();
updateStatus("GPS串口已关闭");
}
/**
* 传感器串口开关
*/
private void toggleSensorPort() {
if (sensorOpened) {
closeSensorPort();
} else {
openSensorPort();
}
}
private void openSensorPort() {
if (sensorConfig.device.equals("没有找到串口设备")) {
updateStatus("传感器串口:没有可用设备");
return;
}
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
int baudrate = Integer.parseInt(sensorConfig.baudrate);
manager.openSerialPort("SENSOR", sensorConfig.device, baudrate,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(globalDatabits)
.setParity(globalParity)
.setStopbits(globalStopbits)
.setStickyPacketHelpers(new CompositeStickPackageHelper(
new SpecifiedStickPackageHelper("\n"),
new BaseStickPackageHelper()))
.build(),
(serialId, success, status) -> {
sensorOpened = success;
runOnUiThread(() -> {
updateButtonStates();
updateStatus(String.format("传感器串口[%s]: %s",
sensorConfig.device, success ? "打开成功" : "打开失败 - " + status));
});
},
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String text = sanitizeForSingleLine(new String(data).trim());
String hex = bytesToHex(data);
Log.i(TAG, "传感器数据: " + text);
runOnUiThread(() -> appendData("RX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex));
}
@Override
public void onDataSent(String serialId, byte[] data) {
String text = sanitizeForSingleLine(new String(data));
String hex = bytesToHex(data);
runOnUiThread(() -> appendData("TX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex));
}
});
}
private void closeSensorPort() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
manager.closeSerialPort("SENSOR");
sensorOpened = false;
updateButtonStates();
updateStatus("传感器串口已关闭");
}
/**
* Modbus串口开关
*/
private void toggleModbusPort() {
if (modbusOpened) {
closeModbusPort();
} else {
openModbusPort();
}
}
private void openModbusPort() {
if (modbusConfig.device.equals("没有找到串口设备")) {
updateStatus("Modbus串口:没有可用设备");
return;
}
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
int baudrate = Integer.parseInt(modbusConfig.baudrate);
manager.openSerialPort("MODBUS", modbusConfig.device, baudrate,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(globalDatabits)
.setParity(globalParity)
.setStopbits(globalStopbits)
.setStickyPacketHelpers(new CompositeStickPackageHelper(
new StaticLenStickPackageHelper(8),
new BaseStickPackageHelper()))
.build(),
(serialId, success, status) -> {
modbusOpened = success;
runOnUiThread(() -> {
updateButtonStates();
updateStatus(String.format("Modbus串口[%s]: %s",
modbusConfig.device, success ? "打开成功" : "打开失败 - " + status));
});
},
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String modbusData = bytesToHex(data);
Log.i(TAG, "Modbus数据: " + modbusData);
runOnUiThread(() -> appendData("RX [" + serialId + "] len=" + data.length + " hex=" + modbusData));
}
@Override
public void onDataSent(String serialId, byte[] data) {
String hex = bytesToHex(data);
runOnUiThread(() -> appendData("TX [" + serialId + "] len=" + data.length + " hex=" + hex));
}
});
}
private void closeModbusPort() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
manager.closeSerialPort("MODBUS");
modbusOpened = false;
updateButtonStates();
updateStatus("Modbus串口已关闭");
}
/**
* 自定义串口开关
*/
private void toggleCustomPort() {
if (customOpened) {
closeCustomPort();
} else {
openCustomPort();
}
}
private void openCustomPort() {
if (customConfig.device.equals("没有找到串口设备")) {
updateStatus("自定义串口:没有可用设备");
return;
}
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
int baudrate = Integer.parseInt(customConfig.baudrate);
manager.openSerialPort("CUSTOM", customConfig.device, baudrate,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(globalDatabits)
.setParity(globalParity)
.setStopbits(globalStopbits)
.setStickyPacketHelpers(new CompositeStickPackageHelper(
new SpecifiedStickPackageHelper("$$START$$", "$$END$$"),
new BaseStickPackageHelper()))
.build(),
(serialId, success, status) -> {
customOpened = success;
runOnUiThread(() -> {
updateButtonStates();
updateStatus(String.format("自定义串口[%s]: %s",
customConfig.device, success ? "打开成功" : "打开失败 - " + status));
});
},
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String text = sanitizeForSingleLine(new String(data));
String hex = bytesToHex(data);
Log.i(TAG, "自定义协议数据: " + text);
runOnUiThread(() -> appendData("RX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex));
}
@Override
public void onDataSent(String serialId, byte[] data) {
String text = sanitizeForSingleLine(new String(data));
String hex = bytesToHex(data);
runOnUiThread(() -> appendData("TX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex));
}
});
}
private void closeCustomPort() {
MultiSerialPortManager manager = SimpleSerialPortManager.multi();
manager.closeSerialPort("CUSTOM");
customOpened = false;
updateButtonStates();
updateStatus("自定义串口已关闭");
}
/**
* 更新按钮状态
*/
private void updateButtonStates() {
binding.btnGpsToggle.setText(gpsOpened ? "关闭" : "打开");
binding.btnGpsToggle.setBackgroundColor(getResources().getColor(
gpsOpened ? R.color.secondary_text : R.color.colorAccent));
binding.btnSensorToggle.setText(sensorOpened ? "关闭" : "打开");
binding.btnSensorToggle.setBackgroundColor(getResources().getColor(
sensorOpened ? R.color.secondary_text : R.color.colorAccent));
binding.btnModbusToggle.setText(modbusOpened ? "关闭" : "打开");
binding.btnModbusToggle.setBackgroundColor(getResources().getColor(
modbusOpened ? R.color.secondary_text : R.color.colorAccent));
binding.btnCustomToggle.setText(customOpened ? "关闭" : "打开");
binding.btnCustomToggle.setBackgroundColor(getResources().getColor(
customOpened ? R.color.secondary_text : R.color.colorAccent));
}
/**
* 清空日志
*/
private void clearLog() {
if (statusText != null) {
statusText.setText("日志已清空");
}
if (dataText != null) {
dataText.setText("数据已清空");
}
}
/**
* 串口配置类
*/
private static class SerialPortConfig {
String device = "";
String baudrate = "9600";
}
/**
* 下拉框选择监听器
*/
private interface OnSpinnerItemSelectedListener {
void onItemSelected(int position);
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/SelectSerialPortActivity.kt
================================================
package com.cl.myapplication
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.cl.myapplication.adapter.DeviceAdapter
import com.cl.myapplication.databinding.ActivitySelectSerialPortBinding
import com.cl.serialportlibrary.SerialPortFinder
class SelectSerialPortActivity : AppCompatActivity(), OnItemClickListener {
private var mDeviceAdapter: DeviceAdapter? = null
val DEVICE = "device"
private lateinit var binding: ActivitySelectSerialPortBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_select_serial_port)
val serialPortFinder = SerialPortFinder()
val devices = serialPortFinder.devices
if (binding.lvDevices != null) {
binding.lvDevices.emptyView = binding.tvEmpty
mDeviceAdapter = DeviceAdapter(applicationContext, devices)
binding.lvDevices.adapter = mDeviceAdapter
binding.lvDevices.onItemClickListener = this
}
}
override fun onItemClick(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
val device = mDeviceAdapter!!.getItem(position)
val intent = Intent(this, MainActivity::class.java)
intent.putExtra(DEVICE, device)
startActivity(intent)
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/SingleSerialPortActivity.java
================================================
package com.cl.myapplication;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.FragmentManager;
import com.cl.myapplication.adapter.SpAdapter;
import com.cl.myapplication.constant.PreferenceKeys;
import com.cl.myapplication.databinding.ActivityMainJavaBinding;
import com.cl.myapplication.fragment.LogFragment;
import com.cl.myapplication.message.ConversionNoticeEvent;
import com.cl.myapplication.message.IMessage;
import com.cl.myapplication.message.LogManager;
import com.cl.myapplication.message.RecvMessage;
import com.cl.myapplication.message.SendMessage;
import com.cl.myapplication.util.PrefHelper;
import com.hjq.toast.ToastUtils;
import com.cl.serialportlibrary.Device;
import com.cl.serialportlibrary.SerialPortFinder;
import com.cl.serialportlibrary.SimpleSerialPortManager;
import com.cl.serialportlibrary.utils.SerialPortLogUtil;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.Arrays;
/**
* 单串口演示
*/
public class SingleSerialPortActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener {
private ActivityMainJavaBinding binding;
private Device mDevice;
private String[] mDevices;
private String[] mBaudrates;
private int mDeviceIndex;
private int mBaudrateIndex;
private boolean mOpened = false;
private boolean mConversionNotice = true;
private LogFragment mLogFragment;
final String[] databits = new String[]{"8", "7", "6", "5"};
final String[] paritys = new String[]{"NONE", "ODD", "EVEN", "SPACE", "MARK"};
final String[] stopbits = new String[]{"1", "2"};
//先定义
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main_java);
verifyStoragePermissions(this);
initFragment();
initDevice();
initSpinners();
//设置数据位
SpAdapter spAdapter1 = new SpAdapter(this);
spAdapter1.setDatas(databits);
binding.spDatabits.setAdapter(spAdapter1);
binding.spDatabits.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
closeSerialPort();
int dataBitValue = Integer.parseInt(databits[position]);
SimpleSerialPortManager.getInstance().setDatabits(dataBitValue);
SerialPortLogUtil.i("MainJavaActivity", "设置数据位: " + dataBitValue);
}
@Override
public void onNothingSelected(AdapterView> parent) {
}
});
//设置数据位
SpAdapter spAdapter2 = new SpAdapter(this);
spAdapter2.setDatas(paritys);
binding.spParity.setAdapter(spAdapter2);
binding.spParity.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
closeSerialPort();
SimpleSerialPortManager.getInstance().setParity(position);
SerialPortLogUtil.i("MainJavaActivity", "设置校验位: " + paritys[position] + " (值: " + position + ")");
}
@Override
public void onNothingSelected(AdapterView> parent) {
}
});
//设置停止位
SpAdapter spAdapter3 = new SpAdapter(this);
spAdapter3.setDatas(stopbits);
binding.spStopbits.setAdapter(spAdapter3);
binding.spStopbits.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
closeSerialPort();
int stopBitValue = Integer.parseInt(stopbits[position]);
SimpleSerialPortManager.getInstance().setStopbits(stopBitValue);
SerialPortLogUtil.i("MainJavaActivity", "设置停止位: " + stopBitValue);
}
@Override
public void onNothingSelected(AdapterView> parent) {
}
});
binding.btnOpenDevice.setOnClickListener(v -> {
if (mOpened) {
closeSerialPort();
} else {
openSerialPort();
}
});
binding.btnSendData.setOnClickListener((view) -> {
onSend();
});
}
/**
* 打开串口
*/
private void openSerialPort() {
String devicePath = mDevice.getName();
int baudRate = Integer.parseInt(mDevice.getRoot());
SerialPortLogUtil.i("MainJavaActivity", "打开的串口为:" + devicePath + "----" + baudRate);
// 使用SimpleSerialPortManager打开串口
boolean success = SimpleSerialPortManager.getInstance()
.openSerialPort(devicePath, baudRate,
// 打开状态回调
(isSuccess, status) -> {
runOnUiThread(() -> {
switch (status) {
case SUCCESS_OPENED:
ToastUtils.show("串口打开成功");
mOpened = true;
updateViewState(true);
break;
case NO_READ_WRITE_PERMISSION:
ToastUtils.show("没有读写权限");
updateViewState(false);
break;
case OPEN_FAIL:
ToastUtils.show("串口打开失败");
updateViewState(false);
break;
}
});
},
// 数据接收回调
new SimpleSerialPortManager.OnDataReceivedCallback() {
@Override
public void onDataReceived(byte[] data) {
SerialPortLogUtil.i("MainJavaActivity", "onDataReceived [ byte[] ]: " + Arrays.toString(data));
SerialPortLogUtil.i("MainJavaActivity", "onDataReceived [ String ]: " + new String(data));
runOnUiThread(() -> {
if (mConversionNotice) {
LogManager.instance().post(new RecvMessage(bytesToHex(data)));
} else {
LogManager.instance().post(new RecvMessage(Arrays.toString(data)));
}
});
}
@Override
public void onDataSent(byte[] data) {
SerialPortLogUtil.i("MainJavaActivity", "onDataSent [ byte[] ]: " + Arrays.toString(data));
SerialPortLogUtil.i("MainJavaActivity", "onDataSent [ String ]: " + new String(data));
runOnUiThread(() -> {
if (mConversionNotice) {
LogManager.instance().post(new SendMessage(bytesToHex(data)));
} else {
LogManager.instance().post(new SendMessage(Arrays.toString(data)));
}
});
}
});
}
private void closeSerialPort() {
SimpleSerialPortManager.getInstance().closeSerialPort();
mOpened = false;
updateViewState(mOpened);
}
public static void verifyStoragePermissions(Activity activity) {
try {
//检测是否有写的权限
int permission = ActivityCompat.checkSelfPermission(activity,
"android.permission.WRITE_EXTERNAL_STORAGE");
if (permission != PackageManager.PERMISSION_GRANTED) {
// 没有写的权限,去申请写的权限,会弹出对话框
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 更新视图状态
*
* @param isSerialPortOpened
*/
private void updateViewState(boolean isSerialPortOpened) {
int stringRes = isSerialPortOpened ? R.string.close_serial_port : R.string.open_serial_port;
binding.btnOpenDevice.setText(stringRes);
binding.spinnerDevices.setEnabled(!isSerialPortOpened);
binding.spinnerBaudrate.setEnabled(!isSerialPortOpened);
binding.btnSendData.setEnabled(isSerialPortOpened);
binding.btnLoadList.setEnabled(isSerialPortOpened);
}
/**
* 初始化设备列表
*/
private void initDevice() {
PrefHelper.initDefault(this);
SerialPortFinder serialPortFinder = new SerialPortFinder();
// 设备
mDevices = serialPortFinder.getAllDevicesPath();
if (mDevices.length == 0) {
mDevices = new String[]{
getString(R.string.no_serial_device)
};
}
// 波特率
mBaudrates = getResources().getStringArray(R.array.baudrates);
mDeviceIndex = PrefHelper.getDefault().getInt(PreferenceKeys.SERIAL_PORT_DEVICES, 0);
mDeviceIndex = mDeviceIndex >= mDevices.length ? mDevices.length - 1 : mDeviceIndex;
mBaudrateIndex = PrefHelper.getDefault().getInt(PreferenceKeys.BAUD_RATE, 0);
mDevice = new Device(mDevices[mDeviceIndex], mBaudrates[mBaudrateIndex], null);
}
/**
* 初始化下拉选项
*/
private void initSpinners() {
ArrayAdapter deviceAdapter =
new ArrayAdapter(this, R.layout.spinner_default_item, mDevices);
deviceAdapter.setDropDownViewResource(R.layout.spinner_item);
binding.spinnerDevices.setAdapter(deviceAdapter);
binding.spinnerDevices.setOnItemSelectedListener(this);
ArrayAdapter baudrateAdapter =
new ArrayAdapter(this, R.layout.spinner_default_item, mBaudrates);
baudrateAdapter.setDropDownViewResource(R.layout.spinner_item);
binding.spinnerBaudrate.setAdapter(baudrateAdapter);
binding.spinnerBaudrate.setOnItemSelectedListener(this);
binding.spinnerDevices.setSelection(mDeviceIndex);
binding.spinnerBaudrate.setSelection(mBaudrateIndex);
}
/**
* 发送数据
*/
public void onSend() {
String sendContent = binding.etData.getText().toString().trim();
if (TextUtils.isEmpty(sendContent)) {
SerialPortLogUtil.i("MainJavaActivity", "onSend: 发送内容为 null");
return;
}
byte[] sendContentBytes = sendContent.getBytes();
// 使用SimpleSerialPortManager发送数据
boolean sendBytes = SimpleSerialPortManager.getInstance().sendData(sendContentBytes);
SerialPortLogUtil.i("MainJavaActivity", "onSend: sendBytes = " + sendBytes);
}
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
// Spinner 选择监听
int parentId = parent.getId();
if (parentId == R.id.spinner_devices) {
mDeviceIndex = position;
mDevice.setName(mDevices[mDeviceIndex]);
} else if (parentId == R.id.spinner_baudrate) {
mBaudrateIndex = position;
mDevice.setRoot(mBaudrates[mBaudrateIndex]);
}
}
@Override
public void onNothingSelected(AdapterView> parent) {
}
@Override
protected void onDestroy() {
SimpleSerialPortManager.getInstance().closeSerialPort();
super.onDestroy();
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Override
protected void onResume() {
super.onResume();
refreshLogList();
}
/**
* 初始化日志Fragment
*/
protected void initFragment() {
FragmentManager fragmentManager = getSupportFragmentManager();
mLogFragment = (LogFragment) fragmentManager.findFragmentById(R.id.log_fragment);
}
/**
* 刷新日志列表
*/
protected void refreshLogList() {
mLogFragment.updateAutoEndButton();
mLogFragment.updateList();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(IMessage message) {
// 收到时间,刷新界面
mLogFragment.add(message);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onConversionNotice(ConversionNoticeEvent messageEvent) {
if (messageEvent.getMessage().equals("1")) {
mConversionNotice = false;
} else {
mConversionNotice = true;
}
}
/**
* 字节数组转16进制
*
* @param bytes 需要转换的byte数组
* @return 转换后的Hex字符串
*/
public static String bytesToHex(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() < 2) {
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/adapter/DeviceAdapter.java
================================================
package com.cl.myapplication.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import androidx.databinding.DataBindingUtil;
import com.cl.myapplication.R;
import com.cl.myapplication.databinding.ItemDeviceBinding;
import com.cl.serialportlibrary.Device;
import java.io.File;
import java.util.ArrayList;
/**
* 串口列表适配器
*/
public class DeviceAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private ArrayList devices;
public DeviceAdapter(Context context, ArrayList devices) {
this.mInflater = LayoutInflater.from(context);
this.devices = devices;
}
@Override
public int getCount() {
return devices.size();
}
@Override
public Device getItem(int position) {
return devices.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ItemDeviceBinding binding;
if (null == convertView) {
binding = DataBindingUtil.inflate(mInflater, R.layout.item_device, parent, false);
convertView = binding.getRoot();
convertView.setTag(binding);
} else {
binding = (ItemDeviceBinding) convertView.getTag();
}
String deviceName = devices.get(position).getName();
String driverName = devices.get(position).getRoot();
File file = devices.get(position).getFile();
boolean canRead = file.canRead();
boolean canWrite = file.canWrite();
boolean canExecute = file.canExecute();
String path = file.getAbsolutePath();
StringBuffer permission = new StringBuffer();
permission.append("\t权限[");
permission.append(canRead ? " 可读 " : " 不可读 ");
permission.append(canWrite ? " 可写 " : " 不可写 ");
permission.append(canExecute ? " 可执行 " : " 不可执行 ");
permission.append("]");
binding.tvDevice.setText(String.format("%s [%s] (%s) %s", deviceName, driverName, path, permission));
return convertView;
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/adapter/SpAdapter.java
================================================
package com.cl.myapplication.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import androidx.databinding.DataBindingUtil;
import com.cl.myapplication.R;
import com.cl.myapplication.databinding.ItemDeviceBinding;
public class SpAdapter extends BaseAdapter {
String[] datas;
Context mContext;
public SpAdapter(Context context) {
this.mContext = context;
}
public void setDatas(String[] datas) {
this.datas = datas;
notifyDataSetChanged();
}
@Override
public int getCount() {
return datas == null ? 0 : datas.length;
}
@Override
public Object getItem(int position) {
return datas == null ? null : datas[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ItemDeviceBinding binding;
if (convertView == null) {
binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.item_device, parent, false);
convertView = binding.getRoot();
convertView.setTag(binding);
} else {
binding = (ItemDeviceBinding) convertView.getTag();
}
binding.tvDevice.setText(datas[position]);
return convertView;
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/constant/PreferenceKeys.java
================================================
package com.cl.myapplication.constant;
public class PreferenceKeys {
/**
* 串口设备
*/
public static String SERIAL_PORT_DEVICES = "serial_port_devices";
/**
* 波特率
*/
public static String BAUD_RATE = "baud_rate";
}
================================================
FILE: app/src/main/java/com/cl/myapplication/fragment/LogFragment.java
================================================
package com.cl.myapplication.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import com.cl.myapplication.R;
import com.cl.myapplication.databinding.FragmentLogBinding;
import com.cl.myapplication.message.ConversionNoticeEvent;
import com.cl.myapplication.message.IMessage;
import com.cl.myapplication.message.LogManager;
import com.cl.myapplication.util.ListViewHolder;
import org.greenrobot.eventbus.EventBus;
public class LogFragment extends Fragment {
private LogAdapter mAdapter;
private boolean mConversionNotice = true;
private FragmentLogBinding binding;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_log, container, false);
binding.btnClearLog.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 清空列表
LogManager.instance().clear();
updateList();
}
});
binding.btnAutoEnd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogManager.instance().changAutoEnd();
updateAutoEndButton();
}
});
binding.btnWhetherHexadecimal.setOnClickListener((view1) -> {
if (mConversionNotice){
EventBus.getDefault().post(new ConversionNoticeEvent("1"));
mConversionNotice=false;
}else {
EventBus.getDefault().post(new ConversionNoticeEvent("2"));
mConversionNotice=true;
}
});
mAdapter = new LogAdapter();
binding.lvLogs.setAdapter(mAdapter);
updateAutoEndButton();
return binding.getRoot();
}
public void updateAutoEndButton() {
if (binding != null) {
if (LogManager.instance().isAutoEnd()) {
binding.btnAutoEnd.setText("禁止自动显示最新日志");
binding.lvLogs.setSelection(mAdapter.getCount() - 1);
} else {
binding.btnAutoEnd.setText("自动显示最新日志");
}
}
}
private static class LogAdapter extends BaseAdapter {
@Override
public int getCount() {
return LogManager.instance().messages.size();
}
@Override
public IMessage getItem(int position) {
return LogManager.instance().messages.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
IMessage message = getItem(position);
ListViewHolder holder;
if (convertView == null) {
holder = new ListViewHolder(R.layout.item_log, parent);
convertView = holder.getItemView();
} else {
holder = (ListViewHolder) convertView.getTag();
}
TextView tvLog = holder.getText(R.id.tv_log);
TextView tvNum = holder.getText(R.id.tv_num);
tvLog.setText(message.getMessage());
tvLog.setEnabled(message.isToSend());
tvNum.setText(String.valueOf(position + 1));
return convertView;
}
}
public void add(IMessage message) {
LogManager.instance().add(message);
updateList();
}
public void updateList() {
if (binding != null) {
mAdapter.notifyDataSetChanged();
if (LogManager.instance().isAutoEnd()) {
binding.lvLogs.setSelection(mAdapter.getCount() - 1);
}
}
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/message/ConversionNoticeEvent.java
================================================
package com.cl.myapplication.message;
/**
* 项目:serialPort
* 作者:Arry
* 创建日期:2021/10/20
* 描述:
* 修订历史:
*/
public class ConversionNoticeEvent {
private String message;
public ConversionNoticeEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/message/IMessage.java
================================================
package com.cl.myapplication.message;
/**
* 日志消息数据接口
*/
public interface IMessage {
/**
* 消息文本
*
* @return
*/
String getMessage();
/**
* 是否发送的消息
*
* @return
*/
boolean isToSend();
}
================================================
FILE: app/src/main/java/com/cl/myapplication/message/LogManager.java
================================================
package com.cl.myapplication.message;
import org.greenrobot.eventbus.EventBus;
import java.util.ArrayList;
import java.util.List;
/**
* log管理类
*/
public class LogManager {
public final List messages;
private boolean mAutoEnd = true;
public LogManager() {
messages = new ArrayList<>();
}
private static class InstanceHolder {
public static LogManager sManager = new LogManager();
}
public static LogManager instance() {
return InstanceHolder.sManager;
}
public void add(IMessage message) {
messages.add(message);
}
public void post(IMessage message) {
EventBus.getDefault().post(message);
}
public void clear() {
messages.clear();
}
public boolean isAutoEnd() {
return mAutoEnd;
}
public void setAutoEnd(boolean autoEnd) {
mAutoEnd = autoEnd;
}
public void changAutoEnd() {
mAutoEnd = !mAutoEnd;
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/message/RecvMessage.java
================================================
package com.cl.myapplication.message;
import com.cl.myapplication.util.TimeUtil;
/**
* 收到的日志
*/
public class RecvMessage implements IMessage {
private String command;
private String message;
public RecvMessage(String command) {
this.command = command;
this.message = TimeUtil.currentTime() + " 收到命令:" + command;
}
@Override
public String getMessage() {
return message;
}
@Override
public boolean isToSend() {
return false;
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/message/SendMessage.java
================================================
package com.cl.myapplication.message;
import com.cl.myapplication.util.TimeUtil;
/**
* 发送的日志
*/
public class SendMessage implements IMessage {
private String command;
private String message;
public SendMessage(String command) {
this.command = command;
this.message = TimeUtil.currentTime() + " 发送命令:" + command;
}
@Override
public String getMessage() {
return message;
}
@Override
public boolean isToSend() {
return true;
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/util/ByteUtil.java
================================================
package com.cl.myapplication.util;
/**
* name:cl
* date:2022/12/27
* desc:Byte工具类
*/
public class ByteUtil {
/**
* 字节数组转16进制
*
* @param bytes 需要转换的byte数组
* @return 转换后的Hex字符串
*/
public static String bytesToHex(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() < 2) {
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
public static String trim(String s) {
int i = s.length();// 字符串最后一个字符的位置
int j = 0;// 字符串第一个字符
int k = 0;// 中间变量
char[] arrayOfChar = s.toCharArray();// 将字符串转换成字符数组
while ((j < i) && (arrayOfChar[(k + j)] <= ' '))
++j;// 确定字符串前面的空格数
while ((j < i) && (arrayOfChar[(k + i - 1)] <= ' '))
--i;// 确定字符串后面的空格数
return (((j > 0) || (i < s.length())) ? s.substring(j, i) : s);// 返回去除空格后的字符串
}
public static String toChineseHex(String s) {
String ss = s;
byte[] bt = new byte[0];
try {
bt = ss.getBytes("UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
String s1 = "";
for (int i = 0; i < bt.length; i++) {
String tempStr = Integer.toHexString(bt[i]);
if (tempStr.length() > 2)
tempStr = tempStr.substring(tempStr.length() - 2);
s1 = s1 + tempStr + "";
}
return s1.toUpperCase();
}
/**
* 异或校验,返回一个字节
*/
public static byte orVerification(byte[] bytes) {
int nAll = 0;
for (int i = 0; i < bytes.length; i++) {
int nTemp = bytes[i];
nAll = nAll ^ nTemp;
}
return (byte) nAll;
}
public static byte complement(byte[] bytes) {
int iSum = 0;
for (int i = 0; i < bytes.length; i++) {
iSum += bytes[i];
}
iSum = 256 - iSum;
return (byte) iSum;
}
/**
* 多个byte数组合并
*/
public static byte[] byteMergerAll(byte[]... values) {
int length_byte = 0;
for (int i = 0; i < values.length; i++) {
length_byte += values[i].length;
}
byte[] all_byte = new byte[length_byte];
int countLength = 0;
for (int i = 0; i < values.length; i++) {
byte[] b = values[i];
System.arraycopy(b, 0, all_byte, countLength, b.length);
countLength += b.length;
}
return all_byte;
}
public static byte[] hex2Byte(String hex) {
String[] parts = hex.split(" ");
byte[] bytes = new byte[parts.length];
for (int i = 0; i < parts.length; i++) {
bytes[i] = (byte) Integer.parseInt(parts[i], 16);
}
return bytes;
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/util/ListViewHolder.java
================================================
package com.cl.myapplication.util;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
public class ListViewHolder {
private SparseArray mViewArray;
public View itemView;
public int position;
public ListViewHolder(View itemView) {
this.itemView = itemView;
mViewArray = new SparseArray<>();
this.itemView.setTag(this);
}
public ListViewHolder(int layoutId, ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
this.itemView = view;
mViewArray = new SparseArray<>();
this.itemView.setTag(this);
}
public View getItemView() {
return itemView;
}
public void bindPosition(int position) {
this.position = position;
}
public int getPosition() {
return position;
}
public V getView(int resId) {
View view = mViewArray.get(resId);
if (view == null) {
view = itemView.findViewById(resId);
mViewArray.put(resId, view);
}
return (V) view;
}
public void setText(int resId, CharSequence text) {
TextView textView = getView(resId);
textView.setText(text);
}
public TextView getText(int id) {
return getView(id);
}
public ImageView getImage(int id) {
return getView(id);
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/util/PrefHelper.java
================================================
package com.cl.myapplication.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
public class PrefHelper {
private static PrefHelper sInstance;
private SharedPreferences mPreferences;
public static void initDefault(Context context) {
sInstance = new PrefHelper(PreferenceManager.getDefaultSharedPreferences(context));
}
public static PrefHelper getDefault() {
return sInstance;
}
public static PrefHelper get(Context context, String name) {
return new PrefHelper(context, name);
}
private PrefHelper(SharedPreferences preferences) {
mPreferences = preferences;
}
private PrefHelper(Context context, String name) {
mPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE);
}
public SharedPreferences.Editor edit() {
return mPreferences.edit();
}
public SharedPreferences.Editor putInt(String key, int value) {
return edit().putInt(key, value);
}
public void saveInt(String key, int value) {
putInt(key, value).apply();
}
public int getInt(String key, int defValue) {
return mPreferences.getInt(key, defValue);
}
public SharedPreferences.Editor putFloat(String key, float value) {
return edit().putFloat(key, value);
}
public void saveFloat(String key, float value) {
putFloat(key, value).apply();
}
public float getFloat(String key, float defValue) {
return mPreferences.getFloat(key, defValue);
}
public SharedPreferences.Editor putBoolean(String key, boolean value) {
return edit().putBoolean(key, value);
}
public void saveBoolean(String key, boolean value) {
putBoolean(key, value).apply();
}
public boolean getBoolean(String key, boolean defValue) {
return mPreferences.getBoolean(key, defValue);
}
public SharedPreferences.Editor putLong(String key, long value) {
return edit().putLong(key, value);
}
public void saveLong(String key, long value) {
putLong(key, value).apply();
}
public long getLong(String key, long defValue) {
return mPreferences.getLong(key, defValue);
}
public SharedPreferences.Editor putString(String key, String value) {
return edit().putString(key, value);
}
public void saveString(String key, String value) {
putString(key, value).apply();
}
public String getString(String key, String defValue) {
return mPreferences.getString(key, defValue);
}
}
================================================
FILE: app/src/main/java/com/cl/myapplication/util/TimeUtil.java
================================================
package com.cl.myapplication.util;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeUtil {
public static final SimpleDateFormat DEFAULT_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
public static String currentTime() {
Date date = new Date();
return DEFAULT_FORMAT.format(date);
}
}
================================================
FILE: app/src/main/res/color/selector_log_text.xml
================================================
================================================
FILE: app/src/main/res/color/selector_spinner_text.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_main_java.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_multi_serial.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_multi_serial_new.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_select_serial_port.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_log.xml
================================================
================================================
FILE: app/src/main/res/layout/include_fragment_container.xml
================================================
================================================
FILE: app/src/main/res/layout/item_device.xml
================================================
================================================
FILE: app/src/main/res/layout/item_log.xml
================================================
================================================
FILE: app/src/main/res/layout/spinner_default_item.xml
================================================
================================================
FILE: app/src/main/res/layout/spinner_item.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#FFBB86FC
#FF6200EE
#FF3700B3
#FF03DAC5
#FF018786
#FF000000
#FFFFFFFF
#3F51B5
#303F9F
#FF4081
#e4e4e4
#55a0a7cc
#ffa0a7cc
#e5e5e5
#00000000
#30bfbfbf
#727272
#B6B6B6
#888888
#20ffffff
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
================================================
FILE: app/src/main/res/values/string_arrays.xml
================================================
- 110
- 300
- 600
- 1200
- 2400
- 4800
- 9600
- 14400
- 19200
- 38400
- 56000
- 57600
- 115200
- 128000
- 256000
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 0:设备默认
- 1:快 20ms
- 2:中 40ms
- 3:慢 60ms
================================================
FILE: app/src/main/res/values/strings.xml
================================================
串口调试助手
串口设备:
波特率
打开串口
关闭串口
找不到串口设备
发送命令
输入命令
加载命令列表
数据位:
校验位:
停止位:
================================================
FILE: app/src/main/res/values/themes.xml
================================================
================================================
FILE: app/src/main/res/values-night/themes.xml
================================================
================================================
FILE: build.gradle
================================================
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}
================================================
FILE: gradle/libs.versions.toml
================================================
[versions]
agp = "8.12.2"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Sun Sep 28 20:08:42 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: serial_lib/.gitignore
================================================
/build
================================================
FILE: serial_lib/build.gradle
================================================
plugins {
id 'com.android.library'
id 'maven-publish'
}
android {
namespace "com.cl.serialportlibrary"
compileSdk 34
defaultConfig {
minSdk 21
targetSdk 34
versionCode 508
versionName "5.0.8"
consumerProguardFiles "consumer-rules.pro"
externalNativeBuild {
cmake {
// 设置 Android 平台和 STL
arguments "-DANDROID_PLATFORM=android-21",
"-DANDROID_STL=c++_shared"
}
}
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.6.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
groupId = 'com.github.cl-6666'
artifactId = 'serialPort'
version = '5.0.6'
artifact bundleReleaseAar
pom {
name = 'SerialPort Library'
description = 'Android Serial Port Library with JNI support'
url = 'https://github.com/cl-6666/serialPort'
licenses {
license {
name = 'The Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id = 'cl-6666'
name = 'cl-6666'
}
}
scm {
connection = 'scm:git:git://github.com/cl-6666/serialPort.git'
developerConnection = 'scm:git:ssh://github.com:cl-6666/serialPort.git'
url = 'https://github.com/cl-6666/serialPort/tree/master'
}
}
}
}
}
}
================================================
FILE: serial_lib/consumer-rules.pro
================================================
# Consumer proguard rules for serial_lib
# Keep all public APIs
-keep public class com.cl.serialportlibrary.** { *; }
# Keep native methods
-keepclasseswithmembernames class * {
native ;
}
# Keep enums
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Keep Serializable classes
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
================================================
FILE: serial_lib/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in D:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
================================================
FILE: serial_lib/src/main/AndroidManifest.xml
================================================
================================================
FILE: serial_lib/src/main/cpp/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.18.1)
project(SerialPort C)
add_library(SerialPort SHARED
SerialPort.c)
# 添加 16KB 页面对齐支持 (arm64-v8a)
if(ANDROID_ABI STREQUAL "arm64-v8a")
target_compile_options(SerialPort PRIVATE -fno-emulated-tls)
target_link_options(SerialPort PRIVATE
"LINKER:-z,max-page-size=16384"
"LINKER:-z,common-page-size=16384")
endif()
# Include libraries needed for libserial_port lib
target_link_libraries(SerialPort
android
log)
================================================
FILE: serial_lib/src/main/cpp/SerialPort.c
================================================
/*
* Copyright 2009-2011 Cedric Priscal
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include
#include
#include
#include
#include
#include
#include
#include "SerialPort.h"
#include "android/log.h"
static const char *TAG="serial_port";
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
static speed_t getBaudrate(jint baudrate)
{
switch(baudrate) {
case 0: return B0;
case 50: return B50;
case 75: return B75;
case 110: return B110;
case 134: return B134;
case 150: return B150;
case 200: return B200;
case 300: return B300;
case 600: return B600;
case 1200: return B1200;
case 1800: return B1800;
case 2400: return B2400;
case 4800: return B4800;
case 9600: return B9600;
case 19200: return B19200;
case 38400: return B38400;
case 57600: return B57600;
case 115200: return B115200;
case 230400: return B230400;
case 460800: return B460800;
case 500000: return B500000;
case 576000: return B576000;
case 921600: return B921600;
case 1000000: return B1000000;
case 1152000: return B1152000;
case 1500000: return B1500000;
case 2000000: return B2000000;
case 2500000: return B2500000;
case 3000000: return B3000000;
case 3500000: return B3500000;
case 4000000: return B4000000;
default: return -1;
}
}
/*
* Class: android_serialport_SerialPort
* Method: open
* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
*/
JNIEXPORT jobject JNICALL Java_com_cl_serialportlibrary_SerialPort_open
(JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags, jint databits, jint stopbits, jint parity)
{
int fd;
speed_t speed;
jobject mFileDescriptor;
/* Check arguments */
{
speed = getBaudrate(baudrate);
if (speed == -1) {
LOGE("Invalid baudrate");
return NULL;
}
}
/* Opening device */
{
jboolean iscopy;
const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);
LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR | flags);
fd = open(path_utf, O_RDWR | flags);
LOGD("open() fd = %d", fd);
(*env)->ReleaseStringUTFChars(env, path, path_utf);
if (fd == -1) {
LOGE("Cannot open port");
return NULL;
}
}
/* Configure device */
{
struct termios cfg;
LOGD("Configuring serial port");
if (tcgetattr(fd, &cfg)) {
LOGE("tcgetattr() failed");
close(fd);
return NULL;
}
// Initialize termios struct
cfmakeraw(&cfg);
// Set data bits
cfg.c_cflag &= ~CSIZE;
switch (databits) {
case 5:
cfg.c_cflag |= CS5;
break;
case 6:
cfg.c_cflag |= CS6;
break;
case 7:
cfg.c_cflag |= CS7;
break;
case 8:
cfg.c_cflag |= CS8;
break;
default:
LOGE("Invalid data bits");
close(fd);
return NULL;
}
// Set stop bits
switch (stopbits) {
case 1:
cfg.c_cflag &= ~CSTOPB;
break;
case 2:
cfg.c_cflag |= CSTOPB;
break;
default:
LOGE("Invalid stop bits");
close(fd);
return NULL;
}
switch (parity) {
case 0:
cfg.c_cflag &= ~PARENB; //PARITY OFF
break;
case 1:
cfg.c_cflag |= (PARODD | PARENB); //PARITY ODD
cfg.c_iflag &= ~IGNPAR;
cfg.c_iflag |= PARMRK;
cfg.c_iflag |= INPCK;
break;
case 2:
cfg.c_iflag &= ~(IGNPAR | PARMRK); //PARITY EVEN
cfg.c_iflag |= INPCK;
cfg.c_cflag |= PARENB;
cfg.c_cflag &= ~PARODD;
break;
case 3:
// PARITY SPACE
cfg.c_iflag &= ~IGNPAR; // Make sure wrong parity is not ignored
cfg.c_iflag |= PARMRK; // Marks parity error, parity error
// is given as three char sequence
cfg.c_iflag |= INPCK; // Enable input parity checking
cfg.c_cflag |= PARENB | CMSPAR; // Enable parity and set space parity
cfg.c_cflag &= ~PARODD; //
break;
case 4:
// PARITY MARK
cfg.c_iflag &= ~IGNPAR; // Make sure wrong parity is not ignored
cfg.c_iflag |= PARMRK; // Marks parity error, parity error
// is given as three char sequence
cfg.c_iflag |= INPCK; // Enable input parity checking
cfg.c_cflag |= PARENB | CMSPAR | PARODD;
break;
default:
cfg.c_cflag &= ~PARENB;
break;
}
// Set baud rate
cfsetispeed(&cfg, speed);
cfsetospeed(&cfg, speed);
if (tcsetattr(fd, TCSANOW, &cfg)) {
LOGE("tcsetattr() failed");
close(fd);
return NULL;
}
}
/* Create a corresponding file descriptor */
{
jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");
jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "", "()V");
jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);
(*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint)fd);
}
return mFileDescriptor;
}
JNIEXPORT void JNICALL Java_com_cl_serialportlibrary_SerialPort_close
(JNIEnv *env, jobject thiz)
{
jclass SerialPortClass = (*env)->GetObjectClass(env, thiz);
jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor");
jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");
jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I");
jobject mFd = (*env)->GetObjectField(env, thiz, mFdID);
jint descriptor = (*env)->GetIntField(env, mFd, descriptorID);
LOGD("close(fd = %d)", descriptor);
close(descriptor);
}
================================================
FILE: serial_lib/src/main/cpp/SerialPort.h
================================================
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class android_serialport_api_SerialPort */
#ifndef _Included_qingwei_kong_serialportlibrary_SerialPort
#define _Included_qingwei_kong_serialportlibrary_SerialPort
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: android_serialport_api_SerialPort
* Method: open
* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
*/
JNIEXPORT jobject JNICALL Java_com_cl_serialportlibrary_SerialPort_open
(JNIEnv *, jclass, jstring, jint, jint,jint,jint,jint);
/*
* Class: android_serialport_api_SerialPort
* Method: close
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_cl_serialportlibrary_SerialPort_close
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/Device.java
================================================
package com.cl.serialportlibrary;
import java.io.File;
import java.io.Serializable;
public class Device implements Serializable{
private static final String TAG = Device.class.getSimpleName();
private String name;
private String root;
private File file;
public Device(String name, String root, File file) {
this.name = name;
this.root = root;
this.file = file;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public File getFile() {
return file;
}
public void setFile(File path) {
this.file = file;
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/Driver.java
================================================
package com.cl.serialportlibrary;
import java.io.File;
import java.util.ArrayList;
/**
* 串口驱动信息类
* 简化版本,仅用于SerialPortFinder
*/
public class Driver {
private String mDriverName;
private String mDeviceRoot;
public Driver(String name, String root) {
mDriverName = name;
mDeviceRoot = root;
}
/**
* 获取驱动名称
*/
public String getName() {
return mDriverName;
}
/**
* 获取设备根路径
*/
public String getRoot() {
return mDeviceRoot;
}
/**
* 获取该驱动下的所有设备
*/
public ArrayList getDevices() {
ArrayList devices = new ArrayList<>();
File dev = new File("/dev");
File[] files = dev.listFiles();
if (files != null) {
for (File file : files) {
if (file.getAbsolutePath().startsWith(mDeviceRoot)) {
devices.add(file);
}
}
}
return devices;
}
@Override
public String toString() {
return "Driver{" +
"name='" + mDriverName + '\'' +
", root='" + mDeviceRoot + '\'' +
'}';
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/MultiSerialPortManager.java
================================================
package com.cl.serialportlibrary;
import android.app.Application;
import android.os.Handler;
import android.os.Looper;
import com.cl.serialportlibrary.enumerate.SerialPortEnum;
import com.cl.serialportlibrary.enumerate.SerialStatus;
import com.cl.serialportlibrary.listener.OnOpenSerialPortListener;
import com.cl.serialportlibrary.listener.OnSerialPortDataListener;
import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
import com.cl.serialportlibrary.utils.SerialPortLogUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 多串口管理器
* 支持同时管理多个串口,每个串口可以有独立的配置
* Author: cl
* Date: 2023/10/26
*/
public class MultiSerialPortManager {
private static final String TAG = "MultiSerialPortManager";
private static MultiSerialPortManager instance;
private Handler handler = new Handler(Looper.getMainLooper());
// 串口管理器映射 <串口ID, SerialPortManager>
private final Map serialPortManagers = new ConcurrentHashMap<>();
// 串口配置映射 <串口ID, SerialConfig>
private final Map serialConfigs = new ConcurrentHashMap<>();
// 回调映射 <串口ID, 回调接口>
private final Map statusCallbacks = new ConcurrentHashMap<>();
private final Map dataCallbacks = new ConcurrentHashMap<>();
// 串口枚举映射 <串口ID, SerialPortEnum>
private final Map serialPortEnums = new ConcurrentHashMap<>();
private MultiSerialPortManager() {}
/**
* 获取单例实例
*/
public static MultiSerialPortManager getInstance() {
if (instance == null) {
synchronized (MultiSerialPortManager.class) {
if (instance == null) {
instance = new MultiSerialPortManager();
}
}
}
return instance;
}
/**
* 配置并打开串口
* @param serialId 串口ID(自定义标识)
* @param devicePath 设备路径
* @param baudRate 波特率
* @param config 串口配置
* @param statusCallback 状态回调
* @param dataCallback 数据回调
* @return 是否打开成功
*/
public boolean openSerialPort(String serialId, String devicePath, int baudRate,
SerialPortConfig config, OnSerialPortStatusCallback statusCallback,
OnSerialPortDataCallback dataCallback) {
SerialPortLogUtil.printSeparator(TAG, "打开串口 " + serialId);
SerialPortLogUtil.i(TAG, String.format("串口[%s] - 设备: %s, 波特率: %d", serialId, devicePath, baudRate));
// 检查串口是否已经打开
if (serialPortManagers.containsKey(serialId)) {
SerialPortLogUtil.w(TAG, "串口[" + serialId + "]已经打开,先关闭旧连接");
closeSerialPort(serialId);
}
// 创建串口配置
SerialConfig serialConfig = new SerialConfig.Builder()
.setEnableLogging(config.enableLogging)
.setIntervalSleep(config.intervalSleep)
.setDatabits(config.databits)
.setParity(config.parity)
.setStopbits(config.stopbits)
.setFlags(config.flags)
.setEnableStickyPacketProcessing(config.stickyPacketHelpers != null && config.stickyPacketHelpers.length > 0)
.setStickyPacketHelpers(config.stickyPacketHelpers != null ? config.stickyPacketHelpers : new AbsStickPackageHelper[]{new BaseStickPackageHelper()})
.build();
// 保存配置和回调
serialConfigs.put(serialId, serialConfig);
if (statusCallback != null) statusCallbacks.put(serialId, statusCallback);
if (dataCallback != null) dataCallbacks.put(serialId, dataCallback);
// 分配串口枚举
SerialPortEnum serialPortEnum = getAvailableSerialPortEnum();
serialPortEnums.put(serialId, serialPortEnum);
SerialPortLogUtil.printSerialConfig(TAG + "_" + serialId, config.databits, config.parity, config.stopbits, config.flags);
if (config.stickyPacketHelpers != null) {
SerialPortLogUtil.i(TAG, String.format("串口[%s] 配置了 %d 个粘包处理器", serialId, config.stickyPacketHelpers.length));
}
// 创建SerialPortManager
SerialPortManager serialPortManager = new SerialPortManager(serialPortEnum);
serialPortManager.setSerialConfig(serialConfig);
// 设置监听器
serialPortManager.setOnOpenSerialPortListener(new OnOpenSerialPortListener() {
@Override
public void openState(SerialPortEnum serialPortEnum, File device, SerialStatus status) {
String logMessage = String.format("串口[%s] 状态变化: %s - %s", serialId, device.getPath(), status);
if (status == SerialStatus.SUCCESS_OPENED) {
SerialPortLogUtil.i(TAG, logMessage);
} else {
SerialPortLogUtil.e(TAG, logMessage);
}
handler.post(() -> {
OnSerialPortStatusCallback callback = statusCallbacks.get(serialId);
if (callback != null) {
callback.onStatusChanged(serialId, status == SerialStatus.SUCCESS_OPENED, status);
}
});
}
});
serialPortManager.setOnSerialPortDataListener(new OnSerialPortDataListener() {
@Override
public void onDataReceived(byte[] data, SerialPortEnum serialPortEnum) {
SerialPortLogUtil.printData(TAG + "_" + serialId, "接收数据", data);
handler.post(() -> {
OnSerialPortDataCallback callback = dataCallbacks.get(serialId);
if (callback != null) {
callback.onDataReceived(serialId, data);
}
});
}
@Override
public void onDataSent(byte[] data, SerialPortEnum serialPortEnum) {
SerialPortLogUtil.printData(TAG + "_" + serialId, "发送数据", data);
handler.post(() -> {
OnSerialPortDataCallback callback = dataCallbacks.get(serialId);
if (callback != null) {
callback.onDataSent(serialId, data);
}
});
}
});
// 打开串口
boolean success = serialPortManager.openSerialPort(devicePath, baudRate);
if (success) {
serialPortManagers.put(serialId, serialPortManager);
SerialPortLogUtil.i(TAG, "串口[" + serialId + "] 打开成功");
} else {
// 清理资源
serialConfigs.remove(serialId);
statusCallbacks.remove(serialId);
dataCallbacks.remove(serialId);
serialPortEnums.remove(serialId);
SerialPortLogUtil.e(TAG, "串口[" + serialId + "] 打开失败");
}
return success;
}
/**
* 简化的打开串口方法
*/
public boolean openSerialPort(String serialId, String devicePath, int baudRate,
OnSerialPortDataCallback dataCallback) {
SerialPortConfig config = new SerialPortConfig.Builder().build();
return openSerialPort(serialId, devicePath, baudRate, config, null, dataCallback);
}
/**
* 发送数据到指定串口
* @param serialId 串口ID
* @param data 数据
* @return 是否发送成功
*/
public boolean sendData(String serialId, byte[] data) {
SerialPortManager manager = serialPortManagers.get(serialId);
if (manager == null) {
SerialPortLogUtil.e(TAG, "串口[" + serialId + "] 未打开,无法发送数据");
return false;
}
if (data == null || data.length == 0) {
SerialPortLogUtil.w(TAG, "串口[" + serialId + "] 尝试发送空数据");
return false;
}
long startTime = System.currentTimeMillis();
SerialPortLogUtil.printData(TAG + "_" + serialId, "准备发送", data);
boolean result = manager.sendBytes(data);
SerialPortLogUtil.printPerformance(TAG + "_" + serialId, "发送数据", startTime);
if (!result) {
SerialPortLogUtil.e(TAG, "串口[" + serialId + "] 数据发送失败");
}
return result;
}
/**
* 发送字符串数据到指定串口
*/
public boolean sendData(String serialId, String data) {
return sendData(serialId, data.getBytes());
}
/**
* 关闭指定串口
* @param serialId 串口ID
*/
public void closeSerialPort(String serialId) {
SerialPortManager manager = serialPortManagers.remove(serialId);
if (manager != null) {
SerialPortLogUtil.i(TAG, "关闭串口[" + serialId + "]");
manager.closeSerialPort();
}
// 清理相关资源
serialConfigs.remove(serialId);
statusCallbacks.remove(serialId);
dataCallbacks.remove(serialId);
serialPortEnums.remove(serialId);
}
/**
* 关闭所有串口
*/
public void closeAllSerialPorts() {
SerialPortLogUtil.printSeparator(TAG, "关闭所有串口");
List serialIds = new ArrayList<>(serialPortManagers.keySet());
for (String serialId : serialIds) {
closeSerialPort(serialId);
}
}
/**
* 检查指定串口是否已打开
*/
public boolean isSerialPortOpened(String serialId) {
SerialPortManager manager = serialPortManagers.get(serialId);
return manager != null && manager.isOpen();
}
/**
* 获取所有已打开的串口ID
*/
public List getOpenedSerialPorts() {
List openedPorts = new ArrayList<>();
for (Map.Entry entry : serialPortManagers.entrySet()) {
if (entry.getValue().isOpen()) {
openedPorts.add(entry.getKey());
}
}
return openedPorts;
}
/**
* 获取指定串口的配置
*/
public SerialConfig getSerialConfig(String serialId) {
return serialConfigs.get(serialId);
}
/**
* 更新指定串口的粘包处理器
*/
public boolean updateStickyPacketHelpers(String serialId, AbsStickPackageHelper[] helpers) {
SerialPortManager manager = serialPortManagers.get(serialId);
SerialConfig config = serialConfigs.get(serialId);
if (manager == null || config == null) {
SerialPortLogUtil.e(TAG, "串口[" + serialId + "] 未打开,无法更新粘包处理器");
return false;
}
config.setStickyPacketHelpers(helpers);
List helperList = new ArrayList<>();
for (AbsStickPackageHelper helper : helpers) {
helperList.add(helper);
}
manager.setStickPackageHelpers(helperList);
SerialPortLogUtil.i(TAG, String.format("串口[%s] 更新粘包处理器,数量: %d", serialId, helpers.length));
return true;
}
/**
* 打印所有串口状态
*/
public void printAllSerialStatus() {
SerialPortLogUtil.printSeparator(TAG, "所有串口状态");
if (serialPortManagers.isEmpty()) {
SerialPortLogUtil.i(TAG, "当前没有打开的串口");
return;
}
for (Map.Entry entry : serialPortManagers.entrySet()) {
String serialId = entry.getKey();
SerialPortManager manager = entry.getValue();
SerialConfig config = serialConfigs.get(serialId);
SerialPortLogUtil.i(TAG, String.format("串口[%s] - 状态: %s",
serialId, manager.isOpen() ? "已打开" : "已关闭"));
if (config != null) {
SerialPortLogUtil.printSerialConfig(TAG + "_" + serialId,
config.getDatabits(), config.getParity(), config.getStopbits(), config.getFlags());
}
}
}
/**
* 获取可用的串口枚举
*/
private SerialPortEnum getAvailableSerialPortEnum() {
// 简单的策略:按顺序分配,最多支持6个串口
SerialPortEnum[] enums = {
SerialPortEnum.SERIAL_ONE,
SerialPortEnum.SERIAL_TWO,
SerialPortEnum.SERIAL_THREE,
SerialPortEnum.SERIAL_FOUR,
SerialPortEnum.SERIAL_FIVE,
SerialPortEnum.SERIAL_SIX
};
for (SerialPortEnum serialPortEnum : enums) {
boolean isUsed = false;
for (SerialPortEnum usedEnum : serialPortEnums.values()) {
if (usedEnum == serialPortEnum) {
isUsed = true;
break;
}
}
if (!isUsed) {
return serialPortEnum;
}
}
// 如果所有枚举都被使用,返回第一个(可能会有冲突,但至少不会崩溃)
SerialPortLogUtil.w(TAG, "所有串口枚举都已被使用,可能会有冲突");
return SerialPortEnum.SERIAL_ONE;
}
/**
* 串口状态回调接口
*/
public interface OnSerialPortStatusCallback {
/**
* 串口状态变化
* @param serialId 串口ID
* @param success 是否成功
* @param status 状态
*/
void onStatusChanged(String serialId, boolean success, SerialStatus status);
}
/**
* 串口数据回调接口
*/
public interface OnSerialPortDataCallback {
/**
* 接收到数据
* @param serialId 串口ID
* @param data 数据
*/
void onDataReceived(String serialId, byte[] data);
/**
* 数据发送完成
* @param serialId 串口ID
* @param data 数据
*/
default void onDataSent(String serialId, byte[] data) {
// 默认空实现
}
}
/**
* 串口配置类
*/
public static class SerialPortConfig {
private boolean enableLogging = true;
private int intervalSleep = 50;
private int databits = 8;
private int parity = 0;
private int stopbits = 1;
private int flags = 0;
private AbsStickPackageHelper[] stickyPacketHelpers;
private SerialPortConfig(Builder builder) {
this.enableLogging = builder.enableLogging;
this.intervalSleep = builder.intervalSleep;
this.databits = builder.databits;
this.parity = builder.parity;
this.stopbits = builder.stopbits;
this.flags = builder.flags;
this.stickyPacketHelpers = builder.stickyPacketHelpers;
}
public static class Builder {
private boolean enableLogging = true;
private int intervalSleep = 50;
private int databits = 8;
private int parity = 0;
private int stopbits = 1;
private int flags = 0;
private AbsStickPackageHelper[] stickyPacketHelpers;
public Builder setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
return this;
}
public Builder setIntervalSleep(int intervalSleep) {
this.intervalSleep = intervalSleep;
return this;
}
public Builder setDatabits(int databits) {
this.databits = databits;
return this;
}
public Builder setParity(int parity) {
this.parity = parity;
return this;
}
public Builder setStopbits(int stopbits) {
this.stopbits = stopbits;
return this;
}
public Builder setFlags(int flags) {
this.flags = flags;
return this;
}
public Builder setStickyPacketHelpers(AbsStickPackageHelper... helpers) {
this.stickyPacketHelpers = helpers;
return this;
}
public SerialPortConfig build() {
return new SerialPortConfig(this);
}
}
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SerialConfig.java
================================================
package com.cl.serialportlibrary;
import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
/**
* name:cl
* date:2023/10/26
* desc:串口配置类
*/
public class SerialConfig {
//配置日志相关参数
private boolean enableLogging;
//串口接收间隔时间
private int intervalSleep;
//串口重连机制
private boolean serialPortReconnection;
int flags;
int databits;
int stopbits;
int parity;
// 黏包处理相关配置
private boolean enableStickyPacketProcessing;
private int maxPacketSize;
private int packetTimeout;
private AbsStickPackageHelper[] stickyPacketHelpers;
private boolean autoReconnect;
private int reconnectInterval;
private int maxReconnectAttempts;
public SerialConfig(Builder builder) {
this.enableLogging=builder.enableLogging;
this.intervalSleep=builder.intervalSleep;
this.serialPortReconnection=builder.serialPortReconnection;
this.flags=builder.flags;
this.databits=builder.databits;
this.stopbits=builder.stopbits;
this.parity=builder.parity;
this.enableStickyPacketProcessing=builder.enableStickyPacketProcessing;
this.maxPacketSize=builder.maxPacketSize;
this.packetTimeout=builder.packetTimeout;
this.stickyPacketHelpers=builder.stickyPacketHelpers;
this.autoReconnect=builder.autoReconnect;
this.reconnectInterval=builder.reconnectInterval;
this.maxReconnectAttempts=builder.maxReconnectAttempts;
}
public boolean isEnableLogging() {
return enableLogging;
}
public void setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
}
public int getIntervalSleep() {
return intervalSleep;
}
public boolean isSerialPortReconnection() {
return serialPortReconnection;
}
public void setIntervalSleep(int intervalSleep) {
this.intervalSleep = intervalSleep;
}
public int getFlags() {
return flags;
}
public int getDatabits() {
return databits;
}
public int getStopbits() {
return stopbits;
}
public int getParity() {
return parity;
}
public void setFlags(int flags) {
this.flags = flags;
}
public void setDatabits(int databits) {
this.databits = databits;
}
public void setStopbits(int stopbits) {
this.stopbits = stopbits;
}
public void setParity(int parity) {
this.parity = parity;
}
public boolean isEnableStickyPacketProcessing() {
return enableStickyPacketProcessing;
}
public void setEnableStickyPacketProcessing(boolean enableStickyPacketProcessing) {
this.enableStickyPacketProcessing = enableStickyPacketProcessing;
}
public int getMaxPacketSize() {
return maxPacketSize;
}
public void setMaxPacketSize(int maxPacketSize) {
this.maxPacketSize = maxPacketSize;
}
public int getPacketTimeout() {
return packetTimeout;
}
public void setPacketTimeout(int packetTimeout) {
this.packetTimeout = packetTimeout;
}
public AbsStickPackageHelper[] getStickyPacketHelpers() {
return stickyPacketHelpers;
}
public void setStickyPacketHelpers(AbsStickPackageHelper[] stickyPacketHelpers) {
this.stickyPacketHelpers = stickyPacketHelpers;
}
public boolean isAutoReconnect() {
return autoReconnect;
}
public void setAutoReconnect(boolean autoReconnect) {
this.autoReconnect = autoReconnect;
}
public int getReconnectInterval() {
return reconnectInterval;
}
public void setReconnectInterval(int reconnectInterval) {
this.reconnectInterval = reconnectInterval;
}
public int getMaxReconnectAttempts() {
return maxReconnectAttempts;
}
public void setMaxReconnectAttempts(int maxReconnectAttempts) {
this.maxReconnectAttempts = maxReconnectAttempts;
}
public static class Builder {
//配置日志相关参数
private boolean enableLogging = true;
//串口接收间隔时间
private int intervalSleep = 50;
//串口重连机制
private boolean serialPortReconnection = false;
// 标志位
int flags = 0;
// 数据位
int databits = 8;
// 停止位
int stopbits = 1;
// 校验位:0 表示无校验位,1 表示奇校验,2 表示偶校验
int parity = 0;
// 黏包处理相关配置
private boolean enableStickyPacketProcessing = true;
private int maxPacketSize = 1024;
private int packetTimeout = 1000;
private AbsStickPackageHelper[] stickyPacketHelpers = {new BaseStickPackageHelper()};
private boolean autoReconnect = false;
private int reconnectInterval = 5000;
private int maxReconnectAttempts = 3;
public Builder setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
return this;
}
public Builder setIntervalSleep(int sleep) {
intervalSleep = sleep;
return this;
}
public Builder setSerialPortReconnection(boolean serialReconnection) {
serialPortReconnection = serialReconnection;
return this;
}
public Builder setFlags(int flags) {
this.flags = flags;
return this;
}
public Builder setDatabits(int databits) {
this.databits = databits;
return this;
}
public Builder setStopbits(int stopbits) {
this.stopbits = stopbits;
return this;
}
public Builder setParity(int parity) {
this.parity = parity;
return this;
}
public Builder setEnableStickyPacketProcessing(boolean enableStickyPacketProcessing) {
this.enableStickyPacketProcessing = enableStickyPacketProcessing;
return this;
}
public Builder setMaxPacketSize(int maxPacketSize) {
this.maxPacketSize = maxPacketSize;
return this;
}
public Builder setPacketTimeout(int packetTimeout) {
this.packetTimeout = packetTimeout;
return this;
}
public Builder setStickyPacketHelpers(AbsStickPackageHelper... stickyPacketHelpers) {
this.stickyPacketHelpers = stickyPacketHelpers;
return this;
}
public Builder setAutoReconnect(boolean autoReconnect) {
this.autoReconnect = autoReconnect;
return this;
}
public Builder setReconnectInterval(int reconnectInterval) {
this.reconnectInterval = reconnectInterval;
return this;
}
public Builder setMaxReconnectAttempts(int maxReconnectAttempts) {
this.maxReconnectAttempts = maxReconnectAttempts;
return this;
}
public SerialConfig build() {
return new SerialConfig(this);
}
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SerialPort.java
================================================
package com.cl.serialportlibrary;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
public class SerialPort {
static {
System.loadLibrary("SerialPort");
}
private static final String TAG = SerialPort.class.getSimpleName();
/**
* 文件设置最高权限 777 可读 可写 可执行
*
* @param file 文件
* @return 权限修改是否成功
*/
boolean chmod777(File file) {
if (null == file || !file.exists()) {
// 文件不存在
return false;
}
try {
// 获取ROOT权限
Process su = Runtime.getRuntime().exec("/system/bin/su");
// 修改文件属性为 [可读 可写 可执行]
String cmd = "chmod 777 " + file.getAbsolutePath() + "\n" + "exit\n";
su.getOutputStream().write(cmd.getBytes());
if (0 == su.waitFor() && file.canRead() && file.canWrite() && file.canExecute()) {
return true;
}
} catch (IOException | InterruptedException e) {
// 没有ROOT权限
e.printStackTrace();
}
return false;
}
// 打开串口
protected native FileDescriptor open(String path, int baudrate, int flags, int databits, int stopbits, int parity);
// 关闭串口
protected native void close();
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SerialPortFinder.java
================================================
package com.cl.serialportlibrary;
import com.cl.serialportlibrary.utils.SerialPortLogUtil;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Vector;
public class SerialPortFinder {
private static final String TAG = SerialPortFinder.class.getSimpleName();
private static final String DRIVERS_PATH = "/proc/tty/drivers";
private static final String SERIAL_FIELD = "serial";
public SerialPortFinder() {
File file = new File(DRIVERS_PATH);
boolean b = file.canRead();
SerialPortLogUtil.i(TAG, "SerialPortFinder: file.canRead() = " + b);
}
/**
* 获取 Drivers
*
* @return Drivers
* @throws IOException IOException
*/
private ArrayList getDrivers() throws IOException {
ArrayList drivers = new ArrayList<>();
LineNumberReader lineNumberReader = new LineNumberReader(new FileReader(DRIVERS_PATH));
String readLine;
while ((readLine = lineNumberReader.readLine()) != null) {
String driverName = readLine.substring(0, 0x15).trim();
String[] fields = readLine.split(" +");
if ((fields.length >= 5) && (fields[fields.length - 1].equals(SERIAL_FIELD))) {
SerialPortLogUtil.d(TAG, "Found new driver " + driverName + " on " + fields[fields.length - 4]);
drivers.add(new Driver(driverName, fields[fields.length - 4]));
}
}
return drivers;
}
/**
* 获取串口
*
* @return 串口
*/
public ArrayList getDevices() {
ArrayList devices = new ArrayList<>();
try {
ArrayList drivers = getDrivers();
for (Driver driver : drivers) {
String driverName = driver.getName();
ArrayList driverDevices = driver.getDevices();
for (File file : driverDevices) {
String devicesName = file.getName();
devices.add(new Device(devicesName, driverName, file));
}
}
} catch (IOException e) {
e.printStackTrace();
}
return devices;
}
public String[] getAllDevicesPath() {
Vector devices = new Vector();
// Parse each driver
Iterator itdriv;
try {
itdriv = getDrivers().iterator();
while (itdriv.hasNext()) {
Driver driver = itdriv.next();
Iterator itdev = driver.getDevices().iterator();
while (itdev.hasNext()) {
String device = itdev.next().getAbsolutePath();
devices.add(device);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return devices.toArray(new String[devices.size()]);
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SerialPortManager.java
================================================
package com.cl.serialportlibrary;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import com.cl.serialportlibrary.utils.SerialPortLogUtil;
import com.cl.serialportlibrary.enumerate.SerialPortEnum;
import com.cl.serialportlibrary.enumerate.SerialStatus;
import com.cl.serialportlibrary.listener.OnOpenSerialPortListener;
import com.cl.serialportlibrary.listener.OnSerialPortDataListener;
import com.cl.serialportlibrary.thread.SerialPortReadThread;
import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.io.Closeable;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class SerialPortManager extends SerialPort {
private static final String TAG = SerialPortManager.class.getSimpleName();
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;
private FileDescriptor mFd;
private OnOpenSerialPortListener mOnOpenSerialPortListener;
private OnSerialPortDataListener mOnSerialPortDataListener;
private HandlerThread mSendingHandlerThread;
private Handler mSendingHandler;
private SerialPortReadThread mSerialPortReadThread;
//串口类型
private final SerialPortEnum mSerialPortEnum;
//串口配置
private SerialConfig mSerialConfig;
//粘包处理器
private List mStickPackageHelpers;
public SerialPortManager() {
this(SerialPortEnum.SERIAL_ONE);
}
public SerialPortManager(SerialPortEnum mSerialPortEnum) {
this.mSerialPortEnum = mSerialPortEnum;
this.mStickPackageHelpers = new ArrayList<>();
this.mStickPackageHelpers.add(new BaseStickPackageHelper());
}
/**
* 设置串口配置
*/
public void setSerialConfig(SerialConfig config) {
this.mSerialConfig = config;
if (config.getStickyPacketHelpers() != null && config.getStickyPacketHelpers().length > 0) {
this.mStickPackageHelpers = Arrays.asList(config.getStickyPacketHelpers());
}
}
/**
* 设置粘包处理器
*/
public void setStickPackageHelpers(List helpers) {
if (helpers != null) {
this.mStickPackageHelpers = helpers;
}
}
/**
* 打开串口
*
* @param devicePath 串口号
* @param baudRate 波特率
*/
public boolean openSerialPort(String devicePath, int baudRate) {
closeSerialPort();
SerialPortLogUtil.i(TAG, "openSerialPort: " + String.format("打开串口 %s 波特率 %s", devicePath, baudRate));
// 校验串口权限
if (!checkSerialPortPermission(devicePath)) {
return false;
}
try {
// 使用配置参数或默认值
int flags = mSerialConfig != null ? mSerialConfig.getFlags() : 0;
int databits = mSerialConfig != null ? mSerialConfig.getDatabits() : 8;
int stopbits = mSerialConfig != null ? mSerialConfig.getStopbits() : 1;
int parity = mSerialConfig != null ? mSerialConfig.getParity() : 0;
SerialPortLogUtil.d(TAG, "串口参数 - flags: " + flags + ", databits: " + databits + ", stopbits: " + stopbits + ", parity: " + parity);
mFd = open(devicePath, baudRate, flags, databits, stopbits, parity);
mFileInputStream = new FileInputStream(mFd);
mFileOutputStream = new FileOutputStream(mFd);
SerialPortLogUtil.i(TAG, "openSerialPort: 串口已经打开 " + mFd);
notifySerialPortOpened(new File(devicePath), SerialStatus.SUCCESS_OPENED);
// 开启发送消息的线程
startSendThread();
// 开启接收消息的线程
startReadThread();
return true;
} catch (Exception e) {
e.printStackTrace();
notifySerialPortOpened(new File(devicePath), SerialStatus.OPEN_FAIL);
return false;
}
}
/**
* 检查串口权限
*
* @param devicePath 串口号
* @return 是否有权限
*/
private boolean checkSerialPortPermission(String devicePath) {
File deviceFile = new File(devicePath);
if (!deviceFile.canRead() || !deviceFile.canWrite()) {
boolean chmod777 = chmod777(deviceFile);
if (!chmod777) {
SerialPortLogUtil.e(TAG, "串口权限不足: " + devicePath);
notifySerialPortOpened(deviceFile, SerialStatus.NO_READ_WRITE_PERMISSION);
return false;
}
}
return true;
}
/**
* 通知串口打开状态
*
* @param deviceFile 串口文件
* @param status 打开状态
*/
private void notifySerialPortOpened(File deviceFile, SerialStatus status) {
if (null != mOnOpenSerialPortListener) {
mOnOpenSerialPortListener.openState(mSerialPortEnum, deviceFile, status);
}
}
/**
* 检查串口是否已打开
*/
public boolean isOpen() {
return mFd != null && mFileInputStream != null && mFileOutputStream != null;
}
/**
* 关闭串口
*/
public void closeSerialPort() {
if (null != mFd) {
close();
mFd = null;
}
// 停止发送消息的线程
stopSendThread();
// 停止接收消息的线程
stopReadThread();
closeStream(mFileInputStream);
closeStream(mFileOutputStream);
}
/**
* 关闭流
*
* @param stream 流对象
*/
private void closeStream(Closeable stream) {
if (null != stream) {
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 添加打开串口监听
*
* @param listener listener
* @return SerialPortManager
*/
public SerialPortManager setOnOpenSerialPortListener(OnOpenSerialPortListener listener) {
mOnOpenSerialPortListener = listener;
return this;
}
/**
* 添加数据通信监听
*
* @param listener listener
* @return SerialPortManager
*/
public SerialPortManager setOnSerialPortDataListener(OnSerialPortDataListener listener) {
mOnSerialPortDataListener = listener;
return this;
}
/**
* 开启发送消息的线程
*/
private void startSendThread() {
// 开启发送消息的线程
mSendingHandlerThread = new HandlerThread("mSendingHandlerThread" + mSerialPortEnum.name());
mSendingHandlerThread.start();
// Handler
mSendingHandler = new Handler(mSendingHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
byte[] sendBytes = (byte[]) msg.obj;
if (null != mFileOutputStream && null != sendBytes && 0 < sendBytes.length) {
try {
mFileOutputStream.write(sendBytes);
if (null != mOnSerialPortDataListener) {
mOnSerialPortDataListener.onDataSent(sendBytes, mSerialPortEnum);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
}
/**
* 停止发送消息线程
*/
private void stopSendThread() {
mSendingHandler = null;
if (null != mSendingHandlerThread) {
mSendingHandlerThread.interrupt();
mSendingHandlerThread.quit();
mSendingHandlerThread = null;
}
}
/**
* 开启接收消息的线程
*/
private void startReadThread() {
mSerialPortReadThread = new SerialPortReadThread(mFileInputStream, mSerialPortEnum, mStickPackageHelpers) {
@Override
public void onDataReceived(byte[] bytes) {
if (null != mOnSerialPortDataListener) {
mOnSerialPortDataListener.onDataReceived(bytes, mSerialPortEnum);
}
}
};
mSerialPortReadThread.start();
SerialPortLogUtil.d(TAG, "启动数据接收线程");
}
/**
* 停止接收消息的线程
*/
private void stopReadThread() {
if (null != mSerialPortReadThread) {
mSerialPortReadThread.release();
}
}
/**
* 发送数据
*
* @param sendBytes 发送数据
* @return 发送是否成功
*/
public boolean sendBytes(byte[] sendBytes) {
if (null != mFd && null != mFileInputStream && null != mFileOutputStream) {
if (null != mSendingHandler) {
Message message = Message.obtain();
message.obj = sendBytes;
return mSendingHandler.sendMessage(message);
}
}
return false;
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SimpleSerialPortManager.java
================================================
package com.cl.serialportlibrary;
import android.app.Application;
import android.os.Handler;
import android.os.Looper;
import com.cl.serialportlibrary.enumerate.SerialPortEnum;
import com.cl.serialportlibrary.enumerate.SerialStatus;
import com.cl.serialportlibrary.listener.OnOpenSerialPortListener;
import com.cl.serialportlibrary.listener.OnSerialPortDataListener;
import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
import com.cl.serialportlibrary.stick.SpecifiedStickPackageHelper;
import com.cl.serialportlibrary.stick.StaticLenStickPackageHelper;
import com.cl.serialportlibrary.stick.VariableLenStickPackageHelper;
import com.cl.serialportlibrary.utils.SerialPortLogUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* 简化的串口管理器,提供更简单的API供外部使用
* 完全独立,不依赖SerialUtils
* Author: cl
* Date: 2023/10/26
*/
public class SimpleSerialPortManager {
private static final String TAG = "SimpleSerialPortManager";
private static SimpleSerialPortManager instance;
private OnOpenSerialPortCallback openCallback;
private OnDataReceivedCallback dataCallback;
// 串口管理器
private SerialPortManager serialPortManager;
private SerialConfig serialConfig;
private Handler handler = new Handler(Looper.getMainLooper());
private List stickPackageHelpers = new ArrayList<>();
private boolean isInitialized = false;
// 串口参数配置
private int databits = 8; // 数据位,默认8
private int parity = 0; // 校验位,默认0(无校验)
private int stopbits = 1; // 停止位,默认1
private int flags = 0; // 标志位,默认0
private SimpleSerialPortManager() {
// 默认添加基础粘包处理器
stickPackageHelpers.add(new BaseStickPackageHelper());
}
/**
* 获取单例实例
*/
public static SimpleSerialPortManager getInstance() {
if (instance == null) {
synchronized (SimpleSerialPortManager.class) {
if (instance == null) {
instance = new SimpleSerialPortManager();
}
}
}
return instance;
}
/**
* 简单初始化 - 最基本的配置
* @param application 应用程序上下文
*/
public SimpleSerialPortManager init(Application application) {
return init(application, true, "SerialPort", 50);
}
/**
* 初始化串口管理器 - 基础配置
* @param application 应用程序上下文
* @param enableLog 是否启用日志
* @param logTag 日志标签
* @param intervalSleep 读取间隔时间(ms)
*/
public SimpleSerialPortManager init(Application application, boolean enableLog, String logTag, int intervalSleep) {
SerialPortLogUtil.setDebugEnabled(enableLog);
SerialPortLogUtil.i(TAG, "初始化SimpleSerialPortManager - enableLog: " + enableLog + ", tag: " + logTag + ", interval: " + intervalSleep);
// 创建默认配置
serialConfig = new SerialConfig.Builder()
.setEnableLogging(enableLog)
.setIntervalSleep(intervalSleep)
.setDatabits(databits)
.setParity(parity)
.setStopbits(stopbits)
.setFlags(flags)
.setEnableStickyPacketProcessing(true)
.setStickyPacketHelpers(stickPackageHelpers.toArray(new AbsStickPackageHelper[0]))
.build();
isInitialized = true;
SerialPortLogUtil.i(TAG, "SimpleSerialPortManager初始化完成");
return this;
}
/**
* 初始化串口管理器 - 使用SerialConfig配置
* @param application 应用程序上下文
* @param config 串口配置
*/
public SimpleSerialPortManager init(Application application, SerialConfig config) {
this.serialConfig = config;
SerialPortLogUtil.setDebugEnabled(config.isEnableLogging());
SerialPortLogUtil.i(TAG, "使用SerialConfig初始化SimpleSerialPortManager");
// 同步串口参数
this.databits = config.getDatabits();
this.parity = config.getParity();
this.stopbits = config.getStopbits();
this.flags = config.getFlags();
isInitialized = true;
SerialPortLogUtil.i(TAG, "SimpleSerialPortManager初始化完成");
return this;
}
/**
* 配置粘包处理策略
*/
public SimpleSerialPortManager configureStickyPacket(StickyPacketStrategy strategy) {
stickPackageHelpers.clear();
AbsStickPackageHelper helper;
switch (strategy) {
case DELIMITER_BASED:
helper = new SpecifiedStickPackageHelper("\n");
SerialPortLogUtil.i(TAG, "配置粘包处理策略: 分隔符模式");
break;
case FIXED_LENGTH:
helper = new StaticLenStickPackageHelper();
SerialPortLogUtil.i(TAG, "配置粘包处理策略: 固定长度模式");
break;
case VARIABLE_LENGTH:
helper = new VariableLenStickPackageHelper(java.nio.ByteOrder.BIG_ENDIAN, 2, 2, 12);
SerialPortLogUtil.i(TAG, "配置粘包处理策略: 可变长度模式");
break;
case NO_PROCESSING:
default:
helper = new BaseStickPackageHelper();
SerialPortLogUtil.i(TAG, "配置粘包处理策略: 无处理模式");
break;
}
stickPackageHelpers.add(helper);
// 如果已经初始化,更新配置
if (serialConfig != null) {
serialConfig.setStickyPacketHelpers(stickPackageHelpers.toArray(new AbsStickPackageHelper[0]));
}
return this;
}
/**
* 设置自定义粘包处理器
*/
public SimpleSerialPortManager setStickyPacketHelpers(AbsStickPackageHelper... helpers) {
stickPackageHelpers.clear();
for (AbsStickPackageHelper helper : helpers) {
stickPackageHelpers.add(helper);
}
// 如果已经初始化,更新配置
if (serialConfig != null) {
serialConfig.setStickyPacketHelpers(helpers);
}
SerialPortLogUtil.i(TAG, "设置自定义粘包处理器,数量: " + helpers.length);
return this;
}
/**
* 打开串口
* @param devicePath 设备路径
* @param baudRate 波特率
* @param callback 数据接收回调
* @return 是否打开成功
*/
public boolean openSerialPort(String devicePath, int baudRate, OnDataReceivedCallback callback) {
return openSerialPort(devicePath, baudRate, null, callback);
}
/**
* 打开串口
* @param devicePath 设备路径
* @param baudRate 波特率
* @param openCallback 打开状态回调
* @param dataCallback 数据接收回调
* @return 是否打开成功
*/
public boolean openSerialPort(String devicePath, int baudRate, OnOpenSerialPortCallback openCallback, OnDataReceivedCallback dataCallback) {
if (!isInitialized) {
SerialPortLogUtil.e(TAG, "SimpleSerialPortManager未初始化,请先调用init()方法");
return false;
}
this.openCallback = openCallback;
this.dataCallback = dataCallback;
SerialPortLogUtil.i(TAG, "尝试打开串口: " + devicePath + ", 波特率: " + baudRate);
SerialPortLogUtil.printSerialConfig(TAG, databits, parity, stopbits, flags);
// 更新配置中的串口参数
updateSerialConfig();
// 创建SerialPortManager并设置配置
serialPortManager = new SerialPortManager();
serialPortManager.setSerialConfig(serialConfig);
serialPortManager.setStickPackageHelpers(stickPackageHelpers);
// 设置监听器
serialPortManager.setOnOpenSerialPortListener(new OnOpenSerialPortListener() {
@Override
public void openState(SerialPortEnum serialPortEnum, File device, SerialStatus status) {
if (status == SerialStatus.SUCCESS_OPENED) {
SerialPortLogUtil.i(TAG, "串口打开成功: " + device.getPath());
handler.post(() -> {
if (openCallback != null) {
openCallback.onStatusChanged(true, status);
}
});
} else {
SerialPortLogUtil.e(TAG, "串口打开失败: " + device.getPath() + ", 状态: " + status);
handler.post(() -> {
if (openCallback != null) {
openCallback.onStatusChanged(false, status);
}
});
}
}
});
serialPortManager.setOnSerialPortDataListener(new OnSerialPortDataListener() {
@Override
public void onDataReceived(byte[] data, SerialPortEnum serialPortEnum) {
SerialPortLogUtil.printData(TAG, "接收数据", data);
handler.post(() -> {
if (dataCallback != null) {
dataCallback.onDataReceived(data);
}
});
}
@Override
public void onDataSent(byte[] data, SerialPortEnum serialPortEnum) {
SerialPortLogUtil.printData(TAG, "发送数据", data);
handler.post(() -> {
if (dataCallback != null) {
dataCallback.onDataSent(data);
}
});
}
});
// 打开串口
return serialPortManager.openSerialPort(devicePath, baudRate);
}
/**
* 更新串口配置参数
*/
private void updateSerialConfig() {
if (serialConfig != null) {
serialConfig.setDatabits(this.databits);
serialConfig.setParity(this.parity);
serialConfig.setStopbits(this.stopbits);
serialConfig.setFlags(this.flags);
SerialPortLogUtil.d(TAG, "更新串口配置 - 数据位: " + databits + ", 校验位: " + parity + ", 停止位: " + stopbits);
}
}
/**
* 发送数据
* @param data 要发送的数据
* @return 是否发送成功
*/
public boolean sendData(byte[] data) {
if (serialPortManager == null) {
SerialPortLogUtil.e(TAG, "串口未打开,无法发送数据");
return false;
}
if (data == null || data.length == 0) {
SerialPortLogUtil.w(TAG, "尝试发送空数据");
return false;
}
long startTime = System.currentTimeMillis();
SerialPortLogUtil.printData(TAG, "准备发送", data);
boolean result = serialPortManager.sendBytes(data);
SerialPortLogUtil.printPerformance(TAG, "发送数据", startTime);
if (!result) {
SerialPortLogUtil.e(TAG, "数据发送失败");
}
return result;
}
/**
* 发送字符串数据
* @param data 要发送的字符串
* @return 是否发送成功
*/
public boolean sendData(String data) {
return sendData(data.getBytes());
}
/**
* 关闭串口
*/
public void closeSerialPort() {
if (serialPortManager != null) {
SerialPortLogUtil.i(TAG, "关闭串口");
serialPortManager.closeSerialPort();
serialPortManager = null;
}
openCallback = null;
dataCallback = null;
}
/**
* 检查串口是否已打开
*/
public boolean isSerialPortOpened() {
return serialPortManager != null && serialPortManager.isOpen();
}
// Getter和Setter方法
public SimpleSerialPortManager setDatabits(int databits) {
this.databits = databits;
SerialPortLogUtil.d(TAG, "设置数据位: " + databits);
return this;
}
public SimpleSerialPortManager setParity(int parity) {
this.parity = parity;
SerialPortLogUtil.d(TAG, "设置校验位: " + parity);
return this;
}
public SimpleSerialPortManager setStopbits(int stopbits) {
this.stopbits = stopbits;
SerialPortLogUtil.d(TAG, "设置停止位: " + stopbits);
return this;
}
public SimpleSerialPortManager setFlags(int flags) {
this.flags = flags;
SerialPortLogUtil.d(TAG, "设置标志位: " + flags);
return this;
}
public int getDatabits() {
return databits;
}
public int getParity() {
return parity;
}
public int getStopbits() {
return stopbits;
}
public int getFlags() {
return flags;
}
public SerialConfig getSerialConfig() {
return serialConfig;
}
/**
* 粘包处理策略枚举
*/
public enum StickyPacketStrategy {
NO_PROCESSING, // 不处理
DELIMITER_BASED, // 基于分隔符
FIXED_LENGTH, // 固定长度
VARIABLE_LENGTH // 可变长度
}
/**
* 串口打开状态回调接口
*/
public interface OnOpenSerialPortCallback {
/**
* 串口状态变化回调
* @param success 是否成功
* @param status 状态
*/
void onStatusChanged(boolean success, SerialStatus status);
}
/**
* 数据接收回调接口
*/
public interface OnDataReceivedCallback {
/**
* 接收到数据
* @param data 接收到的数据
*/
void onDataReceived(byte[] data);
/**
* 数据发送完成
* @param data 发送的数据
*/
default void onDataSent(byte[] data) {
// 默认空实现
}
}
/**
* 多串口管理 - 获取多串口管理器实例
* @return MultiSerialPortManager实例
*/
public static MultiSerialPortManager multi() {
return MultiSerialPortManager.getInstance();
}
/**
* 快速配置构建器
*/
public static class QuickConfig {
private int intervalSleep = 50;
private boolean enableLog = true;
private String logTag = "SerialPort";
private StickyPacketStrategy stickyPacketStrategy = StickyPacketStrategy.NO_PROCESSING;
private int maxPacketSize = 1024;
private boolean autoReconnect = false;
private int databits = 8;
private int parity = 0;
private int stopbits = 1;
private int flags = 0;
public QuickConfig setIntervalSleep(int intervalSleep) {
this.intervalSleep = intervalSleep;
return this;
}
public QuickConfig setEnableLog(boolean enableLog) {
this.enableLog = enableLog;
return this;
}
public QuickConfig setLogTag(String logTag) {
this.logTag = logTag;
return this;
}
public QuickConfig setStickyPacketStrategy(StickyPacketStrategy strategy) {
this.stickyPacketStrategy = strategy;
return this;
}
public QuickConfig setMaxPacketSize(int maxPacketSize) {
this.maxPacketSize = maxPacketSize;
return this;
}
public QuickConfig setAutoReconnect(boolean autoReconnect) {
this.autoReconnect = autoReconnect;
return this;
}
public QuickConfig setDatabits(int databits) {
this.databits = databits;
return this;
}
public QuickConfig setParity(int parity) {
this.parity = parity;
return this;
}
public QuickConfig setStopbits(int stopbits) {
this.stopbits = stopbits;
return this;
}
public QuickConfig setFlags(int flags) {
this.flags = flags;
return this;
}
/**
* 应用配置并返回SimpleSerialPortManager实例
*/
public SimpleSerialPortManager apply(Application application) {
SerialConfig config = new SerialConfig.Builder()
.setIntervalSleep(intervalSleep)
.setEnableLogging(enableLog)
.setEnableStickyPacketProcessing(stickyPacketStrategy != StickyPacketStrategy.NO_PROCESSING)
.setMaxPacketSize(maxPacketSize)
.setAutoReconnect(autoReconnect)
.setDatabits(databits)
.setParity(parity)
.setStopbits(stopbits)
.setFlags(flags)
.build();
SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance()
.init(application, config)
.configureStickyPacket(stickyPacketStrategy);
// 设置串口参数
manager.setDatabits(databits)
.setParity(parity)
.setStopbits(stopbits)
.setFlags(flags);
SerialPortLogUtil.i("QuickConfig", "快速配置完成 - 间隔: " + intervalSleep + "ms, 日志: " + enableLog + ", 策略: " + stickyPacketStrategy);
return manager;
}
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/enumerate/SerialPortEnum.java
================================================
package com.cl.serialportlibrary.enumerate;
/**
* name:cl
* date:2023/2/20
* desc:串口枚举类型
*/
public enum SerialPortEnum {
//串口1
SERIAL_ONE,
//串口2
SERIAL_TWO,
//串口3
SERIAL_THREE,
//串口4
SERIAL_FOUR,
//串口5
SERIAL_FIVE,
//串口6
SERIAL_SIX
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/enumerate/SerialStatus.java
================================================
package com.cl.serialportlibrary.enumerate;
/**
* name:cl
* date:2023/2/20
* desc:
*/
public enum SerialStatus {
NO_READ_WRITE_PERMISSION,
OPEN_FAIL,
SUCCESS_OPENED
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/example/MultiSerialPortExample.java
================================================
package com.cl.serialportlibrary.example;
import android.app.Application;
import android.util.Log;
import com.cl.serialportlibrary.MultiSerialPortManager;
import com.cl.serialportlibrary.enumerate.SerialStatus;
import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
import com.cl.serialportlibrary.stick.SpecifiedStickPackageHelper;
import com.cl.serialportlibrary.stick.StaticLenStickPackageHelper;
import com.cl.serialportlibrary.stick.StickyPacketHelperFactory;
/**
* 多串口使用示例
* 展示如何同时管理多个串口,每个串口使用不同的粘包处理策略
*/
public class MultiSerialPortExample {
private static final String TAG = "MultiSerialPortExample";
/**
* 示例1:基础多串口使用
*/
public void basicMultiSerialExample(Application application) {
MultiSerialPortManager manager = MultiSerialPortManager.getInstance();
// 串口1:不需要粘包处理,用于简单的数据传输
manager.openSerialPort("GPS", "/dev/ttyS1", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(0)
.setStopbits(1)
.setStickyPacketHelpers(new BaseStickPackageHelper()) // 不处理粘包
.build(),
// 状态回调
(serialId, success, status) -> {
Log.i(TAG, String.format("串口[%s] 状态: %s", serialId, success ? "打开成功" : "打开失败"));
},
// 数据回调
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String gpsData = new String(data);
Log.i(TAG, "GPS数据: " + gpsData);
// 处理GPS数据
}
});
// 串口2:需要按换行符分包,用于文本协议
manager.openSerialPort("SENSOR", "/dev/ttyS2", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(0)
.setStopbits(1)
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) // 按换行符分包
.build(),
null, // 不需要状态回调
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String sensorData = new String(data).trim();
Log.i(TAG, "传感器数据: " + sensorData);
// 处理传感器数据
}
});
// 发送数据到不同串口
manager.sendData("GPS", "AT+GPS\r\n");
manager.sendData("SENSOR", "READ_TEMP\n");
}
/**
* 示例2:复杂的多串口场景
*/
public void advancedMultiSerialExample(Application application) {
MultiSerialPortManager manager = MultiSerialPortManager.getInstance();
// 串口1:Modbus RTU协议,固定长度包
manager.openSerialPort("MODBUS", "/dev/ttyS3", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(2) // 偶校验
.setStopbits(1)
.setStickyPacketHelpers(new StaticLenStickPackageHelper(8)) // 固定8字节
.build(),
(serialId, success, status) -> {
if (success) {
Log.i(TAG, "Modbus串口打开成功");
// 发送读取寄存器命令
byte[] readCmd = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, (byte)0x84, 0x0A};
manager.sendData("MODBUS", readCmd);
}
},
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
Log.i(TAG, "Modbus响应: " + bytesToHex(data));
// 解析Modbus响应
parseModbusResponse(data);
}
});
// 串口2:自定义协议,可变长度包
manager.openSerialPort("CUSTOM", "/dev/ttyS4", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(0)
.setStopbits(1)
.setStickyPacketHelpers(
StickyPacketHelperFactory.createVariableLength(
java.nio.ByteOrder.BIG_ENDIAN, 2, 2, 12)) // 可变长度包
.build(),
null,
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
Log.i(TAG, "自定义协议数据: " + bytesToHex(data));
// 处理自定义协议数据
parseCustomProtocol(data);
}
});
// 串口3:AT命令,多种分隔符
manager.openSerialPort("MODEM", "/dev/ttyS5", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setDatabits(8)
.setParity(0)
.setStopbits(1)
.setStickyPacketHelpers(
StickyPacketHelperFactory.Common.createATCommand()) // AT命令分包
.build(),
null,
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String response = new String(data).trim();
Log.i(TAG, "AT响应: " + response);
// 处理AT命令响应
parseATResponse(response);
}
});
}
/**
* 示例3:动态管理串口
*/
public void dynamicSerialManagement() {
MultiSerialPortManager manager = MultiSerialPortManager.getInstance();
// 创建一个通用的数据回调
MultiSerialPortManager.OnSerialPortDataCallback commonCallback =
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
Log.i(TAG, String.format("串口[%s] 收到数据: %s", serialId, new String(data)));
}
@Override
public void onDataSent(String serialId, byte[] data) {
Log.d(TAG, String.format("串口[%s] 发送数据: %s", serialId, new String(data)));
}
};
// 批量打开串口
String[] devices = {"/dev/ttyS1", "/dev/ttyS2", "/dev/ttyS3"};
int[] baudRates = {9600, 115200, 57600};
for (int i = 0; i < devices.length; i++) {
String serialId = "SERIAL_" + (i + 1);
// 根据不同需求配置不同的粘包处理
MultiSerialPortManager.SerialPortConfig config;
switch (i) {
case 0: // 第一个串口不需要粘包处理
config = new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new BaseStickPackageHelper())
.build();
break;
case 1: // 第二个串口需要换行符分包
config = new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n"))
.build();
break;
case 2: // 第三个串口需要固定长度分包
config = new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new StaticLenStickPackageHelper(16))
.build();
break;
default:
config = new MultiSerialPortManager.SerialPortConfig.Builder().build();
break;
}
manager.openSerialPort(serialId, devices[i], baudRates[i], config, null, commonCallback);
}
// 打印所有串口状态
manager.printAllSerialStatus();
// 向所有串口发送测试数据
for (String serialId : manager.getOpenedSerialPorts()) {
manager.sendData(serialId, "Hello from " + serialId + "\n");
}
// 动态更新某个串口的粘包处理器
manager.updateStickyPacketHelpers("SERIAL_1",
new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("END")});
}
/**
* 示例4:串口数据路由
*/
public void serialDataRouting() {
MultiSerialPortManager manager = MultiSerialPortManager.getInstance();
// 主控制串口:接收外部命令
manager.openSerialPort("MAIN_CTRL", "/dev/ttyS1", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
.build(),
null,
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String command = new String(data).trim();
Log.i(TAG, "主控制命令: " + command);
// 根据命令路由到不同的串口
if (command.startsWith("GPS:")) {
String gpsCmd = command.substring(4);
manager.sendData("GPS_MODULE", gpsCmd + "\r\n");
} else if (command.startsWith("SENSOR:")) {
String sensorCmd = command.substring(7);
manager.sendData("SENSOR_MODULE", sensorCmd + "\n");
}
}
});
// GPS模块串口
manager.openSerialPort("GPS_MODULE", "/dev/ttyS2", 9600,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n"))
.build(),
null,
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String gpsResponse = new String(data).trim();
Log.i(TAG, "GPS响应: " + gpsResponse);
// 将GPS响应转发到主控制串口
manager.sendData("MAIN_CTRL", "GPS_RESP:" + gpsResponse + "\r\n");
}
});
// 传感器模块串口
manager.openSerialPort("SENSOR_MODULE", "/dev/ttyS3", 115200,
new MultiSerialPortManager.SerialPortConfig.Builder()
.setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n"))
.build(),
null,
new MultiSerialPortManager.OnSerialPortDataCallback() {
@Override
public void onDataReceived(String serialId, byte[] data) {
String sensorResponse = new String(data).trim();
Log.i(TAG, "传感器响应: " + sensorResponse);
// 将传感器响应转发到主控制串口
manager.sendData("MAIN_CTRL", "SENSOR_RESP:" + sensorResponse + "\r\n");
}
});
}
/**
* 清理资源
*/
public void cleanup() {
// 关闭所有串口
MultiSerialPortManager.getInstance().closeAllSerialPorts();
Log.i(TAG, "所有串口已关闭");
}
// 辅助方法
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
private void parseModbusResponse(byte[] data) {
// Modbus响应解析逻辑
if (data.length >= 3) {
int slaveId = data[0] & 0xFF;
int functionCode = data[1] & 0xFF;
Log.i(TAG, String.format("Modbus - 从机ID: %d, 功能码: %d", slaveId, functionCode));
}
}
private void parseCustomProtocol(byte[] data) {
// 自定义协议解析逻辑
Log.i(TAG, "解析自定义协议数据,长度: " + data.length);
}
private void parseATResponse(String response) {
// AT命令响应解析逻辑
if (response.equals("OK")) {
Log.i(TAG, "AT命令执行成功");
} else if (response.equals("ERROR")) {
Log.e(TAG, "AT命令执行失败");
} else if (response.startsWith("+")) {
Log.i(TAG, "AT命令数据响应: " + response);
}
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/listener/OnOpenSerialPortListener.java
================================================
package com.cl.serialportlibrary.listener;
import com.cl.serialportlibrary.enumerate.SerialPortEnum;
import com.cl.serialportlibrary.enumerate.SerialStatus;
import java.io.File;
/**
* 打开串口监听
*/
public interface OnOpenSerialPortListener {
void openState(SerialPortEnum serialPortEnum, File device, SerialStatus status);
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/listener/OnSerialPortDataListener.java
================================================
package com.cl.serialportlibrary.listener;
import com.cl.serialportlibrary.enumerate.SerialPortEnum;
/**
* 串口消息监听
*/
public interface OnSerialPortDataListener {
/**
* 数据接收
*
* @param bytes 接收到的数据
*/
void onDataReceived(byte[] bytes, SerialPortEnum serialPortEnum);
/**
* 数据发送
*
* @param bytes 发送的数据
*/
void onDataSent(byte[] bytes,SerialPortEnum serialPortEnum);
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/AbsStickPackageHelper.java
================================================
package com.cl.serialportlibrary.stick;
import java.io.InputStream;
/**
* Accept the message, the helper of the sticky packet processing,
* return the final data through the inputstream,
* manually process the sticky packet, and the returned byte[] is the complete data we expect
* Note: This method will be called repeatedly until a complete piece of data is parsed.
* This method is synchronous, try not to do time-consuming operations, otherwise it will block reading data
*/
public interface AbsStickPackageHelper {
byte[] execute(InputStream is);
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/BaseStickPackageHelper.java
================================================
package com.cl.serialportlibrary.stick;
import android.os.SystemClock;
import com.cl.serialportlibrary.utils.SerialPortLogUtil;
import java.io.IOException;
import java.io.InputStream;
/**
* The simplest thing to do is not to deal with sticky packets,
* read directly and return as much as InputStream.available() reads
*/
public class BaseStickPackageHelper implements AbsStickPackageHelper {
public BaseStickPackageHelper() {
}
@Override
public byte[] execute(InputStream is) {
try {
int available = is.available();
if (available > 0) {
byte[] buffer = new byte[available];
int size = is.read(buffer);
if (size > 0) {
return buffer;
}
SerialPortLogUtil.d("BaseStickPackageHelper", "原始数据长度: " + buffer.length);
} else {
SystemClock.sleep(50); // 默认50ms间隔
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/CompositeStickPackageHelper.java
================================================
package com.cl.serialportlibrary.stick;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 组合式黏包处理器
* 首先尝试使用主要处理器,如果失败则使用备用处理器
* Author: cl
* Date: 2023/10/26
*/
public class CompositeStickPackageHelper implements AbsStickPackageHelper {
private final AbsStickPackageHelper primaryHelper;
private final AbsStickPackageHelper fallbackHelper;
private final List buffer = new ArrayList<>();
public CompositeStickPackageHelper(AbsStickPackageHelper primaryHelper, AbsStickPackageHelper fallbackHelper) {
this.primaryHelper = primaryHelper;
this.fallbackHelper = fallbackHelper;
}
@Override
public byte[] execute(InputStream is) {
// 先尝试读取一些数据到缓冲区
try {
int available = is.available();
if (available > 0) {
byte[] tempBuffer = new byte[available];
int readBytes = is.read(tempBuffer);
if (readBytes > 0) {
for (int i = 0; i < readBytes; i++) {
buffer.add(tempBuffer[i]);
}
}
}
if (buffer.isEmpty()) {
return null;
}
// 将缓冲区数据转换为字节数组
byte[] bufferData = new byte[buffer.size()];
for (int i = 0; i < buffer.size(); i++) {
bufferData[i] = buffer.get(i);
}
// 尝试使用主要处理器
ByteArrayInputStream primaryStream = new ByteArrayInputStream(bufferData);
byte[] primaryResult = primaryHelper.execute(primaryStream);
if (primaryResult != null && primaryResult.length > 0) {
// 主要处理器成功,清除已处理的数据
if (primaryResult.length <= buffer.size()) {
for (int i = 0; i < primaryResult.length; i++) {
buffer.remove(0);
}
}
return primaryResult;
}
// 主要处理器失败,尝试备用处理器
ByteArrayInputStream fallbackStream = new ByteArrayInputStream(bufferData);
byte[] fallbackResult = fallbackHelper.execute(fallbackStream);
if (fallbackResult != null && fallbackResult.length > 0) {
// 备用处理器成功,清除已处理的数据
if (fallbackResult.length <= buffer.size()) {
for (int i = 0; i < fallbackResult.length; i++) {
buffer.remove(0);
}
}
return fallbackResult;
}
// 两个处理器都失败,保持缓冲区数据等待更多数据
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/SpecifiedStickPackageHelper.java
================================================
package com.cl.serialportlibrary.stick;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* The sticky packet processing of specific characters,
* one Byte[] at the beginning and the end, cannot be empty at the same time,
* if one of them is empty, then the non-empty is used as the split marker
* Example: The protocol is formulated as ^+data+$, starting with ^ and ending with $
*/
public class SpecifiedStickPackageHelper implements AbsStickPackageHelper {
private final byte[] head;
private final byte[] tail;
private final List bytes;
private final int headLen;
private final int tailLen;
public SpecifiedStickPackageHelper(byte[] head, byte[] tail) {
this.head = head;
this.tail = tail;
if (head == null || tail == null) {
throw new IllegalStateException(" head or tail ==null");
}
if (head.length == 0 && tail.length == 0) {
throw new IllegalStateException(" head and tail length==0");
}
headLen = head.length;
tailLen = tail.length;
bytes = new ArrayList<>();
}
/**
* 构造函数 - 只使用结束标识
* @param tail 结束标识
*/
public SpecifiedStickPackageHelper(byte[] tail) {
this(new byte[0], tail);
}
/**
* 构造函数 - 字符串版本
* @param head 开始标识字符串
* @param tail 结束标识字符串
*/
public SpecifiedStickPackageHelper(String head, String tail) {
this(head != null ? head.getBytes() : new byte[0],
tail != null ? tail.getBytes() : new byte[0]);
}
/**
* 构造函数 - 只使用结束标识字符串
* @param tail 结束标识字符串
*/
public SpecifiedStickPackageHelper(String tail) {
this(new byte[0], tail != null ? tail.getBytes() : new byte[0]);
}
private boolean endWith(Byte[] src, byte[] target) {
if (src.length < target.length) {
return false;
}
for (int i = 0; i < target.length; i++) {
if (target[target.length - i - 1] != src[src.length - i - 1]) {
return false;
}
}
return true;
}
private byte[] getRangeBytes(List list, int start, int end) {
Byte[] temps = Arrays.copyOfRange(list.toArray(new Byte[0]), start, end);
byte[] result = new byte[temps.length];
for (int i = 0; i < result.length; i++) {
result[i] = temps[i];
}
return result;
}
@Override
public byte[] execute(InputStream is) {
bytes.clear();
int len = -1;
byte temp;
int startIndex = -1;
byte[] result = null;
boolean isFindStart = false, isFindEnd = false;
try {
while ((len = is.read()) != -1) {
temp = (byte) len;
bytes.add(temp);
Byte[] byteArray = bytes.toArray(new Byte[]{});
if (headLen == 0 || tailLen == 0) {//Only head or tail markers
if (endWith(byteArray, head) || endWith(byteArray, tail)) {
if (startIndex == -1) {
startIndex = bytes.size() - headLen;
} else {
result = getRangeBytes(bytes, startIndex, bytes.size());
break;
}
}
} else {
if (!isFindStart) {
if (endWith(byteArray, head)) {
startIndex = bytes.size() - headLen;
isFindStart = true;
}
} else if (!isFindEnd) {
if (endWith(byteArray, tail)) {
if (startIndex + headLen <= bytes.size() - tailLen) {
isFindEnd = true;
result = getRangeBytes(bytes, startIndex, bytes.size());
break;
}
}
}
}
}
if (len == -1) {
return null;
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
return result;
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/StaticLenStickPackageHelper.java
================================================
package com.cl.serialportlibrary.stick;
import java.io.IOException;
import java.io.InputStream;
/**
* Fixed-length adhesive package treatment
* Example: The protocol stipulates that the length of each packet is 16
*/
public class StaticLenStickPackageHelper implements AbsStickPackageHelper {
private int stackLen = 16;
public StaticLenStickPackageHelper(int stackLen) {
this.stackLen = stackLen;
}
/**
* 默认构造函数,使用16字节固定长度
*/
public StaticLenStickPackageHelper() {
this.stackLen = 16;
}
@Override
public byte[] execute(InputStream is) {
int count = 0;
int len = -1;
byte temp;
byte[] result = new byte[stackLen];
try {
while (count < stackLen && (len = is.read()) != -1) {
temp = (byte) len;
result[count] = temp;
count++;
}
if (len == -1) {
return null;
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
return result;
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/StickyPacketHelperFactory.java
================================================
package com.cl.serialportlibrary.stick;
import java.nio.charset.StandardCharsets;
/**
* 黏包处理器工厂类
* 提供常用的黏包处理器配置
* Author: cl
* Date: 2023/10/26
*/
public class StickyPacketHelperFactory {
/**
* 创建无黏包处理的处理器
*/
public static AbsStickPackageHelper createNoProcessing() {
return new BaseStickPackageHelper();
}
/**
* 创建固定长度的黏包处理器
* @param length 数据包固定长度
*/
public static AbsStickPackageHelper createFixedLength(int length) {
return new StaticLenStickPackageHelper(length);
}
/**
* 创建基于分隔符的黏包处理器
* @param delimiter 分隔符字符串
*/
public static AbsStickPackageHelper createDelimiterBased(String delimiter) {
byte[] delimiterBytes = delimiter.getBytes(StandardCharsets.UTF_8);
return new SpecifiedStickPackageHelper(new byte[0], delimiterBytes);
}
/**
* 创建基于开始和结束标识的黏包处理器
* @param startMarker 开始标识
* @param endMarker 结束标识
*/
public static AbsStickPackageHelper createMarkerBased(String startMarker, String endMarker) {
byte[] startBytes = startMarker.getBytes(StandardCharsets.UTF_8);
byte[] endBytes = endMarker.getBytes(StandardCharsets.UTF_8);
return new SpecifiedStickPackageHelper(startBytes, endBytes);
}
/**
* 创建基于开始和结束标识的黏包处理器 (字节版本)
* @param startMarker 开始标识字节数组
* @param endMarker 结束标识字节数组
*/
public static AbsStickPackageHelper createMarkerBased(byte[] startMarker, byte[] endMarker) {
return new SpecifiedStickPackageHelper(startMarker, endMarker);
}
/**
* 创建变长黏包处理器
* @param byteOrder 字节序
* @param lenSize 长度字段大小
* @param lenIndex 长度字段位置
* @param offset 偏移量
*/
public static AbsStickPackageHelper createVariableLength(java.nio.ByteOrder byteOrder, int lenSize, int lenIndex, int offset) {
return new VariableLenStickPackageHelper(byteOrder, lenSize, lenIndex, offset);
}
/**
* 创建默认变长黏包处理器
* 默认配置:大端字节序,2字节长度字段,位置在索引2,偏移12字节
*/
public static AbsStickPackageHelper createVariableLength() {
return new VariableLenStickPackageHelper(java.nio.ByteOrder.BIG_ENDIAN, 2, 2, 12);
}
/**
* 创建自定义超时的基础处理器
* @param timeout 超时时间(毫秒)
*/
public static AbsStickPackageHelper createTimeoutBased(int timeout) {
return new TimeoutStickPackageHelper(timeout);
}
/**
* 创建组合式黏包处理器
* @param primaryHelper 主要处理器
* @param fallbackHelper 备用处理器
*/
public static AbsStickPackageHelper createComposite(AbsStickPackageHelper primaryHelper,
AbsStickPackageHelper fallbackHelper) {
return new CompositeStickPackageHelper(primaryHelper, fallbackHelper);
}
/**
* 常用协议的快速创建方法
*/
public static class Common {
/**
* AT指令协议(以\r\n结尾)
*/
public static AbsStickPackageHelper createATCommand() {
return createDelimiterBased("\r\n");
}
/**
* JSON协议(以换行符结尾)
*/
public static AbsStickPackageHelper createJsonLine() {
return createDelimiterBased("\n");
}
/**
* Modbus RTU协议(固定长度,通常8字节)
*/
public static AbsStickPackageHelper createModbusRTU() {
return createFixedLength(8);
}
/**
* 自定义协议(STX/ETX包围)
*/
public static AbsStickPackageHelper createSTXETX() {
return createMarkerBased(new byte[]{0x02}, new byte[]{0x03}); // STX, ETX
}
/**
* 自定义协议(SOH/EOT包围)
*/
public static AbsStickPackageHelper createSOHEOT() {
return createMarkerBased(new byte[]{0x01}, new byte[]{0x04}); // SOH, EOT
}
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/TimeoutStickPackageHelper.java
================================================
package com.cl.serialportlibrary.stick;
import android.os.SystemClock;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 基于超时的黏包处理器
* 在指定时间内没有新数据到达时,认为是一个完整的数据包
* Author: cl
* Date: 2023/10/26
*/
public class TimeoutStickPackageHelper implements AbsStickPackageHelper {
private final int timeout; // 超时时间(毫秒)
private final List buffer = new ArrayList<>();
public TimeoutStickPackageHelper(int timeout) {
this.timeout = timeout;
}
@Override
public byte[] execute(InputStream is) {
buffer.clear();
long lastDataTime = System.currentTimeMillis();
try {
while (true) {
int available = is.available();
if (available > 0) {
// 有数据可读
byte[] tempBuffer = new byte[available];
int readBytes = is.read(tempBuffer);
if (readBytes > 0) {
for (int i = 0; i < readBytes; i++) {
buffer.add(tempBuffer[i]);
}
lastDataTime = System.currentTimeMillis();
}
} else {
// 没有数据,检查超时
if (!buffer.isEmpty() && (System.currentTimeMillis() - lastDataTime) >= timeout) {
// 超时且缓冲区有数据,返回数据包
byte[] result = new byte[buffer.size()];
for (int i = 0; i < buffer.size(); i++) {
result[i] = buffer.get(i);
}
return result;
}
// 短暂休眠,避免CPU过度占用
SystemClock.sleep(10);
}
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/VariableLenStickPackageHelper.java
================================================
package com.cl.serialportlibrary.stick;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
/**
* Variable-length sticky packet processing, used in the protocol with a length field
* Example: The protocol is: type+dataLen+data+md5
* type: Named type, two bytes
* dataLen: The length of the data field, two bytes
* data: Data field, variable length, length dataLen
* md5: md5 field, 8 bytes
* Use: 1.byteOrder: first determine the big and small ends, ByteOrder.BIG_ENDIAN or ByteOrder.LITTLE_ENDIAN;
* 2.lenSize: The length of the len field, 2 in this example
* 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
* 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
*/
public class VariableLenStickPackageHelper implements AbsStickPackageHelper {
private int offset = 0;
private int lenIndex = 0;
private int lenSize = 2;
private ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;
private final List mBytes;
private final int lenStartIndex;
private final int lenEndIndex;
public VariableLenStickPackageHelper(ByteOrder byteOrder, int lenSize, int lenIndex, int offset) {
this.byteOrder = byteOrder;
this.lenSize = lenSize;
this.offset = offset;
this.lenIndex = lenIndex;
mBytes = new ArrayList<>();
lenStartIndex = lenIndex;
lenEndIndex = lenIndex + lenSize - 1;
if (lenStartIndex > lenEndIndex) {
throw new IllegalStateException("lenStartIndex>lenEndIndex");
}
}
private int getLen(byte[] src, ByteOrder order) {
int re = 0;
if (order == ByteOrder.BIG_ENDIAN) {
for (byte b : src) {
re = (re << 8) | (b & 0xff);
}
} else {
for (int i = src.length - 1; i >= 0; i--) {
re = (re << 8) | (src[i] & 0xff);
}
}
return re;
}
@Override
public byte[] execute(InputStream is) {
mBytes.clear();
int count = 0;
int len = -1;
byte temp;
byte[] result;
int msgLen = -1;
byte[] lenField = new byte[lenSize];
try {
while ((len = is.read()) != -1) {
temp = (byte) len;
if (count >= lenStartIndex && count <= lenEndIndex) {
lenField[count - lenStartIndex] = temp;
if (count == lenEndIndex) {
msgLen = getLen(lenField, byteOrder);
}
}
count++;
mBytes.add(temp);
if (msgLen != -1) {
if (count == msgLen + offset) {
break;
} else if (count > msgLen + offset) {
len = -1;
break;
}
}
}
if (len == -1) {
return null;
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
result = new byte[mBytes.size()];
for (int i = 0; i < result.length; i++) {
result[i] = mBytes.get(i);
}
return result;
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/thread/SerialPortReadThread.java
================================================
package com.cl.serialportlibrary.thread;
import com.cl.serialportlibrary.utils.SerialPortLogUtil;
import com.cl.serialportlibrary.enumerate.SerialPortEnum;
import com.cl.serialportlibrary.stick.AbsStickPackageHelper;
import com.cl.serialportlibrary.stick.BaseStickPackageHelper;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* 串口消息读取线程
*/
public abstract class SerialPortReadThread extends Thread {
public abstract void onDataReceived(byte[] bytes);
private InputStream mInputStream;
private SerialPortEnum mSerialPortEnum;
private List mStickPackageHelpers;
public SerialPortReadThread(InputStream inputStream, SerialPortEnum mSerialPortEnum, List stickPackageHelpers) {
mInputStream = inputStream;
this.mSerialPortEnum = mSerialPortEnum;
this.mStickPackageHelpers = stickPackageHelpers;
// 如果没有提供粘包处理器,使用默认的
if (this.mStickPackageHelpers == null || this.mStickPackageHelpers.isEmpty()) {
this.mStickPackageHelpers = new java.util.ArrayList<>();
this.mStickPackageHelpers.add(new BaseStickPackageHelper());
}
}
@Override
public void run() {
if (mInputStream == null) return;
while (!Thread.currentThread().isInterrupted()) {
try {
if (mStickPackageHelpers.size() > mSerialPortEnum.ordinal()) {
AbsStickPackageHelper helper = mStickPackageHelpers.get(mSerialPortEnum.ordinal());
byte[] buffer = helper.execute(mInputStream);
if (buffer != null && buffer.length > 0) {
SerialPortLogUtil.d("SerialPortReadThread", "接收数据,长度: " + buffer.length);
onDataReceived(buffer);
}
} else {
// 使用第一个处理器作为默认
if (!mStickPackageHelpers.isEmpty()) {
AbsStickPackageHelper helper = mStickPackageHelpers.get(0);
byte[] buffer = helper.execute(mInputStream);
if (buffer != null && buffer.length > 0) {
SerialPortLogUtil.d("SerialPortReadThread", "接收数据(默认处理器),长度: " + buffer.length);
onDataReceived(buffer);
}
} else {
SerialPortLogUtil.e("SerialPortReadThread", "没有可用的粘包处理器");
break;
}
}
} catch (Exception e) {
SerialPortLogUtil.e("SerialPortReadThread", "读取数据异常: " + e.getMessage());
e.printStackTrace();
break;
}
}
}
/**
* 关闭线程,释放资源
*/
public void release() {
interrupt();
if (mInputStream != null) {
try {
mInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
mInputStream = null;
}
}
}
================================================
FILE: serial_lib/src/main/java/com/cl/serialportlibrary/utils/SerialPortLogUtil.java
================================================
package com.cl.serialportlibrary.utils;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* 串口日志工具类
* 替代XLog的增强实现,包含更详细的调试信息
* 提供时间戳、调用位置、数据格式化等功能
*/
public class SerialPortLogUtil {
private static final String DEFAULT_TAG = "SerialPort";
private static boolean isDebugEnabled = true;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault());
/**
* 设置是否启用调试日志
* @param enabled 是否启用
*/
public static void setDebugEnabled(boolean enabled) {
isDebugEnabled = enabled;
if (enabled) {
Log.i(DEFAULT_TAG, "================== 串口日志系统已启用 ==================");
}
}
/**
* 获取是否启用调试日志
*/
public static boolean isDebugEnabled() {
return isDebugEnabled;
}
/**
* 获取当前时间戳
*/
private static String getTimeStamp() {
return DATE_FORMAT.format(new Date());
}
/**
* 获取调用者信息
*/
private static String getCallerInfo() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
// 跳过前面的调用栈,找到真正的调用者
for (int i = 4; i < stackTrace.length; i++) {
StackTraceElement element = stackTrace[i];
String className = element.getClassName();
if (!className.equals(SerialPortLogUtil.class.getName())) {
String simpleClassName = className.substring(className.lastIndexOf('.') + 1);
return String.format("[%s.%s:%d]", simpleClassName, element.getMethodName(), element.getLineNumber());
}
}
return "[Unknown]";
}
/**
* 格式化消息
*/
private static String formatMessage(String message) {
return String.format("%s %s %s", getTimeStamp(), getCallerInfo(), message);
}
/**
* 输出调试日志
* @param tag 标签
* @param message 消息
*/
public static void d(String tag, String message) {
if (isDebugEnabled) {
Log.d(tag != null ? tag : DEFAULT_TAG, formatMessage(message));
}
}
/**
* 输出调试日志(使用默认标签)
* @param message 消息
*/
public static void d(String message) {
d(DEFAULT_TAG, message);
}
/**
* 输出信息日志
* @param tag 标签
* @param message 消息
*/
public static void i(String tag, String message) {
if (isDebugEnabled) {
Log.i(tag != null ? tag : DEFAULT_TAG, formatMessage(message));
}
}
/**
* 输出信息日志(使用默认标签)
* @param message 消息
*/
public static void i(String message) {
i(DEFAULT_TAG, message);
}
/**
* 输出警告日志
* @param tag 标签
* @param message 消息
*/
public static void w(String tag, String message) {
if (isDebugEnabled) {
Log.w(tag != null ? tag : DEFAULT_TAG, formatMessage(message));
}
}
/**
* 输出警告日志(使用默认标签)
* @param message 消息
*/
public static void w(String message) {
w(DEFAULT_TAG, message);
}
/**
* 输出错误日志
* @param tag 标签
* @param message 消息
*/
public static void e(String tag, String message) {
// 错误日志始终输出,不受isDebugEnabled控制
Log.e(tag != null ? tag : DEFAULT_TAG, formatMessage(message));
}
/**
* 输出错误日志(使用默认标签)
* @param message 消息
*/
public static void e(String message) {
e(DEFAULT_TAG, message);
}
/**
* 输出错误日志(带异常)
* @param tag 标签
* @param message 消息
* @param throwable 异常
*/
public static void e(String tag, String message, Throwable throwable) {
// 错误日志始终输出,不受isDebugEnabled控制
Log.e(tag != null ? tag : DEFAULT_TAG, formatMessage(message), throwable);
}
/**
* 输出错误日志(带异常,使用默认标签)
* @param message 消息
* @param throwable 异常
*/
public static void e(String message, Throwable throwable) {
e(DEFAULT_TAG, message, throwable);
}
/**
* 打印数据(专门用于串口数据调试)
* @param tag 标签
* @param prefix 前缀
* @param data 数据
*/
public static void printData(String tag, String prefix, byte[] data) {
if (!isDebugEnabled || data == null) return;
StringBuilder sb = new StringBuilder();
sb.append(prefix).append(" [").append(data.length).append(" bytes]: ");
// 十六进制格式
sb.append("HEX[");
for (int i = 0; i < Math.min(data.length, 32); i++) { // 最多显示32字节
if (i > 0) sb.append(" ");
sb.append(String.format("%02X", data[i] & 0xFF));
}
if (data.length > 32) {
sb.append("...");
}
sb.append("] ");
// ASCII格式(可打印字符)
sb.append("ASCII[");
for (int i = 0; i < Math.min(data.length, 32); i++) {
byte b = data[i];
if (b >= 32 && b < 127) {
sb.append((char) b);
} else {
sb.append('.');
}
}
if (data.length > 32) {
sb.append("...");
}
sb.append("]");
d(tag, sb.toString());
}
/**
* 打印数据(使用默认标签)
* @param prefix 前缀
* @param data 数据
*/
public static void printData(String prefix, byte[] data) {
printData(DEFAULT_TAG, prefix, data);
}
/**
* 打印串口状态信息
* @param tag 标签
* @param devicePath 设备路径
* @param baudRate 波特率
* @param isOpen 是否打开
*/
public static void printSerialStatus(String tag, String devicePath, int baudRate, boolean isOpen) {
i(tag, String.format("串口状态 - 设备: %s, 波特率: %d, 状态: %s",
devicePath, baudRate, isOpen ? "已打开" : "已关闭"));
}
/**
* 打印串口配置信息
* @param tag 标签
* @param databits 数据位
* @param parity 校验位
* @param stopbits 停止位
* @param flags 标志位
*/
public static void printSerialConfig(String tag, int databits, int parity, int stopbits, int flags) {
String parityStr;
switch (parity) {
case 0: parityStr = "无校验"; break;
case 1: parityStr = "奇校验"; break;
case 2: parityStr = "偶校验"; break;
default: parityStr = "未知(" + parity + ")"; break;
}
i(tag, String.format("串口配置 - 数据位: %d, 校验位: %s, 停止位: %d, 标志位: 0x%X",
databits, parityStr, stopbits, flags));
}
/**
* 打印性能信息
* @param tag 标签
* @param operation 操作名称
* @param startTime 开始时间
*/
public static void printPerformance(String tag, String operation, long startTime) {
long duration = System.currentTimeMillis() - startTime;
d(tag, String.format("性能统计 - %s 耗时: %dms", operation, duration));
}
/**
* 打印分隔线
* @param tag 标签
* @param title 标题
*/
public static void printSeparator(String tag, String title) {
if (isDebugEnabled) {
String separator = "==================== " + title + " ====================";
i(tag, separator);
}
}
/**
* 打印分隔线(使用默认标签)
* @param title 标题
*/
public static void printSeparator(String title) {
printSeparator(DEFAULT_TAG, title);
}
}
================================================
FILE: serial_lib/src/main/res/values/strings.xml
================================================
SerialPortLibrary
================================================
FILE: settings.gradle
================================================
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
rootProject.name = "serialPort"
include ':serial_lib'
include ':app'