zhuguifei
2026-01-14 09c54e2bfa51aa9800f224fda7ad3754b353bfed
app/src/main/java/com/shlb/comb/fragment/SettingsFragment.java
@@ -1,5 +1,6 @@
package com.shlb.comb.fragment;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -34,32 +35,50 @@
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 EditText etLayer;
    private EditText etStation;
    private Spinner spinnerBaud;
    private QMUIRoundButton btnWriteAll;
    private QMUIRoundButton btnReadData;
    private QMUIRoundButton btnReadParam;
    private QMUIRoundButton btnClearLog;
    private TextView tvStatus;
    private TextView tvLog;
    private TextView tvLayerStatus;
    private TextView tvStationStatus;
    private TextView tvBaudStatus;
    private android.widget.ScrollView svLog;
    private QMUITipDialog mLoadingDialog;
    private GridAdapter mAdapter;
    private List<BoxStatus> boxStatusList = new ArrayList<>();
    private StringBuilder logBuilder = new StringBuilder();
    // 用户界面组件
    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 java.util.Queue<String> cmdQueue = new java.util.LinkedList<>();
    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;
@@ -72,30 +91,61 @@
        }
    }
    private Runnable autoReadRunnable = new Runnable() {
    // 周期性读取数据相关变量
    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("正在同步数据...");
                
                if (btnReadData != null) btnReadData.performClick();
                // 先触发读取数据
                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();
    private static final long DEBOUNCE_DELAY_MS = 1500; // 1.5 seconds debounce
    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();
@@ -105,11 +155,70 @@
        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;
        }
    }
@@ -121,40 +230,66 @@
        }
    }
/**
 * 事件总线监听器
 * <p>
 * 处理蓝牙连接状态变化和设备数据接收事件
 * </p>
 * @param event 事件对象
 */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventRefresh(UpdateEvent event) {
        if (event.getType() == UpdateEvent.Type.CONN_STATU) {
            // Check if obj is integer
            // 检查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) {
            // Received data
            // 收到数据
            String hex = event.getMsg();
            tvStatus.setText("收到数据: " + hex);
            appendLog(hex, false); // false for received (Green)
            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;
        // 根据CMD前8位判断指令类型
        // 根据指令前8位判断指令类型
        if (hex.length() >= 8) {
            // 参数读取返回 (长度至少12位)
            // 读层数: A55A0308...
            // 读站号: A55A0304...
            // 读波特率: A55A0305...
            // 读层数指令: A55A0308...
            // 读站号指令: A55A0304...
            // 读波特率指令: A55A0305...
            if (hex.length() >= 12 && (
                hex.startsWith(CMD.READ_FLOORS.substring(0, 8)) ||
                hex.startsWith(CMD.READ_STATION_NUM.substring(0, 8)) ||
@@ -164,14 +299,14 @@
            }
            // 数据读取返回 (长度至少32位)
            // 读数据: A55A0301...
            // 读数据指令: A55A0301...
            if (hex.length() >= 32 && hex.startsWith(CMD.READ_DATA.substring(0, 8))) {
                parseDataResponse(hex);
                return;
            }
        }
        // 写入指令的返回 (A55A06开头)
        // 写入指令的返回 (以A55A06开头)
        if (hex.startsWith("A55A06") && hex.length() >= 12) {
            parseWriteResponse(hex);
            return;
@@ -208,11 +343,18 @@
                }
                setLabelStatus(tvBaudStatus, "(读取值)", R.color.base_color);
                appendLog("读取波特率: " + value);
                // 读取结束,恢复按钮状态
                restoreCurrentButton();
            } else if ("08".equals(cmdType)) {
                // 层数
                if (etLayer != null) etLayer.setText(String.valueOf(value));
                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();
@@ -259,6 +401,7 @@
                    cmdQueue.clear();
                    currentExecutingCmd = "";
                    tvStatus.setText("写入失败");
                    restoreCurrentButton(); // 恢复按钮
                    
                    if (currentExecutingCmd.startsWith(CMD.WRITE_STATION_NUM)) {
                        setLabelStatus(tvStationStatus, "(写入失败)", R.color.orange);
@@ -278,6 +421,7 @@
            // 异常也视为失败
            setLabelStatus(tvStationStatus, "(写入失败)", R.color.orange);
            setLabelStatus(tvBaudStatus, "(写入失败)", R.color.orange);
            restoreCurrentButton(); // 异常恢复按钮
        }
    }
@@ -303,25 +447,38 @@
            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);
            appendLog("解析玻璃数据: " + glassHex);
            if(!isPeriodicRead){
                appendLog("解析玻璃数据: " + glassHex);
            }
            
            // 直接解析 hex 为 long (Big Endian)
            // "00000004" -> 4 -> ...00100 -> Bit 2 -> Box 3
            // 直接将十六进制解析为长整型 (大端序)
            // "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个格子的状态
@@ -337,6 +494,12 @@
            // 刷新列表显示
            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) {
@@ -358,7 +521,7 @@
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        
        // Initialize box status list
        // 初始化设备状态列表
        boxStatusList.clear();
        for (int i = 1; i <= 30; i++) {
            boxStatusList.add(new BoxStatus(i));
@@ -378,6 +541,82 @@
        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();
@@ -386,25 +625,36 @@
                .setIconType(QMUITipDialog.Builder.ICON_TYPE_LOADING)
                .setTipWord(msg)
                .create();
        // 允许点击外部或返回键取消 loading(只是关闭 dialog)
        // 允许点击外部或返回键取消加载(仅关闭对话框)
        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);
        btnReadData = view.findViewById(R.id.btn_read_data);
        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);
