using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using System.Runtime.InteropServices; using System.Linq; namespace SmartBowSDK { /// /// Windows连接BluetoothLE /// 我的扫描逻辑默认了读写特征都在同一服务下 /// public class BleWinHelper : MonoBehaviour { public string LogTag = "[BleWinHelper]"; private void Log(string text) { //Debug.Log(LogTag + text); SmartBowLogger.Log(this, LogTag + text); } private void Warn(string text) { //Debug.LogWarning(LogTag + text); SmartBowLogger.Log(this, LogTag + text); } private void Error(string text) { //Debug.Log(LogTag + text); SmartBowLogger.Log(this, LogTag + text); } //private string targetDeviceName = "Bbow_20210501 | ARTEMIS Pro | HOUYI Pro | Pistol | Pistol M9 | BGBox_202012"; //private string targetDeviceNameHOUYIPro = "HOUYI Pro"; //private string targetDeviceNameGun = "Pistol | Pistol M9 | Bbow_20210501"; private string targetService = "{0000fff0-0000-1000-8000-00805f9b34fb}"; private string targetCharacteristicsNotify = "{0000fff1-0000-1000-8000-00805f9b34fb}"; private string targetCharacteristicsWrite = "{0000fff2-0000-1000-8000-00805f9b34fb}"; private bool isConnectLocking = false; private bool isScanningDevices = false; private bool isScanningServices = false; private bool isScanningCharacteristics = false; private bool isSubscribed = false; private bool isSubscribing = false; private string lastError = null; [SerializeField] private string selectedDeviceId = null; private Dictionary> deviceList = new Dictionary>(); private List serviceList = new List(); private List characteristicsList = new List(); private float _connectedTime = 0; private float _receiveDataTime = 0; private float _heartBeatInterval = 0; private Action OnScanEnded; private Action OnConnected; /// /// 主动调用Disconnect()不会触发该委托 /// private Action OnConnectionFailed; private static Action OnCharacteristicChanged; public SmartBowHelper smartBowHelper; public BluetoothAim_SDK bluetoothAim; /// /// 注册window对象 /// /// 挂载对象 /// 关联的BluetoothWindows /// 提示的log标签 /// public static BleWinHelper RegisterTo(SmartBowHelper _smartBowHelper, BluetoothWindows bluetoothWindows,string logTip = "first") { //if (_Instance) //{ // Error("Register fail, because only one can be registered."); // return null; //} string bleWinName = "BleWinHelper-" + logTip; GameObject obj = new GameObject(bleWinName); obj.transform.SetParent(_smartBowHelper.transform); BleWinHelper bleWinHelper = obj.AddComponent(); bleWinHelper.smartBowHelper = _smartBowHelper; bleWinHelper.bluetoothAim = _smartBowHelper.bluetoothAim; //日志名字 bleWinHelper.LogTag = logTip + ": "; bluetoothWindows.Connect = bleWinHelper.Connect; bluetoothWindows.Disconnect = bleWinHelper.Disconnect; bluetoothWindows.Write = bleWinHelper.Write; bluetoothWindows.WriteByte = bleWinHelper.WriteByte; // windos 通知 bluetoothAim bleWinHelper.OnScanEnded = (bool bSelectedDeviceId) => bluetoothWindows.OnScanEnded?.Invoke(bSelectedDeviceId); bleWinHelper.OnConnected = () => bluetoothWindows.OnConnected?.Invoke(); bleWinHelper.OnConnectionFailed = () => bluetoothWindows.OnConnectionFailed?.Invoke(); //多个定义共用一个OnCharacteristicChanged OnCharacteristicChanged += (deviceID,bytes) => { if (deviceID == bleWinHelper.selectedDeviceId) { bluetoothWindows.OnCharacteristicChanged?.Invoke(deviceID, bytes); } }; return bleWinHelper; } /// /// 设置心跳检测 /// 1.每次收到的蓝牙数据都视为心跳 /// 2.帮助触发蓝牙断开监听 /// /// 心跳检测间隔 public void SetHeartBeat(float interval) { _heartBeatInterval = interval; } //private static BleWinHelper _Instance; void Awake() { // _Instance = this; } void OnDestroy() { // if (_Instance == this) _Instance = null; BleApi.Quit(); } void Update() { BleApi.ScanStatus status; if (isScanningDevices) { BleApi.DeviceUpdate res = new BleApi.DeviceUpdate(); do { status = BleApi.PollDevice(ref res, false); if (status == BleApi.ScanStatus.AVAILABLE) { // 如果存在 selectedDeviceId,不要重复 if (selectedDeviceId != null) { //Log($"当前的 selectedDeviceId: {selectedDeviceId}"); continue; // 跳过后续 AVAILABLE 的处理,等待 FINISHED } if (!deviceList.ContainsKey(res.id)) { deviceList[res.id] = new Dictionary() { { "name", "" }, { "isConnectable", "False" } }; //这里参考手机蓝牙模式 if (smartBowHelper.GetIsConnectName()) { //后续匹配名字 可以是多个设备 string _filters = string.IsNullOrEmpty(smartBowHelper.GetFilters()) ? bluetoothAim.deviceConfig.deviceName : smartBowHelper.GetFilters(); //如果有定制执行定制 Log($"发现设备{res.name},is fileters empty:{ string.IsNullOrEmpty(smartBowHelper.GetFilters())},current filters:{smartBowHelper.GetFilters()}"); } else { // 统一格式化:去掉非十六进制字符,转大写 string FormatMac(string mac) => string.Concat(mac.Where(c => Uri.IsHexDigit(c))).ToUpper(); string searchMac = FormatMac(ExtractMacFromDeviceId(res.id)); string connectMac = FormatMac(smartBowHelper.GetConnectMacStr()); Log($"发现设备 {searchMac},is connectMacStr:{connectMac },DeviceAddress:{searchMac}"); } } if (res.nameUpdated) deviceList[res.id]["name"] = res.name; if (res.isConnectableUpdated) deviceList[res.id]["isConnectable"] = res.isConnectable.ToString(); //这里参考手机蓝牙模式 if (smartBowHelper.GetIsConnectName()) { //后续匹配名字 可以是多个设备 //Log( $"发现设备{res.name},is fileters empty:{ string.IsNullOrEmpty(smartBowHelper.GetFilters())},name:{smartBowHelper.GetFilters()}"); string _filters = string.IsNullOrEmpty(smartBowHelper.GetFilters()) ? bluetoothAim.deviceConfig.deviceName : smartBowHelper.GetFilters(); string[] filterArray = _filters.Split('|'); // 支持多个名字 foreach (var f in filterArray) { string trimmedFilter = f.Trim(); if (string.Equals(trimmedFilter, deviceList[res.id]["name"].Trim(), StringComparison.OrdinalIgnoreCase) && res.isConnectable) { selectedDeviceId = res.id; StopDeviceScan(); Log($"匹配设备名:{trimmedFilter}"); break; } } } else { // 统一格式化:去掉非十六进制字符,转大写 string FormatMac(string mac) => string.Concat(mac.Where(c => Uri.IsHexDigit(c))).ToUpper(); string searchMac = FormatMac(ExtractMacFromDeviceId(res.id)); string connectMac = FormatMac(smartBowHelper.GetConnectMacStr()); //Log($"发现设备 {searchMac},is connectMacStr:{connectMac },DeviceAddress:{searchMac}"); //按mac地址匹配 if (!string.IsNullOrEmpty(connectMac) && connectMac.Contains(searchMac) && res.isConnectable) { selectedDeviceId = res.id; StopDeviceScan(); Log($"匹配设备 MAC:{searchMac}"); } } } else if (status == BleApi.ScanStatus.FINISHED) { StartCoroutine(ScanServiceAndCharacteristicsToSubscribe()); Log($" ScanStatus FINISHED !"); //停止扫描后通知? OnScanEnded?.Invoke(selectedDeviceId != null); } } while (status == BleApi.ScanStatus.AVAILABLE); } if (isScanningServices) { BleApi.Service res; do { status = BleApi.PollService(out res, false); if (status == BleApi.ScanStatus.AVAILABLE) { serviceList.Add(res.uuid); } else if (status == BleApi.ScanStatus.FINISHED) { isScanningServices = false; } } while (status == BleApi.ScanStatus.AVAILABLE); } if (isScanningCharacteristics) { BleApi.Characteristic res; do { status = BleApi.PollCharacteristic(out res, false); if (status == BleApi.ScanStatus.AVAILABLE) { // Log("res.userDescription:"+ res.userDescription+ ",res.uuid:" + res.uuid); if (res.userDescription == "no description available" || //旧设备 res.userDescription == "SPP Write Channel" || //新设备 res.userDescription == "SPP Read Channel") { characteristicsList.Add(res.uuid); } else { //跟着之前代码 characteristicsList.Add(res.userDescription); } //string name = res.userDescription != "no description available" ? res.userDescription : res.uuid; //characteristicsList.Add(name); } else if (status == BleApi.ScanStatus.FINISHED) { isScanningCharacteristics = false; } } while (status == BleApi.ScanStatus.AVAILABLE); } if (isSubscribed) { BleApi.BLEData res; while (BleApi.PollData(out res, false)) { //string text = BitConverter.ToString(res.buf, 0, res.size); // string text = Encoding.ASCII.GetString(res.buf, 0, res.size); byte[] bytes = new byte[res.size]; Array.Copy(res.buf, bytes, res.size); _receiveDataTime = Time.realtimeSinceStartup; OnCharacteristicChanged?.Invoke(res.deviceId, bytes); } } { BleApi.ErrorMessage res; BleApi.GetError(out res); if (lastError != res.msg) { Error(res.msg); lastError = res.msg; //对应设备才断开 if (lastError.Contains("SendDataAsync") && lastError.Contains(selectedDeviceId) && isSubscribed) { HandleConnectFail(); } } } } void LateUpdate() { if ( _heartBeatInterval > 0 && isSubscribed && Time.realtimeSinceStartup - _connectedTime >= _heartBeatInterval && Time.realtimeSinceStartup - _receiveDataTime >= _heartBeatInterval ) { HandleConnectFail(); } } private bool Connect() { if (isConnectLocking || isScanningDevices) { Warn("Connect Invalid, because is in connect."); return false; } //开始连接时候,重置一下参数 ReinitAfterConnectFail(); BleApi.StartDeviceScan(); isConnectLocking = true; isScanningDevices = true; Log("Start Connect!"); return true; } private void StopDeviceScan() { if (!isScanningDevices) return; BleApi.StopDeviceScan(); isScanningDevices = false; Log($"Stop DeviceScan!"); } private IEnumerator ScanServiceAndCharacteristicsToSubscribe() { isSubscribing = true; if (selectedDeviceId == null) { HandleConnectFail(); smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "选择设备 DeviceId 不存在!"); yield break; } Log("SelectedDeviceId OK"); BleApi.ScanServices(selectedDeviceId); isScanningServices = true; serviceList.Clear(); while (isScanningServices) yield return null; bool findTargetService = false; foreach (string service in serviceList) { if (service == targetService) { findTargetService = true; Log("FindTargetService OK"); break; } } if (!findTargetService) { HandleConnectFail(); //一般都不会出现这个问题,直接使用Unknown smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "目标设备服务 service 不存在!"); yield break; } BleApi.ScanCharacteristics(selectedDeviceId, targetService); isScanningCharacteristics = true; characteristicsList.Clear(); while (isScanningCharacteristics) yield return null; bool findTargetCharacteristicsNotify = false; bool findTargetCharacteristicsWrite = false; foreach (string characteristics in characteristicsList) { if (characteristics == targetCharacteristicsNotify) { findTargetCharacteristicsNotify = true; Log("FindTargetCharacteristicsNotify OK"); } else if (characteristics == targetCharacteristicsWrite) { findTargetCharacteristicsWrite = true; Log("FindTargetCharacteristicsWrite OK"); } } if (!findTargetCharacteristicsNotify || !findTargetCharacteristicsWrite) { HandleConnectFail(); //一般都不会出现这个问题,直接使用Unknown smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "目标设备特征值 Characteristics 不存在!"); yield break; } BleApi.SubscribeCharacteristic(selectedDeviceId, targetService, targetCharacteristicsNotify, false); isSubscribed = true; isSubscribing = false; Log("SubscribeCharacteristicNotify OK"); _connectedTime = Time.realtimeSinceStartup; OnConnected?.Invoke(); } /// /// 连接错误 /// private void HandleConnectFail() { if (selectedDeviceId != null) BleApi.Disconnect(selectedDeviceId); bool isLockBefore = isConnectLocking; ReinitAfterConnectFail(); if (isLockBefore) OnConnectionFailed?.Invoke(); } private void ReinitAfterConnectFail() { isConnectLocking = false; isScanningDevices = false; isScanningServices = false; isScanningCharacteristics = false; isSubscribed = false; isSubscribing = false; selectedDeviceId = null; deviceList.Clear(); serviceList.Clear(); characteristicsList.Clear(); } private bool Write(string text) { if (!isSubscribed) return false; byte[] payload = Encoding.ASCII.GetBytes(text); BleApi.BLEData data = new BleApi.BLEData(); data.buf = new byte[512]; data.size = (short)payload.Length; data.deviceId = selectedDeviceId; data.serviceUuid = targetService; data.characteristicUuid = targetCharacteristicsWrite; for (int i = 0; i < payload.Length; i++) data.buf[i] = payload[i]; // no error code available in non-blocking mode BleApi.SendData(in data, false); //Log("Write(" + text + ")"); return true; } private bool WriteByte(byte[] payload) { if (!isSubscribed) return false; BleApi.BLEData data = new BleApi.BLEData(); data.buf = new byte[512]; data.size = (short)payload.Length; data.deviceId = selectedDeviceId; data.serviceUuid = targetService; data.characteristicUuid = targetCharacteristicsWrite; for (int i = 0; i < payload.Length; i++) data.buf[i] = payload[i]; // no error code available in non-blocking mode BleApi.SendData(in data, false); Log("Write(byte[])"); return true; } private bool Disconnect() { if (!isConnectLocking) { Warn("Disconnect Invalid, because not in connect."); return false; } if (isSubscribing) { Warn("Disconnect Invalid, because is subscribing."); return false; } if (isScanningDevices) StopDeviceScan(); if (selectedDeviceId != null) BleApi.Disconnect(selectedDeviceId); ReinitAfterConnectFail(); Log("Disconnect OK"); return true; } /// /// 其实是 WinRT BLE API 的设备 ID 格式,它包含两部分: /// 前面:BluetoothLE#BluetoothLE00:e0:4c:2a:12:97 → 设备的内部标识(适配器 + 随机 ID)。 /// 后面:d3:5c:89:57:68:de → 真实的 MAC 地址。 /// 微软官方文档也说明了: WinRT 不直接给 BluetoothAddress,而是拼接在 Id 的末尾。 /// /// 参考 BluetoothLE#BluetoothLE00:e0:4c:2a:12:97-d3:5c:89:57:68:de /// string ExtractMacFromDeviceId(string deviceId) { if (string.IsNullOrEmpty(deviceId)) return null; // WinRT 格式一般是:BluetoothLE#BluetoothLEXX:XX:XX:XX:XX:XX-YY:YY:YY:YY:YY:YY int lastDash = deviceId.LastIndexOf('-'); if (lastDash >= 0 && lastDash < deviceId.Length - 1) { return deviceId.Substring(lastDash + 1); // 提取最后一段 MAC } return deviceId; } } }