oxFaceAndroid
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

526 lines
19 KiB

package com.ouxuan.oxface.device;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.kongqw.serialportlibrary.SerialPortManager;
import com.kongqw.serialportlibrary.listener.OnSerialPortDataListener;
import com.kongqw.serialportlibrary.utils.ByteUtils;
import com.ouxuan.oxface.utils.LogManager;
import java.io.File;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* Ox485 串口通信模块
* 基于kongqw/SerialPortLibrary实现485通信功能
* 主要用于获取摄像头人数等485设备通信
* 采用即用即连模式:每次请求时打开连接,完成后立即关闭,确保通信可靠性
*
* @author AI Assistant
* @version 3.0 - 即用即连模式
* @date 2024/09/22
*/
public class Ox485 {
private static final String TAG = "Ox485";
// 485通信配置
private static final String DEFAULT_SERIAL_PORT_PATH = "/dev/ttyS6";
private static final int DEFAULT_BAUD_RATE = 9600;
private static final int DEFAULT_STOP_BITS = 1;
private static final String HEX_COMMAND_GET_PEOPLE_NUM = "0F030001000294E5";
private static final long TIMEOUT_485_MILLIS = 10000; // 10秒超时
// 即用即连模式配置
private static final int MAX_RETRY_COUNT = 3; // 最大重试次数
private static final long RETRY_DELAY = 1000; // 重试延迟1秒
// 单例实例
private static volatile Ox485 instance;
// 485通信相关
private Handler mainHandler;
private boolean gateCamera485OxOn = false;
private Context context;
// 数据接收相关
private long lastRecvTime = 0;
private Handler timeoutHandler;
private Runnable timeoutRunnable;
/**
* 485通信回调接口
*/
public interface PeopleNumCallback {
void onSuccess(int peopleNum);
void onError(String errorMessage);
}
private Ox485() {
mainHandler = new Handler(Looper.getMainLooper());
timeoutHandler = new Handler(Looper.getMainLooper());
}
/**
* 获取Ox485单例实例
* @return Ox485实例
*/
public static Ox485 getInstance() {
if (instance == null) {
synchronized (Ox485.class) {
if (instance == null) {
instance = new Ox485();
}
}
}
return instance;
}
/**
* 初始化Ox485模块
* @param context 上下文
*/
public void initialize(Context context) {
this.context = context;
LogManager.logInfo(TAG, "Ox485模块初始化完成,采用即用即连模式");
}
/**
* 设置485模式开关
* @param enabled 是否启用485模式
*/
public void setGateCamera485OxOn(boolean enabled) {
this.gateCamera485OxOn = enabled;
LogManager.logInfo(TAG, "设置485模式开关: " + enabled);
}
/**
* 获取485模式开关状态
* @return true表示485模式已启用
*/
public boolean isGateCamera485OxOn() {
return gateCamera485OxOn;
}
/**
* 通过485获取摄像头人数
* 采用即用即连模式:每次请求时打开连接,完成后立即关闭
* @param callback 结果回调
*/
public void sendHex485ForPeopleNum(PeopleNumCallback callback) {
LogManager.logInfo(TAG, "开始通过485获取摄像头人数(即用即连模式)");
if (!gateCamera485OxOn) {
String errorMsg = "485模式未开启";
LogManager.logError(TAG, errorMsg);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
return;
}
try {
// 创建临时串口管理器
SerialPortManager serialPortManager = new SerialPortManager();
// 执行通信
sendCommandAndWaitResponse(serialPortManager, callback);
} catch (Exception e) {
String errorMsg = "485通信异常: " + e.getMessage();
LogManager.logError(TAG, errorMsg, e);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
}
}
/**
* 发送命令并等待响应(采用即用即连模式)
* @param serialPortManager 串口管理器实例
* @param callback 结果回调
*/
private void sendCommandAndWaitResponse(SerialPortManager serialPortManager, PeopleNumCallback callback) {
// 设置数据监听器
serialPortManager.setOnSerialPortDataListener(new OnSerialPortDataListener() {
@Override
public void onDataReceived(byte[] bytes) {
long timeInterval = (System.nanoTime() - lastRecvTime) / 1000000;
lastRecvTime = System.nanoTime();
String hexData = ByteUtils.bytesToHexString(bytes);
LogManager.logInfo(TAG, "Ox485接收到数据: " + hexData + ", 字节数组: " + java.util.Arrays.toString(bytes) + ", 数据长度: " + bytes.length);
// 数据包过滤:只处理以[15, 3, 4, 0]开头的正常响应数据包
if (!isNormalResponsePacket(bytes)) {
LogManager.logInfo(TAG, "Ox485接收到非正常响应数据包,已过滤: " + hexData + ", 期望格式: [15, 3, 4, 0]");
// 即用即连模式下,收到任何数据都关闭连接
closeAndCleanup(serialPortManager);
return;
}
// 取消超时处理
cancelTimeout();
// 解析人数数据
int peopleNum = get485PeopleNum(bytes);
if (peopleNum >= 0) {
LogManager.logInfo(TAG, "485获取人数成功: " + peopleNum);
// 完成后关闭连接
closeAndCleanup(serialPortManager);
if (callback != null) {
mainHandler.post(() -> callback.onSuccess(peopleNum));
}
} else {
String errorMsg = "485数据解析失败,数据: " + hexData;
LogManager.logError(TAG, errorMsg);
// 完成后关闭连接
closeAndCleanup(serialPortManager);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
}
}
@Override
public void onDataSent(byte[] bytes) {
String hexData = ByteUtils.bytesToHexString(bytes);
LogManager.logInfo(TAG, "Ox485发送数据成功: " + hexData + ", 数据长度: " + bytes.length);
}
});
// 打开串口并发送HEX命令
try {
File serialPortFile = new File(DEFAULT_SERIAL_PORT_PATH);
// 检查串口文件是否存在
if (!serialPortFile.exists()) {
String errorMsg = "485串口设备文件不存在: " + DEFAULT_SERIAL_PORT_PATH;
LogManager.logError(TAG, errorMsg);
closeAndCleanup(serialPortManager);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
return;
}
// 配置串口参数
boolean openResult = serialPortManager.openSerialPort(serialPortFile, DEFAULT_BAUD_RATE);
LogManager.logInfo(TAG, "485串口打开结果: " + openResult);
if (!openResult) {
String errorMsg = "打开485串口失败: " + DEFAULT_SERIAL_PORT_PATH;
LogManager.logError(TAG, errorMsg);
closeAndCleanup(serialPortManager);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
return;
}
// 启动读写线程
serialPortManager.startReadThread(485);
serialPortManager.startSendThread();
byte[] hexBytes = hexStringToByteArray(HEX_COMMAND_GET_PEOPLE_NUM);
// 验证要发送的数据
LogManager.logInfo(TAG, "准备发送485命令: " + HEX_COMMAND_GET_PEOPLE_NUM + ", 转换后字节数组: " + java.util.Arrays.toString(hexBytes));
boolean sendResult = serialPortManager.sendBytes(hexBytes);
LogManager.logInfo(TAG, "485命令发送结果: " + sendResult);
if (!sendResult) {
String errorMsg = "485命令发送失败,可能串口连接异常";
LogManager.logError(TAG, errorMsg);
closeAndCleanup(serialPortManager);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
return;
}
// 设置超时处理
setupTimeout(serialPortManager, callback);
} catch (Exception e) {
String errorMsg = "发送485命令异常: " + e.getMessage();
LogManager.logError(TAG, errorMsg, e);
closeAndCleanup(serialPortManager);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
}
}
/**
* 设置超时处理
* @param serialPortManager 串口管理器
* @param callback 结果回调
*/
private void setupTimeout(SerialPortManager serialPortManager, PeopleNumCallback callback) {
cancelTimeout(); // 先取消之前的超时任务
long startTime = System.currentTimeMillis();
LogManager.logInfo(TAG, "设置485查询超时处理,超时时间: " + (TIMEOUT_485_MILLIS / 1000) + "秒");
timeoutRunnable = () -> {
long elapsedTime = System.currentTimeMillis() - startTime;
String errorMsg = "485人数查询超时(等待时间: " + elapsedTime + "ms)";
LogManager.logError(TAG, errorMsg);
// 超时后关闭连接
closeAndCleanup(serialPortManager);
if (callback != null) {
callback.onError(errorMsg);
}
};
timeoutHandler.postDelayed(timeoutRunnable, TIMEOUT_485_MILLIS);
}
/**
* 取消超时处理
*/
private void cancelTimeout() {
if (timeoutHandler != null && timeoutRunnable != null) {
timeoutHandler.removeCallbacks(timeoutRunnable);
timeoutRunnable = null;
}
}
/**
* 解析485人数数据
* 对应uniapp中的get485PeopleNum方法
* @param arr 接收到的字节数组
* @return 人数,-1表示解析失败
*/
private int get485PeopleNum(byte[] arr) {
LogManager.logDebug(TAG, "解析485人数数据: " + java.util.Arrays.toString(arr));
// 数据校验
if (arr == null) {
LogManager.logError(TAG, "485数据为空");
return -1;
}
if (arr.length != 9) {
LogManager.logError(TAG, "485数据长度错误,期望9字节,实际: " + arr.length + ", 数据: " + java.util.Arrays.toString(arr));
return -1;
}
// 检查第3个字节是否为4(表示正确响应)
if (arr[2] != 4) {
LogManager.logError(TAG, "485响应格式错误,第3字节应为4,实际: " + arr[2] + ", 数据: " + java.util.Arrays.toString(arr));
return -1;
}
// 提取第5个字节作为人数数据,使用位掩码确保为无符号整数
int peopleNum = arr[4] & 0xFF;
LogManager.logInfo(TAG, "解析485人数成功: " + peopleNum);
return peopleNum;
}
/**
* 异步获取摄像头人数(使用CompletableFuture)
* @return CompletableFuture<Integer> 人数结果
*/
public CompletableFuture<Integer> sendHex485ForPeopleNumAsync() {
LogManager.logInfo(TAG, "异步获取摄像头人数");
CompletableFuture<Integer> future = new CompletableFuture<>();
sendHex485ForPeopleNum(new PeopleNumCallback() {
@Override
public void onSuccess(int peopleNum) {
future.complete(peopleNum);
}
@Override
public void onError(String errorMessage) {
future.completeExceptionally(new RuntimeException(errorMessage));
}
});
return future;
}
/**
* 检查是否需要打开485串口
*/
public void checkIsNeedOpen485() {
LogManager.logInfo(TAG, "检查485串口状态(即用即连模式)");
String status = get485Status();
LogManager.logInfo(TAG, status);
}
/**
* 发送自定义485 HEX命令
* @param hexCommand HEX命令字符串
*/
public void send485HexCommand(String hexCommand) {
LogManager.logInfo(TAG, "发送485 HEX命令: " + hexCommand);
try {
if (!gateCamera485OxOn) {
LogManager.logWarning(TAG, "485模式未开启,无法发送命令");
return;
}
// 创建临时串口管理器发送命令
SerialPortManager serialPortManager = new SerialPortManager();
File serialPortFile = new File(DEFAULT_SERIAL_PORT_PATH);
if (!serialPortFile.exists()) {
LogManager.logWarning(TAG, "串口设备文件不存在: " + DEFAULT_SERIAL_PORT_PATH);
return;
}
boolean openResult = serialPortManager.openSerialPort(serialPortFile, DEFAULT_BAUD_RATE);
if (!openResult) {
LogManager.logWarning(TAG, "打开串口失败: " + DEFAULT_SERIAL_PORT_PATH);
return;
}
serialPortManager.startReadThread(485);
serialPortManager.startSendThread();
byte[] hexBytes = hexStringToByteArray(hexCommand);
serialPortManager.sendBytes(hexBytes);
LogManager.logInfo(TAG, "485命令发送完成");
// 发送完成后关闭连接
closeAndCleanup(serialPortManager);
} catch (Exception e) {
LogManager.logError(TAG, "发送485命令异常: " + e.getMessage(), e);
}
}
/**
* 获取485串口状态(即用即连模式)
* @return 串口状态描述
*/
public String get485Status() {
String status = "串口状态: 即用即连模式" +
", 路径: " + DEFAULT_SERIAL_PORT_PATH +
", 波特率: " + DEFAULT_BAUD_RATE +
", 485模式: " + (gateCamera485OxOn ? "已启用" : "未启用");
LogManager.logInfo(TAG, status);
return status;
}
/**
* 关闭485串口(兼容旧接口)
*/
public void close485SerialPort() {
LogManager.logInfo(TAG, "485串口即用即连模式无需手动关闭");
}
/**
* 释放资源
*/
public void release() {
LogManager.logInfo(TAG, "释放Ox485资源(即用即连模式)");
}
/**
* 485连接诊断方法
*/
public void diagnose485Connection() {
LogManager.logInfo(TAG, "开始485连接诊断");
// 1. 检查串口设备文件
File serialPortFile = new File(DEFAULT_SERIAL_PORT_PATH);
LogManager.logInfo(TAG, "诊断1 - 串口设备文件: " + DEFAULT_SERIAL_PORT_PATH);
LogManager.logInfo(TAG, " - 文件存在: " + serialPortFile.exists());
if (serialPortFile.exists()) {
LogManager.logInfo(TAG, " - 可读权限: " + serialPortFile.canRead());
LogManager.logInfo(TAG, " - 可写权限: " + serialPortFile.canWrite());
LogManager.logInfo(TAG, " - 文件大小: " + serialPortFile.length());
}
// 2. 检查485配置
LogManager.logInfo(TAG, "诊断2 - 485配置:");
LogManager.logInfo(TAG, " - 485模式开关: " + gateCamera485OxOn);
LogManager.logInfo(TAG, " - 串口路径: " + DEFAULT_SERIAL_PORT_PATH);
LogManager.logInfo(TAG, " - 波特率: " + DEFAULT_BAUD_RATE);
LogManager.logInfo(TAG, " - 查询命令: " + HEX_COMMAND_GET_PEOPLE_NUM);
// 3. 尝试通信测试
LogManager.logInfo(TAG, "诊断3 - 通信测试:");
if (gateCamera485OxOn) {
LogManager.logInfo(TAG, " - 485模式已启用,开始测试通信...");
sendHex485ForPeopleNum(new PeopleNumCallback() {
@Override
public void onSuccess(int peopleNum) {
LogManager.logInfo(TAG, "诊断测试成功 - 获取到人数: " + peopleNum);
}
@Override
public void onError(String errorMessage) {
LogManager.logError(TAG, "诊断测试失败: " + errorMessage);
}
});
} else {
LogManager.logWarning(TAG, " - 485模式未启用,无法进行通信测试");
}
LogManager.logInfo(TAG, "485连接诊断完成");
}
/**
* 关闭并清理串口资源
* @param serialPortManager 串口管理器
*/
private void closeAndCleanup(SerialPortManager serialPortManager) {
if (serialPortManager != null) {
try {
serialPortManager.closeSerialPort();
LogManager.logInfo(TAG, "485串口已关闭并清理资源");
} catch (Exception e) {
LogManager.logError(TAG, "关闭485串口时发生异常: " + e.getMessage(), e);
}
}
}
/**
* HEX字符串转字节数组
* @param hexString HEX字符串
* @return 字节数组
*/
private byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
+ Character.digit(hexString.charAt(i + 1), 16));
}
return data;
}
/**
* 检查是否为正常的485响应数据包
* 只处理以[15, 3, 4, 0]开头的正常响应数据包
* @param bytes 接收到的字节数组
* @return true表示是正常响应数据包,false表示是其他数据包
*/
private boolean isNormalResponsePacket(byte[] bytes) {
// 数据校验
if (bytes == null || bytes.length < 4) {
return false;
}
// 检查是否以[15, 3, 4, 0]开头
// 15: 0x0F, 3: 0x03, 4: 0x04, 0: 0x00
return (bytes[0] == 15 && bytes[1] == 3 && bytes[2] == 4 && bytes[3] == 0);
}
}