@@ -412,7 +662,9 @@
        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) -> {
@@ -423,7 +675,7 @@
            return false;
        });
        
        // Restore logs if any
        // 恢复日志(如果有)
        if (logBuilder.length() > 0) {
            tvLog.setText(Html.fromHtml(logBuilder.toString()));
        } else {
@@ -431,21 +683,25 @@
            tvLog.setText(Html.fromHtml(logBuilder.toString()));
        }
        // Grid Setup
        // 10 columns to match the image (10 boxes per row)
        // Since we are in a horizontal scroll view, this will layout correctly
        // 网格布局设置
        // 每行10个格子
        // 由于在水平滚动视图中,布局会自动调整
        rvGrid.setLayoutManager(new GridLayoutManager(getContext(), 10)); 
        mAdapter = new GridAdapter();
        rvGrid.setAdapter(mAdapter);
        // Spinner Setup
        // 波特率选择器设置
        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); // Select 156Kbps by default
        spinnerBaud.setSelection(0); // 默认选择156Kbps
        // Button Listeners
        tvMonitorTitle.setOnClickListener(v -> {
            onMonitorTitleClick();
        });
        // 按钮监听器设置
        btnWriteAll.setOnClickListener(v -> {
            if (!BleGlobalManager.getInstance().isConnected()) {
                Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
@@ -470,6 +726,9 @@
                return;
            }
            currentOperatingButton = btnWriteAll;
            updateButtonState(currentOperatingButton, false); // Disable buttons, gray out write button
            // 构造指令队列
            cmdQueue.clear();
            
@@ -493,26 +752,19 @@
            processNextCmd();
        });
        btnReadData.setOnClickListener(v -> {
             if (BleGlobalManager.getInstance().isConnected()) {
                 tvStatus.setText("状态:正在读取数据...");
                 sendCmdWithCrc(CMD.READ_DATA);
             } else {
                 Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
                 appendLog("错误: 蓝牙未连接");
             }
        });
        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);
                 }, 200);
                 }, 500);
                 new android.os.Handler().postDelayed(() -> {
                     sendCmdWithCrc(CMD.READ_BAUD_RATE);
                 }, 400);
                 }, 1000);
             } else {
                 Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
                 appendLog("错误: 蓝牙未连接");
@@ -526,6 +778,41 @@
        });
    }
/**
 * 监控详情标题点击事件
 * <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);
    }
@@ -579,13 +866,38 @@
        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 = "";
        
        // 如果是 hex 指令 (纯 0-9 A-F a-f),加空格格式化
        // 如果是十六进制指令 (纯 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) {
@@ -603,7 +915,7 @@
        String logLine;
        if (isSent != null) {
            String color = isSent ? "#1890ff" : "#05aa87"; // Blue for sent, Green for received
            String color = isSent ? "#1890ff" : "#05aa87"; // 发送为蓝色,接收为绿色
            // 拼接到前缀后面: "发送-读站号: " 或 "收到-读站号: "
            String prefix = (isSent ? "发送" : "收到") + cmdDesc + ": ";
            logLine = time + " <font color='" + color + "'>" + prefix + displayMsg + "</font><br>";
@@ -621,6 +933,13 @@
        }
    }
/**
 * 发送带 CRC 校验的指令
 * <p>
 * 计算指令的 CRC 校验值并发送,同时记录日志
 * </p>
 * @param cmd 十六进制格式的指令内容
 */
    private void sendCmdWithCrc(String cmd) {
        if (!BleGlobalManager.getInstance().isConnected()) {
            Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show();
@@ -637,7 +956,7 @@
            }
            String fullCmd = cmd + crc.toUpperCase();
            
            appendLog(fullCmd, true); // true for sent (Blue)
            appendLog(fullCmd, true); // true表示发送的指令(蓝色)
            BleGlobalManager.getInstance().sendCmd(fullCmd);
        } else {
            appendLog("错误: 指令转换失败");
@@ -655,14 +974,14 @@
        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            // Calculate Box ID based on 3 rows of 10, Right to Left logic as seen in image
            // Row 1 (pos 0-9): 10 ... 1
            // Row 2 (pos 10-19): 20 ... 11
            // Row 3 (pos 20-29): 30 ... 21
            // 根据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 + 1) * 10 - col;
            int boxId = row * 10 + (col + 1);
            
            holder.tvBoxNumber.setText(String.valueOf(boxId));
            
@@ -676,18 +995,18 @@
            }
            
            if (status != null) {
                // Priority: Online > Glass
                // User requirement: "优先显示是否在线" (Prioritize displaying online status)
                // "19-26为也是16进制,解析成二进制32代码我这30个格子是否在线"
                // Usually this means if offline, show offline color. If online, show state (glass/no glass).
                // 优先级: 在线状态 > 玻璃状态
                // 用户需求: "优先显示是否在线"
                // "19-26位也是16进制,解析成二进制32位表示30个格子的在线状态"
                // 通常意味着如果离线,显示离线颜色;如果在线,显示玻璃状态(有/无)
                
                if (!status.isOnline) {
                    holder.viewBox.setBackgroundResource(R.drawable.bg_box_offline); // Offline (Grey)
                    holder.viewBox.setBackgroundResource(R.drawable.bg_box_offline); // 离线(灰色)
                } else {
                    if (status.hasGlass) {
                        holder.viewBox.setBackgroundResource(R.drawable.bg_box_full); // Green
                        holder.viewBox.setBackgroundResource(R.drawable.bg_box_full); // 绿色
                    } else {
                        holder.viewBox.setBackgroundResource(R.drawable.bg_box_empty); // Online but empty (White)
                        holder.viewBox.setBackgroundResource(R.drawable.bg_box_empty); // 在线但无玻璃(白色)
                    }
                }
            }