新增机器人眼睛UI,优化逻辑代码、提高性能响应,提高代码易读性和健壮性

This commit is contained in:
peng 2025-06-27 16:11:08 +08:00
parent c8aa3c46c3
commit c5338259ec
7 changed files with 372 additions and 235 deletions

View File

@ -2,6 +2,7 @@ package com.ismart.ib86.app;
import android.Manifest; import android.Manifest;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.view.Window; import android.view.Window;
@ -10,6 +11,7 @@ import android.view.WindowInsetsController;
import android.os.Build; import android.os.Build;
import android.view.View; import android.view.View;
import android.util.Log; import android.util.Log;
import android.widget.RelativeLayout;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
@ -24,10 +26,11 @@ import com.ismart.ib86.feature.serial.SerialPort.ASR5515DeviceManager;
import com.ismart.ib86.feature.serial.SerialPort.ASR5515Protocol; import com.ismart.ib86.feature.serial.SerialPort.ASR5515Protocol;
import com.ismart.ib86.common.utils.LogManager; import com.ismart.ib86.common.utils.LogManager;
import com.ismart.ib86.feature.gpio.GpioTest; import com.ismart.ib86.feature.gpio.GpioTest;
import com.ismart.ib86.view.RobotEyesView;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
private static final String DEVICE_PATH = "/dev/ttyS2"; private static final String DEVICE_PATH = "/dev/ttyS3";
private static final int BAUD_RATE = 921600; private static final int BAUD_RATE = 921600;
// 权限请求码 // 权限请求码
@ -40,15 +43,38 @@ public class MainActivity extends AppCompatActivity {
private static final int BT_POWER_EN = 75; private static final int BT_POWER_EN = 75;
private ASR5515DeviceManager deviceManager; private ASR5515DeviceManager deviceManager;
private Handler handler = new Handler(); // 使用弱引用避免内存泄漏
private GpioTest gpioTest; private Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull android.os.Message msg) {
return false;
}
});
private Runnable wearDetectionRunnable = new Runnable() {
@Override
public void run() {
// 发送佩戴检测请求
deviceManager.sendWearDetectionRequest();
// 1秒后再次执行
handler.postDelayed(this, 1000);
}
};
private GpioManager gpioManager; private GpioManager gpioManager;
private RelativeLayout mainLayout;
private RobotEyesView robotEyesView;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
mainLayout = findViewById(R.id.main_layout);
mainLayout.setBackgroundColor(Color.BLACK);
robotEyesView = new RobotEyesView(this);
mainLayout.addView(robotEyesView);
// 检查并请求必要的权限 // 检查并请求必要的权限
checkAndRequestPermissions(); checkAndRequestPermissions();
@ -210,39 +236,50 @@ public class MainActivity extends AppCompatActivity {
// deviceManager.queryBtVersion(); // deviceManager.queryBtVersion();
// } // }
// }, 2000); // }, 2000);
deviceManager.stopGH3220Measure(ASR5515Protocol.GH3220MeasureType.HR); // deviceManager.stopGH3220Measure(ASR5515Protocol.GH3220MeasureType.HR);
deviceManager.stopGH3220Measure(ASR5515Protocol.GH3220MeasureType.SPO2); // deviceManager.stopGH3220Measure(ASR5515Protocol.GH3220MeasureType.SPO2);
handler.postDelayed(new Runnable() {
@Override
public void run() {
deviceManager.startGH3220Measure(ASR5515Protocol.GH3220MeasureType.SPO2);
}
}, 1000);
// handler.postDelayed(new Runnable() { // handler.postDelayed(new Runnable() {
// @Override // @Override
// public void run() { // public void run() {
// deviceManager.sendWearDetectionRequest(); // deviceManager.startGH3220Measure(ASR5515Protocol.GH3220MeasureType.SPO2);
// } // }
// }, 4000); // }, 1000);
handler.postDelayed(wearDetectionRunnable, 1000);
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
// 移除所有Handler消息和回调避免内存泄漏
if (handler != null) {
handler.removeCallbacksAndMessages(null); handler.removeCallbacksAndMessages(null);
handler = null;
}
// 移至后台线程执行耗时操作避免ANR // 移至后台线程执行耗时操作避免ANR
new Thread(() -> { new Thread(() -> {
// if (deviceManager != null) { try {
// deviceManager.syncHostStatus(false); // 停止所有测量
// deviceManager.close(); if (deviceManager != null) {
// } deviceManager.stopGH3220Measure(ASR5515Protocol.GH3220MeasureType.HR);
deviceManager.stopGH3220Measure(ASR5515Protocol.GH3220MeasureType.SPO2);
deviceManager.syncHostStatus(false);
deviceManager.close();
deviceManager = null;
}
// 释放GPIO测试资源 // 关闭GPIO电源
if (gpioTest != null) { if (gpioManager != null) {
gpioTest.release(); try {
gpioTest = null; gpioManager.setValue(BT_POWER_EN, GpioManager.VALUE_LOW);
} catch (Exception e) {
LogManager.e(TAG, "关闭GPIO电源失败: " + e.getMessage());
}
gpioManager = null;
}
} catch (Exception e) {
LogManager.e(TAG, "资源释放过程中发生错误: " + e.getMessage());
} }
}).start(); }).start();
} }

