Full Code of cl-6666/serialPort for AI

master 7b7fafdd03f9 cached
89 files
322.0 KB
76.4k tokens
402 symbols
1 requests
Download .txt
Showing preview only (367K chars total). Download the full file or copy to clipboard to get everything.
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)

[![Version](https://img.shields.io/badge/version-5.0.8-blue.svg)](https://github.com/cl-6666/serialPort)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)
[![License](https://img.shields.io/badge/license-Apache%202-green.svg)](https://www.apache.org/licenses/LICENSE-2.0)

> 一个灵活、高效并且轻量的Android串口通信框架,让串口操作变得简单易用。支持单串口、多串口、粘包处理、自定义配置等功能。

<img src="https://github.com/cl-6666/serialPort/blob/master/img/multiple_images.png" width="650" height="360" alt="演示"/>  

## 📱 体验演示

想要快速体验串口通信框架的强大功能?直接下载演示 APK 安装到您的 Android 设备上试试吧!

<div align="center">

### 📥 [点击下载演示 APK](https://www.pgyer.com/XNzY)

[![Download APK](https://img.shields.io/badge/Download-APK%20v5.0.8-brightgreen.svg?style=for-the-badge&logo=android)](https://www.pgyer.com/XNzY)

**版本**: v5.0.8 | **大小**: ~7 MB | **API**: 21+ | **架构**: arm64-v8a, armeabi-v7a, x86, x86_64

</div>

### 演示 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
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
```

## 📚 使用指南

### 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<String> 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端串口调试助手
<img src="https://github.com/cl-6666/serialPort/blob/master/img/pc_ck.jpg" width="440" height="320" alt="PC调试助手"/>

**下载链接**: https://pan.baidu.com/s/1DL2TOHz9bl9RIKIG3oCSWw?pwd=f7sh  

### QQ技术交流群
<img src="https://github.com/cl-6666/serialPort/blob/master/img/qq2.jpg" width="350" height="560" alt="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

[![Version](https://img.shields.io/badge/version-4.1.1-orange.svg)](https://github.com/cl-6666/serialPort)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](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
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
```

## 📚 使用指南

### 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<Device> 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<Device> 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<Device>)` | 打开多个串口 |
| `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)

[![Version](https://img.shields.io/badge/version-5.0.8-blue.svg)](https://github.com/cl-6666/serialPort)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)
[![License](https://img.shields.io/badge/license-Apache%202-green.svg)](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.

<img src="https://github.com/cl-6666/serialPort/blob/master/img/multiple_images.png" width="650" height="360" alt="Demo"/>  

## 📱 Demo APK

Want to try it quickly? Download the demo APK and install it on your Android device.

<div align="center">

### 📥 [Download Demo APK](https://www.pgyer.com/XNzY)

[![Download APK](https://img.shields.io/badge/Download-APK%20v5.0.8-brightgreen.svg?style=for-the-badge&logo=android)](https://www.pgyer.com/XNzY)

**Version**: v5.0.8 | **Size**: ~7 MB | **API**: 21+ | **ABIs**: arm64-v8a, armeabi-v7a, x86, x86_64

</div>

### 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
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
```

## 📚 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<String> 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
<img src="https://github.com/cl-6666/serialPort/blob/master/img/pc_ck.jpg" width="440" height="320" alt="PC tool"/>

**Download**: https://pan.baidu.com/s/1DL2TOHz9bl9RIKIG3oCSWw?pwd=f7sh  

### QQ technical group
<img src="https://github.com/cl-6666/serialPort/blob/master/img/qq2.jpg" width="350" height="560" alt="QQ 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
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.cl.myapplication">


    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />


    <application
        android:name=".App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication">

        <meta-data
            android:name="design_width_in_dp"
            android:value="1080" />
        <meta-data
            android:name="design_height_in_dp"
            android:value="1920" />

        <activity android:name=".MultiSerialPortActivity"
            android:exported="true"
            android:screenOrientation="landscape">

<!--            <intent-filter>-->
<!--                <action android:name="android.intent.action.MAIN" />-->

<!--                <category android:name="android.intent.category.LAUNCHER" />-->
<!--            </intent-filter>-->

        </activity>

        <activity
            android:name=".SingleSerialPortActivity"
            android:exported="true"
            android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!--查看哪些串口是否可用-->
        <activity android:name=".SelectSerialPortActivity">
<!--            <intent-filter>-->
<!--                <action android:name="android.intent.action.MAIN" />-->

<!--                <category android:name="android.intent.category.LAUNCHER" />-->
<!--            </intent-filter>-->
        </activity>
        <activity
            android:name=".MainActivity"
            android:windowSoftInputMode="stateHidden|stateUnchanged"></activity>
    </application>

</manifest>

================================================
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<String> 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<String> deviceAdapter =
                new ArrayAdapter<String>(this, R.layout.spinner_default_item, mDevices);
        deviceAdapter.setDropDownViewResource(R.layout.spinner_item);
        binding.spinnerDevices.setAdapter(deviceAdapter);
        binding.spinnerDevices.setOnItemSelectedListener(this);

        ArrayAdapter<String> baudrateAdapter =
                new ArrayAdapter<String>(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<Device> devices;

    public DeviceAdapter(Context context, ArrayList<Device> 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<IMessage> 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<View> 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 extends View> 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
================================================
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#e93838" android:state_enabled="false" />
    <item android:color="#519b49" /> 
</selector>

================================================
FILE: app/src/main/res/color/selector_spinner_text.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@android:color/darker_gray" android:state_enabled="false" />
    <item android:color="@color/colorPrimary" /> 
</selector>

================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillColor="#3DDC84"
        android:pathData="M0,0h108v108h-108z" />
    <path
        android:fillColor="#00000000"
        android:pathData="M9,0L9,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,0L19,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,0L29,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,0L39,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,0L49,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,0L59,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,0L69,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,0L79,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M89,0L89,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M99,0L99,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,9L108,9"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,19L108,19"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,29L108,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,39L108,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,49L108,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,59L108,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,69L108,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,79L108,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,89L108,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,99L108,99"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,29L89,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,39L89,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,49L89,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,59L89,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,69L89,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,79L89,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,19L29,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,19L39,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,19L49,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,19L59,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,19L69,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,19L79,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
</vector>


================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
        <aapt:attr name="android:fillColor">
            <gradient
                android:endX="85.84757"
                android:endY="92.4963"
                android:startX="42.9492"
                android:startY="49.59793"
                android:type="linear">
                <item
                    android:color="#44000000"
                    android:offset="0.0" />
                <item
                    android:color="#00000000"
                    android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:fillColor="#FFFFFF"
        android:fillType="nonZero"
        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
        android:strokeWidth="1"
        android:strokeColor="#00000000" />
</vector>

================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <EditText
                android:id="@+id/et_send_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ems="10"
                android:gravity="top"
                android:hint="请输入发送内容,内容转 byte[] 发送"
                android:minLines="5" />

            <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:onClick="onSend"
                android:text="发送" />


            <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:onClick="onDestroy"
                android:text="注销" />

        </LinearLayout>


    </RelativeLayout>

</layout>


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

    <data>

    </data>

    <LinearLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorLightBlue"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/colorPrimary"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="10dp"
                android:text="@string/select_serial_port"
                android:textColor="@color/white"
                android:textSize="17dp"
                android:textStyle="bold" />

            <Spinner
                android:id="@+id/spinner_devices"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="8dp"
                android:layout_weight="1"
                android:background="@color/colorLightBlue"
                android:textSize="17dp"
                tools:entries="@array/baudrates" />


            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:text="@string/select_baud_rate"
                android:textColor="@color/white"
                android:textSize="17dp"
                android:textStyle="bold" />

            <Spinner
                android:id="@+id/spinner_baudrate"
                android:layout_width="200dp"
                android:layout_height="wrap_content"
                android:background="@color/colorLightBlue"
                android:textSize="17dp"
                tools:entries="@array/baudrates" />

            <Button
                android:id="@+id/btn_open_device"
                android:layout_width="90dp"
                android:layout_height="50dp"
                android:layout_marginLeft="10dp"
                android:gravity="center"
                android:text="@string/open_serial_port"
                android:textSize="17dp" />


        </LinearLayout>


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

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


                <EditText
                    android:id="@+id/et_data"
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:digits="0123456789abcdefABCDEF"
                    android:hint="@string/input_data"
                    android:inputType="textCapCharacters"
                    android:singleLine="true"
                    android:textSize="17dp" />

                <Button
                    android:id="@+id/btn_send_data"
                    style="@style/ButtonStyle"
                    android:text="@string/send_data" />

                <Button
                    android:id="@+id/btn_load_list"
                    style="@style/ButtonStyle"
                    android:text="@string/load_cmd_list" />


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

                    <TextView
                        android:id="@+id/tv_databits"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:gravity="center_vertical"
                        android:text="@string/title_data_bits"
                        android:textColor="#000" />

                    <Spinner
                        android:id="@+id/sp_databits"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1" />
                </LinearLayout>

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

                    <TextView
                        android:id="@+id/tv_parity"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:gravity="center_vertical"
                        android:text="@string/title_parity"
                        android:textColor="#000" />

                    <Spinner
                        android:id="@+id/sp_parity"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1" />
                </LinearLayout>

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

                    <TextView
                        android:id="@+id/tv_stopbits"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:gravity="center_vertical"
                        android:text="@string/title_stop_bits"
                        android:textColor="#000" />

                    <Spinner
                        android:id="@+id/sp_stopbits"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1" />
                </LinearLayout>

            </LinearLayout>

            <include
                layout="@layout/include_fragment_container"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

        </LinearLayout>
    </LinearLayout>

</layout>

================================================
FILE: app/src/main/res/layout/activity_multi_serial.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorLightBlue"
        android:orientation="vertical">

        <!-- 顶部串口选择区域 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorPrimary"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:paddingLeft="16dp"
            android:paddingRight="16dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="12dp"
                android:text="多串口管理"
                android:textColor="@color/white"
                android:textSize="18sp"
                android:textStyle="bold" />

            <View
                android:layout_width="0dp"
                android:layout_height="1dp"
                android:layout_weight="1" />

            <Button
                android:id="@+id/btn_refresh_ports"
                android:layout_width="wrap_content"
                android:layout_height="40dp"
                android:background="@color/colorAccent"
                android:paddingLeft="16dp"
                android:paddingRight="16dp"
                android:text="刷新串口"
                android:textColor="@color/white"
                android:textSize="14sp" />

        </LinearLayout>

        <!-- 主内容区域 -->
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:padding="16dp">

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

                <!-- 全局串口参数配置卡片 -->
                <androidx.cardview.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="16dp"
                    app:cardCornerRadius="8dp"
                    app:cardElevation="4dp">

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

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="16dp"
                            android:text="全局串口参数"
                            android:textColor="@color/colorPrimary"
                            android:textSize="18sp"
                            android:textStyle="bold" />

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

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

                                <TextView
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:text="数据位:"
                                    android:textColor="@color/black"
                                    android:textSize="14sp" />

                                <Spinner
                                    android:id="@+id/spinner_databits"
                                    android:layout_width="match_parent"
                                    android:layout_height="40dp"
                                    android:layout_marginTop="4dp"
                                    android:background="@color/colorLightBlue" />

                            </LinearLayout>

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

                                <TextView
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:text="校验位:"
                                    android:textColor="@color/black"
                                    android:textSize="14sp" />

                                <Spinner
                                    android:id="@+id/spinner_parity"
                                    android:layout_width="match_parent"
                                    android:layout_height="40dp"
                                    android:layout_marginTop="4dp"
                                    android:background="@color/colorLightBlue" />

                            </LinearLayout>

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

                                <TextView
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:text="停止位:"
                                    android:textColor="@color/black"
                                    android:textSize="14sp" />

                                <Spinner
                                    android:id="@+id/spinner_stopbits"
                                    android:layout_width="match_parent"
                                    android:layout_height="40dp"
                                    android:layout_marginTop="4dp"
                                    android:background="@color/colorLightBlue" />

                            </LinearLayout>

                        </LinearLayout>

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginTop="8dp"
                            android:text="注意:修改全局参数后,需要重新打开串口才能生效"
                            android:textColor="@color/secondary_text"
                            android:textSize="12sp" />

                    </LinearLayout>

                </androidx.cardview.widget.CardView>

                <!-- 串口配置卡片 -->
                <androidx.cardview.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="16dp"
                    app:cardCornerRadius="8dp"
                    app:cardElevation="4dp">

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

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="16dp"
                            android:text="串口设备配置"
                            android:textColor="@color/colorPrimary"
                            android:textSize="18sp"
                            android:textStyle="bold" />

                        <!-- 串口1配置 -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="12dp"
                            android:orientation="vertical">

                            <TextView
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginBottom="8dp"
                                android:text="串口1 (GPS模块)"
                                android:textColor="@color/black"
                                android:textSize="16sp"
                                android:textStyle="bold" />

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

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="设备:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_gps_device"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="波特率:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_gps_baudrate"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

                                <Button
                                    android:id="@+id/btn_gps_toggle"
                                    android:layout_width="80dp"
                                    android:layout_height="40dp"
                                    android:layout_gravity="bottom"
                                    android:layout_marginLeft="12dp"
                                    android:background="@color/colorAccent"
                                    android:text="打开"
                                    android:textColor="@color/white"
                                    android:textSize="12sp" />

                            </LinearLayout>

                        </LinearLayout>

                        <!-- 分割线 -->
                        <View
                            android:layout_width="match_parent"
                            android:layout_height="1dp"
                            android:layout_marginBottom="12dp"
                            android:background="@color/divider" />

                        <!-- 串口2配置 -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="12dp"
                            android:orientation="vertical">

                            <TextView
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginBottom="8dp"
                                android:text="串口2 (传感器模块)"
                                android:textColor="@color/black"
                                android:textSize="16sp"
                                android:textStyle="bold" />

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

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="设备:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_sensor_device"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="波特率:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_sensor_baudrate"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

                                <Button
                                    android:id="@+id/btn_sensor_toggle"
                                    android:layout_width="80dp"
                                    android:layout_height="40dp"
                                    android:layout_gravity="bottom"
                                    android:layout_marginLeft="12dp"
                                    android:background="@color/colorAccent"
                                    android:text="打开"
                                    android:textColor="@color/white"
                                    android:textSize="12sp" />

                            </LinearLayout>

                        </LinearLayout>

                        <!-- 分割线 -->
                        <View
                            android:layout_width="match_parent"
                            android:layout_height="1dp"
                            android:layout_marginBottom="12dp"
                            android:background="@color/divider" />

                        <!-- 串口3配置 -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="12dp"
                            android:orientation="vertical">

                            <TextView
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginBottom="8dp"
                                android:text="串口3 (Modbus设备)"
                                android:textColor="@color/black"
                                android:textSize="16sp"
                                android:textStyle="bold" />

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

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="设备:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_modbus_device"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="波特率:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_modbus_baudrate"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

                                <Button
                                    android:id="@+id/btn_modbus_toggle"
                                    android:layout_width="80dp"
                                    android:layout_height="40dp"
                                    android:layout_gravity="bottom"
                                    android:layout_marginLeft="12dp"
                                    android:background="@color/colorAccent"
                                    android:text="打开"
                                    android:textColor="@color/white"
                                    android:textSize="12sp" />

                            </LinearLayout>

                        </LinearLayout>

                        <!-- 分割线 -->
                        <View
                            android:layout_width="match_parent"
                            android:layout_height="1dp"
                            android:layout_marginBottom="12dp"
                            android:background="@color/divider" />

                        <!-- 串口4配置 -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="vertical">

                            <TextView
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginBottom="8dp"
                                android:text="串口4 (自定义协议)"
                                android:textColor="@color/black"
                                android:textSize="16sp"
                                android:textStyle="bold" />

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

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="设备:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_custom_device"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="波特率:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_custom_baudrate"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

                                <Button
                                    android:id="@+id/btn_custom_toggle"
                                    android:layout_width="80dp"
                                    android:layout_height="40dp"
                                    android:layout_gravity="bottom"
                                    android:layout_marginLeft="12dp"
                                    android:background="@color/colorAccent"
                                    android:text="打开"
                                    android:textColor="@color/white"
                                    android:textSize="12sp" />

                            </LinearLayout>

                        </LinearLayout>

                    </LinearLayout>

                </androidx.cardview.widget.CardView>

                <!-- 操作按钮卡片 -->
                <androidx.cardview.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="16dp"
                    app:cardCornerRadius="8dp"
                    app:cardElevation="4dp">

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

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="16dp"
                            android:text="批量操作"
                            android:textColor="@color/colorPrimary"
                            android:textSize="18sp"
                            android:textStyle="bold" />

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

                            <Button
                                android:id="@+id/btn_open_all"
                                android:layout_width="0dp"
                                android:layout_height="48dp"
                                android:layout_marginEnd="8dp"
                                android:layout_weight="1"
                                android:background="@color/colorPrimary"
                                android:text="打开所有串口"
                                android:textColor="@color/white"
                                android:textSize="14sp" />

                            <Button
                                android:id="@+id/btn_close_all"
                                android:layout_width="0dp"
                                android:layout_height="48dp"
                                android:layout_marginStart="8dp"
                                android:layout_weight="1"
                                android:background="@color/secondary_text"
                                android:text="关闭所有串口"
                                android:textColor="@color/white"
                                android:textSize="14sp" />

                        </LinearLayout>

                        <Button
                            android:id="@+id/btn_send_test"
                            android:layout_width="match_parent"
                            android:layout_height="48dp"
                            android:layout_marginTop="12dp"
                            android:background="@color/colorAccent"
                            android:text="发送测试数据"
                            android:textColor="@color/white"
                            android:textSize="14sp" />

                    </LinearLayout>

                </androidx.cardview.widget.CardView>

                <!-- 状态日志卡片 -->
                <androidx.cardview.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:cardCornerRadius="8dp"
                    app:cardElevation="4dp">

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

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

                            <TextView
                                android:layout_width="0dp"
                                android:layout_height="wrap_content"
                                android:layout_weight="1"
                                android:text="状态日志"
                                android:textColor="@color/colorPrimary"
                                android:textSize="18sp"
                                android:textStyle="bold" />

                            <Button
                                android:id="@+id/btn_clear_log"
                                android:layout_width="wrap_content"
                                android:layout_height="32dp"
                                android:background="@color/secondary_text"
                                android:paddingLeft="12dp"
                                android:paddingRight="12dp"
                                android:text="清空日志"
                                android:textColor="@color/white"
                                android:textSize="12sp" />

                        </LinearLayout>

                        <ScrollView
                            android:layout_width="match_parent"
                            android:layout_height="200dp">

                            <TextView
                                android:id="@+id/tv_status"
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:background="@color/log_bg"
                                android:fontFamily="monospace"
                                android:padding="12dp"
                                android:text="等待操作..."
                                android:textSize="12sp" />

                        </ScrollView>

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginTop="16dp"
                            android:layout_marginBottom="12dp"
                            android:text="收发数据"
                            android:textColor="@color/colorPrimary"
                            android:textSize="18sp"
                            android:textStyle="bold" />

                        <ScrollView
                            android:layout_width="match_parent"
                            android:layout_height="220dp">

                            <TextView
                                android:id="@+id/tv_data"
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:background="@color/log_bg"
                                android:fontFamily="monospace"
                                android:padding="12dp"
                                android:text="等待数据..."
                                android:textSize="12sp" />

                        </ScrollView>

                    </LinearLayout>

                </androidx.cardview.widget.CardView>

            </LinearLayout>

        </ScrollView>

    </LinearLayout>

</layout>


================================================
FILE: app/src/main/res/layout/activity_multi_serial_new.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorLightBlue"
        android:orientation="vertical">

        <!-- 顶部串口选择区域 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorPrimary"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:paddingLeft="16dp"
            android:paddingRight="16dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="12dp"
                android:text="多串口管理"
                android:textColor="@color/white"
                android:textSize="18sp"
                android:textStyle="bold" />

            <View
                android:layout_width="0dp"
                android:layout_height="1dp"
                android:layout_weight="1" />

            <Button
                android:id="@+id/btn_refresh_ports"
                android:layout_width="wrap_content"
                android:layout_height="40dp"
                android:background="@color/colorAccent"
                android:paddingLeft="16dp"
                android:paddingRight="16dp"
                android:text="刷新串口"
                android:textColor="@color/white"
                android:textSize="14sp" />

        </LinearLayout>

        <!-- 主内容区域 -->
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:padding="16dp">

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

                <!-- 全局串口参数配置卡片 -->
                <androidx.cardview.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="16dp"
                    app:cardCornerRadius="8dp"
                    app:cardElevation="4dp">

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

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="16dp"
                            android:text="全局串口参数"
                            android:textColor="@color/colorPrimary"
                            android:textSize="18sp"
                            android:textStyle="bold" />

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

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

                                <TextView
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:text="数据位:"
                                    android:textColor="@color/black"
                                    android:textSize="14sp" />

                                <Spinner
                                    android:id="@+id/spinner_databits"
                                    android:layout_width="match_parent"
                                    android:layout_height="40dp"
                                    android:layout_marginTop="4dp"
                                    android:background="@color/colorLightBlue" />

                            </LinearLayout>

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

                                <TextView
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:text="校验位:"
                                    android:textColor="@color/black"
                                    android:textSize="14sp" />

                                <Spinner
                                    android:id="@+id/spinner_parity"
                                    android:layout_width="match_parent"
                                    android:layout_height="40dp"
                                    android:layout_marginTop="4dp"
                                    android:background="@color/colorLightBlue" />

                            </LinearLayout>

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

                                <TextView
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:text="停止位:"
                                    android:textColor="@color/black"
                                    android:textSize="14sp" />

                                <Spinner
                                    android:id="@+id/spinner_stopbits"
                                    android:layout_width="match_parent"
                                    android:layout_height="40dp"
                                    android:layout_marginTop="4dp"
                                    android:background="@color/colorLightBlue" />

                            </LinearLayout>

                        </LinearLayout>

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginTop="8dp"
                            android:text="注意:修改全局参数后,需要重新打开串口才能生效"
                            android:textColor="@color/secondary_text"
                            android:textSize="12sp" />

                    </LinearLayout>

                </androidx.cardview.widget.CardView>

                <!-- 串口配置卡片 -->
                <androidx.cardview.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="16dp"
                    app:cardCornerRadius="8dp"
                    app:cardElevation="4dp">

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

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="16dp"
                            android:text="串口设备配置"
                            android:textColor="@color/colorPrimary"
                            android:textSize="18sp"
                            android:textStyle="bold" />

                        <!-- 串口1配置 -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="12dp"
                            android:orientation="vertical">

                            <TextView
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginBottom="8dp"
                                android:text="串口1 (GPS模块)"
                                android:textColor="@color/black"
                                android:textSize="16sp"
                                android:textStyle="bold" />

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

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="设备:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_gps_device"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="波特率:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_gps_baudrate"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

                                <Button
                                    android:id="@+id/btn_gps_toggle"
                                    android:layout_width="80dp"
                                    android:layout_height="40dp"
                                    android:layout_gravity="bottom"
                                    android:layout_marginLeft="12dp"
                                    android:background="@color/colorAccent"
                                    android:text="打开"
                                    android:textColor="@color/white"
                                    android:textSize="12sp" />

                            </LinearLayout>

                        </LinearLayout>

                        <!-- 分割线 -->
                        <View
                            android:layout_width="match_parent"
                            android:layout_height="1dp"
                            android:layout_marginBottom="12dp"
                            android:background="@color/divider" />

                        <!-- 串口2配置 -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="12dp"
                            android:orientation="vertical">

                            <TextView
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginBottom="8dp"
                                android:text="串口2 (传感器模块)"
                                android:textColor="@color/black"
                                android:textSize="16sp"
                                android:textStyle="bold" />

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

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="设备:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_sensor_device"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="波特率:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_sensor_baudrate"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

                                <Button
                                    android:id="@+id/btn_sensor_toggle"
                                    android:layout_width="80dp"
                                    android:layout_height="40dp"
                                    android:layout_gravity="bottom"
                                    android:layout_marginLeft="12dp"
                                    android:background="@color/colorAccent"
                                    android:text="打开"
                                    android:textColor="@color/white"
                                    android:textSize="12sp" />

                            </LinearLayout>

                        </LinearLayout>

                        <!-- 分割线 -->
                        <View
                            android:layout_width="match_parent"
                            android:layout_height="1dp"
                            android:layout_marginBottom="12dp"
                            android:background="@color/divider" />

                        <!-- 串口3配置 -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="12dp"
                            android:orientation="vertical">

                            <TextView
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginBottom="8dp"
                                android:text="串口3 (Modbus设备)"
                                android:textColor="@color/black"
                                android:textSize="16sp"
                                android:textStyle="bold" />

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

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="设备:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_modbus_device"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="波特率:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_modbus_baudrate"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

                                <Button
                                    android:id="@+id/btn_modbus_toggle"
                                    android:layout_width="80dp"
                                    android:layout_height="40dp"
                                    android:layout_gravity="bottom"
                                    android:layout_marginLeft="12dp"
                                    android:background="@color/colorAccent"
                                    android:text="打开"
                                    android:textColor="@color/white"
                                    android:textSize="12sp" />

                            </LinearLayout>

                        </LinearLayout>

                        <!-- 分割线 -->
                        <View
                            android:layout_width="match_parent"
                            android:layout_height="1dp"
                            android:layout_marginBottom="12dp"
                            android:background="@color/divider" />

                        <!-- 串口4配置 -->
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="vertical">

                            <TextView
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginBottom="8dp"
                                android:text="串口4 (自定义协议)"
                                android:textColor="@color/black"
                                android:textSize="16sp"
                                android:textStyle="bold" />

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

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="设备:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_custom_device"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

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

                                    <TextView
                                        android:layout_width="match_parent"
                                        android:layout_height="wrap_content"
                                        android:text="波特率:"
                                        android:textColor="@color/black"
                                        android:textSize="14sp" />

                                    <Spinner
                                        android:id="@+id/spinner_custom_baudrate"
                                        android:layout_width="match_parent"
                                        android:layout_height="40dp"
                                        android:layout_marginTop="4dp"
                                        android:background="@color/colorLightBlue" />

                                </LinearLayout>

                                <Button
                                    android:id="@+id/btn_custom_toggle"
                                    android:layout_width="80dp"
                                    android:layout_height="40dp"
                                    android:layout_gravity="bottom"
                                    android:layout_marginLeft="12dp"
                                    android:background="@color/colorAccent"
                                    android:text="打开"
                                    android:textColor="@color/white"
                                    android:textSize="12sp" />

                            </LinearLayout>

                        </LinearLayout>

                    </LinearLayout>

                </androidx.cardview.widget.CardView>

                <!-- 操作按钮卡片 -->
                <androidx.cardview.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="16dp"
                    app:cardCornerRadius="8dp"
                    app:cardElevation="4dp">

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

                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_marginBottom="16dp"
                            android:text="批量操作"
                            android:textColor="@color/colorPrimary"
                            android:textSize="18sp"
                            android:textStyle="bold" />

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

                            <Button
                                android:id="@+id/btn_open_all"
                                android:layout_width="0dp"
                                android:layout_height="48dp"
                                android:layout_marginEnd="8dp"
                                android:layout_weight="1"
                                android:background="@color/colorPrimary"
                                android:text="打开所有串口"
                                android:textColor="@color/white"
                                android:textSize="14sp" />

                            <Button
                                android:id="@+id/btn_close_all"
                                android:layout_width="0dp"
                                android:layout_height="48dp"
                                android:layout_marginStart="8dp"
                                android:layout_weight="1"
                                android:background="@color/secondary_text"
                                android:text="关闭所有串口"
                                android:textColor="@color/white"
                                android:textSize="14sp" />

                        </LinearLayout>

                        <Button
                            android:id="@+id/btn_send_test"
                            android:layout_width="match_parent"
                            android:layout_height="48dp"
                            android:layout_marginTop="12dp"
                            android:background="@color/colorAccent"
                            android:text="发送测试数据"
                            android:textColor="@color/white"
                            android:textSize="14sp" />

                    </LinearLayout>

                </androidx.cardview.widget.CardView>

                <!-- 状态日志卡片 -->
                <androidx.cardview.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:cardCornerRadius="8dp"
                    app:cardElevation="4dp">

                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
 
Download .txt
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
Download .txt
SYMBOL INDEX (402 symbols across 40 files)

FILE: app/src/main/java/com/cl/myapplication/App.java
  class App (line 15) | public class App extends Application {
    method onCreate (line 17) | @Override

FILE: app/src/main/java/com/cl/myapplication/MultiSerialPortActivity.java
  class MultiSerialPortActivity (line 35) | public class MultiSerialPortActivity extends AppCompatActivity {
    method onCreate (line 70) | @Override
    method initViews (line 82) | private void initViews() {
    method setupMultiSerial (line 100) | private void setupMultiSerial() {
    method sendTestData (line 108) | private void sendTestData() {
    method updateStatus (line 164) | private void updateStatus(String message) {
    method appendData (line 169) | private void appendData(String message) {
    method appendLog (line 173) | private void appendLog(TextView target, String message, int maxLines) {
    method sanitizeForSingleLine (line 191) | private String sanitizeForSingleLine(String text) {
    method bytesToHex (line 201) | private String bytesToHex(byte[] bytes) {
    method onDestroy (line 209) | @Override
    method updateStickyPacketExample (line 219) | public void updateStickyPacketExample(View view) {
    method serialRoutingExample (line 236) | public void serialRoutingExample() {
    method initDevices (line 273) | private void initDevices() {
    method setupGlobalParamSpinners (line 297) | private void setupGlobalParamSpinners() {
    method closeAllOpenedPorts (line 323) | private void closeAllOpenedPorts() {
    method setupSpinners (line 357) | private void setupSpinners() {
    method setupSpinner (line 430) | private void setupSpinner(Spinner spinner, String[] data, int defaultS...
    method refreshSerialPorts (line 450) | private void refreshSerialPorts() {
    method openAllSerialPorts (line 459) | private void openAllSerialPorts() {
    method closeAllSerialPorts (line 470) | private void closeAllSerialPorts() {
    method toggleGpsPort (line 486) | private void toggleGpsPort() {
    method openGpsPort (line 494) | private void openGpsPort() {
    method closeGpsPort (line 536) | private void closeGpsPort() {
    method toggleSensorPort (line 547) | private void toggleSensorPort() {
    method openSensorPort (line 555) | private void openSensorPort() {
    method closeSensorPort (line 599) | private void closeSensorPort() {
    method toggleModbusPort (line 610) | private void toggleModbusPort() {
    method openModbusPort (line 618) | private void openModbusPort() {
    method closeModbusPort (line 660) | private void closeModbusPort() {
    method toggleCustomPort (line 671) | private void toggleCustomPort() {
    method openCustomPort (line 679) | private void openCustomPort() {
    method closeCustomPort (line 723) | private void closeCustomPort() {
    method updateButtonStates (line 734) | private void updateButtonStates() {
    method clearLog (line 755) | private void clearLog() {
    class SerialPortConfig (line 767) | private static class SerialPortConfig {
    type OnSpinnerItemSelectedListener (line 775) | private interface OnSpinnerItemSelectedListener {
      method onItemSelected (line 776) | void onItemSelected(int position);

FILE: app/src/main/java/com/cl/myapplication/SingleSerialPortActivity.java
  class SingleSerialPortActivity (line 41) | public class SingleSerialPortActivity extends AppCompatActivity implemen...
    method onCreate (line 66) | @Override
    method openSerialPort (line 147) | private void openSerialPort() {
    method closeSerialPort (line 208) | private void closeSerialPort() {
    method verifyStoragePermissions (line 214) | public static void verifyStoragePermissions(Activity activity) {
    method updateViewState (line 233) | private void updateViewState(boolean isSerialPortOpened) {
    method initDevice (line 245) | private void initDevice() {
    method initSpinners (line 269) | private void initSpinners() {
    method onSend (line 290) | public void onSend() {
    method onItemSelected (line 302) | @Override
    method onNothingSelected (line 315) | @Override
    method onDestroy (line 320) | @Override
    method onStart (line 326) | @Override
    method onStop (line 332) | @Override
    method onResume (line 338) | @Override
    method initFragment (line 347) | protected void initFragment() {
    method refreshLogList (line 356) | protected void refreshLogList() {
    method onMessageEvent (line 361) | @Subscribe(threadMode = ThreadMode.MAIN)
    method onConversionNotice (line 367) | @Subscribe(threadMode = ThreadMode.MAIN)
    method bytesToHex (line 384) | public static String bytesToHex(byte[] bytes) {

FILE: app/src/main/java/com/cl/myapplication/adapter/DeviceAdapter.java
  class DeviceAdapter (line 21) | public class DeviceAdapter extends BaseAdapter {
    method DeviceAdapter (line 26) | public DeviceAdapter(Context context, ArrayList<Device> devices) {
    method getCount (line 31) | @Override
    method getItem (line 36) | @Override
    method getItemId (line 41) | @Override
    method getView (line 46) | @Override

FILE: app/src/main/java/com/cl/myapplication/adapter/SpAdapter.java
  class SpAdapter (line 15) | public class SpAdapter extends BaseAdapter {
    method SpAdapter (line 20) | public SpAdapter(Context context) {
    method setDatas (line 24) | public void setDatas(String[] datas) {
    method getCount (line 29) | @Override
    method getItem (line 34) | @Override
    method getItemId (line 39) | @Override
    method getView (line 44) | @Override

FILE: app/src/main/java/com/cl/myapplication/constant/PreferenceKeys.java
  class PreferenceKeys (line 4) | public class PreferenceKeys {

FILE: app/src/main/java/com/cl/myapplication/fragment/LogFragment.java
  class LogFragment (line 24) | public class LogFragment extends Fragment {
    method onCreateView (line 30) | @Nullable
    method updateAutoEndButton (line 71) | public void updateAutoEndButton() {
    class LogAdapter (line 82) | private static class LogAdapter extends BaseAdapter {
      method getCount (line 84) | @Override
      method getItem (line 89) | @Override
      method getItemId (line 94) | @Override
      method getView (line 99) | @Override
    method add (line 124) | public void add(IMessage message) {
    method updateList (line 129) | public void updateList() {

FILE: app/src/main/java/com/cl/myapplication/message/ConversionNoticeEvent.java
  class ConversionNoticeEvent (line 10) | public class ConversionNoticeEvent {
    method ConversionNoticeEvent (line 14) | public ConversionNoticeEvent(String message) {
    method getMessage (line 18) | public String getMessage() {
    method setMessage (line 22) | public void setMessage(String message) {

FILE: app/src/main/java/com/cl/myapplication/message/IMessage.java
  type IMessage (line 7) | public interface IMessage {
    method getMessage (line 13) | String getMessage();
    method isToSend (line 20) | boolean isToSend();

FILE: app/src/main/java/com/cl/myapplication/message/LogManager.java
  class LogManager (line 12) | public class LogManager {
    method LogManager (line 17) | public LogManager() {
    class InstanceHolder (line 21) | private static class InstanceHolder {
    method instance (line 26) | public static LogManager instance() {
    method add (line 30) | public void add(IMessage message) {
    method post (line 34) | public void post(IMessage message) {
    method clear (line 38) | public void clear() {
    method isAutoEnd (line 42) | public boolean isAutoEnd() {
    method setAutoEnd (line 46) | public void setAutoEnd(boolean autoEnd) {
    method changAutoEnd (line 50) | public void changAutoEnd() {

FILE: app/src/main/java/com/cl/myapplication/message/RecvMessage.java
  class RecvMessage (line 10) | public class RecvMessage implements IMessage {
    method RecvMessage (line 15) | public RecvMessage(String command) {
    method getMessage (line 20) | @Override
    method isToSend (line 25) | @Override

FILE: app/src/main/java/com/cl/myapplication/message/SendMessage.java
  class SendMessage (line 10) | public class SendMessage implements IMessage {
    method SendMessage (line 15) | public SendMessage(String command) {
    method getMessage (line 20) | @Override
    method isToSend (line 25) | @Override

FILE: app/src/main/java/com/cl/myapplication/util/ByteUtil.java
  class ByteUtil (line 8) | public class ByteUtil {
    method bytesToHex (line 17) | public static String bytesToHex(byte[] bytes) {
    method trim (line 30) | public static String trim(String s) {
    method toChineseHex (line 42) | public static String toChineseHex(String s) {
    method orVerification (line 65) | public static byte orVerification(byte[] bytes) {
    method complement (line 74) | public static byte complement(byte[] bytes) {
    method byteMergerAll (line 87) | public static byte[] byteMergerAll(byte[]... values) {
    method hex2Byte (line 103) | public static byte[] hex2Byte(String hex) {

FILE: app/src/main/java/com/cl/myapplication/util/ListViewHolder.java
  class ListViewHolder (line 10) | public class ListViewHolder {
    method ListViewHolder (line 16) | public ListViewHolder(View itemView) {
    method ListViewHolder (line 22) | public ListViewHolder(int layoutId, ViewGroup parent) {
    method getItemView (line 29) | public View getItemView() {
    method bindPosition (line 33) | public void bindPosition(int position) {
    method getPosition (line 37) | public int getPosition() {
    method getView (line 41) | public <V extends View> V getView(int resId) {
    method setText (line 50) | public void setText(int resId, CharSequence text) {
    method getText (line 55) | public TextView getText(int id) {
    method getImage (line 59) | public ImageView getImage(int id) {

FILE: app/src/main/java/com/cl/myapplication/util/PrefHelper.java
  class PrefHelper (line 8) | public class PrefHelper {
    method initDefault (line 14) | public static void initDefault(Context context) {
    method getDefault (line 18) | public static PrefHelper getDefault() {
    method get (line 22) | public static PrefHelper get(Context context, String name) {
    method PrefHelper (line 26) | private PrefHelper(SharedPreferences preferences) {
    method PrefHelper (line 30) | private PrefHelper(Context context, String name) {
    method edit (line 34) | public SharedPreferences.Editor edit() {
    method putInt (line 38) | public SharedPreferences.Editor putInt(String key, int value) {
    method saveInt (line 42) | public void saveInt(String key, int value) {
    method getInt (line 46) | public int getInt(String key, int defValue) {
    method putFloat (line 50) | public SharedPreferences.Editor putFloat(String key, float value) {
    method saveFloat (line 54) | public void saveFloat(String key, float value) {
    method getFloat (line 58) | public float getFloat(String key, float defValue) {
    method putBoolean (line 62) | public SharedPreferences.Editor putBoolean(String key, boolean value) {
    method saveBoolean (line 66) | public void saveBoolean(String key, boolean value) {
    method getBoolean (line 70) | public boolean getBoolean(String key, boolean defValue) {
    method putLong (line 74) | public SharedPreferences.Editor putLong(String key, long value) {
    method saveLong (line 78) | public void saveLong(String key, long value) {
    method getLong (line 82) | public long getLong(String key, long defValue) {
    method putString (line 86) | public SharedPreferences.Editor putString(String key, String value) {
    method saveString (line 90) | public void saveString(String key, String value) {
    method getString (line 94) | public String getString(String key, String defValue) {

FILE: app/src/main/java/com/cl/myapplication/util/TimeUtil.java
  class TimeUtil (line 7) | public class TimeUtil {
    method currentTime (line 12) | public static String currentTime() {

FILE: serial_lib/src/main/cpp/SerialPort.c
  function speed_t (line 35) | static speed_t getBaudrate(jint baudrate)
  function JNICALL (line 78) | JNICALL Java_com_cl_serialportlibrary_SerialPort_open
  function JNICALL (line 217) | JNICALL Java_com_cl_serialportlibrary_SerialPort_close

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/Device.java
  class Device (line 7) | public class Device implements Serializable{
    method Device (line 14) | public Device(String name, String root, File file) {
    method getName (line 20) | public String getName() {
    method setName (line 24) | public void setName(String name) {
    method getRoot (line 28) | public String getRoot() {
    method setRoot (line 32) | public void setRoot(String root) {
    method getFile (line 36) | public File getFile() {
    method setFile (line 40) | public void setFile(File path) {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/Driver.java
  class Driver (line 10) | public class Driver {
    method Driver (line 15) | public Driver(String name, String root) {
    method getName (line 23) | public String getName() {
    method getRoot (line 30) | public String getRoot() {
    method getDevices (line 37) | public ArrayList<File> getDevices() {
    method toString (line 53) | @Override

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/MultiSerialPortManager.java
  class MultiSerialPortManager (line 28) | public class MultiSerialPortManager {
    method MultiSerialPortManager (line 47) | private MultiSerialPortManager() {}
    method getInstance (line 52) | public static MultiSerialPortManager getInstance() {
    method openSerialPort (line 73) | public boolean openSerialPort(String serialId, String devicePath, int ...
    method openSerialPort (line 180) | public boolean openSerialPort(String serialId, String devicePath, int ...
    method sendData (line 192) | public boolean sendData(String serialId, byte[] data) {
    method sendData (line 220) | public boolean sendData(String serialId, String data) {
    method closeSerialPort (line 228) | public void closeSerialPort(String serialId) {
    method closeAllSerialPorts (line 245) | public void closeAllSerialPorts() {
    method isSerialPortOpened (line 256) | public boolean isSerialPortOpened(String serialId) {
    method getOpenedSerialPorts (line 264) | public List<String> getOpenedSerialPorts() {
    method getSerialConfig (line 277) | public SerialConfig getSerialConfig(String serialId) {
    method updateStickyPacketHelpers (line 284) | public boolean updateStickyPacketHelpers(String serialId, AbsStickPack...
    method printAllSerialStatus (line 307) | public void printAllSerialStatus() {
    method getAvailableSerialPortEnum (line 332) | private SerialPortEnum getAvailableSerialPortEnum() {
    type OnSerialPortStatusCallback (line 364) | public interface OnSerialPortStatusCallback {
      method onStatusChanged (line 371) | void onStatusChanged(String serialId, boolean success, SerialStatus ...
    type OnSerialPortDataCallback (line 377) | public interface OnSerialPortDataCallback {
      method onDataReceived (line 383) | void onDataReceived(String serialId, byte[] data);
      method onDataSent (line 390) | default void onDataSent(String serialId, byte[] data) {
    class SerialPortConfig (line 398) | public static class SerialPortConfig {
      method SerialPortConfig (line 407) | private SerialPortConfig(Builder builder) {
      class Builder (line 417) | public static class Builder {
        method setEnableLogging (line 426) | public Builder setEnableLogging(boolean enableLogging) {
        method setIntervalSleep (line 431) | public Builder setIntervalSleep(int intervalSleep) {
        method setDatabits (line 436) | public Builder setDatabits(int databits) {
        method setParity (line 441) | public Builder setParity(int parity) {
        method setStopbits (line 446) | public Builder setStopbits(int stopbits) {
        method setFlags (line 451) | public Builder setFlags(int flags) {
        method setStickyPacketHelpers (line 456) | public Builder setStickyPacketHelpers(AbsStickPackageHelper... hel...
        method build (line 461) | public SerialPortConfig build() {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SerialConfig.java
  class SerialConfig (line 11) | public class SerialConfig {
    method SerialConfig (line 34) | public SerialConfig(Builder builder) {
    method isEnableLogging (line 52) | public boolean isEnableLogging() {
    method setEnableLogging (line 56) | public void setEnableLogging(boolean enableLogging) {
    method getIntervalSleep (line 60) | public int getIntervalSleep() {
    method isSerialPortReconnection (line 64) | public boolean isSerialPortReconnection() {
    method setIntervalSleep (line 68) | public void setIntervalSleep(int intervalSleep) {
    method getFlags (line 72) | public int getFlags() {
    method getDatabits (line 76) | public int getDatabits() {
    method getStopbits (line 80) | public int getStopbits() {
    method getParity (line 84) | public int getParity() {
    method setFlags (line 88) | public void setFlags(int flags) {
    method setDatabits (line 92) | public void setDatabits(int databits) {
    method setStopbits (line 96) | public void setStopbits(int stopbits) {
    method setParity (line 100) | public void setParity(int parity) {
    method isEnableStickyPacketProcessing (line 104) | public boolean isEnableStickyPacketProcessing() {
    method setEnableStickyPacketProcessing (line 108) | public void setEnableStickyPacketProcessing(boolean enableStickyPacket...
    method getMaxPacketSize (line 112) | public int getMaxPacketSize() {
    method setMaxPacketSize (line 116) | public void setMaxPacketSize(int maxPacketSize) {
    method getPacketTimeout (line 120) | public int getPacketTimeout() {
    method setPacketTimeout (line 124) | public void setPacketTimeout(int packetTimeout) {
    method getStickyPacketHelpers (line 128) | public AbsStickPackageHelper[] getStickyPacketHelpers() {
    method setStickyPacketHelpers (line 132) | public void setStickyPacketHelpers(AbsStickPackageHelper[] stickyPacke...
    method isAutoReconnect (line 136) | public boolean isAutoReconnect() {
    method setAutoReconnect (line 140) | public void setAutoReconnect(boolean autoReconnect) {
    method getReconnectInterval (line 144) | public int getReconnectInterval() {
    method setReconnectInterval (line 148) | public void setReconnectInterval(int reconnectInterval) {
    method getMaxReconnectAttempts (line 152) | public int getMaxReconnectAttempts() {
    method setMaxReconnectAttempts (line 156) | public void setMaxReconnectAttempts(int maxReconnectAttempts) {
    class Builder (line 160) | public static class Builder {
      method setEnableLogging (line 186) | public Builder setEnableLogging(boolean enableLogging) {
      method setIntervalSleep (line 191) | public Builder setIntervalSleep(int sleep) {
      method setSerialPortReconnection (line 196) | public Builder setSerialPortReconnection(boolean serialReconnection) {
      method setFlags (line 201) | public Builder setFlags(int flags) {
      method setDatabits (line 206) | public Builder setDatabits(int databits) {
      method setStopbits (line 211) | public Builder setStopbits(int stopbits) {
      method setParity (line 216) | public Builder setParity(int parity) {
      method setEnableStickyPacketProcessing (line 221) | public Builder setEnableStickyPacketProcessing(boolean enableStickyP...
      method setMaxPacketSize (line 226) | public Builder setMaxPacketSize(int maxPacketSize) {
      method setPacketTimeout (line 231) | public Builder setPacketTimeout(int packetTimeout) {
      method setStickyPacketHelpers (line 236) | public Builder setStickyPacketHelpers(AbsStickPackageHelper... stick...
      method setAutoReconnect (line 241) | public Builder setAutoReconnect(boolean autoReconnect) {
      method setReconnectInterval (line 246) | public Builder setReconnectInterval(int reconnectInterval) {
      method setMaxReconnectAttempts (line 251) | public Builder setMaxReconnectAttempts(int maxReconnectAttempts) {
      method build (line 256) | public SerialConfig build() {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SerialPort.java
  class SerialPort (line 7) | public class SerialPort {
    method chmod777 (line 21) | boolean chmod777(File file) {
    method open (line 43) | protected native FileDescriptor open(String path, int baudrate, int fl...
    method close (line 46) | protected native void close();

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SerialPortFinder.java
  class SerialPortFinder (line 14) | public class SerialPortFinder {
    method SerialPortFinder (line 20) | public SerialPortFinder() {
    method getDrivers (line 32) | private ArrayList<Driver> getDrivers() throws IOException {
    method getDevices (line 52) | public ArrayList<Device> getDevices() {
    method getAllDevicesPath (line 71) | public String[] getAllDevicesPath() {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SerialPortManager.java
  class SerialPortManager (line 28) | public class SerialPortManager extends SerialPort {
    method SerialPortManager (line 46) | public SerialPortManager() {
    method SerialPortManager (line 50) | public SerialPortManager(SerialPortEnum mSerialPortEnum) {
    method setSerialConfig (line 59) | public void setSerialConfig(SerialConfig config) {
    method setStickPackageHelpers (line 69) | public void setStickPackageHelpers(List<AbsStickPackageHelper> helpers) {
    method openSerialPort (line 81) | public boolean openSerialPort(String devicePath, int baudRate) {
    method checkSerialPortPermission (line 119) | private boolean checkSerialPortPermission(String devicePath) {
    method notifySerialPortOpened (line 138) | private void notifySerialPortOpened(File deviceFile, SerialStatus stat...
    method isOpen (line 147) | public boolean isOpen() {
    method closeSerialPort (line 154) | public void closeSerialPort() {
    method closeStream (line 172) | private void closeStream(Closeable stream) {
    method setOnOpenSerialPortListener (line 188) | public SerialPortManager setOnOpenSerialPortListener(OnOpenSerialPortL...
    method setOnSerialPortDataListener (line 199) | public SerialPortManager setOnSerialPortDataListener(OnSerialPortDataL...
    method startSendThread (line 207) | private void startSendThread() {
    method stopSendThread (line 234) | private void stopSendThread() {
    method startReadThread (line 246) | private void startReadThread() {
    method stopReadThread (line 262) | private void stopReadThread() {
    method sendBytes (line 274) | public boolean sendBytes(byte[] sendBytes) {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/SimpleSerialPortManager.java
  class SimpleSerialPortManager (line 28) | public class SimpleSerialPortManager {
    method SimpleSerialPortManager (line 48) | private SimpleSerialPortManager() {
    method getInstance (line 56) | public static SimpleSerialPortManager getInstance() {
    method init (line 71) | public SimpleSerialPortManager init(Application application) {
    method init (line 82) | public SimpleSerialPortManager init(Application application, boolean e...
    method init (line 108) | public SimpleSerialPortManager init(Application application, SerialCon...
    method configureStickyPacket (line 127) | public SimpleSerialPortManager configureStickyPacket(StickyPacketStrat...
    method setStickyPacketHelpers (line 164) | public SimpleSerialPortManager setStickyPacketHelpers(AbsStickPackageH...
    method openSerialPort (line 186) | public boolean openSerialPort(String devicePath, int baudRate, OnDataR...
    method openSerialPort (line 198) | public boolean openSerialPort(String devicePath, int baudRate, OnOpenS...
    method updateSerialConfig (line 269) | private void updateSerialConfig() {
    method sendData (line 284) | public boolean sendData(byte[] data) {
    method sendData (line 313) | public boolean sendData(String data) {
    method closeSerialPort (line 320) | public void closeSerialPort() {
    method isSerialPortOpened (line 333) | public boolean isSerialPortOpened() {
    method setDatabits (line 338) | public SimpleSerialPortManager setDatabits(int databits) {
    method setParity (line 344) | public SimpleSerialPortManager setParity(int parity) {
    method setStopbits (line 350) | public SimpleSerialPortManager setStopbits(int stopbits) {
    method setFlags (line 356) | public SimpleSerialPortManager setFlags(int flags) {
    method getDatabits (line 362) | public int getDatabits() {
    method getParity (line 366) | public int getParity() {
    method getStopbits (line 370) | public int getStopbits() {
    method getFlags (line 374) | public int getFlags() {
    method getSerialConfig (line 378) | public SerialConfig getSerialConfig() {
    type StickyPacketStrategy (line 385) | public enum StickyPacketStrategy {
    type OnOpenSerialPortCallback (line 395) | public interface OnOpenSerialPortCallback {
      method onStatusChanged (line 401) | void onStatusChanged(boolean success, SerialStatus status);
    type OnDataReceivedCallback (line 407) | public interface OnDataReceivedCallback {
      method onDataReceived (line 412) | void onDataReceived(byte[] data);
      method onDataSent (line 418) | default void onDataSent(byte[] data) {
    method multi (line 427) | public static MultiSerialPortManager multi() {
    class QuickConfig (line 434) | public static class QuickConfig {
      method setIntervalSleep (line 446) | public QuickConfig setIntervalSleep(int intervalSleep) {
      method setEnableLog (line 451) | public QuickConfig setEnableLog(boolean enableLog) {
      method setLogTag (line 456) | public QuickConfig setLogTag(String logTag) {
      method setStickyPacketStrategy (line 461) | public QuickConfig setStickyPacketStrategy(StickyPacketStrategy stra...
      method setMaxPacketSize (line 466) | public QuickConfig setMaxPacketSize(int maxPacketSize) {
      method setAutoReconnect (line 471) | public QuickConfig setAutoReconnect(boolean autoReconnect) {
      method setDatabits (line 476) | public QuickConfig setDatabits(int databits) {
      method setParity (line 481) | public QuickConfig setParity(int parity) {
      method setStopbits (line 486) | public QuickConfig setStopbits(int stopbits) {
      method setFlags (line 491) | public QuickConfig setFlags(int flags) {
      method apply (line 499) | public SimpleSerialPortManager apply(Application application) {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/enumerate/SerialPortEnum.java
  type SerialPortEnum (line 8) | public enum SerialPortEnum {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/enumerate/SerialStatus.java
  type SerialStatus (line 8) | public enum SerialStatus {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/example/MultiSerialPortExample.java
  class MultiSerialPortExample (line 18) | public class MultiSerialPortExample {
    method basicMultiSerialExample (line 25) | public void basicMultiSerialExample(Application application) {
    method advancedMultiSerialExample (line 76) | public void advancedMultiSerialExample(Application application) {
    method dynamicSerialManagement (line 148) | public void dynamicSerialManagement() {
    method serialDataRouting (line 214) | public void serialDataRouting() {
    method cleanup (line 276) | public void cleanup() {
    method bytesToHex (line 283) | private String bytesToHex(byte[] bytes) {
    method parseModbusResponse (line 291) | private void parseModbusResponse(byte[] data) {
    method parseCustomProtocol (line 300) | private void parseCustomProtocol(byte[] data) {
    method parseATResponse (line 305) | private void parseATResponse(String response) {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/listener/OnOpenSerialPortListener.java
  type OnOpenSerialPortListener (line 11) | public interface OnOpenSerialPortListener {
    method openState (line 13) | void openState(SerialPortEnum serialPortEnum, File device, SerialStatu...

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/listener/OnSerialPortDataListener.java
  type OnSerialPortDataListener (line 8) | public interface OnSerialPortDataListener {
    method onDataReceived (line 15) | void onDataReceived(byte[] bytes, SerialPortEnum serialPortEnum);
    method onDataSent (line 22) | void onDataSent(byte[] bytes,SerialPortEnum serialPortEnum);

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/AbsStickPackageHelper.java
  type AbsStickPackageHelper (line 12) | public interface AbsStickPackageHelper {
    method execute (line 13) | byte[] execute(InputStream is);

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/BaseStickPackageHelper.java
  class BaseStickPackageHelper (line 14) | public class BaseStickPackageHelper implements AbsStickPackageHelper {
    method BaseStickPackageHelper (line 15) | public BaseStickPackageHelper() {
    method execute (line 18) | @Override

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/CompositeStickPackageHelper.java
  class CompositeStickPackageHelper (line 15) | public class CompositeStickPackageHelper implements AbsStickPackageHelper {
    method CompositeStickPackageHelper (line 21) | public CompositeStickPackageHelper(AbsStickPackageHelper primaryHelper...
    method execute (line 26) | @Override

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/SpecifiedStickPackageHelper.java
  class SpecifiedStickPackageHelper (line 15) | public class SpecifiedStickPackageHelper implements AbsStickPackageHelper {
    method SpecifiedStickPackageHelper (line 22) | public SpecifiedStickPackageHelper(byte[] head, byte[] tail) {
    method SpecifiedStickPackageHelper (line 40) | public SpecifiedStickPackageHelper(byte[] tail) {
    method SpecifiedStickPackageHelper (line 49) | public SpecifiedStickPackageHelper(String head, String tail) {
    method SpecifiedStickPackageHelper (line 58) | public SpecifiedStickPackageHelper(String tail) {
    method endWith (line 62) | private boolean endWith(Byte[] src, byte[] target) {
    method getRangeBytes (line 74) | private byte[] getRangeBytes(List<Byte> list, int start, int end) {
    method execute (line 83) | @Override

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/StaticLenStickPackageHelper.java
  class StaticLenStickPackageHelper (line 10) | public class StaticLenStickPackageHelper implements AbsStickPackageHelper {
    method StaticLenStickPackageHelper (line 13) | public StaticLenStickPackageHelper(int stackLen) {
    method StaticLenStickPackageHelper (line 20) | public StaticLenStickPackageHelper() {
    method execute (line 24) | @Override

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/StickyPacketHelperFactory.java
  class StickyPacketHelperFactory (line 11) | public class StickyPacketHelperFactory {
    method createNoProcessing (line 16) | public static AbsStickPackageHelper createNoProcessing() {
    method createFixedLength (line 24) | public static AbsStickPackageHelper createFixedLength(int length) {
    method createDelimiterBased (line 32) | public static AbsStickPackageHelper createDelimiterBased(String delimi...
    method createMarkerBased (line 42) | public static AbsStickPackageHelper createMarkerBased(String startMark...
    method createMarkerBased (line 53) | public static AbsStickPackageHelper createMarkerBased(byte[] startMark...
    method createVariableLength (line 64) | public static AbsStickPackageHelper createVariableLength(java.nio.Byte...
    method createVariableLength (line 72) | public static AbsStickPackageHelper createVariableLength() {
    method createTimeoutBased (line 80) | public static AbsStickPackageHelper createTimeoutBased(int timeout) {
    method createComposite (line 89) | public static AbsStickPackageHelper createComposite(AbsStickPackageHel...
    class Common (line 97) | public static class Common {
      method createATCommand (line 102) | public static AbsStickPackageHelper createATCommand() {
      method createJsonLine (line 109) | public static AbsStickPackageHelper createJsonLine() {
      method createModbusRTU (line 116) | public static AbsStickPackageHelper createModbusRTU() {
      method createSTXETX (line 123) | public static AbsStickPackageHelper createSTXETX() {
      method createSOHEOT (line 130) | public static AbsStickPackageHelper createSOHEOT() {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/TimeoutStickPackageHelper.java
  class TimeoutStickPackageHelper (line 16) | public class TimeoutStickPackageHelper implements AbsStickPackageHelper {
    method TimeoutStickPackageHelper (line 21) | public TimeoutStickPackageHelper(int timeout) {
    method execute (line 25) | @Override

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/stick/VariableLenStickPackageHelper.java
  class VariableLenStickPackageHelper (line 22) | public class VariableLenStickPackageHelper implements AbsStickPackageHel...
    method VariableLenStickPackageHelper (line 31) | public VariableLenStickPackageHelper(ByteOrder byteOrder, int lenSize,...
    method getLen (line 44) | private int getLen(byte[] src, ByteOrder order) {
    method execute (line 58) | @Override

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/thread/SerialPortReadThread.java
  class SerialPortReadThread (line 16) | public abstract class SerialPortReadThread extends Thread {
    method onDataReceived (line 18) | public abstract void onDataReceived(byte[] bytes);
    method SerialPortReadThread (line 24) | public SerialPortReadThread(InputStream inputStream, SerialPortEnum mS...
    method run (line 36) | @Override
    method release (line 73) | public void release() {

FILE: serial_lib/src/main/java/com/cl/serialportlibrary/utils/SerialPortLogUtil.java
  class SerialPortLogUtil (line 14) | public class SerialPortLogUtil {
    method setDebugEnabled (line 24) | public static void setDebugEnabled(boolean enabled) {
    method isDebugEnabled (line 34) | public static boolean isDebugEnabled() {
    method getTimeStamp (line 41) | private static String getTimeStamp() {
    method getCallerInfo (line 48) | private static String getCallerInfo() {
    method formatMessage (line 65) | private static String formatMessage(String message) {
    method d (line 74) | public static void d(String tag, String message) {
    method d (line 84) | public static void d(String message) {
    method i (line 93) | public static void i(String tag, String message) {
    method i (line 103) | public static void i(String message) {
    method w (line 112) | public static void w(String tag, String message) {
    method w (line 122) | public static void w(String message) {
    method e (line 131) | public static void e(String tag, String message) {
    method e (line 140) | public static void e(String message) {
    method e (line 150) | public static void e(String tag, String message, Throwable throwable) {
    method e (line 160) | public static void e(String message, Throwable throwable) {
    method printData (line 170) | public static void printData(String tag, String prefix, byte[] data) {
    method printData (line 210) | public static void printData(String prefix, byte[] data) {
    method printSerialStatus (line 221) | public static void printSerialStatus(String tag, String devicePath, in...
    method printSerialConfig (line 234) | public static void printSerialConfig(String tag, int databits, int par...
    method printPerformance (line 253) | public static void printPerformance(String tag, String operation, long...
    method printSeparator (line 263) | public static void printSeparator(String tag, String title) {
    method printSeparator (line 274) | public static void printSeparator(String title) {
Condensed preview — 89 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (368K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/提交bug.md",
    "chars": 362,
    "preview": "---\nname: 提交BUG\nabout: Create a report to help us improve\ntitle: 建议\nlabels: ''\nassignees: ''\n\n---\n\n## 【警告:请务必按照 issue 模板"
  },
  {
    "path": ".gitignore",
    "chars": 225,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor."
  },
  {
    "path": "README.md",
    "chars": 14775,
    "preview": "# Android串口通信框架 SerialPort\n\n[中文](README.md) | [English](README_EN.md)\n\n[![Version](https://img.shields.io/badge/version-"
  },
  {
    "path": "README4.1.1.md",
    "chars": 12263,
    "preview": "# Android串口通信框架 SerialPort v4.1.1\n\n[![Version](https://img.shields.io/badge/version-4.1.1-orange.svg)](https://github.co"
  },
  {
    "path": "README_EN.md",
    "chars": 18087,
    "preview": "# Android Serial Communication Framework SerialPort\n\n[English](README_EN.md) | [中文](README.md)\n\n[![Version](https://img."
  },
  {
    "path": "app/build.gradle",
    "chars": 1493,
    "preview": "plugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.kotlin.android)\n}\n\nandroid {\n    namespace "
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 2115,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package="
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/App.java",
    "chars": 1107,
    "preview": "package com.cl.myapplication;\n\nimport android.app.Application;\n\nimport com.hjq.toast.ToastUtils;\nimport com.cl.serialpor"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/MainActivity.kt",
    "chars": 2711,
    "preview": "package com.cl.myapplication\n\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.util.Log\nimport andr"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/MultiSerialPortActivity.java",
    "chars": 27035,
    "preview": "package com.cl.myapplication;\n\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.View;\nimport andro"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/SelectSerialPortActivity.kt",
    "chars": 1561,
    "preview": "package com.cl.myapplication\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.View\nimport and"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/SingleSerialPortActivity.java",
    "chars": 14406,
    "preview": "package com.cl.myapplication;\n\nimport android.app.Activity;\nimport android.content.pm.PackageManager;\nimport android.os."
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/adapter/DeviceAdapter.java",
    "chars": 2239,
    "preview": "package com.cl.myapplication.adapter;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport androi"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/adapter/SpAdapter.java",
    "chars": 1462,
    "preview": "package com.cl.myapplication.adapter;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport androi"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/constant/PreferenceKeys.java",
    "chars": 249,
    "preview": "package com.cl.myapplication.constant;\n\n\npublic class PreferenceKeys {\n\n    /**\n     * 串口设备\n     */\n    public static St"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/fragment/LogFragment.java",
    "chars": 4144,
    "preview": "package com.cl.myapplication.fragment;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.vie"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/message/ConversionNoticeEvent.java",
    "chars": 416,
    "preview": "package com.cl.myapplication.message;\n\n/**\n * 项目:serialPort\n * 作者:Arry\n * 创建日期:2021/10/20\n * 描述:\n * 修订历史:\n */\npublic cla"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/message/IMessage.java",
    "chars": 243,
    "preview": "package com.cl.myapplication.message;\n\n/**\n * 日志消息数据接口\n */\n\npublic interface IMessage {\n    /**\n     * 消息文本\n     *\n     "
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/message/LogManager.java",
    "chars": 979,
    "preview": "package com.cl.myapplication.message;\n\nimport org.greenrobot.eventbus.EventBus;\n\nimport java.util.ArrayList;\nimport java"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/message/RecvMessage.java",
    "chars": 516,
    "preview": "package com.cl.myapplication.message;\n\n\nimport com.cl.myapplication.util.TimeUtil;\n\n/**\n * 收到的日志\n */\n\npublic class RecvM"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/message/SendMessage.java",
    "chars": 511,
    "preview": "package com.cl.myapplication.message;\n\n\nimport com.cl.myapplication.util.TimeUtil;\n\n/**\n * 发送的日志\n */\n\npublic class SendM"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/util/ByteUtil.java",
    "chars": 2930,
    "preview": "package com.cl.myapplication.util;\n\n/**\n * name:cl\n * date:2022/12/27\n * desc:Byte工具类\n */\npublic class ByteUtil {\n\n\n    "
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/util/ListViewHolder.java",
    "chars": 1551,
    "preview": "package com.cl.myapplication.util;\n\nimport android.util.SparseArray;\nimport android.view.LayoutInflater;\nimport android."
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/util/PrefHelper.java",
    "chars": 2635,
    "preview": "package com.cl.myapplication.util;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport and"
  },
  {
    "path": "app/src/main/java/com/cl/myapplication/util/TimeUtil.java",
    "chars": 366,
    "preview": "package com.cl.myapplication.util;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\n\n\npublic class TimeUtil {\n"
  },
  {
    "path": "app/src/main/res/color/selector_log_text.xml",
    "chars": 225,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item a"
  },
  {
    "path": "app/src/main/res/color/selector_spinner_text.xml",
    "chars": 256,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item a"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "chars": 5606,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:wi"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "chars": 1702,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    "
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 1371,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app="
  },
  {
    "path": "app/src/main/res/layout/activity_main_java.xml",
    "chars": 6799,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <data>\n\n"
  },
  {
    "path": "app/src/main/res/layout/activity_multi_serial.xml",
    "chars": 33638,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app="
  },
  {
    "path": "app/src/main/res/layout/activity_multi_serial_new.xml",
    "chars": 32395,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app="
  },
  {
    "path": "app/src/main/res/layout/activity_select_serial_port.xml",
    "chars": 807,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tool"
  },
  {
    "path": "app/src/main/res/layout/fragment_log.xml",
    "chars": 1645,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <data>\n\n"
  },
  {
    "path": "app/src/main/res/layout/include_fragment_container.xml",
    "chars": 291,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<fragment\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    andr"
  },
  {
    "path": "app/src/main/res/layout/item_device.xml",
    "chars": 604,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tool"
  },
  {
    "path": "app/src/main/res/layout/item_log.xml",
    "chars": 1128,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tool"
  },
  {
    "path": "app/src/main/res/layout/spinner_default_item.xml",
    "chars": 598,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tool"
  },
  {
    "path": "app/src/main/res/layout/spinner_item.xml",
    "chars": 598,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tool"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 272,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 272,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 1006,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purpl"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 64,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/string_arrays.xml",
    "chars": 980,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string-array name=\"baudrates\">\n        <item>110</item>\n       "
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 606,
    "preview": "<resources>\n\n    <string name=\"app_name\">串口调试助手</string>\n    <string name=\"select_serial_port\">串口设备:</string>\n    <strin"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "chars": 1034,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.M"
  },
  {
    "path": "app/src/main/res/values-night/themes.xml",
    "chars": 835,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.M"
  },
  {
    "path": "build.gradle",
    "chars": 118,
    "preview": "plugins {\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.kotlin.android) apply false\n}"
  },
  {
    "path": "gradle/libs.versions.toml",
    "chars": 919,
    "preview": "[versions]\nagp = \"8.12.2\"\nkotlin = \"2.0.21\"\ncoreKtx = \"1.10.1\"\njunit = \"4.13.2\"\njunitVersion = \"1.1.5\"\nespressoCore = \"3"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 231,
    "preview": "#Sun Sep 28 20:08:42 CST 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://"
  },
  {
    "path": "gradle.properties",
    "chars": 1184,
    "preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
  },
  {
    "path": "gradlew",
    "chars": 5296,
    "preview": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up"
  },
  {
    "path": "gradlew.bat",
    "chars": 2176,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem "
  },
  {
    "path": "serial_lib/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "serial_lib/build.gradle",
    "chars": 2774,
    "preview": "plugins {\n    id 'com.android.library'\n    id 'maven-publish'\n}\n\nandroid {\n    namespace \"com.cl.serialportlibrary\"\n    "
  },
  {
    "path": "serial_lib/consumer-rules.pro",
    "chars": 723,
    "preview": "# Consumer proguard rules for serial_lib\n\n# Keep all public APIs\n-keep public class com.cl.serialportlibrary.** { *; }\n\n"
  },
  {
    "path": "serial_lib/proguard-rules.pro",
    "chars": 645,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in D:"
  },
  {
    "path": "serial_lib/src/main/AndroidManifest.xml",
    "chars": 217,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application android:allowBackup=\"true\" andro"
  },
  {
    "path": "serial_lib/src/main/cpp/CMakeLists.txt",
    "chars": 527,
    "preview": "cmake_minimum_required(VERSION 3.18.1)\n\nproject(SerialPort C)\n\nadd_library(SerialPort SHARED\n            SerialPort.c)\n\n"
  },
  {
    "path": "serial_lib/src/main/cpp/SerialPort.c",
    "chars": 7540,
    "preview": "\n\n/*\n * Copyright 2009-2011 Cedric Priscal\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you "
  },
  {
    "path": "serial_lib/src/main/cpp/SerialPort.h",
    "chars": 777,
    "preview": "/* DO NOT EDIT THIS FILE - it is machine generated */\n#include <jni.h>\n/* Header for class android_serialport_api_Serial"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/Device.java",
    "chars": 804,
    "preview": "package com.cl.serialportlibrary;\n\nimport java.io.File;\nimport java.io.Serializable;\n\n\npublic class Device implements Se"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/Driver.java",
    "chars": 1217,
    "preview": "package com.cl.serialportlibrary;\n\nimport java.io.File;\nimport java.util.ArrayList;\n\n/**\n * 串口驱动信息类\n * 简化版本,仅用于SerialPor"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/MultiSerialPortManager.java",
    "chars": 16152,
    "preview": "package com.cl.serialportlibrary;\n\nimport android.app.Application;\nimport android.os.Handler;\nimport android.os.Looper;\n"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/SerialConfig.java",
    "chars": 7078,
    "preview": "package com.cl.serialportlibrary;\n\nimport com.cl.serialportlibrary.stick.AbsStickPackageHelper;\nimport com.cl.serialport"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/SerialPort.java",
    "chars": 1268,
    "preview": "package com.cl.serialportlibrary;\n\nimport java.io.File;\nimport java.io.FileDescriptor;\nimport java.io.IOException;\n\npubl"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/SerialPortFinder.java",
    "chars": 3002,
    "preview": "package com.cl.serialportlibrary;\n\n\nimport com.cl.serialportlibrary.utils.SerialPortLogUtil;\n\nimport java.io.File;\nimpor"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/SerialPortManager.java",
    "chars": 8759,
    "preview": "package com.cl.serialportlibrary;\n\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Message"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/SimpleSerialPortManager.java",
    "chars": 16594,
    "preview": "package com.cl.serialportlibrary;\n\nimport android.app.Application;\nimport android.os.Handler;\nimport android.os.Looper;\n"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/enumerate/SerialPortEnum.java",
    "chars": 288,
    "preview": "package com.cl.serialportlibrary.enumerate;\n\n/**\n * name:cl\n * date:2023/2/20\n * desc:串口枚举类型\n */\npublic enum SerialPortE"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/enumerate/SerialStatus.java",
    "chars": 184,
    "preview": "package com.cl.serialportlibrary.enumerate;\n\n/**\n * name:cl\n * date:2023/2/20\n * desc:\n */\npublic enum SerialStatus {\n  "
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/example/MultiSerialPortExample.java",
    "chars": 12247,
    "preview": "package com.cl.serialportlibrary.example;\n\nimport android.app.Application;\nimport android.util.Log;\n\nimport com.cl.seria"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/listener/OnOpenSerialPortListener.java",
    "chars": 332,
    "preview": "package com.cl.serialportlibrary.listener;\n\nimport com.cl.serialportlibrary.enumerate.SerialPortEnum;\nimport com.cl.seri"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/listener/OnSerialPortDataListener.java",
    "chars": 428,
    "preview": "package com.cl.serialportlibrary.listener;\n\nimport com.cl.serialportlibrary.enumerate.SerialPortEnum;\n\n/**\n * 串口消息监听\n */"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/stick/AbsStickPackageHelper.java",
    "chars": 566,
    "preview": "package com.cl.serialportlibrary.stick;\n\nimport java.io.InputStream;\n\n/**\n * Accept the message, the helper of the stick"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/stick/BaseStickPackageHelper.java",
    "chars": 1060,
    "preview": "package com.cl.serialportlibrary.stick;\n\nimport android.os.SystemClock;\n\nimport com.cl.serialportlibrary.utils.SerialPor"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/stick/CompositeStickPackageHelper.java",
    "chars": 2903,
    "preview": "package com.cl.serialportlibrary.stick;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/stick/SpecifiedStickPackageHelper.java",
    "chars": 4357,
    "preview": "package com.cl.serialportlibrary.stick;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayL"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/stick/StaticLenStickPackageHelper.java",
    "chars": 1112,
    "preview": "package com.cl.serialportlibrary.stick;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Fixed-length ad"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/stick/StickyPacketHelperFactory.java",
    "chars": 3858,
    "preview": "package com.cl.serialportlibrary.stick;\n\nimport java.nio.charset.StandardCharsets;\n\n/**\n * 黏包处理器工厂类\n * 提供常用的黏包处理器配置\n * A"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/stick/TimeoutStickPackageHelper.java",
    "chars": 1960,
    "preview": "package com.cl.serialportlibrary.stick;\n\nimport android.os.SystemClock;\n\nimport java.io.IOException;\nimport java.io.Inpu"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/stick/VariableLenStickPackageHelper.java",
    "chars": 3437,
    "preview": "package com.cl.serialportlibrary.stick;\n\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.ByteOr"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/thread/SerialPortReadThread.java",
    "chars": 3087,
    "preview": "package com.cl.serialportlibrary.thread;\n\nimport com.cl.serialportlibrary.utils.SerialPortLogUtil;\nimport com.cl.serialp"
  },
  {
    "path": "serial_lib/src/main/java/com/cl/serialportlibrary/utils/SerialPortLogUtil.java",
    "chars": 7291,
    "preview": "package com.cl.serialportlibrary.utils;\n\nimport android.util.Log;\n\nimport java.text.SimpleDateFormat;\nimport java.util.D"
  },
  {
    "path": "serial_lib/src/main/res/values/strings.xml",
    "chars": 80,
    "preview": "<resources>\n    <string name=\"app_name\">SerialPortLibrary</string>\n</resources>\n"
  },
  {
    "path": "settings.gradle",
    "chars": 640,
    "preview": "pluginManagement {\n    repositories {\n        google {\n            content {\n                includeGroupByRegex(\"com\\\\."
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the cl-6666/serialPort GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 89 files (322.0 KB), approximately 76.4k tokens, and a symbol index with 402 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!