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 *

* 该类负责设备参数的配置、设备状态的监控以及蓝牙通信的处理: * 1. 支持站号、层数、波特率等参数的读取和写入 * 2. 实时监控设备在线状态和玻璃有无情况 * 3. 提供蓝牙连接状态的显示和处理 * 4. 实现日志记录和显示功能 * 5. 支持周期性数据读取(每1秒),避免频繁日志导致页面卡顿 *

*/ 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 boxStatusList = new ArrayList<>(); // 设备状态列表 private StringBuilder logBuilder = new StringBuilder(); // 日志内容构建器 private java.util.Queue 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 可见时调用 *

* 注册 EventBus 并更新蓝牙连接状态 *

*/ @Override public void onStart() { super.onStart(); if (!EventBus.getDefault().isRegistered(this)) { EventBus.getDefault().register(this); } updateConnectionStatus(); } /** * Fragment 不可见时调用 *

* 取消注册 EventBus,避免内存泄漏 *

*/ @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("状态:未连接"); } } /** * 事件总线监听器 *

* 处理蓝牙连接状态变化和设备数据接收事件 *

* @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; } } /** * 解析并刷新数据 *

* 根据指令类型分发到不同的解析方法 *

* @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(); // 全部完成,恢复按钮 } } /** * 解析监控数据返回 (是否有玻璃、在线状态) */ /** * 解析监控数据返回 *

* 解析设备在线状态和玻璃有无情况 *

* @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秒) *

* 专门用于周期性读取数据,不记录日志以避免页面卡顿 *

*/ 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 校验的指令(不记录日志) *

* 专门用于周期性读取数据,避免频繁记录日志导致页面卡顿 *

* @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); } } /** * 显示加载对话框 *

* 用于操作过程中的等待提示 *

* @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(); } } /** * 初始化界面组件 *

* 设置各个UI组件的引用和监听器 *

* @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("日志记录:
"); 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 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("日志记录:
"); tvLog.setText(Html.fromHtml(logBuilder.toString())); }); } /** * 监控详情标题点击事件 *

* 手动触发数据读取,会记录完整日志 *

*/ 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()); } } /** * 记录普通日志 *

* 将日志信息添加到日志构建器并显示 *

* @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; } } /** * 记录日志 *

* 将日志信息添加到日志构建器并显示,支持发送/接收类型的区分 *

* @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 + " " + prefix + displayMsg + "
"; } else { // 普通日志 logLine = time + " " + displayMsg + "
"; } logBuilder.append(logLine); if (tvLog != null) { tvLog.setText(Html.fromHtml(logBuilder.toString())); if (svLog != null) { svLog.post(() -> svLog.fullScroll(View.FOCUS_DOWN)); } } } /** * 发送带 CRC 校验的指令 *

* 计算指令的 CRC 校验值并发送,同时记录日志 *

* @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 { @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); } } } }