zhuguifei
2026-01-14 b7ee99a71e88a08a09fe9daada6675a175d09be1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
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);
            }
        }
    }
}