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.

760 lines
28 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 2.0
* @date 2024/09/15
*/
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 long CONNECTION_CHECK_INTERVAL = 30000; // 30秒检查一次连接状态
private static final int MAX_RETRY_COUNT = 3; // 最大重试次数
private static final long RETRY_DELAY = 1000; // 重试延迟1秒
// 单例实例
private static volatile Ox485 instance;
// 485通信相关
private SerialPortManager serialPortManager;
private Handler mainHandler;
private boolean gateCamera485OxOn = false;
private Context context;
// 懒加载连接管理相关
private boolean isSerialPortOpen = false; // 串口是否已打开
private boolean isConnecting = false; // 是否正在连接中
private Handler connectionCheckHandler; // 连接检查处理器
private Runnable connectionCheckRunnable; // 连接检查任务
private long lastSuccessTime = 0; // 最后一次成功通信时间
private int currentRetryCount = 0; // 当前重试次数
// 数据接收相关
private long lastRecvTime = 0;
private Handler timeoutHandler;
private Runnable timeoutRunnable;
/**
* 485通信回调接口
*/
public interface PeopleNumCallback {
void onSuccess(int peopleNum);
void onError(String errorMessage);
}
/**
* 连接回调接口
*/
private interface ConnectionCallback {
void onConnected();
void onError(String errorMessage);
}
private Ox485() {
mainHandler = new Handler(Looper.getMainLooper());
timeoutHandler = new Handler(Looper.getMainLooper());
connectionCheckHandler = new Handler(Looper.getMainLooper());
serialPortManager = new SerialPortManager();
// 初始化连接检查任务
initConnectionCheckTask();
}
/**
* 获取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, "15:45:22.734 Ox485 D Ox485模块初始化完成,采用懒加载连接管理模式");
// 启动定时连接检查
startConnectionCheck();
}
/**
* 设置485模式开关
* @param enabled 是否启用485模式
*/
public void setGateCamera485OxOn(boolean enabled) {
this.gateCamera485OxOn = enabled;
LogManager.logInfo(TAG, "15:45:22.734 Ox485 D 设置485模式开关: " + enabled);
}
/**
* 获取485模式开关状态
* @return true表示485模式已启用
*/
public boolean isGateCamera485OxOn() {
return gateCamera485OxOn;
}
/**
* 通过485获取摄像头人数
* 对应uniapp中的sendHex485ForPeopleNum方法
* 采用懒加载连接管理,首次使用时才打开串口
* @param callback 结果回调
*/
public void sendHex485ForPeopleNum(PeopleNumCallback callback) {
LogManager.logInfo(TAG, "15:45:22.734 Ox485 D 开始通过485获取摄像头人数(懒加载模式)");
if (!gateCamera485OxOn) {
String errorMsg = "485模式未开启";
LogManager.logError(TAG, errorMsg);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
return;
}
try {
// 懒加载:检查连接状态,需要时才建立连接
ensureConnection(new ConnectionCallback() {
@Override
public void onConnected() {
// 连接成功,发送命令
sendCommandAndWaitResponse(callback);
}
@Override
public void onError(String errorMessage) {
// 连接失败
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMessage));
}
}
});
} catch (Exception e) {
String errorMsg = "485通信异常: " + e.getMessage();
LogManager.logError(TAG, errorMsg, e);
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
}
}
/**
* 发送命令并等待响应(采用懒加载连接复用)
* @param callback 结果回调
*/
private void sendCommandAndWaitResponse(PeopleNumCallback callback) {
// 发送前检查串口状态
if (!isSerialPortReallyOpen()) {
String errorMsg = "串口未打开,无法发送命令";
LogManager.logError(TAG, errorMsg);
isSerialPortOpen = false;
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
return;
}
LogManager.logInfo(TAG, "485串口状态检查通过,开始发送命令");
// 设置数据监听器
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]");
return;
}
// 取消超时处理
cancelTimeout();
// 标记成功通信(用于连接健康检查)
markSuccessfulCommunication();
// 解析人数数据
int peopleNum = get485PeopleNum(bytes);
if (peopleNum >= 0) {
LogManager.logInfo(TAG, "485获取人数成功: " + peopleNum);
if (callback != null) {
mainHandler.post(() -> callback.onSuccess(peopleNum));
}
} else {
String errorMsg = "485数据解析失败,数据: " + hexData;
LogManager.logError(TAG, errorMsg);
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 {
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);
isSerialPortOpen = false;
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
return;
}
// 设置超时处理
setupTimeout(callback);
} catch (Exception e) {
String errorMsg = "发送485命令异常: " + e.getMessage();
LogManager.logError(TAG, errorMsg, e);
// 发送失败可能是连接问题,重置连接状态
isSerialPortOpen = false;
if (callback != null) {
mainHandler.post(() -> callback.onError(errorMsg));
}
}
}
/**
* 设置超时处理
* @param callback 结果回调
*/
private void setupTimeout(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);
LogManager.logError(TAG, "485超时详情 - 串口状态: " + (isSerialPortReallyOpen() ? "已打开" : "未打开") +
", 内部连接状态: " + isSerialPortOpen +
", 最后成功时间: " + (lastSuccessTime > 0 ?
new java.text.SimpleDateFormat("HH:mm:ss.SSS").format(new java.util.Date(lastSuccessTime)) : "无"));
// 超时可能是连接问题,重置连接状态
isSerialPortOpen = false;
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, "15:45:22.734 Ox485 D 解析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 (!isSerialPortOpen) {
LogManager.logWarning(TAG, "串口未打开,无法发送命令");
return;
}
byte[] hexBytes = hexStringToByteArray(hexCommand);
serialPortManager.sendBytes(hexBytes);
LogManager.logInfo(TAG, "485命令发送完成");
} catch (Exception e) {
LogManager.logError(TAG, "发送485命令异常: " + e.getMessage(), e);
}
}
/**
* 485连接诊断方法
* 用于排查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. 检查串口管理器状态
LogManager.logInfo(TAG, "诊断2 - 串口管理器状态:");
LogManager.logInfo(TAG, " - 串口是否打开: " + (isSerialPortReallyOpen() ? "已打开" : "未打开"));
LogManager.logInfo(TAG, " - 内部连接状态: " + isSerialPortOpen);
LogManager.logInfo(TAG, " - 是否连接中: " + isConnecting);
// 3. 检查485模式开关
LogManager.logInfo(TAG, "诊断3 - 485配置:");
LogManager.logInfo(TAG, " - 485模式开关: " + gateCamera485OxOn);
LogManager.logInfo(TAG, " - 串口路径: " + DEFAULT_SERIAL_PORT_PATH);
LogManager.logInfo(TAG, " - 波特率: " + DEFAULT_BAUD_RATE);
LogManager.logInfo(TAG, " - 停止位: " + DEFAULT_STOP_BITS);
LogManager.logInfo(TAG, " - 查询命令: " + HEX_COMMAND_GET_PEOPLE_NUM);
// 4. 检查连接历史
LogManager.logInfo(TAG, "诊断4 - 连接历史:");
String lastSuccessStr = lastSuccessTime > 0 ?
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new java.util.Date(lastSuccessTime)) :
"无";
LogManager.logInfo(TAG, " - 最后成功时间: " + lastSuccessStr);
LogManager.logInfo(TAG, " - 当前重试次数: " + currentRetryCount);
LogManager.logInfo(TAG, " - 最大重试次数: " + MAX_RETRY_COUNT);
// 5. 尝试重新建立连接进行测试
LogManager.logInfo(TAG, "诊断5 - 连接测试:");
if (gateCamera485OxOn) {
LogManager.logInfo(TAG, " - 485模式已启用,尝试测试连接...");
// 强制关闭现有连接
if (isSerialPortOpen) {
LogManager.logInfo(TAG, " - 关闭现有连接进行重新测试");
close485SerialPort();
}
// 尝试重新连接并发送测试命令
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连接诊断完成");
}
/**
* 获取485串口状态(懒加载连接管理模式)
* @return 串口状态描述
*/
public String get485Status() {
String connectionStatus = isSerialPortOpen ? "已连接" : (
isConnecting ? "连接中" : "未连接"
);
String lastSuccessStr = lastSuccessTime > 0 ?
new java.text.SimpleDateFormat("HH:mm:ss.SSS").format(new java.util.Date(lastSuccessTime)) :
"无";
String status = "串口状态: " + connectionStatus +
", 路径: " + DEFAULT_SERIAL_PORT_PATH +
", 波特率: " + DEFAULT_BAUD_RATE +
", 485模式: " + (gateCamera485OxOn ? "已启用" : "未启用") +
", 最后成功时间: " + lastSuccessStr +
", 重试次数: " + currentRetryCount;
LogManager.logInfo(TAG, status);
return status;
}
/**
* 关闭485串口
*/
public void close485SerialPort() {
LogManager.logInfo(TAG, "关闭485串口");
try {
cancelTimeout();
if (serialPortManager != null) {
serialPortManager.closeSerialPort();
LogManager.logInfo(TAG, "485串口已关闭");
}
// 重置连接状态
isSerialPortOpen = false;
isConnecting = false;
lastSuccessTime = 0;
currentRetryCount = 0;
} catch (Exception e) {
LogManager.logError(TAG, "关闭485串口异常: " + e.getMessage(), e);
}
}
/**
* 释放资源
*/
public void release() {
LogManager.logInfo(TAG, "释放Ox485资源");
// 停止连接检查
stopConnectionCheck();
// 关闭串口
close485SerialPort();
}
// ========== 懒加载连接管理相关方法 ==========
/**
* 初始化连接检查任务
*/
private void initConnectionCheckTask() {
connectionCheckRunnable = new Runnable() {
@Override
public void run() {
LogManager.logDebug(TAG, "定时检查485连接状态");
// 检查最后一次成功时间,如果超过一定时间没有成功通信,则认为连接异常
long currentTime = System.currentTimeMillis();
if (isSerialPortOpen && lastSuccessTime > 0 &&
(currentTime - lastSuccessTime) > CONNECTION_CHECK_INTERVAL * 2) {
LogManager.logWarning(TAG, "检测到485连接异常,最后成功时间: " +
new java.text.SimpleDateFormat("HH:mm:ss.SSS").format(new java.util.Date(lastSuccessTime)));
// 重置连接状态
isSerialPortOpen = false;
// 尝试重连
if (gateCamera485OxOn) {
LogManager.logInfo(TAG, "尝试重新建立485连接");
retryConnection();
}
}
// 安排下次检查
connectionCheckHandler.postDelayed(this, CONNECTION_CHECK_INTERVAL);
}
};
}
/**
* 启动连接检查
*/
private void startConnectionCheck() {
LogManager.logInfo(TAG, "启动485连接定时检查,检查间隔: " + (CONNECTION_CHECK_INTERVAL / 1000) + "秒");
connectionCheckHandler.post(connectionCheckRunnable);
}
/**
* 停止连接检查
*/
private void stopConnectionCheck() {
if (connectionCheckHandler != null && connectionCheckRunnable != null) {
connectionCheckHandler.removeCallbacks(connectionCheckRunnable);
LogManager.logInfo(TAG, "485连接定时检查已停止");
}
}
/**
* 确保连接可用(懒加载核心逻辑)
* @param callback 连接结果回调
*/
private void ensureConnection(ConnectionCallback callback) {
// 如果正在连接中,等待连接完成
if (isConnecting) {
LogManager.logInfo(TAG, "连接正在进行中,等待连接完成...");
// 简单等待机制,实际项目中可以优化为队列机制
mainHandler.postDelayed(() -> ensureConnection(callback), 500);
return;
}
// 如果已经连接,直接返回
if (isSerialPortOpen) {
LogManager.logDebug(TAG, "485连接已存在,直接复用");
callback.onConnected();
return;
}
// 需要建立新连接
LogManager.logInfo(TAG, "首次使用或连接已断开,建立485连接");
createConnection(callback);
}
/**
* 建立485连接
* @param callback 连接结果回调
*/
private void createConnection(ConnectionCallback callback) {
isConnecting = true;
currentRetryCount = 0;
try {
LogManager.logInfo(TAG, "开始建立485连接 - 路径: " + DEFAULT_SERIAL_PORT_PATH + ", 波特率: " + DEFAULT_BAUD_RATE);
// 检查串口文件是否存在
File serialPortFile = new File(DEFAULT_SERIAL_PORT_PATH);
if (!serialPortFile.exists()) {
isConnecting = false;
String errorMsg = "485串口设备文件不存在: " + DEFAULT_SERIAL_PORT_PATH;
LogManager.logError(TAG, errorMsg);
callback.onError(errorMsg);
return;
}
LogManager.logInfo(TAG, "485串口设备文件存在,文件权限: 可读=" + serialPortFile.canRead() + ", 可写=" + serialPortFile.canWrite());
// 配置串口参数
boolean openResult = serialPortManager.openSerialPort(serialPortFile, DEFAULT_BAUD_RATE);
LogManager.logInfo(TAG, "485串口打开结果: " + openResult);
if (!openResult) {
isConnecting = false;
String errorMsg = "打开485串口失败: " + DEFAULT_SERIAL_PORT_PATH + ",可能原因:设备不存在、权限不足或设备被占用";
LogManager.logError(TAG, errorMsg);
callback.onError(errorMsg);
return;
}
// 验证串口是否真正打开
if (!isSerialPortReallyOpen()) {
isConnecting = false;
String errorMsg = "485串口打开后状态检查失败,串口可能未正确初始化";
LogManager.logError(TAG, errorMsg);
callback.onError(errorMsg);
return;
}
// 启动读写线程
serialPortManager.startReadThread(485);
serialPortManager.startSendThread();
// 连接成功
isSerialPortOpen = true;
isConnecting = false;
lastSuccessTime = System.currentTimeMillis();
currentRetryCount = 0;
LogManager.logInfo(TAG, "485连接建立成功,进入懒加载复用模式,最后成功时间: " +
new java.text.SimpleDateFormat("HH:mm:ss.SSS").format(new java.util.Date(lastSuccessTime)));
callback.onConnected();
} catch (Exception e) {
isConnecting = false;
isSerialPortOpen = false;
String errorMsg = "建立485连接异常: " + e.getMessage();
LogManager.logError(TAG, errorMsg, e);
callback.onError(errorMsg);
}
}
/**
* 重试连接
*/
private void retryConnection() {
if (currentRetryCount >= MAX_RETRY_COUNT) {
LogManager.logError(TAG, "485连接重试次数超限,停止重试");
return;
}
currentRetryCount++;
LogManager.logInfo(TAG, "485连接重试第 " + currentRetryCount + " 次");
// 延迟重试
mainHandler.postDelayed(() -> {
createConnection(new ConnectionCallback() {
@Override
public void onConnected() {
LogManager.logInfo(TAG, "485连接重试成功");
}
@Override
public void onError(String errorMessage) {
LogManager.logWarning(TAG, "485连接重试失败: " + errorMessage);
// 继续重试
retryConnection();
}
});
}, RETRY_DELAY);
}
/**
* 标记成功通信(用于连接健康检查)
*/
private void markSuccessfulCommunication() {
lastSuccessTime = System.currentTimeMillis();
currentRetryCount = 0; // 重置重试次数
LogManager.logDebug(TAG, "485通信成功,更新最后成功时间");
}
/**
* 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);
}
/**
* 检查串口是否真正打开
* 由于 SerialPortManager 没有 isOpened() 方法,
* 我们通过检查内部状态变量来判断
* @return true表示串口已打开
*/
private boolean isSerialPortReallyOpen() {
return isSerialPortOpen && serialPortManager != null;
}
}