using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using System.Runtime.InteropServices; namespace SmartBowSDK { /// /// Windows连接BluetoothLE /// 我的扫描逻辑默认了读写特征都在同一服务下 /// public class BleWinHelper : MonoBehaviour { private const string LogTag = "BleWinHelper-Log: "; private static void Log(string text) { Debug.Log(LogTag + text); } private static void Warn(string text) { Debug.LogWarning(LogTag + text); } private static void Error(string text) { Debug.Log(LogTag + text); } private string targetDeviceName = "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; 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 OnConnected; /// /// 主动调用Disconnect()不会触发该委托 /// private Action OnConnectionFailed; private Action OnCharacteristicChanged; public static BleWinHelper RegisterTo(GameObject o) { if (_Instance) { Error("Register fail, because only one can be registered."); return null; } BleWinHelper comp = o.AddComponent(); BluetoothWindows.Connect = comp.Connect; BluetoothWindows.Disconnect = comp.Disconnect; BluetoothWindows.Write = comp.Write; comp.OnConnected = () => BluetoothWindows.OnConnected?.Invoke(); comp.OnConnectionFailed = () => BluetoothWindows.OnConnectionFailed?.Invoke(); comp.OnCharacteristicChanged = (bytes) => BluetoothWindows.OnCharacteristicChanged?.Invoke(bytes); return comp; } /// /// 设置心跳检测 /// 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) { if (!deviceList.ContainsKey(res.id)) deviceList[res.id] = new Dictionary() { { "name", "" }, { "isConnectable", "False" } }; if (res.nameUpdated) deviceList[res.id]["name"] = res.name; if (res.isConnectableUpdated) deviceList[res.id]["isConnectable"] = res.isConnectable.ToString(); if (deviceList[res.id]["name"] == targetDeviceName && deviceList[res.id]["isConnectable"] == "True") { selectedDeviceId = res.id; StopDeviceScan(); } } else if (status == BleApi.ScanStatus.FINISHED) { StartCoroutine(ScanServiceAndCharacteristicsToSubscribe()); } } 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) { 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(bytes); } } { BleApi.ErrorMessage res; BleApi.GetError(out res); if (lastError != res.msg) { Error(res.msg); lastError = res.msg; if (lastError.Contains("SendDataAsync") && 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; } BleApi.StartDeviceScan(); isConnectLocking = true; isScanningDevices = true; Log("Start Connect"); return true; } private void StopDeviceScan() { if (!isScanningDevices) return; BleApi.StopDeviceScan(); isScanningDevices = false; } private IEnumerator ScanServiceAndCharacteristicsToSubscribe() { isSubscribing = true; if (selectedDeviceId == null) { HandleConnectFail(); 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(); 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(); 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 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; } } public class BleApi { public enum ScanStatus { PROCESSING, AVAILABLE, FINISHED }; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct DeviceUpdate { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)] public string id; [MarshalAs(UnmanagedType.I1)] public bool isConnectable; [MarshalAs(UnmanagedType.I1)] public bool isConnectableUpdated; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)] public string name; [MarshalAs(UnmanagedType.I1)] public bool nameUpdated; } [DllImport("BleWinrtDll.dll", EntryPoint = "StartDeviceScan")] public static extern void StartDeviceScan(); [DllImport("BleWinrtDll.dll", EntryPoint = "PollDevice")] public static extern ScanStatus PollDevice(ref DeviceUpdate device, bool block); [DllImport("BleWinrtDll.dll", EntryPoint = "StopDeviceScan")] public static extern void StopDeviceScan(); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct Service { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)] public string uuid; }; [DllImport("BleWinrtDll.dll", EntryPoint = "ScanServices", CharSet = CharSet.Unicode)] public static extern void ScanServices(string deviceId); [DllImport("BleWinrtDll.dll", EntryPoint = "PollService")] public static extern ScanStatus PollService(out Service service, bool block); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct Characteristic { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)] public string uuid; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)] public string userDescription; }; [DllImport("BleWinrtDll.dll", EntryPoint = "ScanCharacteristics", CharSet = CharSet.Unicode)] public static extern void ScanCharacteristics(string deviceId, string serviceId); [DllImport("BleWinrtDll.dll", EntryPoint = "PollCharacteristic")] public static extern ScanStatus PollCharacteristic(out Characteristic characteristic, bool block); [DllImport("BleWinrtDll.dll", EntryPoint = "SubscribeCharacteristic", CharSet = CharSet.Unicode)] public static extern bool SubscribeCharacteristic(string deviceId, string serviceId, string characteristicId, bool block); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct BLEData { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)] public byte[] buf; [MarshalAs(UnmanagedType.I2)] public short size; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string deviceId; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string serviceUuid; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string characteristicUuid; }; [DllImport("BleWinrtDll.dll", EntryPoint = "PollData")] public static extern bool PollData(out BLEData data, bool block); [DllImport("BleWinrtDll.dll", EntryPoint = "SendData")] public static extern bool SendData(in BLEData data, bool block); [DllImport("BleWinrtDll.dll", EntryPoint = "Disconnect", CharSet = CharSet.Unicode)] public static extern void Disconnect(string deviceId); [DllImport("BleWinrtDll.dll", EntryPoint = "Quit")] public static extern void Quit(); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct ErrorMessage { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] public string msg; }; [DllImport("BleWinrtDll.dll", EntryPoint = "GetError")] public static extern void GetError(out ErrorMessage buf); } }