package com.shlb.comb.fragment;
|
|
import android.content.Context;
|
import android.os.Bundle;
|
import android.view.LayoutInflater;
|
import android.view.View;
|
import android.view.ViewGroup;
|
import android.widget.ArrayAdapter;
|
import android.widget.EditText;
|
import android.widget.Spinner;
|
import android.widget.TextView;
|
import android.widget.Toast;
|
|
import androidx.annotation.NonNull;
|
import androidx.annotation.Nullable;
|
import androidx.fragment.app.Fragment;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.RecyclerView;
|
|
import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton;
|
import com.shlb.comb.manager.BleGlobalManager;
|
import com.shlb.comb.event.UpdateEvent;
|
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.ThreadMode;
|
import android.bluetooth.BluetoothProfile;
|
import com.shlb.comb.R;
|
|
import java.util.ArrayList;
|
import java.util.List;
|
import com.shlb.comb.util.CMD;
|
import com.shlb.comb.util.CRCutil;
|
|
import com.qmuiteam.qmui.widget.dialog.QMUITipDialog;
|
import android.text.Html;
|
import android.content.DialogInterface;
|
|
/**
|
* 设置界面 Fragment
|
* <p>
|
* 该类负责设备参数的配置、设备状态的监控以及蓝牙通信的处理:
|
* 1. 支持站号、层数、波特率等参数的读取和写入
|
* 2. 实时监控设备在线状态和玻璃有无情况
|
* 3. 提供蓝牙连接状态的显示和处理
|
* 4. 实现日志记录和显示功能
|
* 5. 支持周期性数据读取(每1秒),避免频繁日志导致页面卡顿
|
* </p>
|
*/
|
public class SettingsFragment extends Fragment {
|
|
// 用户界面组件
|
private RecyclerView rvGrid; // 设备状态网格视图
|
private TextView etLayer; // 层数显示文本框
|
private EditText etStation; // 站号输入框
|
private Spinner spinnerBaud; // 波特率选择器
|
private QMUIRoundButton btnWriteAll; // 写入所有参数按钮
|
private QMUIRoundButton btnReadParam; // 读取参数按钮
|
private QMUIRoundButton btnClearLog; // 清除日志按钮
|
private TextView tvMonitorTitle; // 监控详情标题
|
private TextView tvMonitorUpdateTime; // 监控数据更新时间
|
private TextView tvStatus; // 状态显示文本
|
private TextView tvLog; // 日志显示文本
|
private TextView tvLayerStatus; // 层数状态标签
|
private TextView tvStationStatus; // 站号状态标签
|
private TextView tvBaudStatus; // 波特率状态标签
|
|
private android.widget.ScrollView svLog; // 日志滚动视图
|
private androidx.cardview.widget.CardView cvLog; // 日志容器
|
|
private QMUITipDialog mLoadingDialog; // 加载对话框
|
|
private GridAdapter mAdapter; // 网格适配器
|
private List<BoxStatus> boxStatusList = new ArrayList<>(); // 设备状态列表
|
private StringBuilder logBuilder = new StringBuilder(); // 日志内容构建器
|
|
private java.util.Queue<String> cmdQueue = new java.util.LinkedList<>(); // 指令队列,用于按顺序执行指令
|
private String currentExecutingCmd = ""; // 当前正在执行的指令,用于校验响应
|
private boolean isPeriodicRead = false; // 标记是否为周期性读取,用于控制日志记录
|
private boolean isFirstLoad = true; // 标记是否为首次加载,避免重复触发读取参数
|
private View currentOperatingButton; // 当前正在操作的按钮
|
|
private static class BoxStatus {
|
int id;
|
boolean isOnline;
|
boolean hasGlass;
|
|
public BoxStatus(int id) {
|
this.id = id;
|
this.isOnline = false;
|
this.hasGlass = false;
|
}
|
}
|
|
// 周期性读取数据相关变量
|
private android.os.Handler periodicReadHandler = new android.os.Handler(); // 用于处理周期性读取任务的Handler
|
private static final long PERIODIC_READ_INTERVAL = 500; // 周期性读取间隔(毫秒):1秒
|
|
private Runnable periodicReadRunnable = new Runnable() {
|
@Override
|
public void run() {
|
// 每1秒读取一次数据,不记录日志
|
if (BleGlobalManager.getInstance().isConnected()) {
|
periodicReadData();
|
|
// 继续安排下一次读取
|
periodicReadHandler.postDelayed(this, PERIODIC_READ_INTERVAL);
|
}
|
}
|
};
|
|
// 自动读取参数相关变量
|
private Runnable autoReadRunnable = new Runnable() { // 蓝牙连接成功后自动读取参数的任务
|
@Override
|
public void run() {
|
// 蓝牙连接成功后 自动触发监控详情的 读取数据 和 参数设定这里的 读取参数
|
if (BleGlobalManager.getInstance().isConnected()) {
|
showLoading("正在同步数据...");
|
|
// 先触发读取数据
|
tvStatus.setText("状态:正在读取数据...");
|
if (tvMonitorTitle != null) tvMonitorTitle.performClick();
|
|
|
new android.os.Handler().postDelayed(() -> {
|
// 再触发读取参数
|
if (btnReadParam != null) btnReadParam.performClick();
|
|
// 假设参数读取触发后 1.5秒 关闭 loading,或者在解析完所有参数后关闭
|
// 这里简单处理,延时关闭
|
new android.os.Handler().postDelayed(() -> {
|
dismissLoading();
|
// 初始设置完成后启动周期性读取
|
startPeriodicRead(PERIODIC_READ_INTERVAL);
|
isFirstLoad = false; // 首次加载完成
|
}, 1500);
|
}, 1000); // 间隔1秒,避免指令冲突
|
}
|
}
|
};
|
private android.os.Handler debounceHandler = new android.os.Handler(); // 用于防抖处理的Handler
|
private static final long DEBOUNCE_DELAY_MS = 1500; // 防抖延迟时间(毫秒):1.5秒
|
|
/**
|
* Fragment 可见时调用
|
* <p>
|
* 注册 EventBus 并更新蓝牙连接状态
|
* </p>
|
*/
|
@Override
|
public void onStart() {
|
super.onStart();
|
if (!EventBus.getDefault().isRegistered(this)) {
|
EventBus.getDefault().register(this);
|
}
|
updateConnectionStatus();
|
}
|
|
/**
|
* Fragment 不可见时调用
|
* <p>
|
* 取消注册 EventBus,避免内存泄漏
|
* </p>
|
*/
|
@Override
|
public void onStop() {
|
super.onStop();
|
if (EventBus.getDefault().isRegistered(this)) {
|
EventBus.getDefault().unregister(this);
|
}
|
}
|
|
/**
|
* Fragment 恢复可见时调用
|
* \u003cp\u003e
|
* 如果蓝牙已连接,恢复周期性读取数据
|
* \u003c/p\u003e
|
*/
|
@Override
|
public void onResume() {
|
super.onResume();
|
// 根据系统设置控制日志显示
|
boolean isCmdLogEnabled = com.blankj.utilcode.util.SPUtils.getInstance().getBoolean("cmd_log_enabled", false);
|
if (cvLog != null) {
|
cvLog.setVisibility(isCmdLogEnabled ? View.VISIBLE : View.GONE);
|
}
|
|
// 如果蓝牙已连接,恢复周期性读取
|
if (BleGlobalManager.getInstance().isConnected()) {
|
// 只有当不是首次加载(即从其他页面返回)时才手动触发读取参数
|
// 首次加载会走 autoReadRunnable
|
if (!isFirstLoad && btnReadParam != null) {
|
btnReadParam.performClick();
|
}
|
startPeriodicRead(3000);
|
}
|
}
|
|
/**
|
* Fragment 暂停时调用
|
* \u003cp\u003e
|
* 停止周期性读取数据,节省资源
|
* \u003c/p\u003e
|
*/
|
@Override
|
public void onPause() {
|
super.onPause();
|
// 离开页面时停止周期性读取
|
stopPeriodicRead();
|
}
|
|
@Override
|
public void onDestroyView() {
|
super.onDestroyView();
|
// 清理所有 Handler 回调,避免内存泄漏
|
stopPeriodicRead();
|
debounceHandler.removeCallbacks(autoReadRunnable);
|
|
// 清理加载对话框
|
if (mLoadingDialog != null) {
|
mLoadingDialog.dismiss();
|
mLoadingDialog = null;
|
}
|
}
|
|
private void updateConnectionStatus() {
|
if (BleGlobalManager.getInstance().isConnected()) {
|
tvStatus.setText("状态:已连接");
|
} else {
|
tvStatus.setText("状态:未连接");
|
}
|
}
|
|
/**
|
* 事件总线监听器
|
* <p>
|
* 处理蓝牙连接状态变化和设备数据接收事件
|
* </p>
|
* @param event 事件对象
|
*/
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
public void onEventRefresh(UpdateEvent event) {
|
if (event.getType() == UpdateEvent.Type.CONN_STATU) {
|
// 检查obj是否为整数
|
if (event.getObj() instanceof Integer) {
|
int status = (int) event.getObj();
|
if (status == BluetoothProfile.STATE_CONNECTED) {
|
tvStatus.setText("状态:已连接");
|
Toast.makeText(getContext(), "蓝牙已连接", Toast.LENGTH_SHORT).show();
|
|
// 蓝牙连接成功后 自动触发
|
triggerAutoRead();
|
} else {
|
tvStatus.setText("状态:已断开");
|
// 蓝牙断开时停止周期性读取
|
stopPeriodicRead();
|
}
|
}
|
} else if (event.getType() == UpdateEvent.Type.DEVICE_INFO) {
|
// 收到数据
|
String hex = event.getMsg();
|
if (tvStatus != null && !isPeriodicRead) {
|
tvStatus.setText("收到数据: " + hex);
|
}
|
|
// 检查是否为周期性读取
|
if (!isPeriodicRead) {
|
appendLog(hex, false); // false表示接收的数据(绿色)
|
}
|
|
parseAndRefresh(hex);
|
|
// 无论是否为周期性读取,处理完成后都重置标记
|
isPeriodicRead = false;
|
}
|
}
|
|
/**
|
* 解析并刷新数据
|
* <p>
|
* 根据指令类型分发到不同的解析方法
|
* </p>
|
* @param hex 十六进制格式的原始数据
|
*/
|
private void parseAndRefresh(String hex) {
|
if (hex == null) return;
|
|
// 根据指令前8位判断指令类型
|
if (hex.length() >= 8) {
|
// 参数读取返回 (长度至少12位)
|
// 读层数指令: A55A0308...
|
// 读站号指令: A55A0304...
|
// 读波特率指令: A55A0305...
|
if (hex.length() >= 12 && (
|
hex.startsWith(CMD.READ_FLOORS.substring(0, 8)) ||
|
hex.startsWith(CMD.READ_STATION_NUM.substring(0, 8)) ||
|
hex.startsWith(CMD.READ_BAUD_RATE.substring(0, 8)))) {
|
parseParamResponse(hex);
|
return;
|
}
|
|
// 数据读取返回 (长度至少32位)
|
// 读数据指令: A55A0301...
|
if (hex.length() >= 32 && hex.startsWith(CMD.READ_DATA.substring(0, 8))) {
|
parseDataResponse(hex);
|
return;
|
}
|
}
|
|
// 写入指令的返回 (以A55A06开头)
|
if (hex.startsWith("A55A06") && hex.length() >= 12) {
|
parseWriteResponse(hex);
|
return;
|
}
|
}
|
|
private void setLabelStatus(TextView view, String text, int colorResId) {
|
if (view != null && getContext() != null) {
|
view.setText(text);
|
view.setTextColor(androidx.core.content.ContextCompat.getColor(getContext(), colorResId));
|
}
|
}
|
|
/**
|
* 解析参数读取返回 (站号、波特率、层数)
|
*/
|
private void parseParamResponse(String hex) {
|
try {
|
// 命令类型: 第7-8位
|
String cmdType = hex.substring(6, 8);
|
// 数据内容: 第11-12位
|
String dataHex = hex.substring(10, 12);
|
int value = Integer.parseInt(dataHex, 16);
|
|
if ("04".equals(cmdType)) {
|
// 站号
|
if (etStation != null) etStation.setText(String.valueOf(value));
|
setLabelStatus(tvStationStatus, "(读取值)", R.color.base_color);
|
appendLog("读取站号: " + value);
|
} else if ("05".equals(cmdType)) {
|
// 波特率
|
if (spinnerBaud != null && value >= 0 && value < spinnerBaud.getAdapter().getCount()) {
|
spinnerBaud.setSelection(value);
|
}
|
setLabelStatus(tvBaudStatus, "(读取值)", R.color.base_color);
|
appendLog("读取波特率: " + value);
|
|
// 读取结束,恢复按钮状态
|
restoreCurrentButton();
|
} else if ("08".equals(cmdType)) {
|
// 层数
|
if (etLayer != null) {
|
etLayer.setText(String.valueOf(value));
|
etLayer.setTextColor(0xFF333333);
|
}
|
setLabelStatus(tvLayerStatus, "(读取值)", R.color.base_color);
|
appendLog("读取层数: " + value);
|
com.blankj.utilcode.util.SPUtils.getInstance().put("layer_count", value);
|
}
|
} catch (Exception e) {
|
e.printStackTrace();
|
appendLog("参数解析异常: " + e.getMessage());
|
setLabelStatus(tvStationStatus, "(读取失败)", R.color.orange);
|
setLabelStatus(tvBaudStatus, "(读取失败)", R.color.orange);
|
setLabelStatus(tvLayerStatus, "(读取失败)", R.color.orange);
|
}
|
}
|
|
/**
|
* 解析写入指令返回
|
*/
|
private void parseWriteResponse(String hex) {
|
try {
|
// 结果状态: 第11-12位
|
String statusHex = hex.substring(10, 12);
|
boolean isSuccess = "01".equals(statusHex);
|
|
// 只要当前有正在执行的指令,且收到了写入回复(A55A06开头),我们就认为是当前指令的回复
|
// 因为我们采用队列机制,必须发一条等一条,所以不会有乱序问题。
|
// 这种方式规避了 ENTER 和 EXIT 前8位相同导致无法区分的问题。
|
|
if (!currentExecutingCmd.isEmpty()) {
|
if (isSuccess) {
|
// 记录日志:根据当前期待的指令来记录,而不是根据返回的 hex
|
// 使用完整指令前缀匹配,避免 ENTER (3B0101) 和 EXIT (3B0100) 前8位相同导致的误判
|
if (currentExecutingCmd.startsWith(CMD.ENTER_SETTING)) {
|
appendLog("进入设定模式成功");
|
} else if (currentExecutingCmd.startsWith(CMD.WRITE_STATION_NUM)) {
|
appendLog("写入站号成功");
|
setLabelStatus(tvStationStatus, "(写入值)", R.color.base_color_s);
|
} else if (currentExecutingCmd.startsWith(CMD.WRITE_BAUD_RATE)) {
|
appendLog("写入波特率成功");
|
setLabelStatus(tvBaudStatus, "(写入值)", R.color.base_color_s);
|
} else if (currentExecutingCmd.startsWith(CMD.EXIT_SETTING)) {
|
appendLog("退出设定模式成功");
|
}
|
|
// 只有成功才继续执行下一条
|
processNextCmd();
|
} else {
|
// 执行失败
|
cmdQueue.clear();
|
currentExecutingCmd = "";
|
tvStatus.setText("写入失败");
|
restoreCurrentButton(); // 恢复按钮
|
|
if (currentExecutingCmd.startsWith(CMD.WRITE_STATION_NUM)) {
|
setLabelStatus(tvStationStatus, "(写入失败)", R.color.orange);
|
} else if (currentExecutingCmd.startsWith(CMD.WRITE_BAUD_RATE)) {
|
setLabelStatus(tvBaudStatus, "(写入失败)", R.color.orange);
|
}
|
|
appendLog("写入失败: " + hex);
|
Toast.makeText(getContext(), "写入失败", Toast.LENGTH_SHORT).show();
|
}
|
}
|
} catch (Exception e) {
|
e.printStackTrace();
|
cmdQueue.clear();
|
currentExecutingCmd = "";
|
appendLog("写入响应解析异常: " + e.getMessage());
|
// 异常也视为失败
|
setLabelStatus(tvStationStatus, "(写入失败)", R.color.orange);
|
setLabelStatus(tvBaudStatus, "(写入失败)", R.color.orange);
|
restoreCurrentButton(); // 异常恢复按钮
|
}
|
}
|
|
private void processNextCmd() {
|
if (cmdQueue != null && !cmdQueue.isEmpty()) {
|
String nextCmd = cmdQueue.poll();
|
currentExecutingCmd = nextCmd;
|
|
// 更新状态显示
|
if (nextCmd.startsWith(CMD.ENTER_SETTING.substring(0, 10))) {
|
tvStatus.setText("正在进入设定模式...");
|
} else if (nextCmd.startsWith(CMD.WRITE_STATION_NUM.substring(0, 10))) {
|
tvStatus.setText("正在写入站号...");
|
} else if (nextCmd.startsWith(CMD.WRITE_BAUD_RATE.substring(0, 10))) {
|
tvStatus.setText("正在写入波特率...");
|
} else if (nextCmd.startsWith(CMD.EXIT_SETTING.substring(0, 10))) {
|
tvStatus.setText("正在退出设定模式...");
|
}
|
|
sendCmdWithCrc(nextCmd);
|
} else {
|
// 队列为空,全部完成
|
currentExecutingCmd = "";
|
tvStatus.setText("参数写入完成");
|
Toast.makeText(getContext(), "参数写入成功", Toast.LENGTH_SHORT).show();
|
restoreCurrentButton(); // 全部完成,恢复按钮
|
}
|
}
|
|
/**
|
* 解析监控数据返回 (是否有玻璃、在线状态)
|
*/
|
/**
|
* 解析监控数据返回
|
* <p>
|
* 解析设备在线状态和玻璃有无情况
|
* </p>
|
* @param hex 十六进制格式的原始数据
|
*/
|
private void parseDataResponse(String hex) {
|
try {
|
// 解析是否有玻璃: 第11-18位 (8个字符 = 32位)
|
// 索引 10-17
|
String glassHex = hex.substring(10, 18);
|
if(!isPeriodicRead){
|
appendLog("解析玻璃数据: " + glassHex);
|
}
|
|
|
// 直接将十六进制解析为长整型 (大端序)
|
// "00000004" -> 4 -> ...00100 -> 第2位 -> 第3个格子
|
long glassBits = Long.parseLong(glassHex, 16);
|
|
String onlineHex = hex.substring(18, 26);
|
if(!isPeriodicRead){
|
appendLog("解析在线数据: " + onlineHex);
|
}
|
long onlineBits = Long.parseLong(onlineHex, 16);
|
|
// 更新30个格子的状态
|
for (BoxStatus box : boxStatusList) {
|
int bitIndex = box.id - 1; // 对应位索引 0-29
|
if (bitIndex >= 0 && bitIndex < 32) {
|
// 检查对应位是否为1
|
box.hasGlass = ((glassBits >> bitIndex) & 1) == 1;
|
box.isOnline = ((onlineBits >> bitIndex) & 1) == 1;
|
}
|
}
|
|
// 刷新列表显示
|
if (mAdapter != null) {
|
mAdapter.notifyDataSetChanged();
|
}
|
|
// 更新监控数据接收时间
|
if (tvMonitorUpdateTime != null) {
|
String currentTime = com.blankj.utilcode.util.TimeUtils.getNowString(new java.text.SimpleDateFormat("HH:mm:ss"));
|
tvMonitorUpdateTime.setText(currentTime);
|
}
|
|
} catch (Exception e) {
|
e.printStackTrace();
|
tvStatus.setText("数据解析错误: " + e.getMessage());
|
appendLog("数据解析错误: " + e.getMessage());
|
}
|
}
|
|
|
|
@Nullable
|
@Override
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
return inflater.inflate(R.layout.fragment_settings, container, false);
|
}
|
|
@Override
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
super.onViewCreated(view, savedInstanceState);
|
|
// 初始化设备状态列表
|
boxStatusList.clear();
|
for (int i = 1; i <= 30; i++) {
|
boxStatusList.add(new BoxStatus(i));
|
}
|
|
initView(view);
|
|
// 如果已经连接,自动触发读取
|
if (BleGlobalManager.getInstance().isConnected()) {
|
triggerAutoRead();
|
}
|
}
|
|
private void triggerAutoRead() {
|
// 使用防抖机制,避免短时间内多次触发
|
debounceHandler.removeCallbacks(autoReadRunnable);
|
debounceHandler.postDelayed(autoReadRunnable, DEBOUNCE_DELAY_MS);
|
}
|
|
private void startPeriodicRead(long delay) {
|
// 先停止可能存在的任务,避免重复启动
|
stopPeriodicRead();
|
|
// 启动周期性读取
|
periodicReadHandler.postDelayed(periodicReadRunnable, delay);
|
}
|
|
private void stopPeriodicRead() {
|
periodicReadHandler.removeCallbacks(periodicReadRunnable);
|
}
|
|
/**
|
* 专门用于周期性读取数据的方法,不记录日志
|
*/
|
/**
|
* 周期性读取数据(每1秒)
|
* <p>
|
* 专门用于周期性读取数据,不记录日志以避免页面卡顿
|
* </p>
|
*/
|
private void periodicReadData() {
|
try {
|
if (BleGlobalManager.getInstance() != null && BleGlobalManager.getInstance().isConnected()) {
|
if (tvStatus != null && isPeriodicRead) {
|
tvStatus.setText("状态:正在读取数据...");
|
}
|
// 设置周期性读取标记
|
isPeriodicRead = true;
|
// 直接发送命令,不记录日志
|
sendCmdWithCrcNoLog(CMD.READ_DATA);
|
}
|
} catch (Exception e) {
|
e.printStackTrace();
|
// 异常也不记录日志,避免卡顿
|
// 确保标记被重置
|
isPeriodicRead = false;
|
}
|
}
|
|
/**
|
* 发送命令但不记录日志(用于周期性读取)
|
*/
|
/**
|
* 发送带 CRC 校验的指令(不记录日志)
|
* <p>
|
* 专门用于周期性读取数据,避免频繁记录日志导致页面卡顿
|
* </p>
|
* @param cmd 十六进制格式的指令内容
|
*/
|
private void sendCmdWithCrcNoLog(String cmd) {
|
if (!BleGlobalManager.getInstance().isConnected()) {
|
return; // 静默返回,不显示 Toast 或日志
|
}
|
|
byte[] cmdBytes = BleGlobalManager.hexStringToBytes(cmd);
|
if (cmdBytes != null) {
|
String crc = CRCutil.getCRC(cmdBytes);
|
// 确保CRC为4个字符,不足则补零
|
while (crc.length() < 4) {
|
crc = "0" + crc;
|
}
|
String fullCmd = cmd + crc.toUpperCase();
|
|
// 不记录日志,直接发送命令
|
BleGlobalManager.getInstance().sendCmd(fullCmd);
|
}
|
}
|
|
/**
|
* 显示加载对话框
|
* <p>
|
* 用于操作过程中的等待提示
|
* </p>
|
* @param msg 加载提示信息
|
*/
|
private void showLoading(String msg) {
|
if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
|
mLoadingDialog.dismiss();
|
}
|
mLoadingDialog = new QMUITipDialog.Builder(getContext())
|
.setIconType(QMUITipDialog.Builder.ICON_TYPE_LOADING)
|
.setTipWord(msg)
|
.create();
|
// 允许点击外部或返回键取消加载(仅关闭对话框)
|
mLoadingDialog.setCancelable(true);
|
mLoadingDialog.setCanceledOnTouchOutside(true);
|
mLoadingDialog.show();
|
}
|
|
/**
|
* 关闭加载对话框
|
*/
|
private void dismissLoading() {
|
if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
|
mLoadingDialog.dismiss();
|
}
|
}
|
|
/**
|
* 初始化界面组件
|
* <p>
|
* 设置各个UI组件的引用和监听器
|
* </p>
|
* @param view Fragment 的根视图
|
*/
|
private void initView(View view) {
|
rvGrid = view.findViewById(R.id.rv_grid);
|
etLayer = view.findViewById(R.id.et_layer);
|
etStation = view.findViewById(R.id.et_station);
|
spinnerBaud = view.findViewById(R.id.spinner_baud);
|
btnWriteAll = view.findViewById(R.id.btn_write_all);
|
tvMonitorTitle = view.findViewById(R.id.tv_monitor_title);
|
tvMonitorUpdateTime = view.findViewById(R.id.tv_monitor_update_time);
|
btnReadParam = view.findViewById(R.id.btn_read_param);
|
btnClearLog = view.findViewById(R.id.btn_clear_log);
|
tvStatus = view.findViewById(R.id.tv_status);
|
tvLog = view.findViewById(R.id.tv_log);
|
svLog = view.findViewById(R.id.sv_log);
|
tvLayerStatus = view.findViewById(R.id.tv_layer_status);
|
tvStationStatus = view.findViewById(R.id.tv_station_status);
|
|
tvBaudStatus = view.findViewById(R.id.tv_baud_status);
|
cvLog = view.findViewById(R.id.cv_log);
|
|
// 解决日志区域滑动冲突
|
svLog.setOnTouchListener((v, event) -> {
|
v.getParent().requestDisallowInterceptTouchEvent(true);
|
if ((event.getAction() & android.view.MotionEvent.ACTION_MASK) == android.view.MotionEvent.ACTION_UP) {
|
v.getParent().requestDisallowInterceptTouchEvent(false);
|
}
|
return false;
|
});
|
|
// 恢复日志(如果有)
|
if (logBuilder.length() > 0) {
|
tvLog.setText(Html.fromHtml(logBuilder.toString()));
|
} else {
|
logBuilder.append("日志记录:<br>");
|
tvLog.setText(Html.fromHtml(logBuilder.toString()));
|
}
|
|
// 网格布局设置
|
// 每行10个格子
|
// 由于在水平滚动视图中,布局会自动调整
|
rvGrid.setLayoutManager(new GridLayoutManager(getContext(), 10));
|
mAdapter = new GridAdapter();
|
rvGrid.setAdapter(mAdapter);
|
|
// 波特率选择器设置
|
String[] baudRates = new String[]{"156Kbps", "625Kbps", "2.5Mbps", "5Mbps", "10Mbps"};
|
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, baudRates);
|
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
spinnerBaud.setAdapter(spinnerAdapter);
|
spinnerBaud.setSelection(0); // 默认选择156Kbps
|
|
tvMonitorTitle.setOnClickListener(v -> {
|
onMonitorTitleClick();
|
});
|
|
// 按钮监听器设置
|
btnWriteAll.setOnClickListener(v -> {
|
if (!BleGlobalManager.getInstance().isConnected()) {
|
Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
|
appendLog("错误: 蓝牙未连接");
|
return;
|
}
|
|
String stationStr = etStation.getText().toString().trim();
|
if (stationStr.isEmpty()) {
|
Toast.makeText(getContext(), "请输入站号", Toast.LENGTH_SHORT).show();
|
return;
|
}
|
|
try {
|
int station = Integer.parseInt(stationStr);
|
if (station < 1 || station > 64) {
|
Toast.makeText(getContext(), "站号范围无效(1-64)", Toast.LENGTH_SHORT).show();
|
return;
|
}
|
} catch (NumberFormatException e) {
|
Toast.makeText(getContext(), "请输入有效的数字", Toast.LENGTH_SHORT).show();
|
return;
|
}
|
|
currentOperatingButton = btnWriteAll;
|
updateButtonState(currentOperatingButton, false); // Disable buttons, gray out write button
|
|
// 构造指令队列
|
cmdQueue.clear();
|
|
// 1. 进入设定
|
cmdQueue.offer(CMD.ENTER_SETTING);
|
|
// 2. 写入站号
|
int station = Integer.parseInt(stationStr);
|
String stationHex = String.format("%02X", station);
|
cmdQueue.offer(CMD.WRITE_STATION_NUM + stationHex);
|
|
// 3. 写入波特率
|
int baudIndex = spinnerBaud.getSelectedItemPosition();
|
String baudHex = String.format("%02X", baudIndex);
|
cmdQueue.offer(CMD.WRITE_BAUD_RATE + baudHex);
|
|
// 4. 退出设定
|
cmdQueue.offer(CMD.EXIT_SETTING);
|
|
// 开始执行
|
processNextCmd();
|
});
|
|
|
btnReadParam.setOnClickListener(v -> {
|
if (BleGlobalManager.getInstance().isConnected()) {
|
currentOperatingButton = btnReadParam;
|
updateButtonState(currentOperatingButton, false); // Disable buttons, gray out read button
|
tvStatus.setText("状态:正在读取参数...");
|
sendCmdWithCrc(CMD.READ_FLOORS);
|
new android.os.Handler().postDelayed(() -> {
|
sendCmdWithCrc(CMD.READ_STATION_NUM);
|
}, 500);
|
new android.os.Handler().postDelayed(() -> {
|
sendCmdWithCrc(CMD.READ_BAUD_RATE);
|
}, 1000);
|
} else {
|
Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
|
appendLog("错误: 蓝牙未连接");
|
}
|
});
|
|
btnClearLog.setOnClickListener(v -> {
|
logBuilder.setLength(0);
|
logBuilder.append("日志记录:<br>");
|
tvLog.setText(Html.fromHtml(logBuilder.toString()));
|
});
|
}
|
|
/**
|
* 监控详情标题点击事件
|
* <p>
|
* 手动触发数据读取,会记录完整日志
|
* </p>
|
*/
|
public void onMonitorTitleClick() {
|
try {
|
if (BleGlobalManager.getInstance() != null && BleGlobalManager.getInstance().isConnected()) {
|
if (tvStatus != null) {
|
tvStatus.setText("状态:正在读取数据...");
|
}
|
// 确保不是周期性读取,以便记录日志
|
isPeriodicRead = false;
|
sendCmdWithCrc(CMD.READ_DATA);
|
} else {
|
Context context = getContext();
|
if (context != null) {
|
Toast.makeText(context, "请先连接蓝牙", Toast.LENGTH_SHORT).show();
|
}
|
appendLog("错误: 蓝牙未连接");
|
}
|
} catch (Exception e) {
|
e.printStackTrace();
|
appendLog("点击监控详情时发生错误: " + e.getMessage());
|
}
|
}
|
|
/**
|
* 记录普通日志
|
* <p>
|
* 将日志信息添加到日志构建器并显示
|
* </p>
|
* @param msg 日志内容
|
*/
|
private void appendLog(String msg) {
|
appendLog(msg, null);
|
}
|
|
private String getCmdDescription(String hex) {
|
if (hex == null) return "";
|
|
// 发送指令匹配
|
if (hex.startsWith(CMD.READ_DATA)) return "-读数据";
|
if (hex.startsWith(CMD.READ_FLOORS)) return "-读层数";
|
if (hex.startsWith(CMD.READ_STATION_NUM)) return "-读站号";
|
if (hex.startsWith(CMD.READ_BAUD_RATE)) return "-读波特率";
|
|
// 3B指令 (Enter/Exit) 特殊处理
|
if (hex.startsWith(CMD.ENTER_SETTING.substring(0, 8))) {
|
// 1. 上下文优先: 如果当前有正在执行的指令,以当前指令为准
|
if (!currentExecutingCmd.isEmpty()) {
|
if (currentExecutingCmd.startsWith(CMD.ENTER_SETTING)) return "-进入设定";
|
if (currentExecutingCmd.startsWith(CMD.EXIT_SETTING)) return "-退出设定";
|
}
|
|
// 2. 精确匹配退出指令
|
if (hex.startsWith(CMD.EXIT_SETTING)) return "-退出设定";
|
|
// 3. 默认情况: 上下文为空且非明确退出指令,优先判定为退出设定
|
// (修复: 从参数设定页返回时,收到的退出响应会被误判为进入设定)
|
return "-退出设定";
|
}
|
|
if (hex.startsWith(CMD.ENTER_SETTING)) return "-进入设定";
|
if (hex.startsWith(CMD.EXIT_SETTING)) return "-退出设定";
|
if (hex.startsWith(CMD.WRITE_STATION_NUM)) return "-写站号";
|
if (hex.startsWith(CMD.WRITE_BAUD_RATE)) return "-写波特率";
|
|
// 接收数据匹配
|
// 读数据返回: A55A0301...
|
if (hex.startsWith(CMD.READ_DATA.substring(0, 8))) return "-读数据";
|
|
// 读参数返回
|
if (hex.length() >= 8) {
|
if (hex.startsWith(CMD.READ_FLOORS.substring(0, 8))) return "-读层数";
|
if (hex.startsWith(CMD.READ_STATION_NUM.substring(0, 8))) return "-读站号";
|
if (hex.startsWith(CMD.READ_BAUD_RATE.substring(0, 8))) return "-读波特率";
|
}
|
|
// 写入返回 (A55A06开头)
|
if (hex.startsWith("A55A06") && hex.length() >= 12) {
|
return "-写入返回";
|
}
|
|
return "";
|
}
|
|
private void updateButtonState(View btn, boolean enable) {
|
if (btn == null) return;
|
btn.setEnabled(enable);
|
if (enable) {
|
btn.getBackground().clearColorFilter();
|
} else {
|
btn.getBackground().setColorFilter(android.graphics.Color.GRAY, android.graphics.PorterDuff.Mode.MULTIPLY);
|
}
|
}
|
|
private void restoreCurrentButton() {
|
if (currentOperatingButton != null) {
|
updateButtonState(currentOperatingButton, true);
|
currentOperatingButton = null;
|
}
|
}
|
|
/**
|
* 记录日志
|
* <p>
|
* 将日志信息添加到日志构建器并显示,支持发送/接收类型的区分
|
* </p>
|
* @param msg 日志内容
|
* @param isSent true表示发送的指令,false表示接收的数据
|
*/
|
private void appendLog(String msg, Boolean isSent) {
|
String time = com.blankj.utilcode.util.TimeUtils.getNowString(new java.text.SimpleDateFormat("HH:mm:ss.SSS"));
|
|
String displayMsg = msg;
|
String cmdDesc = "";
|
|
// 如果是十六进制指令 (纯 0-9 A-F a-f),加空格格式化
|
if (msg.matches("^[0-9A-Fa-f]+$")) {
|
StringBuilder sb = new StringBuilder();
|
for (int i = 0; i < msg.length(); i += 2) {
|
if (i + 2 <= msg.length()) {
|
sb.append(msg.substring(i, i + 2)).append(" ");
|
} else {
|
sb.append(msg.substring(i));
|
}
|
}
|
displayMsg = sb.toString().trim();
|
|
// 获取指令描述
|
cmdDesc = getCmdDescription(msg);
|
}
|
|
String logLine;
|
if (isSent != null) {
|
String color = isSent ? "#1890ff" : "#05aa87"; // 发送为蓝色,接收为绿色
|
// 拼接到前缀后面: "发送-读站号: " 或 "收到-读站号: "
|
String prefix = (isSent ? "发送" : "收到") + cmdDesc + ": ";
|
logLine = time + " <font color='" + color + "'>" + prefix + displayMsg + "</font><br>";
|
} else {
|
// 普通日志
|
logLine = time + " " + displayMsg + "<br>";
|
}
|
|
logBuilder.append(logLine);
|
if (tvLog != null) {
|
tvLog.setText(Html.fromHtml(logBuilder.toString()));
|
if (svLog != null) {
|
svLog.post(() -> svLog.fullScroll(View.FOCUS_DOWN));
|
}
|
}
|
}
|
|
/**
|
* 发送带 CRC 校验的指令
|
* <p>
|
* 计算指令的 CRC 校验值并发送,同时记录日志
|
* </p>
|
* @param cmd 十六进制格式的指令内容
|
*/
|
private void sendCmdWithCrc(String cmd) {
|
if (!BleGlobalManager.getInstance().isConnected()) {
|
Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
|
appendLog("错误: 蓝牙未连接");
|
return;
|
}
|
|
byte[] cmdBytes = BleGlobalManager.hexStringToBytes(cmd);
|
if (cmdBytes != null) {
|
String crc = CRCutil.getCRC(cmdBytes);
|
// Pad CRC to 4 chars if needed
|
while (crc.length() < 4) {
|
crc = "0" + crc;
|
}
|
String fullCmd = cmd + crc.toUpperCase();
|
|
appendLog(fullCmd, true); // true表示发送的指令(蓝色)
|
BleGlobalManager.getInstance().sendCmd(fullCmd);
|
} else {
|
appendLog("错误: 指令转换失败");
|
}
|
}
|
|
private class GridAdapter extends RecyclerView.Adapter<GridAdapter.ViewHolder> {
|
|
@NonNull
|
@Override
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_grid_box, parent, false);
|
return new ViewHolder(view);
|
}
|
|
@Override
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
// 根据3行10列的网格,从左到右计算格子ID
|
// 第1行 (位置0-9): 1 ... 10
|
// 第2行 (位置10-19): 11 ... 20
|
// 第3行 (位置20-29): 21 ... 30
|
|
int row = position / 10;
|
int col = position % 10;
|
int boxId = row * 10 + (col + 1);
|
|
holder.tvBoxNumber.setText(String.valueOf(boxId));
|
|
// Find status for this box
|
BoxStatus status = null;
|
for (BoxStatus s : boxStatusList) {
|
if (s.id == boxId) {
|
status = s;
|
break;
|
}
|
}
|
|
if (status != null) {
|
// 优先级: 在线状态 > 玻璃状态
|
// 用户需求: "优先显示是否在线"
|
// "19-26位也是16进制,解析成二进制32位表示30个格子的在线状态"
|
// 通常意味着如果离线,显示离线颜色;如果在线,显示玻璃状态(有/无)
|
|
if (!status.isOnline) {
|
holder.viewBox.setBackgroundResource(R.drawable.bg_box_offline); // 离线(灰色)
|
} else {
|
if (status.hasGlass) {
|
holder.viewBox.setBackgroundResource(R.drawable.bg_box_full); // 绿色
|
} else {
|
holder.viewBox.setBackgroundResource(R.drawable.bg_box_empty); // 在线但无玻璃(白色)
|
}
|
}
|
}
|
}
|
|
@Override
|
public int getItemCount() {
|
return 30;
|
}
|
|
class ViewHolder extends RecyclerView.ViewHolder {
|
TextView tvBoxNumber;
|
View viewBox;
|
|
public ViewHolder(@NonNull View itemView) {
|
super(itemView);
|
tvBoxNumber = itemView.findViewById(R.id.tv_box_number);
|
viewBox = itemView.findViewById(R.id.view_box);
|
}
|
}
|
}
|
}
|