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())
{
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);

View File

@ -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);
}
/// <summary>
@ -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
/// <param name="timeout">扫描超时时间默认10秒</param>
public void StartScan(float timeout = 999f)
{
// 蓝牙已被系统关闭时直接跳过:此时 BluetoothLeScanner 为 null
// 插件扫描调用会抛 NullPointerException曾导致主页 Init 中断、设备状态不刷新)
if (!CheckIfBluetoothIsOn())
{
Log("蓝牙未开启,跳过扫描");
return;
}
if (!IsInitialized)
{
Log("蓝牙未初始化,自动触发初始化流程...");
@ -693,6 +724,12 @@ namespace Kill.Bluetooth
/// </summary>
private void DoStartScan(float timeout)
{
if (!CheckIfBluetoothIsOn())
{
Log("蓝牙未开启,跳过扫描");
return;
}
if (IsScanning)
{
Log("已经在扫描中,先停止当前扫描...");
@ -907,7 +944,7 @@ namespace Kill.Bluetooth
/// 断开当前连接(带协议通知)
/// 先发送0x70断开命令给设备再断开BLE连接
/// </summary>
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);
}

View File

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

View File

@ -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];

View File

@ -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))