From 99ffc19f60db9578d9c387278405cac8ebda7013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9C=E8=99=9E=E6=B8=A0=E6=88=90=E2=80=9D?= <“yuqucheng2006@qq.com”> Date: Mon, 7 Sep 2026 09:12:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(bluetooth,ui):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=93=9D=E7=89=99=E5=85=B3=E9=97=AD=E5=90=8E=E7=9A=84=E6=96=AD?= =?UTF-8?q?=E5=BC=80=E9=93=BE=E8=B7=AF=E4=B8=8E=E5=93=8D=E5=BA=94=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=A9=BA=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 关蓝牙时跳过0x70协议告别帧与扫描(BluetoothLeScanner为null会抛NPE中断主页Init) - 初始化前检查蓝牙开关静默跳过,错误回调不再误报初始化失败,断开清理不在蓝牙关闭时自动重扫 - 错误/超时响应统一携带空Data,修复超时回调BitConverter.ToString(null)崩溃 - 主页Init增加异常兜底:保证Loading收尾并记录完整异常 --- .../Bluetooth/BLECommunicationManager.cs | 10 ++-- Assets/Scripts/Bluetooth/BluetoothManager.cs | 59 ++++++++++++++++--- Assets/Scripts/Bluetooth/OTAManager.cs | 4 +- .../Bluetooth/Protocol/BLEProtocolStructs.cs | 2 + .../Scripts/UI/Pages/HomePage/HomePageCtrl.cs | 19 ++++++ 5 files changed, 79 insertions(+), 15 deletions(-) diff --git a/Assets/Scripts/Bluetooth/BLECommunicationManager.cs b/Assets/Scripts/Bluetooth/BLECommunicationManager.cs index 216b7d1..c28844d 100644 --- a/Assets/Scripts/Bluetooth/BLECommunicationManager.cs +++ b/Assets/Scripts/Bluetooth/BLECommunicationManager.cs @@ -2449,7 +2449,7 @@ namespace Kill.Bluetooth if (!CheckConnection()) { LogError($"[BLE-DEBUG] 蓝牙未就绪,无法发送指令: {description}"); - callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR }); + callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] }); return; } @@ -2465,7 +2465,7 @@ namespace Kill.Bluetooth if (!CheckConnection()) { LogError("[BLE-DEBUG] CheckConnection failed, invoking callback with error"); - callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR }); + callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] }); return; } @@ -2516,7 +2516,7 @@ namespace Kill.Bluetooth { LogError($"发送帧异常: {ex.Message}"); IsWaitingResponse = false; - callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR }); + callback?.Invoke(new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] }); } } @@ -2963,7 +2963,9 @@ namespace Kill.Bluetooth var timeoutResponse = new BLEResponse { - Status = BLEConstants.STATUS_TIMEOUT + Status = BLEConstants.STATUS_TIMEOUT, + // 超时无数据,给空数组避免回调里 BitConverter.ToString(response.Data) 抛 ArgumentNullException + Data = new byte[0] }; _pendingCallback?.Invoke(timeoutResponse); diff --git a/Assets/Scripts/Bluetooth/BluetoothManager.cs b/Assets/Scripts/Bluetooth/BluetoothManager.cs index 14f973c..0438bca 100644 --- a/Assets/Scripts/Bluetooth/BluetoothManager.cs +++ b/Assets/Scripts/Bluetooth/BluetoothManager.cs @@ -202,6 +202,18 @@ namespace Kill.Bluetooth return; } +#if UNITY_ANDROID && !UNITY_EDITOR + // 安卓:蓝牙已被系统关闭时直接静默跳过。 + // 断开清理/自动重连等后台流程会反复触发初始化,若继续走到 CheckAndEnableBluetooth + // 会弹"蓝牙未开启"Toast 并广播 OnError,造成关蓝牙后一连串误报。 + // 用户主动触发的初始化走 ForceInitialize,仍能得到提示。 + if (!CheckIfBluetoothIsOn()) + { + Log("蓝牙未开启,跳过蓝牙初始化"); + return; + } +#endif + Log("正在初始化蓝牙..."); // 先检查蓝牙是否开启,如果没有则尝试启用 @@ -218,7 +230,10 @@ namespace Kill.Bluetooth public void ForceInitialize() { _userDeniedBluetooth = false; - Initialize(); + // 用户主动触发:不走 Initialize 的静默跳过守卫, + // 蓝牙未开启时由 CheckAndEnableBluetooth 给出提示 + Log("正在初始化蓝牙..."); + CheckAndEnableBluetooth(DoInitialize); } /// @@ -476,7 +491,8 @@ namespace Kill.Bluetooth } if (IsConnected) { - Disconnect(); + // 蓝牙已被系统关闭,0x70 协议告别帧发不出去,跳过直接清理本地状态 + Disconnect(sendProtocolCommand: false); } if (IsScanning) { @@ -595,6 +611,13 @@ namespace Kill.Bluetooth } else { + // 蓝牙已被系统关闭时,插件所有操作(写入/扫描/断开)的失败都会进入这个全局错误回调, + // 此刻并非初始化失败,状态已由 MonitorBluetoothState 清理过,只记录不广播,避免误报 + if (!CheckIfBluetoothIsOn()) + { + Log($"蓝牙已关闭,忽略蓝牙错误: {error}"); + return; + } Log($"蓝牙初始化失败: {error}"); _enableBtCoroutine = null; OnError?.Invoke($"初始化失败: {error}"); @@ -676,6 +699,14 @@ namespace Kill.Bluetooth /// 扫描超时时间(秒),默认10秒 public void StartScan(float timeout = 999f) { + // 蓝牙已被系统关闭时直接跳过:此时 BluetoothLeScanner 为 null, + // 插件扫描调用会抛 NullPointerException(曾导致主页 Init 中断、设备状态不刷新) + if (!CheckIfBluetoothIsOn()) + { + Log("蓝牙未开启,跳过扫描"); + return; + } + if (!IsInitialized) { Log("蓝牙未初始化,自动触发初始化流程..."); @@ -693,6 +724,12 @@ namespace Kill.Bluetooth /// private void DoStartScan(float timeout) { + if (!CheckIfBluetoothIsOn()) + { + Log("蓝牙未开启,跳过扫描"); + return; + } + if (IsScanning) { Log("已经在扫描中,先停止当前扫描..."); @@ -907,7 +944,7 @@ namespace Kill.Bluetooth /// 断开当前连接(带协议通知) /// 先发送0x70断开命令给设备,再断开BLE连接 /// - public void Disconnect(bool backHome=false) + public void Disconnect(bool backHome=false, bool sendProtocolCommand=true) { needBackHome=backHome; if (!IsConnected) @@ -933,21 +970,23 @@ namespace Kill.Bluetooth } // 先通过BLE协议发送断开命令(0x70) - Log($"检查BLECommunicationManager: Instance={(BLECommunicationManager.Instance != null ? "存在" : "null")}"); - if (BLECommunicationManager.Instance != null) + if (sendProtocolCommand && BLECommunicationManager.Instance != null) { Log("发送断开命令(0x70)到设备..."); BLECommunicationManager.Instance.Disconnect((success) => { - Log($"断开命令发送结果: {(success ? "成功" : "失败")}"); + // 0x70 设备不会回复,success 仅表示已提交插件写入,不代表设备已收到 + Log($"断开命令已提交: {(success ? "是" : "否")}"); // 无论命令是否成功,都执行BLE断开 PerformBLEDisconnect(addressToDisconnect); }); } else { - Log("BLECommunicationManager.Instance 为 null,跳过0x70命令,直接断开BLE"); - // 如果没有BLE通信管理器,直接断开BLE + Log(!sendProtocolCommand + ? "蓝牙不可用,跳过0x70命令,直接断开BLE" + : "BLECommunicationManager.Instance 为 null,跳过0x70命令,直接断开BLE"); + // 如果没有BLE通信管理器或蓝牙不可用,直接断开BLE PerformBLEDisconnect(addressToDisconnect); } } @@ -1000,7 +1039,9 @@ namespace Kill.Bluetooth // ChangeAimMac 切换设备时是 Disconnect + StartScan 同步调用, // 本次清理会停掉刚启动的扫描,这里若仍有连接目标则自动重新扫描,避免连接流程中断 - if (!string.IsNullOrEmpty(aimMac) && !IsConnected) + // 蓝牙已被系统关闭时不重扫(扫描只会触发一轮失败的初始化), + // 等蓝牙重新打开后由 MonitorBluetoothState 的自动初始化流程接管 + if (!string.IsNullOrEmpty(aimMac) && !IsConnected && CheckIfBluetoothIsOn()) { StartScan(10); } diff --git a/Assets/Scripts/Bluetooth/OTAManager.cs b/Assets/Scripts/Bluetooth/OTAManager.cs index dc0e0d6..ac0861d 100644 --- a/Assets/Scripts/Bluetooth/OTAManager.cs +++ b/Assets/Scripts/Bluetooth/OTAManager.cs @@ -199,7 +199,7 @@ namespace Kill.Bluetooth private IEnumerator StartUpgradeCoroutineInternal(BLEFrame frame, Action callback) { - BLEResponse response = new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR }; + BLEResponse response = new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] }; bool completed = false; SendFrame(frame, (resp) => @@ -495,7 +495,7 @@ namespace Kill.Bluetooth private IEnumerator SendFrameCoroutine(BLEFrame frame, Action callback) { bool completed = false; - BLEResponse response = new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR }; + BLEResponse response = new BLEResponse { Status = BLEConstants.STATUS_DEVICE_ERROR, Data = new byte[0] }; SendFrame(frame, (resp) => { diff --git a/Assets/Scripts/Bluetooth/Protocol/BLEProtocolStructs.cs b/Assets/Scripts/Bluetooth/Protocol/BLEProtocolStructs.cs index 622e137..7bbd248 100644 --- a/Assets/Scripts/Bluetooth/Protocol/BLEProtocolStructs.cs +++ b/Assets/Scripts/Bluetooth/Protocol/BLEProtocolStructs.cs @@ -494,6 +494,8 @@ namespace Kill.Bluetooth.Protocol public static BLEResponse FromFrame(BLEFrame frame) { var response = new BLEResponse(); + // 无载荷的响应(如写ACK)也保证 Data 非空,避免回调中 BitConverter.ToString(null) 崩溃 + response.Data = new byte[0]; if (frame.Data != null && frame.Data.Length > 0) { response.Status = frame.Data[0]; diff --git a/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs b/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs index 5fb591a..614860c 100644 --- a/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs +++ b/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs @@ -389,6 +389,7 @@ namespace Kill.UI.Pages if(bluetoothDeviceInfoCoroutine!=null) { StopCoroutine(bluetoothDeviceInfoCoroutine); + bluetoothDeviceInfoCoroutine = null; } // 设备断开重新加载主页(保持原行为,当前在主页也会重新加载) // 但切换设备(ChangeAimMac)导致的断开不重新加载,避免主页被销毁重建导致并发初始化 @@ -1004,6 +1005,24 @@ namespace Kill.UI.Pages } public async Task Init() + { + // Init 在 async 链中抛异常会被静默吞掉:LoadingUI 引用计数无法归零(表现为卡 loading), + // 且页面状态刷新(UpdateDeviceState)不会执行。这里统一兜底:记录完整异常并保证 Loading 收尾。 + try + { + await InitInternal(); + } + catch (Exception ex) + { + Debug.LogError($"[HomePageCtrl] Init 异常: {ex}"); + } + finally + { + LoadingUI.Hide(); + } + } + + private async Task InitInternal() { if (DataManager.Instance.userInfo.device_count == 0 || (OwnedDevices.Count == 0 && SharedDevices.Count == 0))