diff --git a/.gitignore b/.gitignore
index a8b0d1d..aa724b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,35 +1,15 @@
-# ---> Android
-# Gradle files
-.gradle/
-build/
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Log/OS Files
-*.log
-
-# Android Studio generated files and folders
-captures/
-.externalNativeBuild/
-.cxx/
-*.apk
-output.json
-
-# IntelliJ
*.iml
-.idea/
-misc.xml
-deploymentTargetDropDown.xml
-render.experimental.xml
-
-# Keystore files
-*.jks
-*.keystore
-
-# Google Services (e.g. APIs or Firebase)
-google-services.json
-
-# Android Profiling
-*.hprof
-
+.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
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..eaf91e2
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..8b3f102
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..0d46093
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..863b1be
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..48052b2
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..ae3bf2d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..5bd6771
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index fbbeba0..e0f5ca5 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,146 @@
-# iB86
-
+# ASR5515串口通信Android应用
+
+## 项目概述
+这是一个用于与ASR5515设备进行串口通信的Android应用,提供设备管理、状态监控、固件升级等功能。应用通过串口(/dev/ttyS3)与设备通信,波特率115200。
+
+## 主要功能
+- 设备检查与状态监控
+- 设备重启
+- 日志控制
+- 蓝牙版本查询
+- 蓝牙固件升级
+- 主机状态同步
+- 心率血压血氧(HR/BP/BO)测量
+ - 自动测量
+ - 手动测量
+- 动态测量功能
+- Boot bin检查
+- 采集频率设置
+- 支持模拟模式(不实际连接硬件)
+
+## 项目结构
+```
+com.ismart.ib86
+├── app/ # 主应用相关
+│ └── MainActivity.java
+├── common/ # 公共组件
+│ └── utils/ # 工具类
+└── feature/ # 功能模块
+ └── serial/ # 串口通信实现
+ ├── ASR5515DeviceManager.java
+ ├── ASR5515Protocol.java
+ └── SerialPortHelper.java
+```
+
+## 核心类说明
+
+### 1. ASR5515Protocol
+协议核心类,定义ASR5515设备的通信协议格式和命令。
+
+**主要功能:**
+- 定义设备信息结构(DeviceInfo)
+- 定义各种命令(Commands)
+- 定义帧结构(Frame)
+- 提供各种请求和响应的创建与解析方法
+
+### 2. SerialPortHelper
+串口通信核心实现类,封装了与ASR5515设备的通信逻辑。
+
+**主要功能:**
+- 串口初始化和管理
+- 数据发送和接收
+- ASR5515协议命令的封装
+- 回调机制处理各种响应
+- 支持模拟模式
+
+### 3. ASR5515DeviceManager
+设备管理核心类,提供高级设备管理接口。
+
+**主要功能:**
+- 设备状态检查
+- 设备重启
+- 日志控制
+- 蓝牙版本查询
+- 蓝牙升级
+- 主机状态同步
+
+### 4. MainActivity
+应用主界面,提供用户交互界面。
+
+## 使用说明
+
+### 初始化串口
+```java
+SerialPortHelper serialPortHelper = new SerialPortHelper("/dev/ttyS1", 115200);
+serialPortHelper.setDeviceStatusCallback(new DeviceStatusCallback() {
+ @Override
+ public void onDeviceCheckResponse(DeviceInfo deviceInfo) {
+ // 处理设备检查响应
+ }
+
+ // 实现其他回调方法...
+});
+serialPortHelper.open();
+```
+
+### 发送命令
+```java
+// 设备检查
+serialPortHelper.checkDevice();
+
+// 设备重启
+serialPortHelper.restartDevice();
+
+// 查询蓝牙版本
+serialPortHelper.queryBtVersion();
+
+// 蓝牙升级
+serialPortHelper.upgradeBt();
+```
+
+### 关闭串口
+```java
+serialPortHelper.close();
+```
+
+## 开发环境要求
+- Android Studio 最新稳定版
+- Android SDK API 21+
+- JDK 8+
+- 支持串口的Android设备或模拟器
+
+## 硬件连接指南
+1. 确保ASR5515设备已正确供电
+2. 使用USB转串口线连接设备与Android主机
+3. 确认串口设备节点(通常为/dev/ttyS3)
+4. 设置波特率为115200
+
+## 构建与运行
+1. 克隆项目
+```bash
+git clone https://github.com/ismart/ib86.git
+```
+2. 导入Android Studio
+3. 配置设备连接参数(在MainActivity中修改)
+4. 构建并运行应用
+
+## 测试方法
+1. 单元测试
+```bash
+./gradlew test
+```
+2. 功能测试
+- 使用模拟模式测试基本功能
+- 连接实际设备测试通信功能
+- 测试各种测量功能(HR/BP/BO)
+
+## 依赖项
+- Android SerialPort API
+- ASR5515通信协议(参考ASR-5515通信协议.docx)
+
+## 注意事项
+1. 需要设备支持串口通信
+2. 需要正确配置串口名称和波特率
+3. 模拟模式可用于开发测试
+4. 实际使用时请参考ASR5515设备的具体通信协议
+5. 测量功能需要ASR5515设备支持相应传感器
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..8c6bdf4
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,49 @@
+plugins {
+ alias(libs.plugins.android.application)
+}
+
+android {
+ namespace 'com.ismart.ib86.app'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.ismart.ib86.app"
+ minSdk 26
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ ndk {
+ //ֵ֧SOܹ߿ԸҪѡһƽ̨so
+ abiFilters "armeabi-v7a"
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+ sourceSets {
+ main {
+ jniLibs.srcDir 'libs'
+ }
+ }
+}
+
+dependencies {
+
+ implementation libs.appcompat
+ implementation libs.material
+ implementation libs.activity
+ implementation libs.constraintlayout
+ testImplementation libs.junit
+ androidTestImplementation libs.ext.junit
+ androidTestImplementation libs.espresso.core
+}
\ No newline at end of file
diff --git a/app/libs/armeabi-v7a/libserial_port.so b/app/libs/armeabi-v7a/libserial_port.so
new file mode 100644
index 0000000..e4f57b6
Binary files /dev/null and b/app/libs/armeabi-v7a/libserial_port.so differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# 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
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..209f24e
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/android/serialport/SerialPort.java b/app/src/main/java/android/serialport/SerialPort.java
new file mode 100644
index 0000000..39da810
--- /dev/null
+++ b/app/src/main/java/android/serialport/SerialPort.java
@@ -0,0 +1,89 @@
+package android.serialport;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * 串口通信,打开串口,读写数据
+ */
+public class SerialPort {
+
+ private FileDescriptor mFd;
+ private FileInputStream mFileInputStream;
+ private FileOutputStream mFileOutputStream;
+
+ static {
+ System.loadLibrary("serial_port");
+ }
+
+ /**
+ * 打开串口
+ *
+ * @param device
+ * @param baudrate
+ */
+ public SerialPort(File device, int baudrate) throws IOException {
+ if (!device.canRead() || !device.canWrite()) {
+ try {
+ Process su;
+ su = Runtime.getRuntime().exec("su");
+ String cmd = "chmod 777 " + device.getAbsolutePath();
+ su.getOutputStream().write(cmd.getBytes());
+ su.getOutputStream().flush();
+ int waitFor = su.waitFor();
+ boolean canRead = device.canRead();
+ boolean canWrite = device.canWrite();
+ if (waitFor != 0 || !canRead || !canWrite) {
+ throw new SecurityException();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ mFd = open(device.getAbsolutePath(), baudrate);
+
+ if (mFd == null) {
+ throw new IOException();
+ }
+ mFileInputStream = new FileInputStream(mFd);
+ mFileOutputStream = new FileOutputStream(mFd);
+ }
+
+ /**
+ * 关闭串口
+ */
+ public void closePort() {
+ if (this.mFd != null) {
+ try {
+ this.close();
+ this.mFd = null;
+ this.mFileInputStream = null;
+ this.mFileOutputStream = null;
+ } catch (Exception var2) {
+ var2.printStackTrace();
+ }
+ }
+ }
+
+ public InputStream getInputStream() {
+ return mFileInputStream;
+ }
+
+ public OutputStream getOutputStream() {
+ return mFileOutputStream;
+ }
+
+ /**
+ * JNI,设备地址和波特率
+ */
+ private native static FileDescriptor open(String path, int baudrate);
+
+ private native void close();
+
+
+}
diff --git a/app/src/main/java/android/serialport/SerialPortFinder.java b/app/src/main/java/android/serialport/SerialPortFinder.java
new file mode 100644
index 0000000..6107cb9
--- /dev/null
+++ b/app/src/main/java/android/serialport/SerialPortFinder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2009 Cedric Priscal
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.serialport;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.util.Iterator;
+import java.util.Vector;
+
+public class SerialPortFinder {
+
+ private static final String TAG = "info";
+ private Vector mDrivers = null;
+
+ Vector getDrivers() throws IOException {
+ if (mDrivers == null) {
+ mDrivers = new Vector();
+ LineNumberReader r = new LineNumberReader(new FileReader("/proc/tty/drivers"));
+ String l;
+ while ((l = r.readLine()) != null) {
+ String[] w = l.split(" +");
+ if ((w.length == 5) && ("serial".equals(w[4]))) {
+ mDrivers.add(new Driver(w[0], w[1]));
+ }
+ }
+ r.close();
+ }
+ return mDrivers;
+ }
+
+ /**
+ * 获取所有的设备,名字
+ *
+ * @return
+ */
+ public String[] getDevices() {
+ Vector devices = new Vector();
+ Iterator itdriv;
+ try {
+ itdriv = getDrivers().iterator();
+ while (itdriv.hasNext()) {
+ Driver driver = itdriv.next();
+ Iterator itdev = driver.getDevices().iterator();
+ while (itdev.hasNext()) {
+ String device = itdev.next().getName();
+ String value = String.format("%s (%s)", device, driver.getName());
+ devices.add(value);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return devices.toArray(new String[devices.size()]);
+ }
+
+ /**
+ * 获取所有设备的地址
+ *
+ * @return
+ */
+ public String[] getDevicesPaths() {
+ Vector devices = new Vector();
+ Iterator itdriv;
+ try {
+ itdriv = getDrivers().iterator();
+ while (itdriv.hasNext()) {
+ Driver driver = itdriv.next();
+ Iterator itdev = driver.getDevices().iterator();
+ while (itdev.hasNext()) {
+ String device = itdev.next().getAbsolutePath();
+ devices.add(device);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return devices.toArray(new String[devices.size()]);
+ }
+
+ public class Driver {
+ Vector mDevices = null;
+ private String mDriverName;
+ private String mDeviceRoot;
+
+ public Driver(String name, String root) {
+ mDriverName = name;
+ mDeviceRoot = root;
+ }
+
+ public Vector getDevices() {
+ if (mDevices == null) {
+ mDevices = new Vector();
+ File dev = new File("/dev");
+ File[] files = dev.listFiles();
+ int i;
+ for (i = 0; i < files.length; i++) {
+ if (files[i].getAbsolutePath().startsWith(mDeviceRoot)) {
+ mDevices.add(files[i]);
+ }
+ }
+ }
+ return mDevices;
+ }
+
+ public String getName() {
+ return mDriverName;
+ }
+ }
+}
diff --git a/app/src/main/java/com/ismart/ib86/app/MainActivity.java b/app/src/main/java/com/ismart/ib86/app/MainActivity.java
new file mode 100644
index 0000000..32042fb
--- /dev/null
+++ b/app/src/main/java/com/ismart/ib86/app/MainActivity.java
@@ -0,0 +1,297 @@
+package com.ismart.ib86.app;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.os.Build;
+import android.view.View;
+import android.util.Log;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.ismart.ib86.app.R;
+import com.ismart.ib86.feature.serial.SerialPort.ASR5515Protocol;
+import com.ismart.ib86.feature.serial.SerialPort.SerialPortHelper;
+import com.ismart.ib86.feature.gpio.GpioManager;
+import com.ismart.ib86.feature.serial.SerialPort.ASR5515DeviceManager;
+import com.ismart.ib86.feature.serial.SerialPort.ASR5515Protocol;
+import com.ismart.ib86.common.utils.LogManager;
+import com.ismart.ib86.feature.gpio.GpioTest;
+
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "MainActivity";
+ private static final String DEVICE_PATH = "/dev/ttyS2";
+ private static final int BAUD_RATE = 921600;
+
+ // 权限请求码
+ private static final int PERMISSION_REQUEST_CODE = 1001;
+ // 需要请求的权限
+ private static final String[] REQUIRED_PERMISSIONS = {
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ };
+
+ private static final int BT_POWER_EN = 75;
+ private ASR5515DeviceManager deviceManager;
+ private Handler handler = new Handler();
+ private GpioTest gpioTest;
+ private GpioManager gpioManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ // 检查并请求必要的权限
+ checkAndRequestPermissions();
+
+ // 隐藏状态栏和导航栏
+ hideSystemBars();
+ // 初始化设备管理器
+ initDeviceManager();
+
+ // 延迟2秒后开始测试,确保设备管理器初始化完成
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ startProtocolTests();
+ }
+ }, 2000);
+ }
+
+ private void initDeviceManager() {
+ //打开5515电源
+ gpioManager = GpioManager.getInstance();
+ gpioManager.exportGpio(BT_POWER_EN);
+ gpioManager.setDirection(BT_POWER_EN, GpioManager.DIRECTION_OUT);
+ gpioManager.setValue(BT_POWER_EN, GpioManager.VALUE_HIGH);
+
+ deviceManager = new ASR5515DeviceManager(this, DEVICE_PATH, BAUD_RATE);
+ // 设置设备检查回调
+ deviceManager.setDeviceCheckListener(deviceInfo -> {
+ if (deviceInfo != null) {
+ LogManager.d(TAG, "设备检查成功: " + deviceInfo.toString());
+ } else {
+ LogManager.e(TAG, "设备检查失败");
+ }
+ });
+
+ // 设置设备控制回调
+ deviceManager.setDeviceControlListener(new ASR5515DeviceManager.DeviceControlListener() {
+ @Override
+ public void onDeviceRestartResponse(boolean success) {
+ if (success) {
+ LogManager.d(TAG, "设备重启成功");
+ } else {
+ LogManager.e(TAG, "设备重启失败");
+ }
+ }
+ });
+
+ // 设置日志控制回调
+ deviceManager.setLogControlListener(new ASR5515DeviceManager.LogControlListener() {
+ @Override
+ public void onLogControlResponse(boolean success) {
+ if (success) {
+ LogManager.d(TAG, "日志控制设置成功");
+ } else {
+ LogManager.e(TAG, "日志控制设置失败");
+ }
+ }
+ });
+
+ // 设置蓝牙版本回调
+ deviceManager.setBtVersionListener(version -> {
+ if (version != null) {
+ LogManager.d(TAG, "蓝牙版本: " + version);
+ } else {
+ LogManager.e(TAG, "获取蓝牙版本失败");
+ }
+ });
+
+ // 设置蓝牙升级回调
+ deviceManager.setBtUpgradeListener(response ->
+ LogManager.d(TAG, "蓝牙升级: " + response.toString())
+ );
+
+ // 设置主机状态回调
+ deviceManager.setHostStatusListener(new ASR5515DeviceManager.HostStatusListener() {
+ @Override
+ public void onHostStatusResponse(ASR5515Protocol.HostStatusResponse response) {
+ LogManager.d(TAG, "主机状态: " + response.status);
+ }
+ });
+ deviceManager.setBootBinCheckListener(new ASR5515DeviceManager.BootBinCheckListener() {
+ @Override
+ public void onBootBinCheckResponse(ASR5515Protocol.BootBinCheckResponse response) {
+ LogManager.d(TAG, "Boot bin检查: " + response.toString());
+ }
+ });
+ deviceManager.setCollectFreqSetListener(new ASR5515DeviceManager.CollectFreqSetListener() {
+ @Override
+ public void onCollectFreqSetResponse(ASR5515Protocol.CollectFreqSetResponse response) {
+ LogManager.d(TAG, "采集频率设置: " + response.toString());
+ }
+ });
+
+ // 设置心率血压血氧自动测量回调
+ deviceManager.setHrBpBoAutoMeasureListener(new ASR5515DeviceManager.HrBpBoAutoMeasureListener() {
+ @Override
+ public void onHrBpBoAutoMeasureResponse(ASR5515Protocol.HrBpBoAutoMeasureResponse response) {
+ LogManager.d(TAG, "HrBpBo自动测量: " + response.toString());
+ }
+ });
+
+ // 设置心率血压血氧手动测量回调
+ deviceManager.setHrBpBoManualMeasureListener(new ASR5515DeviceManager.HrBpBoManualMeasureListener() {
+ @Override
+ public void onHrBpBoManualMeasureResponse(ASR5515Protocol.HrBpBoManualMeasureResponse response) {
+ LogManager.d(TAG, "HrBpBo手动测量: " + response.toString());
+ }
+ });
+
+ // 设置动态测量回调
+ deviceManager.setDynamicMeasureListener(response ->
+ LogManager.d(TAG, "动态测量: " + response.toString())
+ );
+
+ // 设置佩戴检测回调
+ deviceManager.setWearDetectionListener(response ->
+ LogManager.d(TAG, "佩戴检测: " + response.isWearing)
+ );
+ }
+
+ private void startProtocolTests() {
+ LogManager.d(TAG, "开始协议测试...");
+
+// handler.postDelayed(new Runnable() {
+// @Override
+// public void run() {
+// deviceManager.checkDevice();
+// }
+// }, 1000);
+//
+// handler.postDelayed(new Runnable() {
+// @Override
+// public void run() {
+// deviceManager.queryBtVersion();
+// }
+// }, 2000);
+
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ deviceManager.sendWearDetectionRequest();
+ }
+ }, 1000);
+
+// handler.postDelayed(new Runnable() {
+// @Override
+// public void run() {
+// deviceManager.sendWearDetectionRequest();
+// }
+// }, 4000);
+ }
+
+ @Override
+protected void onDestroy() {
+ super.onDestroy();
+ handler.removeCallbacksAndMessages(null);
+
+ // 移至后台线程执行耗时操作,避免ANR
+ new Thread(() -> {
+ if (deviceManager != null) {
+ deviceManager.syncHostStatus(false);
+ deviceManager.close();
+ }
+
+ // 释放GPIO测试资源
+ if (gpioTest != null) {
+ gpioTest.release();
+ gpioTest = null;
+ }
+ }).start();
+}
+
+private void hideSystemBars() {
+ Window window = getWindow();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ WindowInsetsController insetsController = window.getInsetsController();
+ if (insetsController != null) {
+ // 隐藏状态栏和导航栏 (API 30+)
+ insetsController.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
+ insetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ }
+ } else {
+ // 兼容旧版本 (API < 30)
+ View decorView = window.getDecorView();
+ int flags = View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+ decorView.setSystemUiVisibility(flags);
+ }
+}
+
+@Override
+public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ hideSystemBars();
+ }
+}
+
+/**
+ * 检查并请求必要的权限
+ */
+private void checkAndRequestPermissions() {
+ // 检查是否已经有所需的权限
+ boolean allPermissionsGranted = true;
+ for (String permission : REQUIRED_PERMISSIONS) {
+ if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+ allPermissionsGranted = false;
+ break;
+ }
+ }
+
+ // 如果没有所有权限,则请求它们
+ if (!allPermissionsGranted) {
+ ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, PERMISSION_REQUEST_CODE);
+ }
+}
+
+/**
+ * 处理权限请求结果
+ */
+@Override
+public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (requestCode == PERMISSION_REQUEST_CODE) {
+ boolean allGranted = true;
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ allGranted = false;
+ break;
+ }
+ }
+
+ if (allGranted) {
+ // 所有权限都已授予,可以继续操作
+ LogManager.d(TAG, "所有必要权限已授予");
+
+ } else {
+ // 有些权限被拒绝,显示提示并可能影响功能
+ Toast.makeText(this, "需要所有请求的权限才能正常运行应用", Toast.LENGTH_LONG).show();
+ LogManager.e(TAG, "部分权限被拒绝,可能影响应用功能");
+ }
+ }
+}
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ismart/ib86/common/utils/LogManager.java b/app/src/main/java/com/ismart/ib86/common/utils/LogManager.java
new file mode 100644
index 0000000..9d5b71e
--- /dev/null
+++ b/app/src/main/java/com/ismart/ib86/common/utils/LogManager.java
@@ -0,0 +1,122 @@
+package com.ismart.ib86.common.utils;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+
+public final class LogManager implements Thread.UncaughtExceptionHandler {
+ private static LogManager instance = new LogManager();
+
+ private LogManager() {
+ } // 私有构造方法防止实例化
+
+ private static boolean isLogToFile = false;
+ private static String LOG_DIR = "logs";
+ private static Context appContext;
+ private static final long MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
+ private static final int MAX_LOG_FILES = 30;
+
+ public static void init(Context context) {
+ Thread.setDefaultUncaughtExceptionHandler(instance);
+ appContext = context.getApplicationContext();
+ LOG_DIR = appContext.getExternalFilesDir(null).getAbsolutePath() + "/logs";
+ }
+
+ private static final SimpleDateFormat FILE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd", Locale.getDefault());
+ private static final SimpleDateFormat LOG_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS",
+ Locale.getDefault());
+
+ public static void setLogToFile(boolean enable) {
+ isLogToFile = enable;
+ }
+
+ private static void writeToFile(String tag, String level, String msg) {
+ if (!isLogToFile)
+ return;
+
+ if (appContext == null) {
+ Log.e("LogManager", "Context not initialized, call init() first");
+ return;
+ }
+
+ try {
+ File logDir = new File(LOG_DIR);
+ String fullPath = logDir.getAbsolutePath();
+ Log.d("LogPath", "日志存储路径: " + fullPath);
+ if (!logDir.exists()) {
+ logDir.mkdirs();
+ }
+
+ // 清理旧日志文件
+ File[] logFiles = logDir.listFiles();
+ if (logFiles != null && logFiles.length > MAX_LOG_FILES) {
+ Arrays.sort(logFiles, (f1, f2) -> Long.compare(f1.lastModified(), f2.lastModified()));
+ for (int i = 0; i < logFiles.length - MAX_LOG_FILES; i++) {
+ logFiles[i].delete();
+ }
+ }
+
+ String fileName = "log_" + FILE_DATE_FORMAT.format(new Date()) + ".txt";
+ File logFile = new File(logDir, fileName);
+
+ // 检查当前日志文件大小
+ if (logFile.exists() && logFile.length() > MAX_LOG_SIZE) {
+ logFile.delete();
+ }
+
+ String threadInfo = "[" + Thread.currentThread().getId() + "/" + Thread.currentThread().getName() + "]";
+ String deviceInfo = "[" + android.os.Build.MODEL + "/" + android.os.Build.VERSION.RELEASE + "]";
+ String logContent = LOG_DATE_FORMAT.format(new Date()) + " " + level + "/" + tag + ": " + threadInfo + " "
+ + deviceInfo + " " + msg + "\n";
+
+ FileOutputStream fos = new FileOutputStream(logFile, true);
+ fos.write(logContent.getBytes());
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static int v(String tag, String msg) {
+ writeToFile(tag, "VERBOSE", msg);
+ return Log.v(tag, msg);
+ }
+
+ public static int d(String tag, String msg) {
+ writeToFile(tag, "DEBUG", msg);
+ return Log.d(tag, msg);
+ }
+
+ public static int i(String tag, String msg) {
+ writeToFile(tag, "INFO", msg);
+ return Log.i(tag, msg);
+ }
+
+ public static int w(String tag, String msg) {
+ writeToFile(tag, "WARN", msg);
+ return Log.w(tag, msg);
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ ex.printStackTrace(pw);
+ String stackTrace = sw.toString();
+ e("CRASH", "Uncaught exception in thread " + thread.getName() + ": " + ex.getMessage() + "\n" + stackTrace);
+ }
+
+ public static int e(String tag, String msg) {
+ writeToFile(tag, "ERROR", msg);
+ return Log.e(tag, msg);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ismart/ib86/feature/gpio/GpioManager.java b/app/src/main/java/com/ismart/ib86/feature/gpio/GpioManager.java
new file mode 100644
index 0000000..2db1f87
--- /dev/null
+++ b/app/src/main/java/com/ismart/ib86/feature/gpio/GpioManager.java
@@ -0,0 +1,368 @@
+package com.ismart.ib86.feature.gpio;
+
+import com.ismart.ib86.common.utils.LogManager;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * GPIO管理类,提供GPIO的基本操作
+ * 纯Java实现,直接操作/sys/class/gpio/目录下的文件
+ */
+public class GpioManager {
+ private static final String TAG = "GpioManager";
+ private static volatile GpioManager instance;
+
+ // GPIO方向常量
+ public static final int DIRECTION_IN = 0;
+ public static final int DIRECTION_OUT = 1;
+
+ // GPIO值常量
+ public static final int VALUE_LOW = 0;
+ public static final int VALUE_HIGH = 1;
+
+ // GPIO路径常量
+ private static final String GPIO_PATH = "/sys/class/gpio/";
+ private static final String GPIO_EXPORT_PATH = GPIO_PATH + "export";
+ private static final String GPIO_UNEXPORT_PATH = GPIO_PATH + "unexport";
+
+ // 文件缓存,避免频繁打开关闭文件
+ private final Map fileCache = new HashMap<>();
+
+ // 最后的错误信息
+ private String lastErrorMsg = "";
+
+ private GpioManager() {
+ init();
+ }
+
+ public static GpioManager getInstance() {
+ if (instance == null) {
+ synchronized (GpioManager.class) {
+ if (instance == null) {
+ instance = new GpioManager();
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * 初始化GPIO管理器
+ * @return 初始化是否成功
+ */
+ private boolean init() {
+ LogManager.d(TAG, "初始化GPIO管理器");
+
+ // 检查GPIO目录是否存在
+ File gpioDir = new File(GPIO_PATH);
+ if (!gpioDir.exists() || !gpioDir.isDirectory()) {
+ setLastError("GPIO sysfs接口不可用: 目录不存在");
+ return false;
+ }
+ // 检查export和unexport文件的访问权限
+ File exportFile = new File(GPIO_EXPORT_PATH);
+ File unexportFile = new File(GPIO_UNEXPORT_PATH);
+
+ if (!exportFile.exists() || !exportFile.canWrite()) {
+ setLastError("无法访问文件 " + GPIO_EXPORT_PATH + ": 文件不存在或无写入权限");
+ return false;
+ }
+
+ if (!unexportFile.exists() || !unexportFile.canWrite()) {
+ setLastError("无法访问文件 " + GPIO_UNEXPORT_PATH + ": 文件不存在或无写入权限");
+ return false;
+ }
+
+ LogManager.d(TAG, "GPIO管理器初始化成功");
+ return true;
+ }
+
+ /**
+ * 导出GPIO引脚
+ * @param pin GPIO引脚号
+ * @return 操作是否成功
+ */
+ public boolean exportGpio(int pin) {
+ //LogManager.d(TAG, "导出GPIO: " + pin);
+
+ // 检查GPIO是否已经导出
+ String gpioPath = GPIO_PATH + "gpio" + pin;
+ File gpioDir = new File(gpioPath);
+
+ if (gpioDir.exists() && gpioDir.isDirectory()) {
+ //LogManager.d(TAG, "GPIO " + pin + " 已经导出");
+ return true;
+ }
+
+ // 导出GPIO
+ if (!writeToFile(GPIO_EXPORT_PATH, String.valueOf(pin))) {
+ return false;
+ }
+
+ // 等待GPIO目录创建完成
+ for (int i = 0; i < 10; i++) {
+ if (gpioDir.exists() && gpioDir.isDirectory()) {
+ //LogManager.d(TAG, "GPIO " + pin + " 导出成功");
+ return true;
+ }
+
+ try {
+ TimeUnit.MILLISECONDS.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LogManager.e(TAG, "等待GPIO导出被中断: " + e.getMessage());
+ }
+ }
+
+ setLastError("GPIO " + pin + " 导出超时");
+ return false;
+ }
+
+ /**
+ * 取消导出GPIO引脚
+ * @param pin GPIO引脚号
+ * @return 操作是否成功
+ */
+ public boolean unexportGpio(int pin) {
+ LogManager.d(TAG, "取消导出GPIO: " + pin);
+
+ // 检查GPIO是否已经导出
+ String gpioPath = GPIO_PATH + "gpio" + pin;
+ File gpioDir = new File(gpioPath);
+
+ if (!gpioDir.exists() || !gpioDir.isDirectory()) {
+ LogManager.d(TAG, "GPIO " + pin + " 未导出");
+ return true;
+ }
+
+ // 从缓存中移除相关文件
+ String directionPath = gpioPath + "/direction";
+ String valuePath = gpioPath + "/value";
+
+ synchronized (fileCache) {
+ fileCache.remove(directionPath);
+ fileCache.remove(valuePath);
+ }
+
+ // 取消导出GPIO
+ if (!writeToFile(GPIO_UNEXPORT_PATH, String.valueOf(pin))) {
+ return false;
+ }
+
+ // 等待GPIO目录删除完成
+ for (int i = 0; i < 10; i++) {
+ if (!gpioDir.exists()) {
+ LogManager.d(TAG, "GPIO " + pin + " 取消导出成功");
+ return true;
+ }
+
+ try {
+ TimeUnit.MILLISECONDS.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LogManager.e(TAG, "等待GPIO取消导出被中断: " + e.getMessage());
+ }
+ }
+
+ setLastError("GPIO " + pin + " 取消导出超时");
+ return false;
+ }
+
+ /**
+ * 设置GPIO方向
+ * @param pin GPIO引脚号
+ * @param direction 方向(DIRECTION_IN或DIRECTION_OUT)
+ * @return 操作是否成功
+ */
+ public boolean setDirection(int pin, int direction) {
+ if (direction != DIRECTION_IN && direction != DIRECTION_OUT) {
+ LogManager.e(TAG, "无效的方向值: " + direction);
+ return false;
+ }
+
+ //LogManager.d(TAG, "设置GPIO " + pin + " 方向为: " + (direction == DIRECTION_IN ? "IN" : "OUT"));
+
+ // 检查GPIO是否已导出
+ String gpioPath = GPIO_PATH + "gpio" + pin;
+ File gpioDir = new File(gpioPath);
+
+ if (!gpioDir.exists() || !gpioDir.isDirectory()) {
+ setLastError("GPIO " + pin + " 未导出");
+ return false;
+ }
+
+ // 设置方向
+ String directionPath = gpioPath + "/direction";
+ String dirStr = (direction == DIRECTION_IN) ? "in" : "out";
+
+ if (!writeToFile(directionPath, dirStr)) {
+ return false;
+ }
+
+ //LogManager.d(TAG, "GPIO " + pin + " 方向设置为 " + dirStr + " 成功");
+ return true;
+ }
+
+ /**
+ * 设置GPIO值
+ * @param pin GPIO引脚号
+ * @param value 值(VALUE_LOW或VALUE_HIGH)
+ * @return 操作是否成功
+ */
+ public boolean setValue(int pin, int value) {
+ if (value != VALUE_LOW && value != VALUE_HIGH) {
+ LogManager.e(TAG, "无效的值: " + value);
+ return false;
+ }
+
+ //LogManager.d(TAG, "设置GPIO " + pin + " 值为: " + value);
+
+ // 检查GPIO是否已导出
+ String gpioPath = GPIO_PATH + "gpio" + pin;
+ File gpioDir = new File(gpioPath);
+
+ if (!gpioDir.exists() || !gpioDir.isDirectory()) {
+ setLastError("GPIO " + pin + " 未导出");
+ return false;
+ }
+
+ // 设置值
+ String valuePath = gpioPath + "/value";
+ String valueStr = String.valueOf(value);
+
+ if (!writeToFile(valuePath, valueStr)) {
+ return false;
+ }
+
+ //LogManager.d(TAG, "GPIO " + pin + " 值设置为 " + valueStr + " 成功");
+ return true;
+ }
+
+ /**
+ * 获取GPIO值
+ * @param pin GPIO引脚号
+ * @return GPIO值,-1表示获取失败
+ */
+ public int getValue(int pin) {
+ //LogManager.d(TAG, "获取GPIO " + pin + " 值");
+
+ // 检查GPIO是否已导出
+ String gpioPath = GPIO_PATH + "gpio" + pin;
+ File gpioDir = new File(gpioPath);
+
+ if (!gpioDir.exists() || !gpioDir.isDirectory()) {
+ setLastError("GPIO " + pin + " 未导出");
+ return -1;
+ }
+
+ // 读取值
+ String valuePath = gpioPath + "/value";
+ String value = readFromFile(valuePath);
+
+ if (value == null || value.isEmpty()) {
+ return -1;
+ }
+
+ // 解析值
+ try {
+ int result = Integer.parseInt(value.trim());
+ //LogManager.d(TAG, "GPIO " + pin + " 值读取为 " + result);
+ return result;
+ } catch (NumberFormatException e) {
+ setLastError("无法解析GPIO " + pin + " 的值: " + e.getMessage());
+ return -1;
+ }
+ }
+
+ /**
+ * 释放资源
+ */
+ public void release() {
+ LogManager.d(TAG, "释放GPIO资源");
+
+ // 清空文件缓存
+ synchronized (fileCache) {
+ fileCache.clear();
+ }
+
+ instance = null;
+ }
+
+ /**
+ * 获取最后的错误信息
+ * @return 错误信息
+ */
+ public String getLastError() {
+ return lastErrorMsg;
+ }
+
+ /**
+ * 设置最后的错误信息
+ * @param message 错误信息
+ */
+ private void setLastError(String message) {
+ lastErrorMsg = message;
+ LogManager.e(TAG, message);
+ }
+
+ /**
+ * 写入文件
+ * @param path 文件路径
+ * @param content 内容
+ * @return 是否成功
+ */
+ private boolean writeToFile(String path, String content) {
+ BufferedWriter writer = null;
+
+ try {
+ writer = new BufferedWriter(new FileWriter(path));
+ writer.write(content);
+ writer.flush();
+ return true;
+ } catch (IOException e) {
+ setLastError("写入文件 " + path + " 失败: " + e.getMessage());
+ return false;
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ LogManager.e(TAG, "关闭文件写入流失败: " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * 从文件读取
+ * @param path 文件路径
+ * @return 文件内容,失败返回null
+ */
+ private String readFromFile(String path) {
+ BufferedReader reader = null;
+
+ try {
+ reader = new BufferedReader(new FileReader(path));
+ return reader.readLine();
+ } catch (IOException e) {
+ setLastError("读取文件 " + path + " 失败: " + e.getMessage());
+ return null;
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ LogManager.e(TAG, "关闭文件读取流失败: " + e.getMessage());
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ismart/ib86/feature/gpio/GpioTest.java b/app/src/main/java/com/ismart/ib86/feature/gpio/GpioTest.java
new file mode 100644
index 0000000..b25e993
--- /dev/null
+++ b/app/src/main/java/com/ismart/ib86/feature/gpio/GpioTest.java
@@ -0,0 +1,132 @@
+package com.ismart.ib86.feature.gpio;
+
+import com.ismart.ib86.common.utils.LogManager;
+
+/**
+ * GPIO测试类,用于测试GPIO功能
+ */
+public class GpioTest {
+ private static final String TAG = "GpioTest";
+
+ // 测试用的GPIO引脚号,根据实际硬件调整
+ private static final int TEST_GPIO_PIN = 26; // 示例引脚号,需要根据实际硬件调整
+
+ private GpioManager gpioManager;
+
+ public GpioTest() {
+ gpioManager = GpioManager.getInstance();
+ LogManager.d(TAG, "GpioTest初始化完成");
+ }
+
+ /**
+ * 开始GPIO测试
+ */
+ public void startTest() {
+ LogManager.d(TAG, "开始GPIO测试...");
+
+ try {
+ // 测试1: 导出GPIO引脚
+ testExportGpio();
+
+ // 测试2: 设置GPIO方向
+ testSetDirection();
+
+ // 测试3: 设置和获取GPIO值
+ testSetAndGetValue();
+
+ // 测试4: 取消导出GPIO引脚
+ testUnexportGpio();
+
+ LogManager.d(TAG, "GPIO测试完成");
+ } catch (Exception e) {
+ LogManager.e(TAG, "GPIO测试异常: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 测试导出GPIO引脚
+ */
+ private void testExportGpio() {
+ LogManager.d(TAG, "测试导出GPIO引脚 " + TEST_GPIO_PIN);
+ boolean result = gpioManager.exportGpio(TEST_GPIO_PIN);
+ if (result) {
+ LogManager.d(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 导出成功");
+ } else {
+ LogManager.e(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 导出失败");
+ }
+ }
+
+ /**
+ * 测试设置GPIO方向
+ */
+ private void testSetDirection() {
+ LogManager.d(TAG, "测试设置GPIO方向为输出");
+ boolean result = gpioManager.setDirection(TEST_GPIO_PIN, GpioManager.DIRECTION_OUT);
+ if (result) {
+ LogManager.d(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 方向设置为输出成功");
+ } else {
+ LogManager.e(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 方向设置为输出失败");
+ }
+ }
+
+ /**
+ * 测试设置和获取GPIO值
+ */
+ private void testSetAndGetValue() {
+ // 设置为高电平
+ LogManager.d(TAG, "测试设置GPIO值为高电平");
+ boolean setHighResult = gpioManager.setValue(TEST_GPIO_PIN, GpioManager.VALUE_HIGH);
+ if (setHighResult) {
+ LogManager.d(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 设置为高电平成功");
+ } else {
+ LogManager.e(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 设置为高电平失败");
+ }
+
+ // 读取值
+ int highValue = gpioManager.getValue(TEST_GPIO_PIN);
+ LogManager.d(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 当前值: " + highValue);
+
+ // 等待一段时间
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ LogManager.e(TAG, "线程睡眠被中断: " + e.getMessage());
+ }
+
+ // 设置为低电平
+ LogManager.d(TAG, "测试设置GPIO值为低电平");
+ boolean setLowResult = gpioManager.setValue(TEST_GPIO_PIN, GpioManager.VALUE_LOW);
+ if (setLowResult) {
+ LogManager.d(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 设置为低电平成功");
+ } else {
+ LogManager.e(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 设置为低电平失败");
+ }
+
+ // 读取值
+ int lowValue = gpioManager.getValue(TEST_GPIO_PIN);
+ LogManager.d(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 当前值: " + lowValue);
+ }
+
+ /**
+ * 测试取消导出GPIO引脚
+ */
+ private void testUnexportGpio() {
+ LogManager.d(TAG, "测试取消导出GPIO引脚 " + TEST_GPIO_PIN);
+ boolean result = gpioManager.unexportGpio(TEST_GPIO_PIN);
+ if (result) {
+ LogManager.d(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 取消导出成功");
+ } else {
+ LogManager.e(TAG, "GPIO引脚 " + TEST_GPIO_PIN + " 取消导出失败");
+ }
+ }
+
+ /**
+ * 释放资源
+ */
+ public void release() {
+ LogManager.d(TAG, "释放GPIO测试资源");
+ if (gpioManager != null) {
+ gpioManager.release();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/ASR5515DeviceManager.java b/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/ASR5515DeviceManager.java
new file mode 100644
index 0000000..a9ba430
--- /dev/null
+++ b/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/ASR5515DeviceManager.java
@@ -0,0 +1,422 @@
+package com.ismart.ib86.feature.serial.SerialPort;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.ismart.ib86.common.utils.LogManager;
+import com.ismart.ib86.feature.gpio.GpioManager;
+
+/**
+ * ASR-5515设备管理器
+ * 用于管理与ASR-5515设备的通信
+ */
+public class ASR5515DeviceManager {
+ private static final String TAG = "ASR5515DeviceManager";
+ private static final int GPIO_BT_WAKE = 33; // 蓝牙唤醒GPIO
+
+ private final SerialPortHelper serialPortHelper;
+ private final Handler mainHandler;
+ private final GpioManager gpioManager;
+ // 细分回调实例
+ private DeviceCheckListener deviceCheckListener;
+ private DeviceControlListener deviceControlListener;
+ private LogControlListener logControlListener;
+ private BtVersionListener btVersionListener;
+ private BtUpgradeListener btUpgradeListener;
+ private HostStatusListener hostStatusListener;
+ private BootBinCheckListener bootBinCheckListener;
+ private CollectFreqSetListener collectFreqSetListener;
+ private HrBpBoAutoMeasureListener hrBpBoAutoMeasureListener;
+ private HrBpBoManualMeasureListener hrBpBoManualMeasureListener;
+ private DynamicMeasureListener dynamicMeasureListener;
+ private WearDetectionListener wearDetectionListener;
+
+ // 细分回调接口
+ public interface DeviceCheckListener {
+ void onDeviceCheckResponse(ASR5515Protocol.DeviceInfo deviceInfo);
+ }
+
+ public interface DeviceControlListener {
+ void onDeviceRestartResponse(boolean success);
+ }
+
+ public interface LogControlListener {
+ void onLogControlResponse(boolean success);
+ }
+
+ public interface BtVersionListener {
+ void onBtVersionResponse(String version);
+ }
+
+ public interface BtUpgradeListener {
+ void onBtUpgradeResponse(ASR5515Protocol.BtUpgradeResponse response);
+ }
+
+ public interface HostStatusListener {
+ void onHostStatusResponse(ASR5515Protocol.HostStatusResponse response);
+ }
+
+ public interface BootBinCheckListener {
+ void onBootBinCheckResponse(ASR5515Protocol.BootBinCheckResponse response);
+ }
+
+ public interface CollectFreqSetListener {
+ void onCollectFreqSetResponse(ASR5515Protocol.CollectFreqSetResponse response);
+ }
+
+ public interface HrBpBoAutoMeasureListener {
+ void onHrBpBoAutoMeasureResponse(ASR5515Protocol.HrBpBoAutoMeasureResponse response);
+ }
+
+ public interface HrBpBoManualMeasureListener {
+ void onHrBpBoManualMeasureResponse(ASR5515Protocol.HrBpBoManualMeasureResponse response);
+ }
+
+ public interface DynamicMeasureListener {
+ void onDynamicMeasureResponse(ASR5515Protocol.DynamicMeasureResponse response);
+ }
+
+ public interface WearDetectionListener {
+ void onWearDetectionResponse(ASR5515Protocol.WearDetectionResponse response);
+ }
+
+ /**
+ * 构造函数
+ * @param context 上下文
+ * @param devicePath 设备路径
+ * @param baudRate 波特率
+ */
+ public ASR5515DeviceManager(Context context, String devicePath, int baudRate) {
+ serialPortHelper = new SerialPortHelper(devicePath, baudRate);
+ mainHandler = new Handler(Looper.getMainLooper());
+ gpioManager = GpioManager.getInstance();
+ serialPortHelper.setSimulationMode(false);
+ // 初始化串口
+ boolean result = serialPortHelper.open();
+ if (!result) {
+ LogManager.e(TAG, "Failed to open serial port: " + devicePath);
+ }
+
+ // 设置设备状态回调
+ serialPortHelper.setDeviceCheckCallback(new SerialPortHelper.DeviceCheckCallback() {
+ @Override
+ public void onDeviceCheckResponse(final ASR5515Protocol.DeviceInfo deviceInfo) {
+ if (deviceCheckListener != null) {
+ mainHandler.post(() -> deviceCheckListener.onDeviceCheckResponse(deviceInfo));
+ }
+ }
+ });
+
+ serialPortHelper.setDeviceControlCallback(new SerialPortHelper.DeviceControlCallback() {
+ @Override
+ public void onDeviceRestartResponse(final boolean success) {
+ if (deviceControlListener != null) {
+ mainHandler.post(() -> deviceControlListener.onDeviceRestartResponse(success));
+ }
+ }
+ });
+
+ serialPortHelper.setLogControlCallback(new SerialPortHelper.LogControlCallback() {
+ @Override
+ public void onLogControlResponse(final boolean success) {
+ if (logControlListener != null) {
+ mainHandler.post(() -> logControlListener.onLogControlResponse(success));
+ }
+ }
+ });
+
+ serialPortHelper.setBtVersionCallback(new SerialPortHelper.BtVersionCallback() {
+ @Override
+ public void onBtVersionResponse(final String version) {
+ if (btVersionListener != null) {
+ mainHandler.post(() -> btVersionListener.onBtVersionResponse(version));
+ }
+ }
+ });
+
+ serialPortHelper.setBtUpgradeCallback(new SerialPortHelper.BtUpgradeCallback() {
+ @Override
+ public void onBtUpgradeResponse(final ASR5515Protocol.BtUpgradeResponse response) {
+ if (btUpgradeListener != null) {
+ mainHandler.post(() -> btUpgradeListener.onBtUpgradeResponse(response));
+ }
+ }
+ });
+
+ serialPortHelper.setHostStatusCallback(new SerialPortHelper.HostStatusCallback() {
+ @Override
+ public void onHostStatusResponse(final ASR5515Protocol.HostStatusResponse response) {
+ if (hostStatusListener != null) {
+ mainHandler.post(() -> hostStatusListener.onHostStatusResponse(response));
+ }
+ }
+ });
+
+ serialPortHelper.setBootBinCheckCallback(new SerialPortHelper.BootBinCheckCallback() {
+ @Override
+ public void onBootBinCheckResponse(final ASR5515Protocol.BootBinCheckResponse response) {
+ if (bootBinCheckListener != null) {
+ mainHandler.post(() -> bootBinCheckListener.onBootBinCheckResponse(response));
+ }
+ }
+ });
+
+ serialPortHelper.setCollectFreqSetCallback(new SerialPortHelper.CollectFreqSetCallback() {
+ @Override
+ public void onCollectFreqSetResponse(final ASR5515Protocol.CollectFreqSetResponse response) {
+ if (collectFreqSetListener != null) {
+ mainHandler.post(() -> collectFreqSetListener.onCollectFreqSetResponse(response));
+ }
+ }
+ });
+
+ serialPortHelper.setHrBpBoAutoMeasureCallback(new SerialPortHelper.HrBpBoAutoMeasureCallback() {
+ @Override
+ public void onHrBpBoAutoMeasureResponse(ASR5515Protocol.HrBpBoAutoMeasureResponse response) {
+ if (hrBpBoAutoMeasureListener != null) {
+ mainHandler.post(() -> hrBpBoAutoMeasureListener.onHrBpBoAutoMeasureResponse(response));
+ }
+ }
+ });
+
+ serialPortHelper.setHrBpBoManualMeasureCallback(new SerialPortHelper.HrBpBoManualMeasureCallback() {
+ @Override
+ public void onHrBpBoManualMeasureResponse(ASR5515Protocol.HrBpBoManualMeasureResponse response) {
+ if (hrBpBoManualMeasureListener != null) {
+ mainHandler.post(() -> hrBpBoManualMeasureListener.onHrBpBoManualMeasureResponse(response));
+ }
+ }
+ });
+
+ serialPortHelper.setDynamicMeasureCallback(new SerialPortHelper.DynamicMeasureCallback() {
+ @Override
+ public void onDynamicMeasureResponse(ASR5515Protocol.DynamicMeasureResponse response) {
+ if (dynamicMeasureListener != null) {
+ mainHandler.post(() -> dynamicMeasureListener.onDynamicMeasureResponse(response));
+ }
+ }
+ });
+
+ serialPortHelper.setWearDetectionCallback(new SerialPortHelper.WearDetectionCallback() {
+ @Override
+ public void onWearDetectionResponse(ASR5515Protocol.WearDetectionResponse response) {
+ if (wearDetectionListener != null) {
+ mainHandler.post(() -> wearDetectionListener.onWearDetectionResponse(response));
+ }
+ }
+ });
+ }
+
+ /**
+ * 设置设备检查监听器
+ */
+ public void setDeviceCheckListener(DeviceCheckListener listener) {
+ this.deviceCheckListener = listener;
+ }
+
+ /**
+ * 设置设备控制监听器
+ */
+ public void setDeviceControlListener(DeviceControlListener listener) {
+ this.deviceControlListener = listener;
+ }
+
+ public void setLogControlListener(LogControlListener listener) {
+ this.logControlListener = listener;
+ }
+
+ /**
+ * 设置蓝牙版本监听器
+ */
+ public void setBtVersionListener(BtVersionListener listener) {
+ this.btVersionListener = listener;
+ }
+
+ /**
+ * 设置蓝牙升级监听器
+ */
+ public void setBtUpgradeListener(BtUpgradeListener listener) {
+ this.btUpgradeListener = listener;
+ }
+
+ /**
+ * 设置主机状态监听器
+ */
+ public void setHostStatusListener(HostStatusListener listener) {
+ this.hostStatusListener = listener;
+ }
+
+ /**
+ * 设置心率血压血氧自动测量监听器
+ */
+ public void setHrBpBoAutoMeasureListener(HrBpBoAutoMeasureListener listener) {
+ this.hrBpBoAutoMeasureListener = listener;
+ }
+
+ /**
+ * 设置心率血压血氧手动测量监听器
+ */
+ public void setHrBpBoManualMeasureListener(HrBpBoManualMeasureListener listener) {
+ this.hrBpBoManualMeasureListener = listener;
+ }
+
+ /**
+ * 设置动态测量监听器
+ */
+ public void setDynamicMeasureListener(DynamicMeasureListener listener) {
+ this.dynamicMeasureListener = listener;
+ }
+
+ /**
+ * 设置佩戴检测监听器
+ */
+ public void setWearDetectionListener(WearDetectionListener listener) {
+ this.wearDetectionListener = listener;
+ }
+
+ /**
+ * 设置Boot Bin检查监听器
+ */
+ public void setBootBinCheckListener(BootBinCheckListener listener) {
+ this.bootBinCheckListener = listener;
+ }
+
+ /**
+ * 设置采集频率设置监听器
+ */
+ public void setCollectFreqSetListener(CollectFreqSetListener listener) {
+ this.collectFreqSetListener = listener;
+ }
+
+ /**
+ * 检查设备状态
+ */
+ public void checkDevice() {
+ wakeupDevice();
+ serialPortHelper.checkDevice();
+ }
+
+ /**
+ * 重启设备
+ */
+ public void restartDevice() {
+ wakeupDevice();
+ serialPortHelper.restartDevice();
+ }
+
+ /**
+ * 控制日志输出
+ * @param enable 是否启用
+ */
+ public void setLogControl(boolean enable) {
+ wakeupDevice();
+ serialPortHelper.setLogControl(enable);
+ }
+
+ /**
+ * 查询蓝牙版本
+ */
+ public void queryBtVersion() {
+ wakeupDevice();
+ serialPortHelper.queryBtVersion();
+ }
+
+ /**
+ * 发送蓝牙升级请求
+ */
+ public void upgradeBt() {
+ wakeupDevice();
+ serialPortHelper.upgradeBt();
+ }
+
+ /**
+ * 发送主机状态同步请求
+ * @param screenOn 屏幕状态(true:亮屏,false:灭屏)
+ */
+ public void syncHostStatus(boolean screenOn) {
+ wakeupDevice();
+ serialPortHelper.syncHostStatus(screenOn);
+ }
+
+ /**
+ * 关闭设备
+ */
+ public void close() {
+ serialPortHelper.close();
+ }
+
+ /**
+ * 发送boot bin检查请求
+ */
+ public void checkBootBin() {
+ wakeupDevice();
+ serialPortHelper.checkBootBin();
+ }
+
+ /**
+ * 设置采集频率
+ * @param len 长度
+ * @param freq 频率值
+ */
+ public void setCollectFreq(byte len, byte freq) {
+ wakeupDevice();
+ serialPortHelper.setCollectFreq(len, freq);
+ }
+
+ /**
+ * 发送HrBpBo自动测量请求
+ */
+ public void startHrBpBoAutoMeasure() {
+ wakeupDevice();
+ serialPortHelper.startHrBpBoAutoMeasure();
+ }
+
+ /**
+ * 发送HrBpBo手动测量请求
+ */
+ public void startHrBpBoManualMeasure() {
+ wakeupDevice();
+ serialPortHelper.startHrBpBoManualMeasure();
+ }
+
+ /**
+ * 发送动态测量请求
+ */
+ public void startDynamicMeasure() {
+ wakeupDevice();
+ serialPortHelper.startDynamicMeasure();
+ }
+
+ /**
+ * 发送佩戴检测请求
+ */
+ public void sendWearDetectionRequest() {
+ wakeupDevice();
+ serialPortHelper.sendWearDetectionRequest();
+ }
+
+ /**
+ * 唤醒设备
+ * 通过GPIO33唤醒蓝牙,ASR休眠时会自动发送'w'字符
+ */
+ public void wakeupDevice() {
+ try {
+ // 配置GPIO33为输出模式
+ gpioManager.exportGpio(GPIO_BT_WAKE);
+ gpioManager.setDirection(GPIO_BT_WAKE, GpioManager.DIRECTION_OUT);
+
+ // 拉低GPIO33
+ gpioManager.setValue(GPIO_BT_WAKE, 0);
+
+ // 等待30ms
+ Thread.sleep(30);
+
+ // 拉高GPIO33
+ gpioManager.setValue(GPIO_BT_WAKE, 1);
+
+ } catch (Exception e) {
+ LogManager.e(TAG, "Failed to wakeup bluetooth: " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/ASR5515Protocol.java b/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/ASR5515Protocol.java
new file mode 100644
index 0000000..de25c28
--- /dev/null
+++ b/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/ASR5515Protocol.java
@@ -0,0 +1,648 @@
+package com.ismart.ib86.feature.serial.SerialPort;
+
+import static com.ismart.ib86.feature.serial.SerialPort.SerialPortHelper.bytesToHexString;
+
+import android.os.Build;
+import android.util.Log;
+
+import com.ismart.ib86.common.utils.LogManager;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HexFormat;
+
+public class ASR5515Protocol {
+ private static final String TAG = "ASR5515Protocol";
+ // 设备检查回调接口
+ public interface DeviceCheckCallback {
+ void onDeviceCheckResponse(DeviceInfo deviceInfo);
+ }
+
+ // 设备控制回调接口
+ public interface DeviceControlCallback {
+ void onDeviceRestartResponse(boolean success);
+ void onLogControlResponse(boolean success);
+ }
+
+ // 蓝牙版本回调接口
+ public interface BtVersionCallback {
+ void onBtVersionResponse(String version);
+ }
+
+ // 蓝牙升级回调接口
+ public interface BtUpgradeCallback {
+ void onBtUpgradeResponse(BtUpgradeResponse response);
+ }
+
+ // 主机状态回调接口
+ public interface HostStatusCallback {
+ void onHostStatusResponse(HostStatusResponse response);
+ }
+
+ // Boot Bin检查回调接口
+ public interface BootBinCheckCallback {
+ void onBootBinCheckResponse(BootBinCheckResponse response);
+ }
+
+ // 采集频率设置回调接口
+ public interface CollectFreqSetCallback {
+ void onCollectFreqSetResponse(CollectFreqSetResponse response);
+ }
+
+ // HrBpBo自动测量回调接口
+ public interface HrBpBoAutoMeasureCallback {
+ void onHrBpBoAutoMeasureResponse(HrBpBoAutoMeasureResponse response);
+ }
+
+ // HrBpBo手动测量回调接口
+ public interface HrBpBoManualMeasureCallback {
+ void onHrBpBoManualMeasureResponse(HrBpBoManualMeasureResponse response);
+ }
+
+ // 动态测量回调接口
+ public interface DynamicMeasureCallback {
+ void onDynamicMeasureResponse(DynamicMeasureResponse response);
+ }
+
+ // 佩戴检测回调接口
+ public interface WearDetectionCallback {
+ void onWearDetectionResponse(WearDetectionResponse response);
+ }
+ private static final byte FRAME_START = (byte) '[';
+ private static final byte FRAME_END = (byte) ']';
+
+ public static class DeviceInfo {
+ public byte sn; // 序列号
+ public byte len; // 长度
+ public byte heartRate; // 心率
+ public byte heartRateId; // 心率ID
+ public byte stepCount; // 计步
+ public byte stepCountId; // 计步ID
+ public String deviceId; // 设备ID
+ public String hardwareVersion; // 硬件版本
+ public String softwareVersion; // 软件版本
+ public String status; // 设备状态
+
+ public DeviceInfo(byte sn, byte len, byte heartRate, byte heartRateId, byte stepCount, byte stepCountId,
+ String deviceId, String hardwareVersion, String softwareVersion, String status) {
+ this.sn = sn;
+ this.len = len;
+ this.heartRate = heartRate;
+ this.heartRateId = heartRateId;
+ this.stepCount = stepCount;
+ this.stepCountId = stepCountId;
+ this.deviceId = deviceId;
+ this.hardwareVersion = hardwareVersion;
+ this.softwareVersion = softwareVersion;
+ this.status = status;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("设备信息:\n设备ID: %s\n硬件版本: %s\n软件版本: %s\n状态: %s",
+ deviceId, hardwareVersion, softwareVersion, status);
+ }
+ }
+
+ // 第一个parseDeviceCheckResponse方法被删除,保留文件末尾的更详细实现
+
+ public static class RebootResponse {
+ public byte sn;
+ public byte status;
+
+ public RebootResponse(byte sn, byte status) {
+ this.sn = sn;
+ this.status = status;
+ }
+ }
+
+ public static RebootResponse parseRebootResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_DEVICE_REBOOT_RESP || frame.data.length < 2) {
+ return null;
+ }
+ return new RebootResponse(frame.data[0], frame.data[1]);
+ }
+
+ public static class TimeSyncResponse {
+ public byte sn;
+ public byte status;
+
+ public TimeSyncResponse(byte sn, byte status) {
+ this.sn = sn;
+ this.status = status;
+ }
+ }
+
+ public static TimeSyncResponse parseTimeSyncResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_TIME_SYNC_RESP || frame.data.length < 2) {
+ return null;
+ }
+ return new TimeSyncResponse(frame.data[0], frame.data[1]);
+ }
+
+ public static class LogControlResponse {
+ public byte sn;
+ public byte status;
+
+ public LogControlResponse(byte sn, byte status) {
+ this.sn = sn;
+ this.status = status;
+ }
+ }
+
+ public static LogControlResponse parseLogControlResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_LOG_CONTROL_RESP || frame.data.length < 2) {
+ return null;
+ }
+ return new LogControlResponse(frame.data[0], frame.data[1]);
+ }
+
+ // 创建设备重启请求帧
+ public static byte[] createRebootRequest(byte sn) {
+ return createFrame(Commands.CMD_DEVICE_REBOOT, new byte[]{sn, 0});
+ }
+
+ // 创建时间同步请求帧
+ public static byte[] createTimeSyncRequest(byte sn, byte len, int timestamp) {
+ byte[] data = new byte[6];
+ data[0] = sn;
+ data[1] = len;
+ // 将timestamp转换为4字节数组
+ data[2] = (byte) (timestamp >> 24);
+ data[3] = (byte) (timestamp >> 16);
+ data[4] = (byte) (timestamp >> 8);
+ data[5] = (byte) timestamp;
+ return createFrame(Commands.CMD_TIME_SYNC, data);
+ }
+
+ // 创建日志打印控制请求帧
+ public static byte[] createLogControlRequest(byte sn, byte len, byte enable) {
+ return createFrame(Commands.CMD_LOG_CONTROL, new byte[]{sn, len, enable});
+ }
+
+ public static class Commands {
+ public static final short CMD_CHECK_DEVICE = 0x0065; // 设备检查请求
+ public static final short CMD_CHECK_DEVICE_RESP = 0x0066; // 设备检查响应
+ public static final short CMD_DEVICE_REBOOT = 0x0067; // 设备重启请求
+ public static final short CMD_DEVICE_REBOOT_RESP = 0x0068;// 设备重启响应
+ public static final short CMD_TIME_SYNC = 0x0069; // 时间同步请求
+ public static final short CMD_TIME_SYNC_RESP = 0x006A; // 时间同步响应
+ public static final short CMD_RESTART_DEVICE = 0x0067; // 设备重启请求
+ public static final short CMD_RESTART_DEVICE_RESP = 0x0068; // 设备重启响应
+ public static final short CMD_LOG_CONTROL = 0x006B; // 日志打印控制请求
+ public static final short CMD_LOG_CONTROL_RESP = 0x006C; // 日志打印控制响应
+ public static final short CMD_BT_VERSION = 0x006D; // 蓝牙版本请求
+ public static final short CMD_BT_VERSION_RESP = 0x006E; // 蓝牙版本响应
+ public static final short CMD_BT_UPGRADE = 0x0071; // 本地、蓝牙升级请求
+ public static final short CMD_BT_UPGRADE_RESP = 0x0078; // 本地、蓝牙升级响应
+ public static final short CMD_HOST_STATUS = 0x0079; // 主机状态同步请求
+ public static final short CMD_HOST_STATUS_RESP = 0x007A; // 主机状态同步响应
+ public static final short CMD_HOST_STATUS_BOOT_RESP = 0x007C; //主机状态,启动状态
+ // 新增命令
+ public static final short CMD_BOOT_BIN_CHECK = 0x007D; // boot bin检查请求 [125 0 0]
+ public static final short CMD_BOOT_BIN_CHECK_RESP = 0x007E; // boot bin检查响应 [126 sn len 0/1]
+ public static final short CMD_COLLECT_FREQ_SET = 0x00C9; // 采集频率设置 [201 0 len 32]
+ public static final short CMD_COLLECT_FREQ_SET_RESP = 0x00CA; // 采集频率设置响应 [202 sn 0]
+ public static final short CMD_HRBPBO_AUTO_MEASURE = 0x00CB; // HrBpBo自动测量 [203 0 0]
+ public static final short CMD_HRBPBO_AUTO_MEASURE_RESP = 0x00CC; // HrBpBo自动测量响应 [204 sn 0]
+ public static final short CMD_HRBPBO_MANUAL_MEASURE = 0x00CD; // HrBpBo手动测量 [205 0 0]
+ public static final short CMD_HRBPBO_MANUAL_MEASURE_RESP = 0x00CE; // HrBpBo手动测量响应 [206 sn 0]
+ public static final short CMD_DYNAMIC_MEASURE = 0x00CF; // 动态测量 [207 0 0]
+ public static final short CMD_DYNAMIC_MEASURE_RESP = 0x00D0; // 动态测量响应 [208 sn 0]
+ public static final short CMD_WEAR_DETECTION = 0x0105; // 佩戴检测查询 [261 0 0]
+ public static final short CMD_WEAR_DETECTION_RESP = 0x0106; // 佩戴检测响应 [262 sn len 0/1]
+ }
+
+ public static class Frame {
+ public short command; // 修改为short类型以支持2字节命令
+ public byte[] data;
+ public byte[] remainingData; // 解析后剩余的数据
+
+ public Frame(short command, byte[] data) {
+ this.command = command;
+ this.data = data;
+ }
+ }
+
+ public static byte[] createDeviceCheckRequest() {
+ // 发送格式:[101 0 0]
+ byte[] data = new byte[2]; // 两个0
+ data[0] = 0;
+ data[1] = 0;
+ return createFrame(Commands.CMD_CHECK_DEVICE, data);
+ }
+
+ public static byte[] createFrame(short command, byte[] data) {
+ LogManager.d(TAG, "LEN :" + data.length);
+ int frameLength = 1 + 2 + 2 + 2 + data.length + 1; // start + command(2bytes) + sn(2bytes) + len(2bytes) + data + end
+ ByteBuffer buffer = ByteBuffer.allocate(frameLength);
+
+ buffer.put(FRAME_START); // 添加帧起始符'['
+ buffer.put((byte)(command & 0xFF)); // 命令字节低位
+ buffer.put((byte)((command >> 8) & 0xFF)); // 命令字节高位
+ buffer.put((byte)0x00); // sn
+ buffer.put((byte)0x00); //sn
+ buffer.put((byte)(data.length & 0xFF)); // len
+ buffer.put((byte)((data.length >> 8) & 0xFF)); //len
+ if(data.length > 0){
+ buffer.put(data); // 数据部分
+ }
+ buffer.put(FRAME_END); // 添加帧结束符']'
+
+ return buffer.array();
+ }
+
+ public static Frame parseFrame(byte[] data) {
+ if (data == null || data.length < 8) { // 最小帧长度:head(1) + type(2) + sn(2) + len(2) + end(1)
+ return null;
+ }
+
+ // 查找帧起始符'['
+ int startIndex = -1;
+ for (int i = 0; i < data.length; i++) {
+ if (data[i] == FRAME_START) {
+ startIndex = i;
+ break;
+ }
+ }
+
+ if (startIndex == -1 || startIndex + 7 >= data.length) {
+ return null;
+ }
+
+ // 从起始符后开始查找帧结束符']'
+ int endIndex = -1;
+ for (int i = startIndex + 7; i < data.length; i++) {
+ if (data[i] == FRAME_END) {
+ endIndex = i;
+ break;
+ }
+ }
+
+ if (endIndex == -1) {
+ return null;
+ }
+
+ try {
+ // 提取type(2字节)
+ short type = (short)(((data[startIndex + 2] & 0xFF) << 8) | (data[startIndex + 1] & 0xFF));
+
+ // 提取sn(2字节)
+ short sn = (short)(((data[startIndex + 4] & 0xFF) << 8) | (data[startIndex + 3] & 0xFF));
+
+ // 提取len(2字节)
+ short len = (short)(((data[startIndex + 6] & 0xFF) << 8) | (data[startIndex + 5] & 0xFF));
+
+ // 提取数据部分(如果有的话)
+ int dataLength = endIndex - (startIndex + 7); // 7 = head(1) + type(2) + sn(2) + len(2)
+ //LogManager.d(TAG, "len: "+ len + " dataLength: " + dataLength + " end data" + data[endIndex]);
+ byte[] frameData;
+ if (dataLength > 0) {
+ frameData = new byte[dataLength];
+ System.arraycopy(data, startIndex + 7, frameData, 0, dataLength);
+// LogManager.d(TAG,"data :"+ bytesToHexString(frameData));
+ } else {
+ frameData = new byte[0];
+ }
+
+ // 创建帧对象
+ Frame frame = new Frame(type, frameData);
+
+ // 如果还有剩余数据,保存到remainingData
+ if (endIndex + 1 < data.length) {
+ frame.remainingData = new byte[data.length - endIndex - 1];
+ System.arraycopy(data, endIndex + 1, frame.remainingData, 0, frame.remainingData.length);
+ }
+
+ return frame;
+ } catch (Exception e) {
+ LogManager.e(TAG, "Parse frame error: " + e.getMessage());
+ return null;
+ }
+ }
+
+ public static DeviceInfo parseDeviceCheckResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_CHECK_DEVICE_RESP || frame.data.length < 7) {
+ return null;
+ }
+
+ try {
+ ByteBuffer buffer = ByteBuffer.wrap(frame.data);
+
+ // 解析响应数据:[sn len 心率 ID 计步 ID 设备ID]
+ byte sn = buffer.get(); // 序列号
+ byte len = buffer.get(); // 长度
+ byte heartRate = buffer.get(); // 心率
+ byte heartRateId = buffer.get(); // 心率ID
+ byte stepCount = buffer.get(); // 计步
+ byte stepCountId = buffer.get(); // 计步ID
+ byte rawDeviceId = buffer.get(); // 设备ID
+
+ // 将deviceId转换为十六进制字符串
+ String deviceId = String.format("%02X", rawDeviceId);
+
+ // 硬件版本和软件版本可能不在帧数据中,设置默认值
+ String hardwareVersion = "V1.0";
+ String softwareVersion = "V1.0";
+ String status = "正常"; // 默认状态
+
+ // 如果帧数据中包含更多信息,可以继续解析
+ if (frame.data.length > 7) {
+ // 这里需要根据实际协议格式解析硬件版本、软件版本和状态
+ byte statusByte = frame.data[7];
+ status = parseStatus(statusByte);
+ }
+
+ return new DeviceInfo(sn, len, heartRate, heartRateId, stepCount, stepCountId, deviceId, hardwareVersion, softwareVersion, status);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private static String parseStatus(byte statusByte) {
+ switch(statusByte) {
+ case 0:
+ return "正常";
+ case 1:
+ return "异常";
+ case 2:
+ return "离线";
+ default:
+ return "未知状态(" + statusByte + ")";
+ }
+ }
+
+ // 创建蓝牙版本请求帧
+ public static byte[] createBtVersionRequest() {
+ return createFrame(Commands.CMD_BT_VERSION, new byte[]{});
+ }
+
+ // 创建设备重启请求帧
+ public static byte[] createDeviceRestartRequest() {
+ return createFrame(Commands.CMD_RESTART_DEVICE, new byte[]{});
+ }
+
+ // 解析设备重启响应帧
+ public static boolean parseDeviceRestartResponse(Frame frame) {
+ return frame != null && frame.command == Commands.CMD_RESTART_DEVICE_RESP;
+ }
+
+ public static String parseBtVersionResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_BT_VERSION_RESP || frame.data.length == 0) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (byte b : frame.data) {
+ // 只保留可打印的 ASCII 字符(32-126)
+ if (b >= 32 && b <= 126) {
+ sb.append((char) b);
+ }
+ }
+ try {
+ return sb.toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ // 创建本地、蓝牙升级请求帧
+ public static byte[] createBtUpgradeRequest() {
+ // 发送格式:[113 0 0]
+ byte[] data = new byte[2]; // 两个0
+ data[0] = 0;
+ data[1] = 0;
+ return createFrame(Commands.CMD_BT_UPGRADE, data);
+ }
+
+ // 解析本地、蓝牙升级响应帧
+ public static class BtUpgradeResponse {
+ public byte sn;
+ public byte len;
+ public String data;
+
+ public BtUpgradeResponse(byte sn, byte len, String data) {
+ this.sn = sn;
+ this.len = len;
+ this.data = data;
+ }
+ }
+
+ public static BtUpgradeResponse parseBtUpgradeResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_BT_UPGRADE_RESP || frame.data.length < 2) {
+ return null;
+ }
+
+ try {
+ byte sn = frame.data[0];
+ byte len = frame.data[1];
+
+ // 提取数据部分(如果有的话)
+ String data = "";
+ if (frame.data.length > 2) {
+ byte[] dataBytes = new byte[frame.data.length - 2];
+ System.arraycopy(frame.data, 2, dataBytes, 0, dataBytes.length);
+ data = new String(dataBytes, StandardCharsets.UTF_8);
+ }
+
+ return new BtUpgradeResponse(sn, len, data);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ // 创建主机状态同步请求帧
+ public static byte[] createHostStatusRequest(boolean screenOn) {
+ // 发送格式:[121 0 len 0/1],其中0/1表示屏幕状态(灭/亮)
+ byte[] data = new byte[3];
+ data[0] = 0; // 保留字节
+ data[1] = 1;
+ data[2] = (byte)(screenOn ? 1 : 0); // 1表示亮屏,0表示灭屏
+ return createFrame(Commands.CMD_HOST_STATUS, data);
+ }
+
+ // 创建boot bin检查请求帧 [125 0 0]
+ public static byte[] createBootBinCheckRequest() {
+ byte[] data = new byte[2];
+ data[0] = 0;
+ data[1] = 0;
+ return createFrame(Commands.CMD_BOOT_BIN_CHECK, data);
+ }
+
+ // 创建采集频率设置请求帧 [201 0 len 32]
+ public static byte[] createCollectFreqSetRequest(byte len, byte freq) {
+ byte[] data = new byte[3];
+ data[0] = 0; // 保留字节
+ data[1] = len;
+ data[2] = freq;
+ return createFrame(Commands.CMD_COLLECT_FREQ_SET, data);
+ }
+
+ // 创建HrBpBo自动测量请求帧 [203 0 0]
+ public static byte[] createHrBpBoAutoMeasureRequest() {
+ byte[] data = new byte[2];
+ data[0] = 0;
+ data[1] = 0;
+ return createFrame(Commands.CMD_HRBPBO_AUTO_MEASURE, data);
+ }
+
+ // 创建HrBpBo手动测量请求帧 [205 0 0]
+ public static byte[] createHrBpBoManualMeasureRequest() {
+ byte[] data = new byte[2];
+ data[0] = 0;
+ data[1] = 0;
+ return createFrame(Commands.CMD_HRBPBO_MANUAL_MEASURE, data);
+ }
+
+ // 创建动态测量请求帧 [207 0 0]
+ public static byte[] createDynamicMeasureRequest() {
+ byte[] data = new byte[2];
+ data[0] = 0;
+ data[1] = 0;
+ return createFrame(Commands.CMD_DYNAMIC_MEASURE, data);
+ }
+
+ // 解析主机状态同步响应帧
+ public static class HostStatusResponse {
+ public byte sn;
+ public byte status;
+
+ public HostStatusResponse(byte sn, byte status) {
+ this.sn = sn;
+ this.status = status;
+ }
+ }
+
+ public static HostStatusResponse parseHostStatusResponse(Frame frame) {
+ if (frame == null || frame.data.length < 2) {
+ return null;
+ }
+
+ return new HostStatusResponse(frame.data[0], frame.data[1]);
+ }
+
+ // BootBinCheck响应类
+ public static class BootBinCheckResponse {
+ public byte sn;
+ public byte len;
+ public byte status;
+
+ public BootBinCheckResponse(byte sn, byte len, byte status) {
+ this.sn = sn;
+ this.len = len;
+ this.status = status;
+ }
+ }
+
+ public static BootBinCheckResponse parseBootBinCheckResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_BOOT_BIN_CHECK_RESP || frame.data.length < 3) {
+ return null;
+ }
+ return new BootBinCheckResponse(frame.data[0], frame.data[1], frame.data[2]);
+ }
+
+ // CollectFreqSet响应类
+ public static class CollectFreqSetResponse {
+ public byte sn;
+ public byte status;
+
+ public CollectFreqSetResponse(byte sn, byte status) {
+ this.sn = sn;
+ this.status = status;
+ }
+ }
+
+ public static CollectFreqSetResponse parseCollectFreqSetResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_COLLECT_FREQ_SET_RESP || frame.data.length < 2) {
+ return null;
+ }
+ return new CollectFreqSetResponse(frame.data[0], frame.data[1]);
+ }
+
+ // HrBpBoAutoMeasure响应类
+ public static class HrBpBoAutoMeasureResponse {
+ public byte sn;
+ public byte status;
+
+ public HrBpBoAutoMeasureResponse(byte sn, byte status) {
+ this.sn = sn;
+ this.status = status;
+ }
+ }
+
+ public static HrBpBoAutoMeasureResponse parseHrBpBoAutoMeasureResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_HRBPBO_AUTO_MEASURE_RESP || frame.data.length < 2) {
+ return null;
+ }
+ return new HrBpBoAutoMeasureResponse(frame.data[0], frame.data[1]);
+ }
+
+ // HrBpBoManualMeasure响应类
+ public static class HrBpBoManualMeasureResponse {
+ public byte sn;
+ public byte status;
+
+ public HrBpBoManualMeasureResponse(byte sn, byte status) {
+ this.sn = sn;
+ this.status = status;
+ }
+ }
+
+ public static HrBpBoManualMeasureResponse parseHrBpBoManualMeasureResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_HRBPBO_MANUAL_MEASURE_RESP || frame.data.length < 2) {
+ return null;
+ }
+ return new HrBpBoManualMeasureResponse(frame.data[0], frame.data[1]);
+ }
+
+ // DynamicMeasure响应类
+ public static class DynamicMeasureResponse {
+ public byte sn;
+ public byte status;
+
+ public DynamicMeasureResponse(byte sn, byte status) {
+ this.sn = sn;
+ this.status = status;
+ }
+ }
+
+ public static DynamicMeasureResponse parseDynamicMeasureResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_DYNAMIC_MEASURE_RESP || frame.data.length < 2) {
+ return null;
+ }
+ return new DynamicMeasureResponse(frame.data[0], frame.data[1]);
+ }
+
+ // 创建佩戴检测查询请求帧 [261 0 0]
+ public static byte[] createWearDetectionRequest() {
+ return createFrame(Commands.CMD_WEAR_DETECTION, new byte[]{});
+ }
+
+ // 佩戴检测响应类
+ public static class WearDetectionResponse {
+ public boolean isWearing; // true表示已佩戴,false表示未佩戴
+
+ public WearDetectionResponse( boolean isWearing) {
+ this.isWearing = isWearing;
+ }
+
+ @Override
+ public String toString() {
+ return "WearDetectionResponse{" +
+ ", isWearing=" + isWearing +
+ '}';
+ }
+ }
+
+ // 解析佩戴检测响应帧 [262 sn len 0/1]
+ public static WearDetectionResponse parseWearDetectionResponse(Frame frame) {
+ if (frame == null || frame.command != Commands.CMD_WEAR_DETECTION_RESP ) {
+ return null;
+ }
+ boolean isWearing = frame.data[0] == 1;
+ return new WearDetectionResponse(isWearing);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/SerialPortHelper.java b/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/SerialPortHelper.java
new file mode 100644
index 0000000..ead9e09
--- /dev/null
+++ b/app/src/main/java/com/ismart/ib86/feature/serial/SerialPort/SerialPortHelper.java
@@ -0,0 +1,591 @@
+package com.ismart.ib86.feature.serial.SerialPort;
+
+import android.serialport.SerialPort;
+
+import com.ismart.ib86.common.utils.LogManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class SerialPortHelper {
+ private final static String TAG = "SerialPortHelper";
+ private SerialPort mSerialPort;
+ private String mPortName;
+ private int mBaudRate;
+ private final BlockingQueue sendQueue = new LinkedBlockingQueue<>();
+ private final BlockingQueue receiveQueue = new LinkedBlockingQueue<>(100);
+ private Thread sendThread;
+ private Thread receiveThread;
+ private Thread processThread;
+ private volatile boolean isRunning;
+ private SendCallback sendCallback;
+ private ReceiveCallback receiveCallback;
+ private DeviceCheckCallback deviceCheckCallback;
+ private DeviceControlCallback deviceControlCallback;
+ private LogControlCallback logControlCallback;
+ private BtVersionCallback btVersionCallback;
+ private BtUpgradeCallback btUpgradeCallback;
+ private HostStatusCallback hostStatusCallback;
+ private BootBinCheckCallback bootBinCheckCallback;
+ private CollectFreqSetCallback collectFreqSetCallback;
+ private HrBpBoAutoMeasureCallback hrBpBoAutoMeasureCallback;
+ private HrBpBoManualMeasureCallback hrBpBoManualMeasureCallback;
+ private DynamicMeasureCallback dynamicMeasureCallback;
+ private WearDetectionCallback wearDetectionCallback;
+ private boolean isSimulationMode = false;
+ private final ByteBuffer frameBuffer = ByteBuffer.allocate(1024);
+
+ public interface SendCallback {
+ void onSendSuccess(byte[] data);
+
+ void onSendFailed(byte[] data, Exception e);
+ }
+
+ public interface ReceiveCallback {
+ void onDataReceived(short command, byte[] data);
+
+ void onReceiveError(Exception e);
+ }
+
+ public interface DeviceCheckCallback {
+ void onDeviceCheckResponse(ASR5515Protocol.DeviceInfo deviceInfo);
+ }
+
+ public interface DeviceControlCallback {
+ void onDeviceRestartResponse(boolean success);
+ }
+
+ public interface LogControlCallback {
+ void onLogControlResponse(boolean success);
+ }
+
+ public interface BtVersionCallback {
+ void onBtVersionResponse(String version);
+ }
+
+ public interface BtUpgradeCallback {
+ void onBtUpgradeResponse(ASR5515Protocol.BtUpgradeResponse response);
+ }
+
+ public interface HostStatusCallback {
+ void onHostStatusResponse(ASR5515Protocol.HostStatusResponse response);
+ }
+
+ public interface BootBinCheckCallback {
+ void onBootBinCheckResponse(ASR5515Protocol.BootBinCheckResponse response);
+ }
+
+ public interface CollectFreqSetCallback {
+ void onCollectFreqSetResponse(ASR5515Protocol.CollectFreqSetResponse response);
+ }
+
+ public interface HrBpBoAutoMeasureCallback {
+ void onHrBpBoAutoMeasureResponse(ASR5515Protocol.HrBpBoAutoMeasureResponse response);
+ }
+
+ public interface HrBpBoManualMeasureCallback {
+ void onHrBpBoManualMeasureResponse(ASR5515Protocol.HrBpBoManualMeasureResponse response);
+ }
+
+ public interface DynamicMeasureCallback {
+ void onDynamicMeasureResponse(ASR5515Protocol.DynamicMeasureResponse response);
+ }
+
+ public interface WearDetectionCallback {
+ void onWearDetectionResponse(ASR5515Protocol.WearDetectionResponse response);
+ }
+
+ public SerialPortHelper(String portName, int baudRate) {
+ this.mPortName = portName;
+ this.mBaudRate = baudRate;
+ this.isRunning = false;
+ this.isSimulationMode = false;
+ }
+
+ public void setSimulationMode(boolean enabled) {
+ this.isSimulationMode = enabled;
+ }
+
+ public void setSendCallback(SendCallback callback) {
+ this.sendCallback = callback;
+ }
+
+ public void setReceiveCallback(ReceiveCallback callback) {
+ this.receiveCallback = callback;
+ }
+
+ public void setDeviceCheckCallback(DeviceCheckCallback callback) {
+ this.deviceCheckCallback = callback;
+ }
+
+ public void setDeviceControlCallback(DeviceControlCallback callback) {
+ this.deviceControlCallback = callback;
+ }
+
+ public void setLogControlCallback(LogControlCallback callback) {
+ this.logControlCallback = callback;
+ }
+
+ public void setBtVersionCallback(BtVersionCallback callback) {
+ this.btVersionCallback = callback;
+ }
+
+ public void setBtUpgradeCallback(BtUpgradeCallback callback) {
+ this.btUpgradeCallback = callback;
+ }
+
+ public void setHostStatusCallback(HostStatusCallback callback) {
+ this.hostStatusCallback = callback;
+ }
+
+ public void setBootBinCheckCallback(BootBinCheckCallback callback) {
+ this.bootBinCheckCallback = callback;
+ }
+
+ public void setCollectFreqSetCallback(CollectFreqSetCallback callback) {
+ this.collectFreqSetCallback = callback;
+ }
+
+ public void setHrBpBoAutoMeasureCallback(HrBpBoAutoMeasureCallback callback) {
+ this.hrBpBoAutoMeasureCallback = callback;
+ }
+
+ public void setHrBpBoManualMeasureCallback(HrBpBoManualMeasureCallback callback) {
+ this.hrBpBoManualMeasureCallback = callback;
+ }
+
+ public void setDynamicMeasureCallback(DynamicMeasureCallback callback) {
+ this.dynamicMeasureCallback = callback;
+ }
+
+ public void setWearDetectionCallback(WearDetectionCallback callback) {
+ this.wearDetectionCallback = callback;
+ }
+
+ /**
+ * 发送佩戴检测查询请求
+ */
+ public void sendWearDetectionRequest() {
+ byte[] data = ASR5515Protocol.createWearDetectionRequest();
+ sendData(data, null);
+ }
+
+ // ASR-5515协议命令发送方法
+ public void checkDevice() {
+ byte[] data = ASR5515Protocol.createDeviceCheckRequest();
+ sendData(data, null);
+ }
+
+ public void restartDevice() {
+ byte[] data = ASR5515Protocol.createDeviceRestartRequest();
+ sendData(data, null);
+ }
+
+ public void setLogControl(boolean enable) {
+ byte[] data = ASR5515Protocol.createLogControlRequest((byte)0, (byte)1, (byte)(enable ? 1 : 0));
+ sendData(data, null);
+ }
+
+ public void queryBtVersion() {
+ byte[] data = ASR5515Protocol.createBtVersionRequest();
+ sendData(data, null);
+ }
+
+ /**
+ * 发送蓝牙升级请求
+ */
+ public void upgradeBt() {
+ byte[] data = ASR5515Protocol.createBtUpgradeRequest();
+ sendData(data, null);
+ }
+
+ /**
+ * 发送主机状态同步请求
+ * @param screenOn 屏幕状态(true:亮屏,false:灭屏)
+ */
+ public void syncHostStatus(boolean screenOn) {
+ byte[] data = ASR5515Protocol.createHostStatusRequest(screenOn);
+ sendData(data, null);
+ }
+
+ /**
+ * 发送boot bin检查请求
+ */
+ public void checkBootBin() {
+ byte[] data = ASR5515Protocol.createBootBinCheckRequest();
+ sendData(data, null);
+ }
+
+ /**
+ * 设置采集频率
+ * @param len 长度
+ * @param freq 频率值
+ */
+ public void setCollectFreq(byte len, byte freq) {
+ byte[] data = ASR5515Protocol.createCollectFreqSetRequest(len, freq);
+ sendData(data, null);
+ }
+
+ /**
+ * 发送HrBpBo自动测量请求
+ */
+ public void startHrBpBoAutoMeasure() {
+ byte[] data = ASR5515Protocol.createHrBpBoAutoMeasureRequest();
+ sendData(data, null);
+ }
+
+ /**
+ * 发送HrBpBo手动测量请求
+ */
+ public void startHrBpBoManualMeasure() {
+ byte[] data = ASR5515Protocol.createHrBpBoManualMeasureRequest();
+ sendData(data, null);
+ }
+
+ /**
+ * 发送动态测量请求
+ */
+ public void startDynamicMeasure() {
+ byte[] data = ASR5515Protocol.createDynamicMeasureRequest();
+ sendData(data, null);
+ }
+
+ public boolean open() {
+ try {
+ LogManager.d(TAG, "Opening serial port: " + mPortName + " with baud rate: " + mBaudRate);
+ if (!isSimulationMode) {
+ mSerialPort = new SerialPort(new File(mPortName), mBaudRate);
+ LogManager.d(TAG, "Serial port opened successfully");
+ } else {
+ LogManager.d(TAG, "Running in simulation mode, skipping actual port opening");
+ }
+ startSendThread();
+ startReceiveThread();
+ startProcessThread();
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ public void close() {
+ isRunning = false;
+ if (sendThread != null) {
+ sendThread.interrupt();
+ try {
+ sendThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ sendThread = null;
+ }
+ if (receiveThread != null) {
+ receiveThread.interrupt();
+ try {
+ receiveThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ receiveThread = null;
+ }
+ if (processThread != null) {
+ processThread.interrupt();
+ try {
+ processThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ processThread = null;
+ }
+ if (mSerialPort != null) {
+ mSerialPort.closePort();
+ mSerialPort = null;
+ }
+ }
+
+ public void sendAsync(byte[] data) {
+ if (data != null) {
+ sendQueue.offer(data);
+ }
+ }
+
+ public void sendData(byte[] data, SendCallback callback) {
+ if (data == null || data.length == 0) {
+ if (callback != null) {
+ callback.onSendFailed(data, new IllegalArgumentException("Data is null or empty"));
+ }
+ return;
+ }
+
+ if (isSimulationMode) {
+
+ if (callback != null) {
+ callback.onSendSuccess(data);
+ }
+ return;
+ }
+// LogManager.d(TAG, "ASR->5515 DATA: " + bytesToHexString(data));
+ sendAsync(data);
+ if (callback != null) {
+ this.sendCallback = callback;
+ }
+ }
+
+ /**
+ * 将字节数组转换为16进制字符串
+ * @param bytes 字节数组
+ * @return 16进制字符串,格式如: "01 A2 FF"
+ */
+ public static String bytesToHexString(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02X ", b));
+ }
+ return sb.toString().trim();
+ }
+
+
+ private void startSendThread() {
+ if (sendThread == null) {
+ LogManager.d(TAG, "Starting send thread");
+ isRunning = true;
+ sendThread = new Thread(() -> {
+ LogManager.d(TAG, "Send thread started");
+ while (isRunning) {
+ try {
+ byte[] data = sendQueue.take();
+ if (isSimulationMode) {
+ LogManager.d(TAG, "[SimulationMode] Sending data: " + bytesToHexString(data));
+ if (sendCallback != null) {
+ sendCallback.onSendSuccess(data);
+ }
+ } else if (mSerialPort != null) {
+ try {
+ LogManager.d(TAG, "ASR->5515 DATA: " + Arrays.toString(data));
+ LogManager.d(TAG, "ASR->5515 DATA: " + bytesToHexString(data));
+ mSerialPort.getOutputStream().write(data);
+ if (sendCallback != null) {
+ sendCallback.onSendSuccess(data);
+ }
+ } catch (IOException e) {
+ //LogManager.e(TAG, "Failed to send data: " + Arrays.toString(data));
+ if (sendCallback != null) {
+ sendCallback.onSendFailed(data, e);
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ sendThread.start();
+ }
+ }
+
+ private void startReceiveThread() {
+ if (receiveThread == null) {
+ receiveThread = new Thread(() -> {
+ byte[] buffer = new byte[1024];
+ while (isRunning) {
+ if (mSerialPort != null) {
+ try {
+ int bytesRead = mSerialPort.getInputStream().read(buffer);
+ if (bytesRead > 0) {
+ byte[] receivedData = Arrays.copyOf(buffer, bytesRead);
+ LogManager.d(TAG, "5515->ASR DATA: " + Arrays.toString(receivedData));
+ LogManager.d(TAG, "5515->ASR DATA: " + bytesToHexString(receivedData));
+ receiveQueue.put(receivedData);
+ }
+ } catch (IOException e) {
+ if (receiveCallback != null) {
+ receiveCallback.onReceiveError(e);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ });
+ receiveThread.start();
+ }
+ }
+
+ private void startProcessThread() {
+ if (processThread == null) {
+ processThread = new Thread(() -> {
+ while (isRunning) {
+ try {
+ byte[] receivedData = receiveQueue.poll(100, TimeUnit.MILLISECONDS);
+ if (receivedData != null) {
+ handleReceivedData(receivedData);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ processThread.start();
+ }
+ }
+
+ public int write(byte[] buffer, int size) {
+ if (mSerialPort != null) {
+ try {
+ mSerialPort.getOutputStream().write(buffer, 0, size);
+ return size; // 返回实际写入的字节数
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return -1;
+ }
+
+ public int read(byte[] buffer, int size) {
+ if (mSerialPort != null) {
+ try {
+ return mSerialPort.getInputStream().read(buffer, 0, size);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return -1;
+ }
+
+ private void handleReceivedData(byte[] data) {
+ // 将新数据添加到帧缓冲区
+ if (frameBuffer.position() + data.length > frameBuffer.capacity()) {
+ // 缓冲区溢出,清空缓冲区
+ frameBuffer.clear();
+ LogManager.e(TAG, "Frame buffer overflow, cleared");
+ return;
+ }
+ frameBuffer.put(data);
+
+ // 尝试解析完整的帧
+ while (frameBuffer.position() > 0) {
+ frameBuffer.flip(); // 切换到读取模式
+ byte[] bufferArray = new byte[frameBuffer.remaining()];
+ frameBuffer.get(bufferArray);
+ frameBuffer.clear();
+
+ ASR5515Protocol.Frame frame = ASR5515Protocol.parseFrame(bufferArray);
+ if (frame == null) {
+ // 解析失败,可能是数据不完整,保留数据等待下次处理
+ frameBuffer.put(bufferArray);
+ break;
+ }
+
+ // 处理解析出的帧
+ processFrame(frame);
+
+ // 如果还有剩余数据,放回缓冲区
+ if (frame.remainingData != null && frame.remainingData.length > 0) {
+ frameBuffer.put(frame.remainingData);
+ }
+ }
+ }
+
+ private void processFrame(ASR5515Protocol.Frame frame) {
+ // 通知原始数据接收回调
+ if (receiveCallback != null) {
+ receiveCallback.onDataReceived(frame.command, frame.data);
+ }
+ LogManager.d(TAG,"CMD TYPE:"+frame.command);
+ // 处理特定类型的响应
+ switch (frame.command) {
+ case ASR5515Protocol.Commands.CMD_CHECK_DEVICE_RESP:
+ ASR5515Protocol.DeviceInfo deviceInfo = ASR5515Protocol.parseDeviceCheckResponse(frame);
+ if (deviceInfo != null && deviceCheckCallback != null) {
+ deviceCheckCallback.onDeviceCheckResponse(deviceInfo);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_RESTART_DEVICE_RESP:
+ if (deviceControlCallback != null) {
+ deviceControlCallback.onDeviceRestartResponse(frame.data.length == 0);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_LOG_CONTROL_RESP:
+ if (logControlCallback != null) {
+ logControlCallback.onLogControlResponse(frame.data.length == 0);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_BT_VERSION_RESP:
+ String version = ASR5515Protocol.parseBtVersionResponse(frame);
+ if (version != null && btVersionCallback != null) {
+ btVersionCallback.onBtVersionResponse(version);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_BT_UPGRADE_RESP:
+ ASR5515Protocol.BtUpgradeResponse upgradeResponse = ASR5515Protocol.parseBtUpgradeResponse(frame);
+ if (upgradeResponse != null && btUpgradeCallback != null) {
+ btUpgradeCallback.onBtUpgradeResponse(upgradeResponse);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_HOST_STATUS_RESP:
+ case ASR5515Protocol.Commands.CMD_HOST_STATUS_BOOT_RESP:
+ ASR5515Protocol.HostStatusResponse statusResponse = ASR5515Protocol.parseHostStatusResponse(frame);
+ if (statusResponse != null && hostStatusCallback != null) {
+ hostStatusCallback.onHostStatusResponse(statusResponse);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_BOOT_BIN_CHECK_RESP:
+ ASR5515Protocol.BootBinCheckResponse bootBinCheckResponse = ASR5515Protocol.parseBootBinCheckResponse(frame);
+ if (bootBinCheckResponse != null && bootBinCheckCallback != null) {
+ bootBinCheckCallback.onBootBinCheckResponse(bootBinCheckResponse);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_COLLECT_FREQ_SET_RESP:
+ ASR5515Protocol.CollectFreqSetResponse collectFreqSetResponse = ASR5515Protocol.parseCollectFreqSetResponse(frame);
+ if (collectFreqSetResponse != null && collectFreqSetCallback != null) {
+ collectFreqSetCallback.onCollectFreqSetResponse(collectFreqSetResponse);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_HRBPBO_AUTO_MEASURE_RESP:
+ ASR5515Protocol.HrBpBoAutoMeasureResponse hrBpBoAutoMeasureResponse = ASR5515Protocol.parseHrBpBoAutoMeasureResponse(frame);
+ if (hrBpBoAutoMeasureResponse != null && hrBpBoAutoMeasureCallback != null) {
+ hrBpBoAutoMeasureCallback.onHrBpBoAutoMeasureResponse(hrBpBoAutoMeasureResponse);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_HRBPBO_MANUAL_MEASURE_RESP:
+ ASR5515Protocol.HrBpBoManualMeasureResponse hrBpBoManualMeasureResponse = ASR5515Protocol.parseHrBpBoManualMeasureResponse(frame);
+ if (hrBpBoManualMeasureResponse != null && hrBpBoManualMeasureCallback != null) {
+ hrBpBoManualMeasureCallback.onHrBpBoManualMeasureResponse(hrBpBoManualMeasureResponse);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_DYNAMIC_MEASURE_RESP:
+ ASR5515Protocol.DynamicMeasureResponse dynamicMeasureResponse = ASR5515Protocol.parseDynamicMeasureResponse(frame);
+ if (dynamicMeasureResponse != null && dynamicMeasureCallback != null) {
+ dynamicMeasureCallback.onDynamicMeasureResponse(dynamicMeasureResponse);
+ }
+ break;
+
+ case ASR5515Protocol.Commands.CMD_WEAR_DETECTION_RESP:
+ ASR5515Protocol.WearDetectionResponse wearDetectionResponse = ASR5515Protocol.parseWearDetectionResponse(frame);
+ if (wearDetectionResponse != null && wearDetectionCallback != null) {
+ wearDetectionCallback.onWearDetectionResponse(wearDetectionResponse);
+ }
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_asr5515_demo.xml b/app/src/main/res/layout/activity_asr5515_demo.xml
new file mode 100644
index 0000000..5c7b45d
--- /dev/null
+++ b/app/src/main/res/layout/activity_asr5515_demo.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..ea7185c
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_wear_detection.xml b/app/src/main/res/layout/activity_wear_detection.xml
new file mode 100644
index 0000000..459179c
--- /dev/null
+++ b/app/src/main/res/layout/activity_wear_detection.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..4df685e
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c8524cd
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d2f80f3
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ iB86
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..b9e96e2
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..565f8c2
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,4 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+alias(libs.plugins.android.application) apply false
+}
\ No newline at end of file
diff --git a/docs/ASR-5515通信协议.docx b/docs/ASR-5515通信协议.docx
new file mode 100644
index 0000000..dfcb28b
Binary files /dev/null and b/docs/ASR-5515通信协议.docx differ
diff --git a/docs/iB86.zip b/docs/iB86.zip
new file mode 100644
index 0000000..6479411
Binary files /dev/null and b/docs/iB86.zip differ
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..4387edc
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..08c8f02
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,22 @@
+[versions]
+agp = "8.7.2"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+appcompat = "1.6.1"
+material = "1.10.0"
+activity = "1.8.0"
+constraintlayout = "2.1.4"
+
+[libraries]
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..1bba451
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu May 29 08:58:17 GMT+08:00 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..329133a
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "iB86"
+include ':app'