|
|
@@ -0,0 +1,454 @@
|
|
|
+using System;
|
|
|
+using System.Collections;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Text;
|
|
|
+using UnityEngine;
|
|
|
+using System.Runtime.InteropServices;
|
|
|
+
|
|
|
+namespace SmartBowSDK
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// Windows连接BluetoothLE
|
|
|
+ /// 我的扫描逻辑默认了读写特征都在同一服务下
|
|
|
+ /// </summary>
|
|
|
+ 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<string, Dictionary<string, string>> deviceList = new Dictionary<string, Dictionary<string, string>>();
|
|
|
+ private List<string> serviceList = new List<string>();
|
|
|
+ private List<string> characteristicsList = new List<string>();
|
|
|
+
|
|
|
+ private float _connectedTime = 0;
|
|
|
+ private float _receiveDataTime = 0;
|
|
|
+ private float _heartBeatInterval = 0;
|
|
|
+
|
|
|
+ private Action OnConnected;
|
|
|
+ /// <summary>
|
|
|
+ /// 主动调用Disconnect()不会触发该委托
|
|
|
+ /// </summary>
|
|
|
+ private Action OnConnectionFailed;
|
|
|
+ private Action<byte[]> OnCharacteristicChanged;
|
|
|
+
|
|
|
+ public static BleWinHelper RegisterTo(GameObject o)
|
|
|
+ {
|
|
|
+ if (_Instance)
|
|
|
+ {
|
|
|
+ Error("Register fail, because only one can be registered.");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ BleWinHelper comp = o.AddComponent<BleWinHelper>();
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 设置心跳检测
|
|
|
+ /// 1.每次收到的蓝牙数据都视为心跳
|
|
|
+ /// 2.帮助触发蓝牙断开监听
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="interval">心跳检测间隔</param>
|
|
|
+ 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<string, string>() {
|
|
|
+ { "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);
|
|
|
+ }
|
|
|
+}
|