fix(bluetooth,ui): 修复蓝牙关闭后的断开链路与响应数据空引用

- 关蓝牙时跳过0x70协议告别帧与扫描(BluetoothLeScanner为null会抛NPE中断主页Init)
- 初始化前检查蓝牙开关静默跳过,错误回调不再误报初始化失败,断开清理不在蓝牙关闭时自动重扫
- 错误/超时响应统一携带空Data,修复超时回调BitConverter.ToString(null)崩溃
- 主页Init增加异常兜底:保证Loading收尾并记录完整异常
This commit is contained in:
“虞渠成” 2026-09-07 09:12:14 +08:00
parent 320ece473f
commit 99ffc19f60
5 changed files with 79 additions and 15 deletions

View File

@ -2449,7 +2449,7 @@ namespace Kill.Bluetooth
if (!CheckConnection()) if (!CheckConnection())
{ {
LogError($"[BLE-DEBUG] 蓝牙未就绪,无法发送指令: {description}"); 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; return;
} }
@ -2465,7 +2465,7 @@ namespace Kill.Bluetooth
if (!CheckConnection()) if (!CheckConnection())
{ {
LogError("[BLE-DEBUG] CheckConnection failed, invoking callback with error"); 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; return;
} }
@ -2516,7 +2516,7 @@ namespace Kill.Bluetooth
{ {
LogError($"发送帧异常: {ex.Message}"); LogError($"发送帧异常: {ex.Message}");
IsWaitingResponse = false; 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 var timeoutResponse = new BLEResponse
{ {
Status = BLEConstants.STATUS_TIMEOUT Status = BLEConstants.STATUS_TIMEOUT,
// 超时无数据,给空数组避免回调里 BitConverter.ToString(response.Data) 抛 ArgumentNullException
Data = new byte[0]
}; };
_pendingCallback?.Invoke(timeoutResponse); _pendingCallback?.Invoke(timeoutResponse);

View File

@ -202,6 +202,18 @@ namespace Kill.Bluetooth
return; return;
} }
#if UNITY_ANDROID && !UNITY_EDITOR
// 安卓:蓝牙已被系统关闭时直接静默跳过。
// 断开清理/自动重连等后台流程会反复触发初始化,若继续走到 CheckAndEnableBluetooth
// 会弹"蓝牙未开启"Toast 并广播 OnError造成关蓝牙后一连串误报。
// 用户主动触发的初始化走 ForceInitialize仍能得到提示。
if (!CheckIfBluetoothIsOn())
{
Log("蓝牙未开启,跳过蓝牙初始化");
return;
}
#endif
Log("正在初始化蓝牙..."); Log("正在初始化蓝牙...");
// 先检查蓝牙是否开启,如果没有则尝试启用 // 先检查蓝牙是否开启,如果没有则尝试启用
@ -218,7 +230,10 @@ namespace Kill.Bluetooth
public void ForceInitialize() public void ForceInitialize()
{ {
_userDeniedBluetooth = false; _userDeniedBluetooth = false;
Initialize(); // 用户主动触发:不走 Initialize 的静默跳过守卫,
// 蓝牙未开启时由 CheckAndEnableBluetooth 给出提示
Log("正在初始化蓝牙...");
CheckAndEnableBluetooth(DoInitialize);
} }
/// <summary> /// <summary>
@ -476,7 +491,8 @@ namespace Kill.Bluetooth
} }
if (IsConnected) if (IsConnected)
{ {
Disconnect(); // 蓝牙已被系统关闭0x70 协议告别帧发不出去,跳过直接清理本地状态
Disconnect(sendProtocolCommand: false);
} }
if (IsScanning) if (IsScanning)
{ {
@ -595,6 +611,13 @@ namespace Kill.Bluetooth
} }
else else
{ {
// 蓝牙已被系统关闭时,插件所有操作(写入/扫描/断开)的失败都会进入这个全局错误回调,
// 此刻并非初始化失败,状态已由 MonitorBluetoothState 清理过,只记录不广播,避免误报
if (!CheckIfBluetoothIsOn())
{
Log($"蓝牙已关闭,忽略蓝牙错误: {error}");
return;
}
Log($"蓝牙初始化失败: {error}"); Log($"蓝牙初始化失败: {error}");
_enableBtCoroutine = null; _enableBtCoroutine = null;
OnError?.Invoke($"初始化失败: {error}"); OnError?.Invoke($"初始化失败: {error}");
@ -676,6 +699,14 @@ namespace Kill.Bluetooth
/// <param name="timeout">扫描超时时间默认10秒</param> /// <param name="timeout">扫描超时时间默认10秒</param>
public void StartScan(float timeout = 999f) public void StartScan(float timeout = 999f)
{ {
// 蓝牙已被系统关闭时直接跳过:此时 BluetoothLeScanner 为 null
// 插件扫描调用会抛 NullPointerException曾导致主页 Init 中断、设备状态不刷新)
if (!CheckIfBluetoothIsOn())
{
Log("蓝牙未开启,跳过扫描");
return;
}
if (!IsInitialized) if (!IsInitialized)
{ {
Log("蓝牙未初始化,自动触发初始化流程..."); Log("蓝牙未初始化,自动触发初始化流程...");
@ -693,6 +724,12 @@ namespace Kill.Bluetooth
/// </summary> /// </summary>
private void DoStartScan(float timeout) private void DoStartScan(float timeout)
{ {
if (!CheckIfBluetoothIsOn())
{
Log("蓝牙未开启,跳过扫描");
return;
}
if (IsScanning) if (IsScanning)
{ {
Log("已经在扫描中,先停止当前扫描..."); Log("已经在扫描中,先停止当前扫描...");
@ -907,7 +944,7 @@ namespace Kill.Bluetooth
/// 断开当前连接(带协议通知) /// 断开当前连接(带协议通知)
/// 先发送0x70断开命令给设备再断开BLE连接 /// 先发送0x70断开命令给设备再断开BLE连接
/// </summary> /// </summary>
public void Disconnect(bool backHome=false) public void Disconnect(bool backHome=false, bool sendProtocolCommand=true)
{ {
needBackHome=backHome; needBackHome=backHome;
if (!IsConnected) if (!IsConnected)
@ -933,21 +970,23 @@ namespace Kill.Bluetooth
} }
// 先通过BLE协议发送断开命令0x70 // 先通过BLE协议发送断开命令0x70
Log($"检查BLECommunicationManager: Instance={(BLECommunicationManager.Instance != null ? "" : "null")}"); if (sendProtocolCommand && BLECommunicationManager.Instance != null)
if (BLECommunicationManager.Instance != null)
{ {
Log("发送断开命令(0x70)到设备..."); Log("发送断开命令(0x70)到设备...");
BLECommunicationManager.Instance.Disconnect((success) => BLECommunicationManager.Instance.Disconnect((success) =>
{ {
Log($"断开命令发送结果: {(success ? "" : "")}"); // 0x70 设备不会回复success 仅表示已提交插件写入,不代表设备已收到
Log($"断开命令已提交: {(success ? "" : "")}");
// 无论命令是否成功都执行BLE断开 // 无论命令是否成功都执行BLE断开
PerformBLEDisconnect(addressToDisconnect); PerformBLEDisconnect(addressToDisconnect);
}); });
} }
else else
{ {
Log("BLECommunicationManager.Instance 为 null跳过0x70命令直接断开BLE"); Log(!sendProtocolCommand
// 如果没有BLE通信管理器直接断开BLE ? "蓝牙不可用跳过0x70命令直接断开BLE"
: "BLECommunicationManager.Instance 为 null跳过0x70命令直接断开BLE");
// 如果没有BLE通信管理器或蓝牙不可用直接断开BLE
PerformBLEDisconnect(addressToDisconnect); PerformBLEDisconnect(addressToDisconnect);
} }
} }
@ -1000,7 +1039,9 @@ namespace Kill.Bluetooth
// ChangeAimMac 切换设备时是 Disconnect + StartScan 同步调用, // ChangeAimMac 切换设备时是 Disconnect + StartScan 同步调用,
// 本次清理会停掉刚启动的扫描,这里若仍有连接目标则自动重新扫描,避免连接流程中断 // 本次清理会停掉刚启动的扫描,这里若仍有连接目标则自动重新扫描,避免连接流程中断
if (!string.IsNullOrEmpty(aimMac) && !IsConnected) // 蓝牙已被系统关闭时不重扫(扫描只会触发一轮失败的初始化),
// 等蓝牙重新打开后由 MonitorBluetoothState 的自动初始化流程接管
if (!string.IsNullOrEmpty(aimMac) && !IsConnected && CheckIfBluetoothIsOn())
{ {
StartScan(10); StartScan(10);
} }

View File

@ -199,7 +199,7 @@ namespace Kill.Bluetooth
private IEnumerator StartUpgradeCoroutineInternal(BLEFrame frame, Action<bool> callback) private IEnumerator StartUpgradeCoroutineInternal(BLEFrame frame, Action<bool> 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; bool completed = false;
SendFrame(frame, (resp) => SendFrame(frame, (resp) =>
@ -495,7 +495,7 @@ namespace Kill.Bluetooth
private IEnumerator SendFrameCoroutine(BLEFrame frame, Action<BLEResponse> callback) private IEnumerator SendFrameCoroutine(BLEFrame frame, Action<BLEResponse> callback)
{ {
bool completed = false; 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) => SendFrame(frame, (resp) =>
{ {

View File

@ -494,6 +494,8 @@ namespace Kill.Bluetooth.Protocol
public static BLEResponse FromFrame(BLEFrame frame) public static BLEResponse FromFrame(BLEFrame frame)
{ {
var response = new BLEResponse(); var response = new BLEResponse();
// 无载荷的响应如写ACK也保证 Data 非空,避免回调中 BitConverter.ToString(null) 崩溃
response.Data = new byte[0];
if (frame.Data != null && frame.Data.Length > 0) if (frame.Data != null && frame.Data.Length > 0)
{ {
response.Status = frame.Data[0]; response.Status = frame.Data[0];

View File

@ -389,6 +389,7 @@ namespace Kill.UI.Pages
if(bluetoothDeviceInfoCoroutine!=null) if(bluetoothDeviceInfoCoroutine!=null)
{ {
StopCoroutine(bluetoothDeviceInfoCoroutine); StopCoroutine(bluetoothDeviceInfoCoroutine);
bluetoothDeviceInfoCoroutine = null;
} }
// 设备断开重新加载主页(保持原行为,当前在主页也会重新加载) // 设备断开重新加载主页(保持原行为,当前在主页也会重新加载)
// 但切换设备ChangeAimMac导致的断开不重新加载避免主页被销毁重建导致并发初始化 // 但切换设备ChangeAimMac导致的断开不重新加载避免主页被销毁重建导致并发初始化
@ -1004,6 +1005,24 @@ namespace Kill.UI.Pages
} }
public async Task Init() 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)) if (DataManager.Instance.userInfo.device_count == 0 || (OwnedDevices.Count == 0 && SharedDevices.Count == 0))