| | |
| | | package com.shlb.comb.fragment; |
| | | |
| | | import android.content.Context; |
| | | import android.os.Bundle; |
| | | import android.view.LayoutInflater; |
| | | import android.view.View; |
| | |
| | | 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 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 QMUITipDialog mLoadingDialog; |
| | | private android.widget.ScrollView svLog; // 日志滚动视图 |
| | | private androidx.cardview.widget.CardView cvLog; // 日志容器 |
| | | |
| | | private GridAdapter mAdapter; |
| | | private List<BoxStatus> boxStatusList = new ArrayList<>(); |
| | | private StringBuilder logBuilder = new StringBuilder(); |
| | | private QMUITipDialog mLoadingDialog; // 加载对话框 |
| | | |
| | | private java.util.Queue<String> cmdQueue = new java.util.LinkedList<>(); |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | 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(); |
| | |
| | | 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; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 事件总线监听器 |
| | | * <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) { |
| | |
| | | triggerAutoRead(); |
| | | } else { |
| | | tvStatus.setText("状态:已断开"); |
| | | // 蓝牙断开时停止周期性读取 |
| | | stopPeriodicRead(); |
| | | } |
| | | } |
| | | } else if (event.getType() == UpdateEvent.Type.DEVICE_INFO) { |
| | | // Received data |
| | | // 收到数据 |
| | | String hex = event.getMsg(); |
| | | if (tvStatus != null && !isPeriodicRead) { |
| | | tvStatus.setText("收到数据: " + hex); |
| | | appendLog(hex, false); // false for received (Green) |
| | | } |
| | | |
| | | // 检查是否为周期性读取 |
| | | 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)) || |
| | |
| | | } |
| | | |
| | | // 数据读取返回 (长度至少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; |
| | |
| | | } |
| | | 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(); |
| | |
| | | cmdQueue.clear(); |
| | | currentExecutingCmd = ""; |
| | | tvStatus.setText("写入失败"); |
| | | restoreCurrentButton(); // 恢复按钮 |
| | | |
| | | if (currentExecutingCmd.startsWith(CMD.WRITE_STATION_NUM)) { |
| | | setLabelStatus(tvStationStatus, "(写入失败)", R.color.orange); |
| | |
| | | // 异常也视为失败 |
| | | setLabelStatus(tvStationStatus, "(写入失败)", R.color.orange); |
| | | setLabelStatus(tvBaudStatus, "(写入失败)", R.color.orange); |
| | | restoreCurrentButton(); // 异常恢复按钮 |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | | |
| | | // 直接解析 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个格子的状态 |
| | |
| | | // 刷新列表显示 |
| | | 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) { |
| | |
| | | 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)); |
| | |
| | | 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(); |
| | |
| | | .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); |
| | |
| | | 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) -> { |
| | |
| | | return false; |
| | | }); |
| | | |
| | | // Restore logs if any |
| | | // 恢复日志(如果有) |
| | | if (logBuilder.length() > 0) { |
| | | tvLog.setText(Html.fromHtml(logBuilder.toString())); |
| | | } else { |
| | |
| | | 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(); |
| | |
| | | return; |
| | | } |
| | | |
| | | currentOperatingButton = btnWriteAll; |
| | | updateButtonState(currentOperatingButton, false); // Disable buttons, gray out write button |
| | | |
| | | // 构造指令队列 |
| | | cmdQueue.clear(); |
| | | |
| | |
| | | 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("错误: 蓝牙未连接"); |
| | |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 监控详情标题点击事件 |
| | | * <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); |
| | | } |
| | |
| | | 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) { |
| | |
| | | |
| | | 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>"; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 发送带 CRC 校验的指令 |
| | | * <p> |
| | | * 计算指令的 CRC 校验值并发送,同时记录日志 |
| | | * </p> |
| | | * @param cmd 十六进制格式的指令内容 |
| | | */ |
| | | private void sendCmdWithCrc(String cmd) { |
| | | if (!BleGlobalManager.getInstance().isConnected()) { |
| | | Toast.makeText(getContext(), "请先连接蓝牙", Toast.LENGTH_SHORT).show(); |
| | |
| | | } |
| | | String fullCmd = cmd + crc.toUpperCase(); |
| | | |
| | | appendLog(fullCmd, true); // true for sent (Blue) |
| | | appendLog(fullCmd, true); // true表示发送的指令(蓝色) |
| | | BleGlobalManager.getInstance().sendCmd(fullCmd); |
| | | } else { |
| | | appendLog("错误: 指令转换失败"); |
| | |
| | | |
| | | @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)); |
| | | |
| | |
| | | } |
| | | |
| | | 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); // 在线但无玻璃(白色) |
| | | } |
| | | } |
| | | } |