View File

@ -13,11 +13,47 @@ import com.ismart.ib86.feature.gpio.GpioManager;
*/ */
public class ASR5515DeviceManager { public class ASR5515DeviceManager {
private static final String TAG = "ASR5515DeviceManager"; private static final String TAG = "ASR5515DeviceManager";
// 设备状态标志
private volatile boolean isDeviceOpen = false;
private final Object sendLock = new Object(); // 发送数据的同步锁
/**
* 检查设备是否就绪
* @return true 如果设备已打开且就绪
*/
private boolean isDeviceReady() {
return isDeviceOpen && serialPortHelper != null;
}
/**
* 安全发送数据的方法
* @param data 要发送的数据
* @return true 如果发送成功
*/
protected boolean safeSendData(byte[] data) {
if (!isDeviceReady()) {
LogManager.e(TAG, "设备未就绪,无法发送数据");
return false;
}
synchronized (sendLock) {
try {
serialPortHelper.sendAsync(data);
return true;
} catch (Exception e) {
LogManager.e(TAG, "发送数据失败: " + e.getMessage());
return false;
}
}
}
private static final int GPIO_BT_WAKE = 33; // 蓝牙唤醒GPIO private static final int GPIO_BT_WAKE = 33; // 蓝牙唤醒GPIO
private final SerialPortHelper serialPortHelper; private final SerialPortHelper serialPortHelper;
private final Handler mainHandler; private final Handler mainHandler;
private final GpioManager gpioManager; private final GpioManager gpioManager;
private final Object gpioLock = new Object(); // GPIO操作的同步锁
private volatile boolean isInitialized = false; // 初始化状态标志
// 细分回调实例 // 细分回调实例
private DeviceCheckListener deviceCheckListener; private DeviceCheckListener deviceCheckListener;
private DeviceControlListener deviceControlListener; private DeviceControlListener deviceControlListener;
@ -106,16 +142,36 @@ public class ASR5515DeviceManager {
* @param context 上下文 * @param context 上下文
* @param devicePath 设备路径 * @param devicePath 设备路径
* @param baudRate 波特率 * @param baudRate 波特率
* @throws IllegalStateException 如果串口打开失败
*/ */
public ASR5515DeviceManager(Context context, String devicePath, int baudRate) { public ASR5515DeviceManager(Context context, String devicePath, int baudRate) {
synchronized (sendLock) {
try {
serialPortHelper = new SerialPortHelper(devicePath, baudRate); serialPortHelper = new SerialPortHelper(devicePath, baudRate);
mainHandler = new Handler(Looper.getMainLooper()); mainHandler = new Handler(Looper.getMainLooper());
gpioManager = GpioManager.getInstance(); gpioManager = GpioManager.getInstance();
serialPortHelper.setSimulationMode(false); serialPortHelper.setSimulationMode(false);
// 初始化串口 // 初始化串口
boolean result = serialPortHelper.open(); boolean result = serialPortHelper.open();
if (!result) { if (!result) {
LogManager.e(TAG, "Failed to open serial port: " + devicePath); String errorMsg = "Failed to open serial port: " + devicePath;
LogManager.e(TAG, errorMsg);
updateDeviceState(false);
isInitialized = false;
// 记录错误但不抛出异常允许应用继续运行
// 在实际使用时会检查isDeviceOpen状态
} else {
updateDeviceState(true);
isInitialized = true;
LogManager.i(TAG, "Serial port opened successfully: " + devicePath);
}
} catch (Exception e) {
LogManager.e(TAG, "Device initialization failed: " + e.getMessage());
updateDeviceState(false);
isInitialized = false;
throw new IllegalStateException("Failed to initialize device manager", e);
}
} }
// 设置设备状态回调 // 设置设备状态回调
@ -346,28 +402,72 @@ public class ASR5515DeviceManager {
} }
/** /**
* 检查设备状态 * 检查设备是否已打开且可用
* @return true 如果设备已打开且可用
*/ */
public void checkDevice() { public boolean isDeviceAvailable() {
return isDeviceReady();
}
/**
* 检查设备状态
* @return true 如果命令发送成功
*/
public boolean checkDevice() {
if (!isDeviceReady()) {
LogManager.e(TAG, "设备未就绪,无法检查设备状态");
return false;
}
try {
wakeupDevice(); wakeupDevice();
serialPortHelper.checkDevice(); serialPortHelper.checkDevice();
return true;
} catch (Exception e) {
LogManager.e(TAG, "检查设备状态失败: " + e.getMessage());
return false;
}
} }
/** /**
* 重启设备 * 重启设备
* @return true 如果命令发送成功
*/ */
public void restartDevice() { public boolean restartDevice() {
if (!isDeviceReady()) {
LogManager.e(TAG, "设备未就绪,无法重启设备");
return false;
}
try {
wakeupDevice(); wakeupDevice();
serialPortHelper.restartDevice(); serialPortHelper.restartDevice();
return true;
} catch (Exception e) {
LogManager.e(TAG, "重启设备失败: " + e.getMessage());
return false;
}
} }
/** /**
* 控制日志输出 * 控制日志输出
* @param enable 是否启用 * @param enable 是否启用
* @return true 如果命令发送成功
*/ */
public void setLogControl(boolean enable) { public boolean setLogControl(boolean enable) {
if (!isDeviceReady()) {
LogManager.e(TAG, "设备未就绪,无法控制日志输出");
return false;
}
try {
wakeupDevice(); wakeupDevice();
serialPortHelper.setLogControl(enable); serialPortHelper.setLogControl(enable);
return true;
} catch (Exception e) {
LogManager.e(TAG, "控制日志输出失败: " + e.getMessage());
return false;
}
} }
/** /**
@ -396,10 +496,31 @@ public class ASR5515DeviceManager {
} }
/** /**
* 关闭设备 * 设备状态变化的回调接口
*/ */
public void close() { public interface DeviceStateCallback {
serialPortHelper.close(); void onDeviceStateChanged(boolean isOpen);
}
private DeviceStateCallback deviceStateCallback;
/**
* 设置设备状态变化回调
* @param callback 回调接口
*/
public void setDeviceStateCallback(DeviceStateCallback callback) {
this.deviceStateCallback = callback;
}
/**
* 更新设备状态并通知回调
* @param newState 新的设备状态
*/
private void updateDeviceState(boolean newState) {
isDeviceOpen = newState;
if (deviceStateCallback != null) {
mainHandler.post(() -> deviceStateCallback.onDeviceStateChanged(newState));
}
} }
/** /**
@ -473,8 +594,10 @@ public class ASR5515DeviceManager {
/** /**
* 唤醒设备 * 唤醒设备
* 通过GPIO33唤醒蓝牙ASR休眠时会自动发送'w'字符 * 通过GPIO33唤醒蓝牙ASR休眠时会自动发送'w'字符
* 使用同步锁确保GPIO操作的线程安全
*/ */
public void wakeupDevice() { public void wakeupDevice() {
synchronized (gpioLock) {
try { try {
// 配置GPIO33为输出模式 // 配置GPIO33为输出模式
gpioManager.exportGpio(GPIO_BT_WAKE); gpioManager.exportGpio(GPIO_BT_WAKE);
@ -489,8 +612,67 @@ public class ASR5515DeviceManager {
// 拉高GPIO33 // 拉高GPIO33
gpioManager.setValue(GPIO_BT_WAKE, 1); gpioManager.setValue(GPIO_BT_WAKE, 1);
//LogManager.d(TAG, "设备唤醒成功");
} catch (InterruptedException e) {
//LogManager.e(TAG, "设备唤醒被中断: " + e.getMessage());
Thread.currentThread().interrupt(); // 重置中断状态
} catch (Exception e) { } catch (Exception e) {
LogManager.e(TAG, "Failed to wakeup bluetooth: " + e.getMessage()); //LogManager.e(TAG, "设备唤醒失败: " + e.getMessage());
// 尝试恢复GPIO状态
try {
gpioManager.setValue(GPIO_BT_WAKE, 1); // 确保GPIO处于高电平
} catch (Exception ex) {
//LogManager.e(TAG, "恢复GPIO状态失败: " + ex.getMessage());
} }
} }
} }
}
/**
* 关闭设备释放所有资源
*/
public void close() {
synchronized (sendLock) {
try {
// 清除所有回调避免内存泄漏
clearAllListeners();
// 关闭串口
if (serialPortHelper != null) {
serialPortHelper.close();
}
// 清除所有Handler消息和回调
if (mainHandler != null) {
mainHandler.removeCallbacksAndMessages(null);
}
// 更新设备状态
updateDeviceState(false);
isInitialized = false;
} catch (Exception e) {
LogManager.e(TAG, "Error closing device manager: " + e.getMessage());
}
}
}
/**
* 清除所有监听器避免内存泄漏
*/
private void clearAllListeners() {
deviceCheckListener = null;
deviceControlListener = null;
logControlListener = null;
btVersionListener = null;
btUpgradeListener = null;
hostStatusListener = null;
bootBinCheckListener = null;
collectFreqSetListener = null;
hrBpBoAutoMeasureListener = null;
hrBpBoManualMeasureListener = null;
dynamicMeasureListener = null;
wearDetectionListener = null;
gh3220MeasureListener = null;
algoResultListener = null;
}
}

View File

@ -200,15 +200,6 @@ public class SerialPortHelper {
this.algoResultCallback = callback; this.algoResultCallback = callback;
} }
/**
* 发送佩戴检测查询请求
*/
public void sendWearDetectionRequest() {
byte[] data = ASR5515Protocol.createWearDetectionRequest();
sendData(data, null);
}
// ASR-5515协议命令发送方法
public void checkDevice() { public void checkDevice() {
byte[] data = ASR5515Protocol.createDeviceCheckRequest(); byte[] data = ASR5515Protocol.createDeviceCheckRequest();
sendData(data, null); sendData(data, null);
@ -306,6 +297,14 @@ public class SerialPortHelper {
sendData(data, null); sendData(data, null);
} }
/**
* 发送佩戴检测查询请求
*/
public void sendWearDetectionRequest() {
byte[] data = ASR5515Protocol.createWearDetectionRequest();
sendData(data, null);
}
public boolean open() { public boolean open() {
try { try {
LogManager.d(TAG, "Opening serial port: " + mPortName + " with baud rate: " + mBaudRate); LogManager.d(TAG, "Opening serial port: " + mPortName + " with baud rate: " + mBaudRate);

View File

@ -0,0 +1,78 @@
package com.ismart.ib86.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;
public class RobotEyesView extends View {
private Paint paint;
private float eyeWidth = 100f; // Increased size for eyes
private float eyeHeight = 100f; // Increased size for eyes
private float borderRadius = 25f; // Adjusted border radius for larger eyes
private float blinkDuration = 3000f; // 3 seconds in milliseconds
public RobotEyesView(Context context) {
super(context);
init();
}
public RobotEyesView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RobotEyesView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paint = new Paint();
paint.setColor(Color.rgb(26, 249, 248));
paint.setStyle(Paint.Style.FILL);
startBlinkAnimation();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float centerXLeft = getWidth() / 4f;
float centerXRight = getWidth() * 3 / 4f;
float centerY = getHeight() / 3f; // Set eyes to one-third of screen height
drawEye(canvas, centerXLeft, centerY);
drawEye(canvas, centerXRight, centerY);
}
private void drawEye(Canvas canvas, float centerX, float centerY) {
canvas.drawRoundRect(centerX - eyeWidth / 2, centerY - eyeHeight / 2,
centerX + eyeWidth / 2, centerY + eyeHeight / 2,
borderRadius, borderRadius, paint);
}
private void startBlinkAnimation() {
final Animation animation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime >= 0.2 && interpolatedTime < 0.25) {
eyeHeight = 20f; // Adjusted size for blinking effect
} else if (interpolatedTime >= 0.25 && interpolatedTime < 0.3) {
eyeHeight = 100f; // Reset to original size
}
invalidate();
}
};
animation.setDuration((long) blinkDuration);
animation.setRepeatCount(Animation.INFINITE);
startAnimation(animation);
}
}

View File

@ -1,82 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设备信息"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tv_device_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#F5F5F5"
android:padding="8dp"
android:textSize="14sp"
android:minHeight="100dp"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tv_bt_version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#F5F5F5"
android:padding="8dp"
android:textSize="14sp"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<Button
android:id="@+id/btn_check_device"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="检查设备"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btn_restart_device"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="重启设备"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<Button
android:id="@+id/btn_query_bt_version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="查询蓝牙版本"
android:layout_marginEnd="8dp"/>
<ToggleButton
android:id="@+id/toggle_log"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textOn="关闭日志"
android:textOff="开启日志"/>
</LinearLayout>
</LinearLayout>

View File

@ -1,23 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main" android:id="@+id/main_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity"> tools:context=".MainActivity">
<TextView <com.ismart.ib86.view.RobotEyesView
android:id="@+id/titleTextView" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="match_parent"/>
android:layout_height="wrap_content" </RelativeLayout>
android:text="iB86 功能演示"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="佩戴检测"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/detectButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="检测佩戴状态"
app:layout_constraintTop_toBottomOf="@id/titleTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp" />
<TextView
android:id="@+id/statusLabelTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="佩戴状态:"
android:textSize="18sp"
app:layout_constraintTop_toBottomOf="@id/detectButton"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="32dp" />
<TextView
android:id="@+id/statusTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="未检测"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="@id/statusLabelTextView"
app:layout_constraintStart_toEndOf="@id/statusLabelTextView"
android:layout_marginStart="8dp" />
<TextView
android:id="@+id/logTextView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#F5F5F5"
android:padding="8dp"
android:scrollbars="vertical"
android:gravity="top|start"
app:layout_constraintTop_toBottomOf="@id/statusLabelTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="32dp" />
</androidx.constraintlayout.widget.ConstraintLayout>