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
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);
|
|
}
|
|
}
|