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串口通信框架,让串口操作变得简单易用。支持单串口、多串口、粘包处理、自定义配置等功能。 演示 ## 📱 体验演示 想要快速体验串口通信框架的强大功能?直接下载演示 APK 安装到您的 Android 设备上试试吧!
### 📥 [点击下载演示 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
### 演示 APK 功能 - ✅ 单串口通信演示 - ✅ 多串口管理演示 - ✅ 粘包处理策略切换 - ✅ 串口参数配置(数据位、校验位、停止位) - ✅ 实时数据收发测试 - ✅ 十六进制/ASCII 数据显示 - ✅ 性能测试与统计 > **提示**: 演示 APK 需要在具有串口的 Android 设备上运行(如工控设备、开发板等)。如果您的设备没有串口,可以查看源码了解使用方法。 ## ⭐ 特性 - 🚀 **简单易用** - 链式调用,一行代码完成配置 - 🔧 **多串口支持** - 同时管理多个串口,独立配置 - 📦 **智能粘包处理** - 支持多种粘包策略,可动态切换 - ⚡ **高性能** - 多线程处理,线程安全设计 - 🛡️ **稳定可靠** - 完善的错误处理和资源管理 - 📝 **详细日志** - 丰富的调试信息,方便排查问题 - 🎯 **灵活配置** - 支持数据位、校验位、停止位等参数配置 - ✨ **Google Play 认证** - 支持 16KB 页面对齐,完全符合 Google Play 上架要求 ## 📖 版本说明 - **当前版本**: 5.0.8 (推荐) - 全新架构,功能强大,支持 Google Play 16KB 页面对齐 - **历史版本**: [4.1.1版本文档](README4.1.1.md) - 稳定版本 ### 5.0.8 版本更新 🔥 (2025-12-25) - ✅ **16KB 页面对齐**: 完全适配 Google Play 16KB 页面大小要求 - ✅ **Android 15 支持**: 兼容最新 Android 15 系统 - ✅ **原生库优化**: arm64-v8a 架构原生库已通过 Google Play 审核标准 - ✅ **向后兼容**: 完全兼容旧版本 Android 设备,无需修改代码 > **重要提示**: 从 2024 年开始,Google Play 要求所有 arm64-v8a 原生库必须支持 16KB 页面大小。5.0.8 版本已完全适配此要求,可放心上架 Google Play。 ## 🚀 快速开始 ### 依赖集成 在项目的 `build.gradle` 中添加依赖: ```gradle dependencies { implementation 'com.github.cl-6666:serialPort:v5.0.8' } ``` 在项目根目录的 `build.gradle` 中添加: ```gradle allprojects { repositories { maven { url 'https://jitpack.io' } } } ``` ### 权限配置 在 `AndroidManifest.xml` 中添加必要权限: ```xml ``` ## 📚 使用指南 ### 1️⃣ 单串口使用 - 基础示例 #### 最简单的使用方式 ```java public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 一行代码打开串口并接收数据 SimpleSerialPortManager.getInstance() .openSerialPort("/dev/ttyS4", 115200, data -> { String receivedData = new String(data); Log.i("Serial", "收到数据: " + receivedData); // 处理接收到的数据 }); } // 发送数据 private void sendData() { SimpleSerialPortManager.getInstance().sendData("Hello World"); } @Override protected void onDestroy() { super.onDestroy(); // 关闭串口 SimpleSerialPortManager.getInstance().closeSerialPort(); } } ``` #### 完整配置示例 ```java public class App extends Application { @Override public void onCreate() { super.onCreate(); // 全局配置(可选) new SimpleSerialPortManager.QuickConfig() .setIntervalSleep(50) // 读取间隔50ms .setEnableLog(true) // 启用日志 .setLogTag("SerialPortApp") // 设置日志标签 .setDatabits(8) // 数据位8 .setParity(0) // 无校验 .setStopbits(1) // 停止位1 .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING) .apply(this); } } ``` ### 2️⃣ 数据位、校验位、停止位配置 ```java public class SerialConfigExample { public void configureSerialParams() { SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance(); // 方式1:使用QuickConfig配置 new SimpleSerialPortManager.QuickConfig() .setDatabits(8) // 数据位:5, 6, 7, 8 .setParity(0) // 校验位:0=无校验, 1=奇校验, 2=偶校验 .setStopbits(1) // 停止位:1 或 2 .setFlags(0) // 标志位 .apply(getApplication()); // 方式2:动态设置 manager.setDatabits(8) // 设置数据位 .setParity(2) // 设置偶校验 .setStopbits(1) // 设置停止位1 .setFlags(0); // 设置标志位 // 打开串口 manager.openSerialPort("/dev/ttyS4", 115200, data -> { Log.i("Serial", "数据: " + new String(data)); }); } // 常用配置组合 public void commonConfigurations() { SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance(); // 标准配置 8N1 (8数据位, 无校验, 1停止位) manager.setDatabits(8).setParity(0).setStopbits(1); // Modbus RTU 8E1 (8数据位, 偶校验, 1停止位) manager.setDatabits(8).setParity(2).setStopbits(1); // 老式设备 7E2 (7数据位, 偶校验, 2停止位) manager.setDatabits(7).setParity(2).setStopbits(2); } } ``` ### 3️⃣ 粘包处理详解 粘包是串口通信中常见的问题,5.0.0版本提供了多种处理策略: ```java public class StickyPacketExample { public void noProcessing() { // 策略1:不处理粘包 - 适用于简单数据流 new SimpleSerialPortManager.QuickConfig() .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING) .apply(this); } public void delimiterBased() { // 策略2:基于分隔符 - 适用于文本协议 new SimpleSerialPortManager.QuickConfig() .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED) .apply(this); // 自定义分隔符 SimpleSerialPortManager.getInstance() .configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED); } public void fixedLength() { // 策略3:固定长度 - 适用于固定长度协议 new SimpleSerialPortManager.QuickConfig() .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.FIXED_LENGTH) .apply(this); } public void variableLength() { // 策略4:可变长度 - 适用于带长度字段的协议 new SimpleSerialPortManager.QuickConfig() .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.VARIABLE_LENGTH) .apply(this); } } ``` ### 4️⃣ 多串口管理 - 强大功能 ```java public class MultiSerialExample { public void basicMultiSerial() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // 串口1:GPS模块,不需要粘包处理 manager.openSerialPort("GPS", "/dev/ttyS1", 9600, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(8) .setParity(0) .setStopbits(1) .setStickyPacketHelpers(new BaseStickPackageHelper()) // 不处理粘包 .build(), // 状态回调 (serialId, success, status) -> { Log.i("GPS", "状态: " + (success ? "成功" : "失败")); }, // 数据回调 (serialId, data) -> { String gpsData = new String(data); Log.i("GPS", "数据: " + gpsData); handleGpsData(gpsData); }); // 串口2:传感器模块,需要换行符分包 manager.openSerialPort("SENSOR", "/dev/ttyS2", 115200, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(8) .setParity(0) .setStopbits(1) .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) // 换行符分包 .build(), null, // 不需要状态回调 (serialId, data) -> { String sensorData = new String(data).trim(); Log.i("SENSOR", "数据: " + sensorData); handleSensorData(sensorData); }); // 发送数据到不同串口 manager.sendData("GPS", "AT+GPS?\r\n"); manager.sendData("SENSOR", "READ_TEMP\n"); } // 动态管理串口 public void dynamicManagement() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // 查看串口状态 List openedPorts = manager.getOpenedSerialPorts(); boolean isOpened = manager.isSerialPortOpened("GPS"); manager.printAllSerialStatus(); // 动态更新粘包策略 manager.updateStickyPacketHelpers("GPS", new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("\r\n")}); // 关闭特定串口 manager.closeSerialPort("GPS"); // 关闭所有串口 manager.closeAllSerialPorts(); } } ``` ## 🎯 实际应用场景 ### 工业控制场景 ```java public class IndustrialControlExample { public void setupIndustrialPorts() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // PLC通信 - Modbus RTU manager.openSerialPort("PLC", "/dev/ttyS1", 9600, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(8).setParity(2).setStopbits(1) // 8E1 .setStickyPacketHelpers(new StaticLenStickPackageHelper(8)) .build(), null, this::handlePlcData); // 传感器数据采集 - 文本协议 manager.openSerialPort("SENSORS", "/dev/ttyS3", 9600, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(7).setParity(2).setStopbits(1) // 7E1 .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n")) .build(), null, this::handleSensorData); } } ``` ### 通信网关场景 ```java public class GatewayExample { public void setupGateway() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // 上行通信(与服务器) manager.openSerialPort("UPLINK", "/dev/ttyS1", 115200, new MultiSerialPortManager.SerialPortConfig.Builder() .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) .build(), null, this::handleUplinkData); // 下行设备1 - GPS manager.openSerialPort("GPS", "/dev/ttyS2", 9600, new MultiSerialPortManager.SerialPortConfig.Builder() .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n")) .build(), null, data -> forwardToUplink("GPS", data)); } private void forwardToUplink(String deviceId, byte[] data) { String message = String.format("[%s]%s\n", deviceId, new String(data)); SimpleSerialPortManager.multi().sendData("UPLINK", message); } } ``` ## 🔧 高级功能 ### 日志系统 ```java // 启用详细日志 SerialPortLogUtil.setDebugEnabled(true); // 自定义日志输出 SerialPortLogUtil.i("MyTag", "自定义日志信息"); SerialPortLogUtil.printData("发送", data); // 十六进制+ASCII显示 SerialPortLogUtil.printSerialConfig("MySerial", 8, 0, 1, 0); // 配置信息 ``` ### 错误处理 ```java manager.openSerialPort("TEST", "/dev/ttyS1", 9600, (serialId, success, status) -> { if (!success) { switch (status) { case NO_READ_WRITE_PERMISSION: Log.e("Serial", "权限不足"); break; case OPEN_FAIL: Log.e("Serial", "打开失败"); break; } } }, dataCallback); ``` ## 🛠️ 故障排查 ### 常见问题 1. **串口打开失败** ```java // 检查设备路径 String[] devices = new SerialPortFinder().getAllDevicesPath(); // 检查权限 File deviceFile = new File("/dev/ttyS4"); boolean canRead = deviceFile.canRead(); boolean canWrite = deviceFile.canWrite(); ``` 2. **数据接收不完整** ```java // 启用日志查看原始数据 SerialPortLogUtil.setDebugEnabled(true); // 尝试不同的粘包策略 manager.configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING); ``` ## 📖 API参考 ### SimpleSerialPortManager (单串口) | 方法 | 说明 | |------|------| | `getInstance()` | 获取单例实例 | | `openSerialPort(path, baudRate, callback)` | 打开串口 | | `sendData(data)` | 发送数据 | | `closeSerialPort()` | 关闭串口 | | `setDatabits(databits)` | 设置数据位 | | `setParity(parity)` | 设置校验位 | | `setStopbits(stopbits)` | 设置停止位 | ### MultiSerialPortManager (多串口) | 方法 | 说明 | |------|------| | `getInstance()` | 获取实例 | | `openSerialPort(id, path, baudRate, config, statusCallback, dataCallback)` | 打开串口 | | `sendData(serialId, data)` | 发送数据到指定串口 | | `closeSerialPort(serialId)` | 关闭指定串口 | | `closeAllSerialPorts()` | 关闭所有串口 | | `isSerialPortOpened(serialId)` | 检查串口状态 | ## 🎯 版本迁移 ### 从4.1.1迁移到5.0.0 **旧版本 (4.1.1)**: ```java // 在Application中初始化 SerialUtils.getInstance().init(this, true, "TAG", 50, 8, 0, 1); // 使用 SerialUtils.getInstance().setmSerialPortDirectorListens(...); SerialUtils.getInstance().manyOpenSerialPort(list); ``` **新版本 (5.0.0)**: ```java // 简化的初始化(可选) new SimpleSerialPortManager.QuickConfig() .setDatabits(8).setParity(0).setStopbits(1) .apply(this); // 直接使用 SimpleSerialPortManager.getInstance() .openSerialPort("/dev/ttyS4", 115200, data -> { // 处理数据 }); ``` ## 📞 联系我们 - **QQ群**: 458173716 - **博客**: https://blog.csdn.net/a214024475/article/details/113735085 - **GitHub**: https://github.com/cl-6666/serialPort ### PC端串口调试助手 PC调试助手 **下载链接**: https://pan.baidu.com/s/1DL2TOHz9bl9RIKIG3oCSWw?pwd=f7sh ### QQ技术交流群 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 ``` ## 📚 使用指南 ### 1. Application中初始化 ```java public class App extends Application { @Override public void onCreate() { super.onCreate(); // 方式1:使用XLogConfig配置日志 XLogConfig logConfig = new XLogConfig.Builder() .logSwitch(true) // 开启日志 .tag("SerialPort") // 设置tag .build(); SerialConfig serialConfig = new SerialConfig.Builder() .setXLogConfig(logConfig) // 配置日志参数 .setIntervalSleep(50) // 设置读取间隔 .setSerialPortReconnection(false) // 是否开启串口重连 .setFlags(0) // 标志位 .setDatabits(8) // 数据位 .setStopbits(1) // 停止位 .setParity(0) // 校验位:0无校验,1奇校验,2偶校验 .build(); SerialUtils.getInstance().init(this, serialConfig); // 方式2:简化初始化 SerialUtils.getInstance().init(this, true, "SerialPort", 50, 8, 0, 1); } } ``` ### 2. 单串口使用 ```java public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 设置串口监听 SerialUtils.getInstance().setmSerialPortDirectorListens(new SerialPortDirectorListens() { @Override public void onSerialPortOpenSuccess(File device, SerialPortEnum serialPortEnum) { Log.i("Serial", "串口打开成功: " + device.getPath()); } @Override public void onSerialPortOpenFail(File device, SerialStatus status) { Log.e("Serial", "串口打开失败: " + status); } @Override public void onDataReceive(byte[] bytes, SerialPortEnum serialPortEnum) { String data = new String(bytes); Log.i("Serial", "接收数据: " + data); // 处理接收到的数据 } @Override public void onDataSend(byte[] bytes, SerialPortEnum serialPortEnum) { Log.i("Serial", "发送数据: " + new String(bytes)); } }); // 打开串口 List deviceList = new ArrayList<>(); deviceList.add(new Device("/dev/ttyS4", "115200", new File("/dev/ttyS4"))); SerialUtils.getInstance().manyOpenSerialPort(deviceList); } // 发送数据 private void sendData() { String data = "Hello World"; SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, data.getBytes()); } @Override protected void onDestroy() { super.onDestroy(); // 关闭串口 SerialUtils.getInstance().serialPortClose(); } } ``` ### 3. 多串口使用 ```java public class MultiSerialActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 设置粘包处理 SerialUtils.getInstance().setStickPackageHelper( new BaseStickPackageHelper(), // 串口1:不处理粘包 new SpecifiedStickPackageHelper("\n"), // 串口2:换行符分包 new StaticLenStickPackageHelper(8) // 串口3:固定8字节 ); // 设置监听 SerialUtils.getInstance().setmSerialPortDirectorListens(new SerialPortDirectorListens() { @Override public void onSerialPortOpenSuccess(File device, SerialPortEnum serialPortEnum) { Log.i("Serial", "串口[" + serialPortEnum + "]打开成功: " + device.getPath()); } @Override public void onSerialPortOpenFail(File device, SerialStatus status) { Log.e("Serial", "串口打开失败: " + status); } @Override public void onDataReceive(byte[] bytes, SerialPortEnum serialPortEnum) { String data = new String(bytes); Log.i("Serial", "串口[" + serialPortEnum + "]收到: " + data); // 根据串口类型处理数据 switch (serialPortEnum) { case SERIAL_ONE: handleGpsData(data); break; case SERIAL_TWO: handleSensorData(data); break; case SERIAL_THREE: handleModbusData(bytes); break; } } @Override public void onDataSend(byte[] bytes, SerialPortEnum serialPortEnum) { Log.i("Serial", "串口[" + serialPortEnum + "]发送: " + new String(bytes)); } }); // 打开多个串口 List deviceList = new ArrayList<>(); deviceList.add(new Device("/dev/ttyS1", "9600", new File("/dev/ttyS1"))); // GPS deviceList.add(new Device("/dev/ttyS2", "115200", new File("/dev/ttyS2"))); // 传感器 deviceList.add(new Device("/dev/ttyS3", "9600", new File("/dev/ttyS3"))); // Modbus SerialUtils.getInstance().manyOpenSerialPort(deviceList); } // 向不同串口发送数据 private void sendToSerial() { // 向串口1发送GPS命令 SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, "AT+GPS?\r\n".getBytes()); // 向串口2发送传感器命令 SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_TWO, "READ_TEMP\n".getBytes()); // 向串口3发送Modbus命令 byte[] modbusCmd = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, (byte)0x84, 0x0A}; SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_THREE, modbusCmd); } private void handleGpsData(String data) { // 处理GPS数据 } private void handleSensorData(String data) { // 处理传感器数据 } private void handleModbusData(byte[] data) { // 处理Modbus数据 } } ``` ### 4. 粘包处理配置 ```java public class StickyPacketConfig { public void configureStickyPacket() { // 1. 不处理粘包(默认) SerialUtils.getInstance().setStickPackageHelper(new BaseStickPackageHelper()); // 2. 按分隔符分包 SerialUtils.getInstance().setStickPackageHelper( new SpecifiedStickPackageHelper("\r\n".getBytes()) // 按\r\n分包 ); // 3. 固定长度分包 SerialUtils.getInstance().setStickPackageHelper( new StaticLenStickPackageHelper(16) // 固定16字节 ); // 4. 可变长度分包 SerialUtils.getInstance().setStickPackageHelper( new VariableLenStickPackageHelper( java.nio.ByteOrder.BIG_ENDIAN, // 字节序 2, // 长度字段大小 2, // 长度字段位置 12 // 包头长度 ) ); // 5. 多串口不同策略 SerialUtils.getInstance().setStickPackageHelper( new BaseStickPackageHelper(), // 串口1:不处理 new SpecifiedStickPackageHelper("\n"), // 串口2:换行符 new StaticLenStickPackageHelper(8), // 串口3:固定长度 new VariableLenStickPackageHelper( // 串口4:可变长度 java.nio.ByteOrder.BIG_ENDIAN, 2, 2, 12) ); } } ``` ### 5. 串口参数配置 ```java public class SerialParamConfig { public void configureParams() { SerialConfig serialConfig = new SerialConfig.Builder() .setIntervalSleep(50) // 读取间隔50ms .setDatabits(8) // 数据位8 .setStopbits(1) // 停止位1 .setParity(0) // 校验位:0=无校验 .setFlags(0) // 标志位 .setSerialPortReconnection(false) // 是否重连 .build(); SerialUtils.getInstance().init(getApplication(), serialConfig); } // 常用配置 public void commonConfigs() { // 标准配置 8N1 SerialConfig config8N1 = new SerialConfig.Builder() .setDatabits(8).setParity(0).setStopbits(1) .build(); // Modbus RTU 8E1 SerialConfig configModbus = new SerialConfig.Builder() .setDatabits(8).setParity(2).setStopbits(1) // 偶校验 .build(); // 老式设备 7E2 SerialConfig configOld = new SerialConfig.Builder() .setDatabits(7).setParity(2).setStopbits(2) .build(); } } ``` ## 📖 API参考 ### SerialUtils 主要方法 | 方法 | 说明 | |------|------| | `init(Application, SerialConfig)` | 初始化串口框架 | | `init(Application, boolean, String, int, int, int, int)` | 简化初始化 | | `setmSerialPortDirectorListens(SerialPortDirectorListens)` | 设置串口监听 | | `setStickPackageHelper(AbsStickPackageHelper...)` | 设置粘包处理 | | `manyOpenSerialPort(List)` | 打开多个串口 | | `sendData(SerialPortEnum, byte[])` | 发送数据 | | `serialPortClose()` | 关闭串口 | ### SerialConfig 配置项 | 参数 | 说明 | 默认值 | |------|------|--------| | `intervalSleep` | 读取间隔(ms) | 50 | | `databits` | 数据位 | 8 | | `stopbits` | 停止位 | 1 | | `parity` | 校验位 | 0 | | `flags` | 标志位 | 0 | | `serialPortReconnection` | 是否重连 | false | ### 粘包处理器 | 类型 | 说明 | 适用场景 | |------|------|----------| | `BaseStickPackageHelper` | 不处理粘包 | 简单数据流 | | `SpecifiedStickPackageHelper` | 分隔符分包 | 文本协议 | | `StaticLenStickPackageHelper` | 固定长度分包 | 固定格式协议 | | `VariableLenStickPackageHelper` | 可变长度分包 | 复杂二进制协议 | ## 🛠️ 故障排查 ### 常见问题 1. **串口打开失败** ```java // 检查设备路径 String[] devices = new SerialPortFinder().getAllDevicesPath(); // 检查权限 File deviceFile = new File("/dev/ttyS4"); if (!deviceFile.canRead() || !deviceFile.canWrite()) { Log.e("Serial", "设备权限不足"); } ``` 2. **数据接收不完整** ```java // 尝试不处理粘包 SerialUtils.getInstance().setStickPackageHelper(new BaseStickPackageHelper()); // 或者调整读取间隔 SerialConfig config = new SerialConfig.Builder() .setIntervalSleep(20) // 减少到20ms .build(); ``` 3. **日志输出问题** ```java // 确保启用日志 XLogConfig logConfig = new XLogConfig.Builder() .logSwitch(true) .tag("SerialPort") .build(); ``` ## 🎯 升级到5.0.0 如果你想升级到最新的5.0.0版本,以下是主要的变化: ### 4.1.1版本 ```java // 初始化 SerialUtils.getInstance().init(this, true, "TAG", 50, 8, 0, 1); // 使用 SerialUtils.getInstance().setmSerialPortDirectorListens(...); SerialUtils.getInstance().manyOpenSerialPort(deviceList); SerialUtils.getInstance().sendData(SerialPortEnum.SERIAL_ONE, data); ``` ### 5.0.0版本 (推荐) ```java // 简化初始化 new SimpleSerialPortManager.QuickConfig() .setDatabits(8).setParity(0).setStopbits(1) .apply(this); // 简化使用 SimpleSerialPortManager.getInstance() .openSerialPort("/dev/ttyS4", 115200, data -> { // 处理数据 }); // 多串口 MultiSerialPortManager manager = SimpleSerialPortManager.multi(); manager.openSerialPort("GPS", "/dev/ttyS1", 9600, config, statusCallback, dataCallback); ``` **升级优势**: - API更简单易用 - 支持真正的多串口管理 - 更好的错误处理 - 增强的日志系统 - 更高的性能 详细的5.0.0版本使用请查看 [最新文档](README.md)。 ## 📞 联系我们 - **QQ群**: 458173716 - **博客**: https://blog.csdn.net/a214024475/article/details/113735085 - **GitHub**: https://github.com/cl-6666/serialPort ## 📄 许可证 ``` Licensed under the Apache License, Version 2.0 ``` --- ⚠️ **再次提醒**: 4.1.1版本为历史版本,新项目建议使用 [5.0.0版本](README.md),功能更强大,使用更简单! ================================================ FILE: README_EN.md ================================================ # Android Serial Communication Framework SerialPort [English](README_EN.md) | [中文](README.md) [![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. Demo ## 📱 Demo APK Want to try it quickly? Download the demo APK and install it on your Android device.
### 📥 [Download Demo APK](https://www.pgyer.com/XNzY) [![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
### Demo Features - ✅ Single serial port demo - ✅ Multi-serial management demo - ✅ Sticky-packet strategy switching - ✅ Serial params configuration (data bits, parity, stop bits) - ✅ Real-time send/receive test - ✅ Hex/ASCII display - ✅ Performance test & statistics > Note: the demo APK must run on an Android device with serial ports (industrial devices, development boards, etc.). If your device has no serial port, check the source code for usage. ## ⭐ Features - 🚀 **Easy to use** - fluent APIs, configure with one line - 🔧 **Multi-serial support** - manage multiple ports with independent configs - 📦 **Smart sticky-packet handling** - multiple strategies, switch at runtime - ⚡ **High performance** - multithreaded, thread-safe design - 🛡️ **Stable & reliable** - solid error handling and resource management - 📝 **Detailed logs** - rich debug information for troubleshooting - 🎯 **Flexible config** - data bits, parity, stop bits, etc. - ✨ **Google Play ready** - supports 16 KB page alignment and passes Play requirements ## 📖 Versions - **Current**: 5.0.8 (recommended) - new architecture, powerful features, supports Google Play 16 KB page alignment - **Legacy**: [4.1.1 docs](README4.1.1.md) - stable legacy version ### 5.0.8 Changes 🔥 (2025-12-25) - ✅ **16 KB page alignment**: fully compatible with Google Play 16 KB page size requirements - ✅ **Android 15 support**: compatible with Android 15 - ✅ **Native library optimization**: `arm64-v8a` native lib meets Google Play checks - ✅ **Backward compatible**: works on older Android devices without code changes > Important: since 2024, Google Play requires all `arm64-v8a` native libraries to support 16 KB page size. v5.0.8 fully meets this requirement. ### 5.0.0 Major Update 🎉 - ✅ **Architecture refactor**: removed `SerialUtils` dependency, clearer design - ✅ **Simplified API**: introduced `SimpleSerialPortManager`, easier usage - ✅ **Multi-serial management**: new `MultiSerialPortManager` for complex scenarios - ✅ **Enhanced logging**: built-in logging system for better debugging - ✅ **Independent config**: each port can use its own sticky-packet strategy - ✅ **Performance improvements**: reduced ~30% redundant code ## 🚀 Quick Start ### Dependency Add dependency in your module `build.gradle`: ```gradle dependencies { implementation 'com.github.cl-6666:serialPort:v5.0.8' } ``` Add JitPack in the root `build.gradle`: ```gradle allprojects { repositories { maven { url 'https://jitpack.io' } } } ``` ### Permissions Add required permissions in `AndroidManifest.xml`: ```xml ``` ## 📚 Usage Guide ### 1️⃣ Single Port - Basic Example #### Minimal usage ```java public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Open serial port and receive data with one line SimpleSerialPortManager.getInstance() .openSerialPort("/dev/ttyS4", 115200, data -> { String receivedData = new String(data); Log.i("Serial", "Received: " + receivedData); // Handle received data }); } // Send data private void sendData() { SimpleSerialPortManager.getInstance().sendData("Hello World"); } @Override protected void onDestroy() { super.onDestroy(); // Close serial port SimpleSerialPortManager.getInstance().closeSerialPort(); } } ``` #### Full configuration example ```java public class App extends Application { @Override public void onCreate() { super.onCreate(); // Global config (optional) new SimpleSerialPortManager.QuickConfig() .setIntervalSleep(50) // Read interval: 50ms .setEnableLog(true) // Enable logs .setLogTag("SerialPortApp") // Log tag .setDatabits(8) // Data bits: 8 .setParity(0) // Parity: none .setStopbits(1) // Stop bits: 1 .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING) .apply(this); } } ``` ### 2️⃣ Data Bits / Parity / Stop Bits ```java public class SerialConfigExample { public void configureSerialParams() { SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance(); // Option 1: use QuickConfig new SimpleSerialPortManager.QuickConfig() .setDatabits(8) // Data bits: 5, 6, 7, 8 .setParity(0) // Parity: 0=none, 1=odd, 2=even .setStopbits(1) // Stop bits: 1 or 2 .setFlags(0) // Flags .apply(getApplication()); // Option 2: set dynamically manager.setDatabits(8) // Set data bits .setParity(2) // Set even parity .setStopbits(1) // Set stop bits to 1 .setFlags(0); // Set flags // Open serial port manager.openSerialPort("/dev/ttyS4", 115200, data -> { Log.i("Serial", "Data: " + new String(data)); }); } // Common configurations public void commonConfigurations() { SimpleSerialPortManager manager = SimpleSerialPortManager.getInstance(); // Standard 8N1 (8 data bits, no parity, 1 stop bit) manager.setDatabits(8).setParity(0).setStopbits(1); // Modbus RTU 8E1 (8 data bits, even parity, 1 stop bit) manager.setDatabits(8).setParity(2).setStopbits(1); // Legacy devices 7E2 (7 data bits, even parity, 2 stop bits) manager.setDatabits(7).setParity(2).setStopbits(2); } } ``` ### 3️⃣ Sticky-Packet Handling Sticky packets are common in serial communication. Since v5.0.0, multiple strategies are provided: ```java public class StickyPacketExample { public void noProcessing() { // Strategy 1: no processing - good for simple streams new SimpleSerialPortManager.QuickConfig() .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING) .apply(this); } public void delimiterBased() { // Strategy 2: delimiter-based - good for text protocols new SimpleSerialPortManager.QuickConfig() .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED) .apply(this); // Custom delimiter SimpleSerialPortManager.getInstance() .configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.DELIMITER_BASED); } public void fixedLength() { // Strategy 3: fixed-length - good for fixed-length protocols new SimpleSerialPortManager.QuickConfig() .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.FIXED_LENGTH) .apply(this); } public void variableLength() { // Strategy 4: variable-length - good for protocols with length fields new SimpleSerialPortManager.QuickConfig() .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.VARIABLE_LENGTH) .apply(this); } } ``` ### 4️⃣ Multi-Serial Management ```java public class MultiSerialExample { public void basicMultiSerial() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // Port 1: GPS module, no sticky-packet processing manager.openSerialPort("GPS", "/dev/ttyS1", 9600, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(8) .setParity(0) .setStopbits(1) .setStickyPacketHelpers(new BaseStickPackageHelper()) // No processing .build(), // Status callback (serialId, success, status) -> { Log.i("GPS", "Status: " + (success ? "Success" : "Failed")); }, // Data callback (serialId, data) -> { String gpsData = new String(data); Log.i("GPS", "Data: " + gpsData); handleGpsData(gpsData); }); // Port 2: sensor module, split by newline manager.openSerialPort("SENSOR", "/dev/ttyS2", 115200, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(8) .setParity(0) .setStopbits(1) .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) // Newline delimiter .build(), null, // No status callback needed (serialId, data) -> { String sensorData = new String(data).trim(); Log.i("SENSOR", "Data: " + sensorData); handleSensorData(sensorData); }); // Send to different ports manager.sendData("GPS", "AT+GPS?\r\n"); manager.sendData("SENSOR", "READ_TEMP\n"); } // Dynamic management public void dynamicManagement() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // Query status List openedPorts = manager.getOpenedSerialPorts(); boolean isOpened = manager.isSerialPortOpened("GPS"); manager.printAllSerialStatus(); // Update sticky-packet strategy dynamically manager.updateStickyPacketHelpers("GPS", new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("\r\n")}); // Close one port manager.closeSerialPort("GPS"); // Close all ports manager.closeAllSerialPorts(); } } ``` ## 🎯 Real-world Scenarios ### Industrial control ```java public class IndustrialControlExample { public void setupIndustrialPorts() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // PLC - Modbus RTU manager.openSerialPort("PLC", "/dev/ttyS1", 9600, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(8).setParity(2).setStopbits(1) // 8E1 .setStickyPacketHelpers(new StaticLenStickPackageHelper(8)) .build(), null, this::handlePlcData); // Sensor acquisition - text protocol manager.openSerialPort("SENSORS", "/dev/ttyS3", 9600, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(7).setParity(2).setStopbits(1) // 7E1 .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n")) .build(), null, this::handleSensorData); } } ``` ### Communication gateway ```java public class GatewayExample { public void setupGateway() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // Uplink (to server) manager.openSerialPort("UPLINK", "/dev/ttyS1", 115200, new MultiSerialPortManager.SerialPortConfig.Builder() .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\n")) .build(), null, this::handleUplinkData); // Downlink device 1 - GPS manager.openSerialPort("GPS", "/dev/ttyS2", 9600, new MultiSerialPortManager.SerialPortConfig.Builder() .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n")) .build(), null, data -> forwardToUplink("GPS", data)); } private void forwardToUplink(String deviceId, byte[] data) { String message = String.format("[%s]%s\n", deviceId, new String(data)); SimpleSerialPortManager.multi().sendData("UPLINK", message); } } ``` ## 🔧 Advanced ### Logging ```java // Enable verbose logs SerialPortLogUtil.setDebugEnabled(true); // Custom output SerialPortLogUtil.i("MyTag", "Custom log message"); SerialPortLogUtil.printData("Send", data); // Hex + ASCII SerialPortLogUtil.printSerialConfig("MySerial", 8, 0, 1, 0); // Config info ``` ### Error handling ```java manager.openSerialPort("TEST", "/dev/ttyS1", 9600, (serialId, success, status) -> { if (!success) { switch (status) { case NO_READ_WRITE_PERMISSION: Log.e("Serial", "No permission"); break; case OPEN_FAIL: Log.e("Serial", "Open failed"); break; } } }, dataCallback); ``` ## 🛠️ Troubleshooting ### Common issues 1. **Failed to open serial port** ```java // Check device paths String[] devices = new SerialPortFinder().getAllDevicesPath(); // Check permission File deviceFile = new File("/dev/ttyS4"); boolean canRead = deviceFile.canRead(); boolean canWrite = deviceFile.canWrite(); ``` 2. **Incomplete received data** ```java // Enable logs to inspect raw data SerialPortLogUtil.setDebugEnabled(true); // Try different sticky-packet strategies manager.configureStickyPacket(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING); ``` ## 📖 API Reference ### SimpleSerialPortManager (single port) | Method | Description | |------|------| | `getInstance()` | Get singleton instance | | `openSerialPort(path, baudRate, callback)` | Open serial port | | `sendData(data)` | Send data | | `closeSerialPort()` | Close serial port | | `setDatabits(databits)` | Set data bits | | `setParity(parity)` | Set parity | | `setStopbits(stopbits)` | Set stop bits | ### MultiSerialPortManager (multi port) | Method | Description | |------|------| | `getInstance()` | Get instance | | `openSerialPort(id, path, baudRate, config, statusCallback, dataCallback)` | Open serial port | | `sendData(serialId, data)` | Send data to a port | | `closeSerialPort(serialId)` | Close a port | | `closeAllSerialPorts()` | Close all ports | | `isSerialPortOpened(serialId)` | Check port status | ## 🎯 Migration ### From 4.1.1 to 5.0.0 **Old (4.1.1)**: ```java // Init in Application SerialUtils.getInstance().init(this, true, "TAG", 50, 8, 0, 1); // Usage SerialUtils.getInstance().setmSerialPortDirectorListens(...); SerialUtils.getInstance().manyOpenSerialPort(list); ``` **New (5.0.0)**: ```java // Simplified init (optional) new SimpleSerialPortManager.QuickConfig() .setDatabits(8).setParity(0).setStopbits(1) .apply(this); // Direct usage SimpleSerialPortManager.getInstance() .openSerialPort("/dev/ttyS4", 115200, data -> { // Handle data }); ``` ## 📞 Contact - **QQ group**: 458173716 - **Blog**: https://blog.csdn.net/a214024475/article/details/113735085 - **GitHub**: https://github.com/cl-6666/serialPort ### PC serial debugging assistant PC tool **Download**: https://pan.baidu.com/s/1DL2TOHz9bl9RIKIG3oCSWw?pwd=f7sh ### QQ technical group 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 ================================================ ================================================ FILE: app/src/main/java/com/cl/myapplication/App.java ================================================ package com.cl.myapplication; import android.app.Application; import com.hjq.toast.ToastUtils; import com.cl.serialportlibrary.SimpleSerialPortManager; /** * 项目:serialPort * 作者:Arry * 创建日期:2021/10/20 * 描述: * 修订历史: */ public class App extends Application { @Override public void onCreate() { super.onCreate(); // 初始化 Toast 框架 ToastUtils.init(this); // 使用新的SimpleSerialPortManager进行全局初始化 new SimpleSerialPortManager.QuickConfig() .setIntervalSleep(50) // 读取间隔50ms .setEnableLog(true) // 启用日志 .setLogTag("SerialPortApp") // 设置日志标签 .setDatabits(8) // 数据位8 .setParity(0) // 无校验 .setStopbits(1) // 停止位1 .setStickyPacketStrategy(SimpleSerialPortManager.StickyPacketStrategy.NO_PROCESSING) // 不处理黏包 .setMaxPacketSize(1024) // 最大包大小1KB .apply(this); } } ================================================ FILE: app/src/main/java/com/cl/myapplication/MainActivity.kt ================================================ package com.cl.myapplication import android.os.Bundle import android.text.TextUtils import android.util.Log import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import com.cl.myapplication.databinding.ActivityMainBinding import com.cl.serialportlibrary.Device import com.cl.serialportlibrary.SimpleSerialPortManager class MainActivity : AppCompatActivity(){ private val TAG = MainActivity::class.java.simpleName val DEVICE = "device" private var isSerialPortOpened = false private var mToast: Toast? = null private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val device = intent.getSerializableExtra(DEVICE) as Device? Log.i(TAG, "onCreate: device = $device") if (null == device) { finish() return } // 使用SimpleSerialPortManager打开串口 val devicePath = device.name val baudRate = device.root.toInt() SimpleSerialPortManager.getInstance().openSerialPort(devicePath, baudRate) { data -> runOnUiThread { val receivedData = String(data) Log.i(TAG, "接收到数据: $receivedData") // binding.tvReceiveContent.text = receivedData } } isSerialPortOpened = true } fun onSend(view: View) { val editTextSendContent = binding.etSendContent.text.toString() if (TextUtils.isEmpty(editTextSendContent)) { Log.i(TAG, "onSend: 发送内容为 null") return } val sendContentBytes = editTextSendContent.toByteArray() val sendBytes = SimpleSerialPortManager.getInstance().sendData(sendContentBytes) Log.i(TAG, "onSend: sendBytes = $sendBytes") showToast(if (sendBytes) "发送成功" else "发送失败") } fun onDestroy(view: View) { if (isSerialPortOpened) { SimpleSerialPortManager.getInstance().closeSerialPort() isSerialPortOpened = false } finish() } override fun onDestroy() { super.onDestroy() if (isSerialPortOpened) { SimpleSerialPortManager.getInstance().closeSerialPort() } } /** * Toast * * @param content content */ private fun showToast(content: String) { if (null == mToast) { mToast = Toast.makeText(applicationContext, null, Toast.LENGTH_SHORT) } mToast?.setText(content) mToast?.show() } } ================================================ FILE: app/src/main/java/com/cl/myapplication/MultiSerialPortActivity.java ================================================ package com.cl.myapplication; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.databinding.DataBindingUtil; import com.cl.myapplication.adapter.SpAdapter; import com.cl.myapplication.databinding.ActivityMultiSerialBinding; import com.cl.serialportlibrary.MultiSerialPortManager; import com.cl.serialportlibrary.SerialPortFinder; import com.cl.serialportlibrary.SimpleSerialPortManager; import com.cl.serialportlibrary.enumerate.SerialStatus; import com.cl.serialportlibrary.stick.AbsStickPackageHelper; import com.cl.serialportlibrary.stick.BaseStickPackageHelper; import com.cl.serialportlibrary.stick.CompositeStickPackageHelper; import com.cl.serialportlibrary.stick.SpecifiedStickPackageHelper; import com.cl.serialportlibrary.stick.StaticLenStickPackageHelper; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * 多串口演示Activity * 展示如何同时管理多个串口,每个串口使用不同的粘包策略 */ public class MultiSerialPortActivity extends AppCompatActivity { private static final String TAG = "MultiSerialPortActivity"; private ActivityMultiSerialBinding binding; private TextView statusText; private TextView dataText; private final SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()); // 串口设备列表 private String[] mDevices; private String[] mBaudrates = {"9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"}; // 全局串口参数 private String[] mDatabits = {"8", "7", "6", "5"}; private String[] mParitys = {"NONE", "ODD", "EVEN", "SPACE", "MARK"}; private String[] mStopbits = {"1", "2"}; private int globalDatabits = 8; private int globalParity = 0; private int globalStopbits = 1; // 各串口的配置 private SerialPortConfig gpsConfig = new SerialPortConfig(); private SerialPortConfig sensorConfig = new SerialPortConfig(); private SerialPortConfig modbusConfig = new SerialPortConfig(); private SerialPortConfig customConfig = new SerialPortConfig(); // 串口状态 private boolean gpsOpened = false; private boolean sensorOpened = false; private boolean modbusOpened = false; private boolean customOpened = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_multi_serial); initViews(); initDevices(); setupGlobalParamSpinners(); setupSpinners(); setupMultiSerial(); } private void initViews() { statusText = binding.tvStatus; dataText = binding.tvData; // 设置按钮点击事件 binding.btnRefreshPorts.setOnClickListener(v -> refreshSerialPorts()); binding.btnOpenAll.setOnClickListener(v -> openAllSerialPorts()); binding.btnCloseAll.setOnClickListener(v -> closeAllSerialPorts()); binding.btnSendTest.setOnClickListener(v -> sendTestData()); binding.btnClearLog.setOnClickListener(v -> clearLog()); // 各串口的开关按钮 binding.btnGpsToggle.setOnClickListener(v -> toggleGpsPort()); binding.btnSensorToggle.setOnClickListener(v -> toggleSensorPort()); binding.btnModbusToggle.setOnClickListener(v -> toggleModbusPort()); binding.btnCustomToggle.setOnClickListener(v -> toggleCustomPort()); } private void setupMultiSerial() { updateStatus("多串口管理器初始化完成"); } /** * 发送测试数据到所有串口 */ private void sendTestData() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); int sentCount = 0; // 向GPS发送AT命令 if (gpsOpened) { boolean ok = manager.sendData("GPS", "AT+GPS?\r\n"); if (!ok) { appendData("TX_FAIL [GPS] AT+GPS?\\r\\n"); } if (ok) { sentCount++; } } // 向传感器发送读取命令 if (sensorOpened) { boolean ok = manager.sendData("SENSOR", "READ_TEMP\n"); if (!ok) { appendData("TX_FAIL [SENSOR] READ_TEMP\\n"); } if (ok) { sentCount++; } } // 向Modbus设备发送读取寄存器命令 if (modbusOpened) { byte[] modbusCmd = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, (byte)0x84, 0x0A}; boolean ok = manager.sendData("MODBUS", modbusCmd); if (!ok) { appendData("TX_FAIL [MODBUS] " + bytesToHex(modbusCmd)); } if (ok) { sentCount++; } } // 向自定义协议设备发送命令 if (customOpened) { String payload = "$$START$$GET_STATUS$$END$$"; boolean ok = manager.sendData("CUSTOM", payload); if (!ok) { appendData("TX_FAIL [CUSTOM] " + payload); } if (ok) { sentCount++; } } updateStatus("测试数据发送请求已提交到 " + sentCount + " 个串口"); } /** * 更新状态显示 */ private void updateStatus(String message) { appendLog(statusText, message, 20); Log.i(TAG, message); } private void appendData(String message) { appendLog(dataText, message, 200); } private void appendLog(TextView target, String message, int maxLines) { if (target == null) { return; } String time = timeFormatter.format(new Date()); String currentText = target.getText().toString(); String newText = time + " " + message + "\n" + currentText; String[] lines = newText.split("\n"); if (lines.length > maxLines) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < maxLines; i++) { sb.append(lines[i]).append("\n"); } newText = sb.toString(); } target.setText(newText); } private String sanitizeForSingleLine(String text) { if (text == null) { return ""; } return text.replace("\r", "\\r").replace("\n", "\\n"); } /** * 字节数组转十六进制字符串 */ private String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02X ", b)); } return sb.toString().trim(); } @Override protected void onDestroy() { super.onDestroy(); // 清理资源 closeAllSerialPorts(); } /** * 演示动态更新粘包处理器 */ public void updateStickyPacketExample(View view) { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // 动态更新GPS串口的粘包处理器,改为按回车换行分包 boolean success = manager.updateStickyPacketHelpers("GPS", new AbsStickPackageHelper[]{new SpecifiedStickPackageHelper("\r\n")}); if (success) { updateStatus("GPS串口粘包处理器已更新为\\r\\n分包"); } else { updateStatus("GPS串口粘包处理器更新失败"); } } /** * 展示串口数据路由功能 */ public void serialRoutingExample() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); // 主控制串口:接收外部命令并路由到其他串口 manager.openSerialPort("MAIN_CTRL", "/dev/ttyS0", 115200, new MultiSerialPortManager.SerialPortConfig.Builder() .setStickyPacketHelpers(new SpecifiedStickPackageHelper("\r\n")) .build(), null, new MultiSerialPortManager.OnSerialPortDataCallback() { @Override public void onDataReceived(String serialId, byte[] data) { String command = new String(data).trim(); Log.i(TAG, "主控制命令: " + command); // 根据命令前缀路由到不同串口 if (command.startsWith("GPS:")) { String gpsCmd = command.substring(4); manager.sendData("GPS", gpsCmd + "\r\n"); updateStatus("路由命令到GPS: " + gpsCmd); } else if (command.startsWith("SENSOR:")) { String sensorCmd = command.substring(7); manager.sendData("SENSOR", sensorCmd + "\n"); updateStatus("路由命令到传感器: " + sensorCmd); } else if (command.startsWith("MODBUS:")) { // 这里可以解析十六进制字符串并发送到Modbus updateStatus("路由命令到Modbus: " + command); } } }); updateStatus("串口路由功能已启用,可通过主控制串口发送命令"); } /** * 初始化设备列表 */ private void initDevices() { SerialPortFinder serialPortFinder = new SerialPortFinder(); mDevices = serialPortFinder.getAllDevicesPath(); if (mDevices.length == 0) { mDevices = new String[]{"没有找到串口设备"}; } // 初始化默认配置 gpsConfig.device = mDevices.length > 0 ? mDevices[0] : ""; gpsConfig.baudrate = "9600"; sensorConfig.device = mDevices.length > 1 ? mDevices[1] : (mDevices.length > 0 ? mDevices[0] : ""); sensorConfig.baudrate = "115200"; modbusConfig.device = mDevices.length > 2 ? mDevices[2] : (mDevices.length > 0 ? mDevices[0] : ""); modbusConfig.baudrate = "9600"; customConfig.device = mDevices.length > 3 ? mDevices[3] : (mDevices.length > 0 ? mDevices[0] : ""); customConfig.baudrate = "115200"; } /** * 设置全局参数下拉框 */ private void setupGlobalParamSpinners() { // 数据位配置 setupSpinner(binding.spinnerDatabits, mDatabits, 0, (position) -> { globalDatabits = Integer.parseInt(mDatabits[position]); updateStatus("全局数据位已设置为: " + globalDatabits); closeAllOpenedPorts(); }); // 校验位配置 setupSpinner(binding.spinnerParity, mParitys, 0, (position) -> { globalParity = position; updateStatus("全局校验位已设置为: " + mParitys[position]); closeAllOpenedPorts(); }); // 停止位配置 setupSpinner(binding.spinnerStopbits, mStopbits, 0, (position) -> { globalStopbits = Integer.parseInt(mStopbits[position]); updateStatus("全局停止位已设置为: " + globalStopbits); closeAllOpenedPorts(); }); } /** * 关闭所有已打开的串口(参数变更时使用) */ private void closeAllOpenedPorts() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); boolean hasOpenPorts = false; if (gpsOpened) { manager.closeSerialPort("GPS"); gpsOpened = false; hasOpenPorts = true; } if (sensorOpened) { manager.closeSerialPort("SENSOR"); sensorOpened = false; hasOpenPorts = true; } if (modbusOpened) { manager.closeSerialPort("MODBUS"); modbusOpened = false; hasOpenPorts = true; } if (customOpened) { manager.closeSerialPort("CUSTOM"); customOpened = false; hasOpenPorts = true; } if (hasOpenPorts) { updateButtonStates(); updateStatus("参数变更,已关闭所有串口,请重新打开"); } } /** * 设置下拉框 */ private void setupSpinners() { // GPS串口配置 setupSpinner(binding.spinnerGpsDevice, mDevices, 0, (position) -> { gpsConfig.device = mDevices[position]; if (gpsOpened) { updateStatus("GPS串口设备已更改,请重新打开串口"); closeGpsPort(); } }); setupSpinner(binding.spinnerGpsBaudrate, mBaudrates, 0, (position) -> { gpsConfig.baudrate = mBaudrates[position]; if (gpsOpened) { updateStatus("GPS串口波特率已更改,请重新打开串口"); closeGpsPort(); } }); // 传感器串口配置 setupSpinner(binding.spinnerSensorDevice, mDevices, mDevices.length > 1 ? 1 : 0, (position) -> { sensorConfig.device = mDevices[position]; if (sensorOpened) { updateStatus("传感器串口设备已更改,请重新打开串口"); closeSensorPort(); } }); setupSpinner(binding.spinnerSensorBaudrate, mBaudrates, 4, (position) -> { sensorConfig.baudrate = mBaudrates[position]; if (sensorOpened) { updateStatus("传感器串口波特率已更改,请重新打开串口"); closeSensorPort(); } }); // Modbus串口配置 setupSpinner(binding.spinnerModbusDevice, mDevices, mDevices.length > 2 ? 2 : 0, (position) -> { modbusConfig.device = mDevices[position]; if (modbusOpened) { updateStatus("Modbus串口设备已更改,请重新打开串口"); closeModbusPort(); } }); setupSpinner(binding.spinnerModbusBaudrate, mBaudrates, 0, (position) -> { modbusConfig.baudrate = mBaudrates[position]; if (modbusOpened) { updateStatus("Modbus串口波特率已更改,请重新打开串口"); closeModbusPort(); } }); // 自定义串口配置 setupSpinner(binding.spinnerCustomDevice, mDevices, mDevices.length > 3 ? 3 : 0, (position) -> { customConfig.device = mDevices[position]; if (customOpened) { updateStatus("自定义串口设备已更改,请重新打开串口"); closeCustomPort(); } }); setupSpinner(binding.spinnerCustomBaudrate, mBaudrates, 4, (position) -> { customConfig.baudrate = mBaudrates[position]; if (customOpened) { updateStatus("自定义串口波特率已更改,请重新打开串口"); closeCustomPort(); } }); } /** * 设置单个下拉框 */ private void setupSpinner(Spinner spinner, String[] data, int defaultSelection, OnSpinnerItemSelectedListener listener) { ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.spinner_default_item, data); adapter.setDropDownViewResource(R.layout.spinner_item); spinner.setAdapter(adapter); spinner.setSelection(defaultSelection); spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { listener.onItemSelected(position); } @Override public void onNothingSelected(AdapterView parent) { } }); } /** * 刷新串口列表 */ private void refreshSerialPorts() { initDevices(); setupSpinners(); updateStatus("串口列表已刷新,共找到 " + mDevices.length + " 个设备"); } /** * 打开所有串口 */ private void openAllSerialPorts() { updateStatus("开始打开所有串口..."); if (!gpsOpened) toggleGpsPort(); if (!sensorOpened) toggleSensorPort(); if (!modbusOpened) toggleModbusPort(); if (!customOpened) toggleCustomPort(); } /** * 关闭所有串口 */ private void closeAllSerialPorts() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); manager.closeAllSerialPorts(); gpsOpened = false; sensorOpened = false; modbusOpened = false; customOpened = false; updateButtonStates(); updateStatus("所有串口已关闭"); } /** * GPS串口开关 */ private void toggleGpsPort() { if (gpsOpened) { closeGpsPort(); } else { openGpsPort(); } } private void openGpsPort() { if (gpsConfig.device.equals("没有找到串口设备")) { updateStatus("GPS串口:没有可用设备"); return; } MultiSerialPortManager manager = SimpleSerialPortManager.multi(); int baudrate = Integer.parseInt(gpsConfig.baudrate); manager.openSerialPort("GPS", gpsConfig.device, baudrate, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(globalDatabits) .setParity(globalParity) .setStopbits(globalStopbits) .setStickyPacketHelpers(new BaseStickPackageHelper()) .build(), (serialId, success, status) -> { gpsOpened = success; runOnUiThread(() -> { updateButtonStates(); updateStatus(String.format("GPS串口[%s]: %s", gpsConfig.device, success ? "打开成功" : "打开失败 - " + status)); }); }, new MultiSerialPortManager.OnSerialPortDataCallback() { @Override public void onDataReceived(String serialId, byte[] data) { String text = sanitizeForSingleLine(new String(data)); String hex = bytesToHex(data); Log.i(TAG, "GPS数据: " + text); runOnUiThread(() -> appendData("RX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex)); } @Override public void onDataSent(String serialId, byte[] data) { String text = sanitizeForSingleLine(new String(data)); String hex = bytesToHex(data); runOnUiThread(() -> appendData("TX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex)); } }); } private void closeGpsPort() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); manager.closeSerialPort("GPS"); gpsOpened = false; updateButtonStates(); updateStatus("GPS串口已关闭"); } /** * 传感器串口开关 */ private void toggleSensorPort() { if (sensorOpened) { closeSensorPort(); } else { openSensorPort(); } } private void openSensorPort() { if (sensorConfig.device.equals("没有找到串口设备")) { updateStatus("传感器串口:没有可用设备"); return; } MultiSerialPortManager manager = SimpleSerialPortManager.multi(); int baudrate = Integer.parseInt(sensorConfig.baudrate); manager.openSerialPort("SENSOR", sensorConfig.device, baudrate, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(globalDatabits) .setParity(globalParity) .setStopbits(globalStopbits) .setStickyPacketHelpers(new CompositeStickPackageHelper( new SpecifiedStickPackageHelper("\n"), new BaseStickPackageHelper())) .build(), (serialId, success, status) -> { sensorOpened = success; runOnUiThread(() -> { updateButtonStates(); updateStatus(String.format("传感器串口[%s]: %s", sensorConfig.device, success ? "打开成功" : "打开失败 - " + status)); }); }, new MultiSerialPortManager.OnSerialPortDataCallback() { @Override public void onDataReceived(String serialId, byte[] data) { String text = sanitizeForSingleLine(new String(data).trim()); String hex = bytesToHex(data); Log.i(TAG, "传感器数据: " + text); runOnUiThread(() -> appendData("RX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex)); } @Override public void onDataSent(String serialId, byte[] data) { String text = sanitizeForSingleLine(new String(data)); String hex = bytesToHex(data); runOnUiThread(() -> appendData("TX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex)); } }); } private void closeSensorPort() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); manager.closeSerialPort("SENSOR"); sensorOpened = false; updateButtonStates(); updateStatus("传感器串口已关闭"); } /** * Modbus串口开关 */ private void toggleModbusPort() { if (modbusOpened) { closeModbusPort(); } else { openModbusPort(); } } private void openModbusPort() { if (modbusConfig.device.equals("没有找到串口设备")) { updateStatus("Modbus串口:没有可用设备"); return; } MultiSerialPortManager manager = SimpleSerialPortManager.multi(); int baudrate = Integer.parseInt(modbusConfig.baudrate); manager.openSerialPort("MODBUS", modbusConfig.device, baudrate, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(globalDatabits) .setParity(globalParity) .setStopbits(globalStopbits) .setStickyPacketHelpers(new CompositeStickPackageHelper( new StaticLenStickPackageHelper(8), new BaseStickPackageHelper())) .build(), (serialId, success, status) -> { modbusOpened = success; runOnUiThread(() -> { updateButtonStates(); updateStatus(String.format("Modbus串口[%s]: %s", modbusConfig.device, success ? "打开成功" : "打开失败 - " + status)); }); }, new MultiSerialPortManager.OnSerialPortDataCallback() { @Override public void onDataReceived(String serialId, byte[] data) { String modbusData = bytesToHex(data); Log.i(TAG, "Modbus数据: " + modbusData); runOnUiThread(() -> appendData("RX [" + serialId + "] len=" + data.length + " hex=" + modbusData)); } @Override public void onDataSent(String serialId, byte[] data) { String hex = bytesToHex(data); runOnUiThread(() -> appendData("TX [" + serialId + "] len=" + data.length + " hex=" + hex)); } }); } private void closeModbusPort() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); manager.closeSerialPort("MODBUS"); modbusOpened = false; updateButtonStates(); updateStatus("Modbus串口已关闭"); } /** * 自定义串口开关 */ private void toggleCustomPort() { if (customOpened) { closeCustomPort(); } else { openCustomPort(); } } private void openCustomPort() { if (customConfig.device.equals("没有找到串口设备")) { updateStatus("自定义串口:没有可用设备"); return; } MultiSerialPortManager manager = SimpleSerialPortManager.multi(); int baudrate = Integer.parseInt(customConfig.baudrate); manager.openSerialPort("CUSTOM", customConfig.device, baudrate, new MultiSerialPortManager.SerialPortConfig.Builder() .setDatabits(globalDatabits) .setParity(globalParity) .setStopbits(globalStopbits) .setStickyPacketHelpers(new CompositeStickPackageHelper( new SpecifiedStickPackageHelper("$$START$$", "$$END$$"), new BaseStickPackageHelper())) .build(), (serialId, success, status) -> { customOpened = success; runOnUiThread(() -> { updateButtonStates(); updateStatus(String.format("自定义串口[%s]: %s", customConfig.device, success ? "打开成功" : "打开失败 - " + status)); }); }, new MultiSerialPortManager.OnSerialPortDataCallback() { @Override public void onDataReceived(String serialId, byte[] data) { String text = sanitizeForSingleLine(new String(data)); String hex = bytesToHex(data); Log.i(TAG, "自定义协议数据: " + text); runOnUiThread(() -> appendData("RX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex)); } @Override public void onDataSent(String serialId, byte[] data) { String text = sanitizeForSingleLine(new String(data)); String hex = bytesToHex(data); runOnUiThread(() -> appendData("TX [" + serialId + "] len=" + data.length + " text=" + text + " hex=" + hex)); } }); } private void closeCustomPort() { MultiSerialPortManager manager = SimpleSerialPortManager.multi(); manager.closeSerialPort("CUSTOM"); customOpened = false; updateButtonStates(); updateStatus("自定义串口已关闭"); } /** * 更新按钮状态 */ private void updateButtonStates() { binding.btnGpsToggle.setText(gpsOpened ? "关闭" : "打开"); binding.btnGpsToggle.setBackgroundColor(getResources().getColor( gpsOpened ? R.color.secondary_text : R.color.colorAccent)); binding.btnSensorToggle.setText(sensorOpened ? "关闭" : "打开"); binding.btnSensorToggle.setBackgroundColor(getResources().getColor( sensorOpened ? R.color.secondary_text : R.color.colorAccent)); binding.btnModbusToggle.setText(modbusOpened ? "关闭" : "打开"); binding.btnModbusToggle.setBackgroundColor(getResources().getColor( modbusOpened ? R.color.secondary_text : R.color.colorAccent)); binding.btnCustomToggle.setText(customOpened ? "关闭" : "打开"); binding.btnCustomToggle.setBackgroundColor(getResources().getColor( customOpened ? R.color.secondary_text : R.color.colorAccent)); } /** * 清空日志 */ private void clearLog() { if (statusText != null) { statusText.setText("日志已清空"); } if (dataText != null) { dataText.setText("数据已清空"); } } /** * 串口配置类 */ private static class SerialPortConfig { String device = ""; String baudrate = "9600"; } /** * 下拉框选择监听器 */ private interface OnSpinnerItemSelectedListener { void onItemSelected(int position); } } ================================================ FILE: app/src/main/java/com/cl/myapplication/SelectSerialPortActivity.kt ================================================ package com.cl.myapplication import android.content.Intent import android.os.Bundle import android.view.View import android.widget.AdapterView import android.widget.AdapterView.OnItemClickListener import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import com.cl.myapplication.adapter.DeviceAdapter import com.cl.myapplication.databinding.ActivitySelectSerialPortBinding import com.cl.serialportlibrary.SerialPortFinder class SelectSerialPortActivity : AppCompatActivity(), OnItemClickListener { private var mDeviceAdapter: DeviceAdapter? = null val DEVICE = "device" private lateinit var binding: ActivitySelectSerialPortBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_select_serial_port) val serialPortFinder = SerialPortFinder() val devices = serialPortFinder.devices if (binding.lvDevices != null) { binding.lvDevices.emptyView = binding.tvEmpty mDeviceAdapter = DeviceAdapter(applicationContext, devices) binding.lvDevices.adapter = mDeviceAdapter binding.lvDevices.onItemClickListener = this } } override fun onItemClick(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val device = mDeviceAdapter!!.getItem(position) val intent = Intent(this, MainActivity::class.java) intent.putExtra(DEVICE, device) startActivity(intent) } } ================================================ FILE: app/src/main/java/com/cl/myapplication/SingleSerialPortActivity.java ================================================ package com.cl.myapplication; import android.app.Activity; import android.content.pm.PackageManager; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.FragmentManager; import com.cl.myapplication.adapter.SpAdapter; import com.cl.myapplication.constant.PreferenceKeys; import com.cl.myapplication.databinding.ActivityMainJavaBinding; import com.cl.myapplication.fragment.LogFragment; import com.cl.myapplication.message.ConversionNoticeEvent; import com.cl.myapplication.message.IMessage; import com.cl.myapplication.message.LogManager; import com.cl.myapplication.message.RecvMessage; import com.cl.myapplication.message.SendMessage; import com.cl.myapplication.util.PrefHelper; import com.hjq.toast.ToastUtils; import com.cl.serialportlibrary.Device; import com.cl.serialportlibrary.SerialPortFinder; import com.cl.serialportlibrary.SimpleSerialPortManager; import com.cl.serialportlibrary.utils.SerialPortLogUtil; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.util.Arrays; /** * 单串口演示 */ public class SingleSerialPortActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { private ActivityMainJavaBinding binding; private Device mDevice; private String[] mDevices; private String[] mBaudrates; private int mDeviceIndex; private int mBaudrateIndex; private boolean mOpened = false; private boolean mConversionNotice = true; private LogFragment mLogFragment; final String[] databits = new String[]{"8", "7", "6", "5"}; final String[] paritys = new String[]{"NONE", "ODD", "EVEN", "SPACE", "MARK"}; final String[] stopbits = new String[]{"1", "2"}; //先定义 private static final int REQUEST_EXTERNAL_STORAGE = 1; private static String[] PERMISSIONS_STORAGE = { "android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main_java); verifyStoragePermissions(this); initFragment(); initDevice(); initSpinners(); //设置数据位 SpAdapter spAdapter1 = new SpAdapter(this); spAdapter1.setDatas(databits); binding.spDatabits.setAdapter(spAdapter1); binding.spDatabits.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { closeSerialPort(); int dataBitValue = Integer.parseInt(databits[position]); SimpleSerialPortManager.getInstance().setDatabits(dataBitValue); SerialPortLogUtil.i("MainJavaActivity", "设置数据位: " + dataBitValue); } @Override public void onNothingSelected(AdapterView parent) { } }); //设置数据位 SpAdapter spAdapter2 = new SpAdapter(this); spAdapter2.setDatas(paritys); binding.spParity.setAdapter(spAdapter2); binding.spParity.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { closeSerialPort(); SimpleSerialPortManager.getInstance().setParity(position); SerialPortLogUtil.i("MainJavaActivity", "设置校验位: " + paritys[position] + " (值: " + position + ")"); } @Override public void onNothingSelected(AdapterView parent) { } }); //设置停止位 SpAdapter spAdapter3 = new SpAdapter(this); spAdapter3.setDatas(stopbits); binding.spStopbits.setAdapter(spAdapter3); binding.spStopbits.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { closeSerialPort(); int stopBitValue = Integer.parseInt(stopbits[position]); SimpleSerialPortManager.getInstance().setStopbits(stopBitValue); SerialPortLogUtil.i("MainJavaActivity", "设置停止位: " + stopBitValue); } @Override public void onNothingSelected(AdapterView parent) { } }); binding.btnOpenDevice.setOnClickListener(v -> { if (mOpened) { closeSerialPort(); } else { openSerialPort(); } }); binding.btnSendData.setOnClickListener((view) -> { onSend(); }); } /** * 打开串口 */ private void openSerialPort() { String devicePath = mDevice.getName(); int baudRate = Integer.parseInt(mDevice.getRoot()); SerialPortLogUtil.i("MainJavaActivity", "打开的串口为:" + devicePath + "----" + baudRate); // 使用SimpleSerialPortManager打开串口 boolean success = SimpleSerialPortManager.getInstance() .openSerialPort(devicePath, baudRate, // 打开状态回调 (isSuccess, status) -> { runOnUiThread(() -> { switch (status) { case SUCCESS_OPENED: ToastUtils.show("串口打开成功"); mOpened = true; updateViewState(true); break; case NO_READ_WRITE_PERMISSION: ToastUtils.show("没有读写权限"); updateViewState(false); break; case OPEN_FAIL: ToastUtils.show("串口打开失败"); updateViewState(false); break; } }); }, // 数据接收回调 new SimpleSerialPortManager.OnDataReceivedCallback() { @Override public void onDataReceived(byte[] data) { SerialPortLogUtil.i("MainJavaActivity", "onDataReceived [ byte[] ]: " + Arrays.toString(data)); SerialPortLogUtil.i("MainJavaActivity", "onDataReceived [ String ]: " + new String(data)); runOnUiThread(() -> { if (mConversionNotice) { LogManager.instance().post(new RecvMessage(bytesToHex(data))); } else { LogManager.instance().post(new RecvMessage(Arrays.toString(data))); } }); } @Override public void onDataSent(byte[] data) { SerialPortLogUtil.i("MainJavaActivity", "onDataSent [ byte[] ]: " + Arrays.toString(data)); SerialPortLogUtil.i("MainJavaActivity", "onDataSent [ String ]: " + new String(data)); runOnUiThread(() -> { if (mConversionNotice) { LogManager.instance().post(new SendMessage(bytesToHex(data))); } else { LogManager.instance().post(new SendMessage(Arrays.toString(data))); } }); } }); } private void closeSerialPort() { SimpleSerialPortManager.getInstance().closeSerialPort(); mOpened = false; updateViewState(mOpened); } public static void verifyStoragePermissions(Activity activity) { try { //检测是否有写的权限 int permission = ActivityCompat.checkSelfPermission(activity, "android.permission.WRITE_EXTERNAL_STORAGE"); if (permission != PackageManager.PERMISSION_GRANTED) { // 没有写的权限,去申请写的权限,会弹出对话框 ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); } } catch (Exception e) { e.printStackTrace(); } } /** * 更新视图状态 * * @param isSerialPortOpened */ private void updateViewState(boolean isSerialPortOpened) { int stringRes = isSerialPortOpened ? R.string.close_serial_port : R.string.open_serial_port; binding.btnOpenDevice.setText(stringRes); binding.spinnerDevices.setEnabled(!isSerialPortOpened); binding.spinnerBaudrate.setEnabled(!isSerialPortOpened); binding.btnSendData.setEnabled(isSerialPortOpened); binding.btnLoadList.setEnabled(isSerialPortOpened); } /** * 初始化设备列表 */ private void initDevice() { PrefHelper.initDefault(this); SerialPortFinder serialPortFinder = new SerialPortFinder(); // 设备 mDevices = serialPortFinder.getAllDevicesPath(); if (mDevices.length == 0) { mDevices = new String[]{ getString(R.string.no_serial_device) }; } // 波特率 mBaudrates = getResources().getStringArray(R.array.baudrates); mDeviceIndex = PrefHelper.getDefault().getInt(PreferenceKeys.SERIAL_PORT_DEVICES, 0); mDeviceIndex = mDeviceIndex >= mDevices.length ? mDevices.length - 1 : mDeviceIndex; mBaudrateIndex = PrefHelper.getDefault().getInt(PreferenceKeys.BAUD_RATE, 0); mDevice = new Device(mDevices[mDeviceIndex], mBaudrates[mBaudrateIndex], null); } /** * 初始化下拉选项 */ private void initSpinners() { ArrayAdapter deviceAdapter = new ArrayAdapter(this, R.layout.spinner_default_item, mDevices); deviceAdapter.setDropDownViewResource(R.layout.spinner_item); binding.spinnerDevices.setAdapter(deviceAdapter); binding.spinnerDevices.setOnItemSelectedListener(this); ArrayAdapter baudrateAdapter = new ArrayAdapter(this, R.layout.spinner_default_item, mBaudrates); baudrateAdapter.setDropDownViewResource(R.layout.spinner_item); binding.spinnerBaudrate.setAdapter(baudrateAdapter); binding.spinnerBaudrate.setOnItemSelectedListener(this); binding.spinnerDevices.setSelection(mDeviceIndex); binding.spinnerBaudrate.setSelection(mBaudrateIndex); } /** * 发送数据 */ public void onSend() { String sendContent = binding.etData.getText().toString().trim(); if (TextUtils.isEmpty(sendContent)) { SerialPortLogUtil.i("MainJavaActivity", "onSend: 发送内容为 null"); return; } byte[] sendContentBytes = sendContent.getBytes(); // 使用SimpleSerialPortManager发送数据 boolean sendBytes = SimpleSerialPortManager.getInstance().sendData(sendContentBytes); SerialPortLogUtil.i("MainJavaActivity", "onSend: sendBytes = " + sendBytes); } @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { // Spinner 选择监听 int parentId = parent.getId(); if (parentId == R.id.spinner_devices) { mDeviceIndex = position; mDevice.setName(mDevices[mDeviceIndex]); } else if (parentId == R.id.spinner_baudrate) { mBaudrateIndex = position; mDevice.setRoot(mBaudrates[mBaudrateIndex]); } } @Override public void onNothingSelected(AdapterView parent) { } @Override protected void onDestroy() { SimpleSerialPortManager.getInstance().closeSerialPort(); super.onDestroy(); } @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override public void onStop() { super.onStop(); EventBus.getDefault().unregister(this); } @Override protected void onResume() { super.onResume(); refreshLogList(); } /** * 初始化日志Fragment */ protected void initFragment() { FragmentManager fragmentManager = getSupportFragmentManager(); mLogFragment = (LogFragment) fragmentManager.findFragmentById(R.id.log_fragment); } /** * 刷新日志列表 */ protected void refreshLogList() { mLogFragment.updateAutoEndButton(); mLogFragment.updateList(); } @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(IMessage message) { // 收到时间,刷新界面 mLogFragment.add(message); } @Subscribe(threadMode = ThreadMode.MAIN) public void onConversionNotice(ConversionNoticeEvent messageEvent) { if (messageEvent.getMessage().equals("1")) { mConversionNotice = false; } else { mConversionNotice = true; } } /** * 字节数组转16进制 * * @param bytes 需要转换的byte数组 * @return 转换后的Hex字符串 */ public static String bytesToHex(byte[] bytes) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() < 2) { sb.append(0); } sb.append(hex); } return sb.toString(); } } ================================================ FILE: app/src/main/java/com/cl/myapplication/adapter/DeviceAdapter.java ================================================ package com.cl.myapplication.adapter; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import androidx.databinding.DataBindingUtil; import com.cl.myapplication.R; import com.cl.myapplication.databinding.ItemDeviceBinding; import com.cl.serialportlibrary.Device; import java.io.File; import java.util.ArrayList; /** * 串口列表适配器 */ public class DeviceAdapter extends BaseAdapter { private LayoutInflater mInflater; private ArrayList devices; public DeviceAdapter(Context context, ArrayList devices) { this.mInflater = LayoutInflater.from(context); this.devices = devices; } @Override public int getCount() { return devices.size(); } @Override public Device getItem(int position) { return devices.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ItemDeviceBinding binding; if (null == convertView) { binding = DataBindingUtil.inflate(mInflater, R.layout.item_device, parent, false); convertView = binding.getRoot(); convertView.setTag(binding); } else { binding = (ItemDeviceBinding) convertView.getTag(); } String deviceName = devices.get(position).getName(); String driverName = devices.get(position).getRoot(); File file = devices.get(position).getFile(); boolean canRead = file.canRead(); boolean canWrite = file.canWrite(); boolean canExecute = file.canExecute(); String path = file.getAbsolutePath(); StringBuffer permission = new StringBuffer(); permission.append("\t权限["); permission.append(canRead ? " 可读 " : " 不可读 "); permission.append(canWrite ? " 可写 " : " 不可写 "); permission.append(canExecute ? " 可执行 " : " 不可执行 "); permission.append("]"); binding.tvDevice.setText(String.format("%s [%s] (%s) %s", deviceName, driverName, path, permission)); return convertView; } } ================================================ FILE: app/src/main/java/com/cl/myapplication/adapter/SpAdapter.java ================================================ package com.cl.myapplication.adapter; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import androidx.databinding.DataBindingUtil; import com.cl.myapplication.R; import com.cl.myapplication.databinding.ItemDeviceBinding; public class SpAdapter extends BaseAdapter { String[] datas; Context mContext; public SpAdapter(Context context) { this.mContext = context; } public void setDatas(String[] datas) { this.datas = datas; notifyDataSetChanged(); } @Override public int getCount() { return datas == null ? 0 : datas.length; } @Override public Object getItem(int position) { return datas == null ? null : datas[position]; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ItemDeviceBinding binding; if (convertView == null) { binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.item_device, parent, false); convertView = binding.getRoot(); convertView.setTag(binding); } else { binding = (ItemDeviceBinding) convertView.getTag(); } binding.tvDevice.setText(datas[position]); return convertView; } } ================================================ FILE: app/src/main/java/com/cl/myapplication/constant/PreferenceKeys.java ================================================ package com.cl.myapplication.constant; public class PreferenceKeys { /** * 串口设备 */ public static String SERIAL_PORT_DEVICES = "serial_port_devices"; /** * 波特率 */ public static String BAUD_RATE = "baud_rate"; } ================================================ FILE: app/src/main/java/com/cl/myapplication/fragment/LogFragment.java ================================================ package com.cl.myapplication.fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.Fragment; import com.cl.myapplication.R; import com.cl.myapplication.databinding.FragmentLogBinding; import com.cl.myapplication.message.ConversionNoticeEvent; import com.cl.myapplication.message.IMessage; import com.cl.myapplication.message.LogManager; import com.cl.myapplication.util.ListViewHolder; import org.greenrobot.eventbus.EventBus; public class LogFragment extends Fragment { private LogAdapter mAdapter; private boolean mConversionNotice = true; private FragmentLogBinding binding; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_log, container, false); binding.btnClearLog.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 清空列表 LogManager.instance().clear(); updateList(); } }); binding.btnAutoEnd.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { LogManager.instance().changAutoEnd(); updateAutoEndButton(); } }); binding.btnWhetherHexadecimal.setOnClickListener((view1) -> { if (mConversionNotice){ EventBus.getDefault().post(new ConversionNoticeEvent("1")); mConversionNotice=false; }else { EventBus.getDefault().post(new ConversionNoticeEvent("2")); mConversionNotice=true; } }); mAdapter = new LogAdapter(); binding.lvLogs.setAdapter(mAdapter); updateAutoEndButton(); return binding.getRoot(); } public void updateAutoEndButton() { if (binding != null) { if (LogManager.instance().isAutoEnd()) { binding.btnAutoEnd.setText("禁止自动显示最新日志"); binding.lvLogs.setSelection(mAdapter.getCount() - 1); } else { binding.btnAutoEnd.setText("自动显示最新日志"); } } } private static class LogAdapter extends BaseAdapter { @Override public int getCount() { return LogManager.instance().messages.size(); } @Override public IMessage getItem(int position) { return LogManager.instance().messages.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { IMessage message = getItem(position); ListViewHolder holder; if (convertView == null) { holder = new ListViewHolder(R.layout.item_log, parent); convertView = holder.getItemView(); } else { holder = (ListViewHolder) convertView.getTag(); } TextView tvLog = holder.getText(R.id.tv_log); TextView tvNum = holder.getText(R.id.tv_num); tvLog.setText(message.getMessage()); tvLog.setEnabled(message.isToSend()); tvNum.setText(String.valueOf(position + 1)); return convertView; } } public void add(IMessage message) { LogManager.instance().add(message); updateList(); } public void updateList() { if (binding != null) { mAdapter.notifyDataSetChanged(); if (LogManager.instance().isAutoEnd()) { binding.lvLogs.setSelection(mAdapter.getCount() - 1); } } } } ================================================ FILE: app/src/main/java/com/cl/myapplication/message/ConversionNoticeEvent.java ================================================ package com.cl.myapplication.message; /** * 项目:serialPort * 作者:Arry * 创建日期:2021/10/20 * 描述: * 修订历史: */ public class ConversionNoticeEvent { private String message; public ConversionNoticeEvent(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } ================================================ FILE: app/src/main/java/com/cl/myapplication/message/IMessage.java ================================================ package com.cl.myapplication.message; /** * 日志消息数据接口 */ public interface IMessage { /** * 消息文本 * * @return */ String getMessage(); /** * 是否发送的消息 * * @return */ boolean isToSend(); } ================================================ FILE: app/src/main/java/com/cl/myapplication/message/LogManager.java ================================================ package com.cl.myapplication.message; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; import java.util.List; /** * log管理类 */ public class LogManager { public final List messages; private boolean mAutoEnd = true; public LogManager() { messages = new ArrayList<>(); } private static class InstanceHolder { public static LogManager sManager = new LogManager(); } public static LogManager instance() { return InstanceHolder.sManager; } public void add(IMessage message) { messages.add(message); } public void post(IMessage message) { EventBus.getDefault().post(message); } public void clear() { messages.clear(); } public boolean isAutoEnd() { return mAutoEnd; } public void setAutoEnd(boolean autoEnd) { mAutoEnd = autoEnd; } public void changAutoEnd() { mAutoEnd = !mAutoEnd; } } ================================================ FILE: app/src/main/java/com/cl/myapplication/message/RecvMessage.java ================================================ package com.cl.myapplication.message; import com.cl.myapplication.util.TimeUtil; /** * 收到的日志 */ public class RecvMessage implements IMessage { private String command; private String message; public RecvMessage(String command) { this.command = command; this.message = TimeUtil.currentTime() + " 收到命令:" + command; } @Override public String getMessage() { return message; } @Override public boolean isToSend() { return false; } } ================================================ FILE: app/src/main/java/com/cl/myapplication/message/SendMessage.java ================================================ package com.cl.myapplication.message; import com.cl.myapplication.util.TimeUtil; /** * 发送的日志 */ public class SendMessage implements IMessage { private String command; private String message; public SendMessage(String command) { this.command = command; this.message = TimeUtil.currentTime() + " 发送命令:" + command; } @Override public String getMessage() { return message; } @Override public boolean isToSend() { return true; } } ================================================ FILE: app/src/main/java/com/cl/myapplication/util/ByteUtil.java ================================================ package com.cl.myapplication.util; /** * name:cl * date:2022/12/27 * desc:Byte工具类 */ public class ByteUtil { /** * 字节数组转16进制 * * @param bytes 需要转换的byte数组 * @return 转换后的Hex字符串 */ public static String bytesToHex(byte[] bytes) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() < 2) { sb.append(0); } sb.append(hex); } return sb.toString(); } public static String trim(String s) { int i = s.length();// 字符串最后一个字符的位置 int j = 0;// 字符串第一个字符 int k = 0;// 中间变量 char[] arrayOfChar = s.toCharArray();// 将字符串转换成字符数组 while ((j < i) && (arrayOfChar[(k + j)] <= ' ')) ++j;// 确定字符串前面的空格数 while ((j < i) && (arrayOfChar[(k + i - 1)] <= ' ')) --i;// 确定字符串后面的空格数 return (((j > 0) || (i < s.length())) ? s.substring(j, i) : s);// 返回去除空格后的字符串 } public static String toChineseHex(String s) { String ss = s; byte[] bt = new byte[0]; try { bt = ss.getBytes("UTF-8"); } catch (Exception e) { e.printStackTrace(); } String s1 = ""; for (int i = 0; i < bt.length; i++) { String tempStr = Integer.toHexString(bt[i]); if (tempStr.length() > 2) tempStr = tempStr.substring(tempStr.length() - 2); s1 = s1 + tempStr + ""; } return s1.toUpperCase(); } /** * 异或校验,返回一个字节 */ public static byte orVerification(byte[] bytes) { int nAll = 0; for (int i = 0; i < bytes.length; i++) { int nTemp = bytes[i]; nAll = nAll ^ nTemp; } return (byte) nAll; } public static byte complement(byte[] bytes) { int iSum = 0; for (int i = 0; i < bytes.length; i++) { iSum += bytes[i]; } iSum = 256 - iSum; return (byte) iSum; } /** * 多个byte数组合并 */ public static byte[] byteMergerAll(byte[]... values) { int length_byte = 0; for (int i = 0; i < values.length; i++) { length_byte += values[i].length; } byte[] all_byte = new byte[length_byte]; int countLength = 0; for (int i = 0; i < values.length; i++) { byte[] b = values[i]; System.arraycopy(b, 0, all_byte, countLength, b.length); countLength += b.length; } return all_byte; } public static byte[] hex2Byte(String hex) { String[] parts = hex.split(" "); byte[] bytes = new byte[parts.length]; for (int i = 0; i < parts.length; i++) { bytes[i] = (byte) Integer.parseInt(parts[i], 16); } return bytes; } } ================================================ FILE: app/src/main/java/com/cl/myapplication/util/ListViewHolder.java ================================================ package com.cl.myapplication.util; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; public class ListViewHolder { private SparseArray mViewArray; public View itemView; public int position; public ListViewHolder(View itemView) { this.itemView = itemView; mViewArray = new SparseArray<>(); this.itemView.setTag(this); } public ListViewHolder(int layoutId, ViewGroup parent) { View view = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); this.itemView = view; mViewArray = new SparseArray<>(); this.itemView.setTag(this); } public View getItemView() { return itemView; } public void bindPosition(int position) { this.position = position; } public int getPosition() { return position; } public V getView(int resId) { View view = mViewArray.get(resId); if (view == null) { view = itemView.findViewById(resId); mViewArray.put(resId, view); } return (V) view; } public void setText(int resId, CharSequence text) { TextView textView = getView(resId); textView.setText(text); } public TextView getText(int id) { return getView(id); } public ImageView getImage(int id) { return getView(id); } } ================================================ FILE: app/src/main/java/com/cl/myapplication/util/PrefHelper.java ================================================ package com.cl.myapplication.util; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; public class PrefHelper { private static PrefHelper sInstance; private SharedPreferences mPreferences; public static void initDefault(Context context) { sInstance = new PrefHelper(PreferenceManager.getDefaultSharedPreferences(context)); } public static PrefHelper getDefault() { return sInstance; } public static PrefHelper get(Context context, String name) { return new PrefHelper(context, name); } private PrefHelper(SharedPreferences preferences) { mPreferences = preferences; } private PrefHelper(Context context, String name) { mPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE); } public SharedPreferences.Editor edit() { return mPreferences.edit(); } public SharedPreferences.Editor putInt(String key, int value) { return edit().putInt(key, value); } public void saveInt(String key, int value) { putInt(key, value).apply(); } public int getInt(String key, int defValue) { return mPreferences.getInt(key, defValue); } public SharedPreferences.Editor putFloat(String key, float value) { return edit().putFloat(key, value); } public void saveFloat(String key, float value) { putFloat(key, value).apply(); } public float getFloat(String key, float defValue) { return mPreferences.getFloat(key, defValue); } public SharedPreferences.Editor putBoolean(String key, boolean value) { return edit().putBoolean(key, value); } public void saveBoolean(String key, boolean value) { putBoolean(key, value).apply(); } public boolean getBoolean(String key, boolean defValue) { return mPreferences.getBoolean(key, defValue); } public SharedPreferences.Editor putLong(String key, long value) { return edit().putLong(key, value); } public void saveLong(String key, long value) { putLong(key, value).apply(); } public long getLong(String key, long defValue) { return mPreferences.getLong(key, defValue); } public SharedPreferences.Editor putString(String key, String value) { return edit().putString(key, value); } public void saveString(String key, String value) { putString(key, value).apply(); } public String getString(String key, String defValue) { return mPreferences.getString(key, defValue); } } ================================================ FILE: app/src/main/java/com/cl/myapplication/util/TimeUtil.java ================================================ package com.cl.myapplication.util; import java.text.SimpleDateFormat; import java.util.Date; public class TimeUtil { public static final SimpleDateFormat DEFAULT_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); public static String currentTime() { Date date = new Date(); return DEFAULT_FORMAT.format(date); } } ================================================ FILE: app/src/main/res/color/selector_log_text.xml ================================================ ================================================ FILE: app/src/main/res/color/selector_spinner_text.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================