From 6fab61f97de0a45280543c232d8dc375174cd26a Mon Sep 17 00:00:00 2001 From: lishuo Date: Fri, 20 Aug 2021 18:45:16 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E8=93=9D=E7=89=99?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E5=88=9D=E6=AD=A5=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Plugins/Android/AndroidManifest.xml | 19 +- .../Plugins/Android/AndroidManifest.xml.meta | 2 +- .../Android/unityandroidbluetoothlelib.jar | Bin 0 -> 31910 bytes .../unityandroidbluetoothlelib.jar.meta | 33 + Assets/Plugins/BluetoothDeviceScript.cs | 368 +++++ Assets/Plugins/BluetoothDeviceScript.cs.meta | 8 + Assets/Plugins/BluetoothHardwareInterface.cs | 998 +++++++++++++ .../BluetoothHardwareInterface.cs.meta | 8 + Assets/Plugins/BluetoothLEOSX.bundle.meta | 46 + .../BluetoothLEOSX.bundle/Contents.meta | 8 + .../BluetoothLEOSX.bundle/Contents/Info.plist | 48 + .../Contents/Info.plist.meta | 7 + .../BluetoothLEOSX.bundle/Contents/MacOS.meta | 8 + .../Contents/MacOS/BluetoothLEOSX | Bin 0 -> 122464 bytes .../Contents/MacOS/BluetoothLEOSX.meta | 7 + .../Contents/_CodeSignature.meta | 8 + .../Contents/_CodeSignature/CodeResources | 115 ++ .../_CodeSignature/CodeResources.meta | 7 + Assets/Plugins/iOS.meta | 5 + Assets/Plugins/iOS/UnityBluetoothLE.h | 107 ++ Assets/Plugins/iOS/UnityBluetoothLE.h.meta | 95 ++ Assets/Plugins/iOS/UnityBluetoothLE.mm | 1316 +++++++++++++++++ Assets/Plugins/iOS/UnityBluetoothLE.mm.meta | 124 ++ Assets/Scripts/Devices/Ble/BleResponse.cs | 7 +- .../Devices/Ble/mobile/BleMobileInterface.cs | 482 +++--- .../Devices/Ble/mobile/BleMobileThread.cs | 66 + .../Ble/mobile/BleMobileThread.cs.meta | 11 + Assets/Scripts/Devices/MainDeviceAdapter.cs | 10 +- Assets/Shatalmic.meta | 8 + Assets/Shatalmic/Demo.unity | 439 ++++++ Assets/Shatalmic/Demo.unity.meta | 7 + Assets/Shatalmic/Editor.meta | 5 + .../Editor/BluetoothPostProcessBuild.cs | 30 + .../Editor/BluetoothPostProcessBuild.cs.meta | 12 + .../Shatalmic/Editor/PostprocessBuildPlayer | 93 ++ .../Editor/PostprocessBuildPlayer.meta | 9 + Assets/Shatalmic/README.txt | 1 + Assets/Shatalmic/README.txt.meta | 7 + Assets/Shatalmic/plugin.unitypackage.meta | 7 + 39 files changed, 4293 insertions(+), 238 deletions(-) create mode 100644 Assets/Plugins/Android/unityandroidbluetoothlelib.jar create mode 100644 Assets/Plugins/Android/unityandroidbluetoothlelib.jar.meta create mode 100644 Assets/Plugins/BluetoothDeviceScript.cs create mode 100644 Assets/Plugins/BluetoothDeviceScript.cs.meta create mode 100644 Assets/Plugins/BluetoothHardwareInterface.cs create mode 100644 Assets/Plugins/BluetoothHardwareInterface.cs.meta create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle.meta create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents.meta create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents/Info.plist create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents/Info.plist.meta create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS.meta create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS/BluetoothLEOSX create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS/BluetoothLEOSX.meta create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature.meta create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature/CodeResources create mode 100644 Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature/CodeResources.meta create mode 100644 Assets/Plugins/iOS.meta create mode 100644 Assets/Plugins/iOS/UnityBluetoothLE.h create mode 100644 Assets/Plugins/iOS/UnityBluetoothLE.h.meta create mode 100644 Assets/Plugins/iOS/UnityBluetoothLE.mm create mode 100644 Assets/Plugins/iOS/UnityBluetoothLE.mm.meta create mode 100644 Assets/Scripts/Devices/Ble/mobile/BleMobileThread.cs create mode 100644 Assets/Scripts/Devices/Ble/mobile/BleMobileThread.cs.meta create mode 100644 Assets/Shatalmic.meta create mode 100644 Assets/Shatalmic/Demo.unity create mode 100644 Assets/Shatalmic/Demo.unity.meta create mode 100644 Assets/Shatalmic/Editor.meta create mode 100644 Assets/Shatalmic/Editor/BluetoothPostProcessBuild.cs create mode 100644 Assets/Shatalmic/Editor/BluetoothPostProcessBuild.cs.meta create mode 100644 Assets/Shatalmic/Editor/PostprocessBuildPlayer create mode 100644 Assets/Shatalmic/Editor/PostprocessBuildPlayer.meta create mode 100644 Assets/Shatalmic/README.txt create mode 100644 Assets/Shatalmic/README.txt.meta create mode 100644 Assets/Shatalmic/plugin.unitypackage.meta diff --git a/Assets/Plugins/Android/AndroidManifest.xml b/Assets/Plugins/Android/AndroidManifest.xml index 3e6a69ac..88ad0757 100644 --- a/Assets/Plugins/Android/AndroidManifest.xml +++ b/Assets/Plugins/Android/AndroidManifest.xml @@ -1,17 +1,18 @@ - - - + + + + + - - - - - - + + + + + \ No newline at end of file diff --git a/Assets/Plugins/Android/AndroidManifest.xml.meta b/Assets/Plugins/Android/AndroidManifest.xml.meta index 83e598e6..99cac0a9 100644 --- a/Assets/Plugins/Android/AndroidManifest.xml.meta +++ b/Assets/Plugins/Android/AndroidManifest.xml.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 506e447c8b55f0449ae2b5b5ed37dd99 +guid: 4abd69b18c4ab45ec976851430122248 TextScriptImporter: externalObjects: {} userData: diff --git a/Assets/Plugins/Android/unityandroidbluetoothlelib.jar b/Assets/Plugins/Android/unityandroidbluetoothlelib.jar new file mode 100644 index 0000000000000000000000000000000000000000..437b1c9886048332b03d04a98f9f88cb580a0fa3 GIT binary patch literal 31910 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Μc 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 literal 0 HcmV?d00001 diff --git a/Assets/Plugins/Android/unityandroidbluetoothlelib.jar.meta b/Assets/Plugins/Android/unityandroidbluetoothlelib.jar.meta new file mode 100644 index 00000000..28af6907 --- /dev/null +++ b/Assets/Plugins/Android/unityandroidbluetoothlelib.jar.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: f158ceee465c745bc89002ae57bc033e +timeCreated: 1539484944 +licenseType: Pro +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Android: Android + second: + enabled: 1 + settings: {} + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/BluetoothDeviceScript.cs b/Assets/Plugins/BluetoothDeviceScript.cs new file mode 100644 index 00000000..f056aeed --- /dev/null +++ b/Assets/Plugins/BluetoothDeviceScript.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +public class BluetoothDeviceScript : MonoBehaviour +{ +#if UNITY_IOS + public Dictionary BLEStandardUUIDs = new Dictionary(); +#endif + + public List DiscoveredDeviceList; + + public Action InitializedAction; + public Action DeinitializedAction; + public Action ErrorAction; + public Action ServiceAddedAction; + public Action StartedAdvertisingAction; + public Action StoppedAdvertisingAction; + public Action DiscoveredPeripheralAction; + public Action DiscoveredPeripheralWithAdvertisingInfoAction; + public Action DiscoveredBeaconAction; + public Action RetrievedConnectedPeripheralAction; + public Action PeripheralReceivedWriteDataAction; + public Action ConnectedPeripheralAction; + public Action ConnectedDisconnectPeripheralAction; + public Action DisconnectedPeripheralAction; + public Action DiscoveredServiceAction; + public Action DiscoveredCharacteristicAction; + public Action DidWriteCharacteristicAction; + public Dictionary>> DidUpdateNotificationStateForCharacteristicAction; + public Dictionary>> DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction; + public Dictionary>> DidUpdateCharacteristicValueAction; + public Dictionary>> DidUpdateCharacteristicValueWithDeviceAddressAction; + public Action RequestMtuAction; + + // Use this for initialization + void Start () + { + DiscoveredDeviceList = new List (); + DidUpdateNotificationStateForCharacteristicAction = new Dictionary>> (); + DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction = new Dictionary>> (); + DidUpdateCharacteristicValueAction = new Dictionary>> (); + DidUpdateCharacteristicValueWithDeviceAddressAction = new Dictionary>> (); + +#if UNITY_IOS + BLEStandardUUIDs["Heart Rate Measurement"] = "00002A37-0000-1000-8000-00805F9B34FB"; +#endif + } + + // Update is called once per frame + void Update () + { + } + + const string deviceInitializedString = "Initialized"; + const string deviceDeInitializedString = "DeInitialized"; + const string deviceErrorString = "Error"; + const string deviceServiceAdded = "ServiceAdded"; + const string deviceStartedAdvertising = "StartedAdvertising"; + const string deviceStoppedAdvertising = "StoppedAdvertising"; + const string deviceDiscoveredPeripheral = "DiscoveredPeripheral"; + const string deviceDiscoveredBeacon = "DiscoveredBeacon"; + const string deviceRetrievedConnectedPeripheral = "RetrievedConnectedPeripheral"; + const string devicePeripheralReceivedWriteData = "PeripheralReceivedWriteData"; + const string deviceConnectedPeripheral = "ConnectedPeripheral"; + const string deviceDisconnectedPeripheral = "DisconnectedPeripheral"; + const string deviceDiscoveredService = "DiscoveredService"; + const string deviceDiscoveredCharacteristic = "DiscoveredCharacteristic"; + const string deviceDidWriteCharacteristic = "DidWriteCharacteristic"; + const string deviceDidUpdateNotificationStateForCharacteristic = "DidUpdateNotificationStateForCharacteristic"; + const string deviceDidUpdateValueForCharacteristic = "DidUpdateValueForCharacteristic"; + const string deviceLog = "Log"; + const string deviceRequestMtu = "MtuChanged"; + + public void OnBluetoothMessage (string message) + { + if (message != null) + { + char[] delim = new char[] { '~' }; + string[] parts = message.Split (delim); + + for (int i = 0; i < parts.Length; ++i) + BluetoothLEHardwareInterface.Log(string.Format("Part: {0} - {1}", i, parts[i])); + + if (message.Length >= deviceInitializedString.Length && message.Substring (0, deviceInitializedString.Length) == deviceInitializedString) + { + if (InitializedAction != null) + InitializedAction (); + } + else if (message.Length >= deviceLog.Length && message.Substring (0, deviceLog.Length) == deviceLog) + { + BluetoothLEHardwareInterface.Log (parts[1]); + } + else if (message.Length >= deviceDeInitializedString.Length && message.Substring (0, deviceDeInitializedString.Length) == deviceDeInitializedString) + { + BluetoothLEHardwareInterface.FinishDeInitialize (); + + if (DeinitializedAction != null) + DeinitializedAction (); + } + else if (message.Length >= deviceErrorString.Length && message.Substring (0, deviceErrorString.Length) == deviceErrorString) + { + string error = ""; + + if (parts.Length >= 2) + error = parts[1]; + + if (ErrorAction != null) + ErrorAction (error); + } + else if (message.Length >= deviceServiceAdded.Length && message.Substring (0, deviceServiceAdded.Length) == deviceServiceAdded) + { + if (parts.Length >= 2) + { + if (ServiceAddedAction != null) + ServiceAddedAction (parts[1]); + } + } + else if (message.Length >= deviceStartedAdvertising.Length && message.Substring (0, deviceStartedAdvertising.Length) == deviceStartedAdvertising) + { + BluetoothLEHardwareInterface.Log ("Started Advertising"); + + if (StartedAdvertisingAction != null) + StartedAdvertisingAction (); + } + else if (message.Length >= deviceStoppedAdvertising.Length && message.Substring (0, deviceStoppedAdvertising.Length) == deviceStoppedAdvertising) + { + BluetoothLEHardwareInterface.Log ("Stopped Advertising"); + + if (StoppedAdvertisingAction != null) + StoppedAdvertisingAction (); + } + else if (message.Length >= deviceDiscoveredPeripheral.Length && message.Substring (0, deviceDiscoveredPeripheral.Length) == deviceDiscoveredPeripheral) + { + if (parts.Length >= 3) + { + // the first callback will only get called the first time this device is seen + // this is because it gets added to the a list in the DiscoveredDeviceList + // after that only the second callback will get called and only if there is + // advertising data available + if (!DiscoveredDeviceList.Contains (parts[1] + "|" + parts[2])) + { + DiscoveredDeviceList.Add (parts[1] + "|" + parts[2]); + + if (DiscoveredPeripheralAction != null) + DiscoveredPeripheralAction (parts[1], parts[2]); + } + + if (parts.Length >= 5 && DiscoveredPeripheralWithAdvertisingInfoAction != null) + { + // get the rssi from the 4th value + int rssi = 0; + if (!int.TryParse (parts[3], out rssi)) + rssi = 0; + + // parse the base 64 encoded data that is the 5th value + byte[] bytes = System.Convert.FromBase64String (parts[4]); + + DiscoveredPeripheralWithAdvertisingInfoAction (parts[1], parts[2], rssi, bytes); + } + } + } + else if (message.Length >= deviceDiscoveredBeacon.Length && message.Substring (0, deviceDiscoveredBeacon.Length) == deviceDiscoveredBeacon) + { + if (parts.Length >= 7) + { + var iBeaconData = new BluetoothLEHardwareInterface.iBeaconData (); + + iBeaconData.UUID = parts[1]; + if (!int.TryParse (parts[2], out iBeaconData.Major)) + iBeaconData.Major = 0; + if (!int.TryParse (parts[3], out iBeaconData.Minor)) + iBeaconData.Minor = 0; + if (!int.TryParse (parts[4], out iBeaconData.RSSI)) + iBeaconData.RSSI = 0; + if (!int.TryParse (parts[5], out iBeaconData.AndroidSignalPower)) + iBeaconData.AndroidSignalPower = 0; + int iOSProximity = 0; + if (!int.TryParse (parts[6], out iOSProximity)) + iOSProximity = 0; + iBeaconData.iOSProximity = (BluetoothLEHardwareInterface.iOSProximity)iOSProximity; + + if (DiscoveredBeaconAction != null) + DiscoveredBeaconAction (iBeaconData); + } + } + else if (message.Length >= deviceRetrievedConnectedPeripheral.Length && message.Substring (0, deviceRetrievedConnectedPeripheral.Length) == deviceRetrievedConnectedPeripheral) + { + if (parts.Length >= 3) + { + DiscoveredDeviceList.Add (parts[1]); + + if (RetrievedConnectedPeripheralAction != null) + RetrievedConnectedPeripheralAction (parts[1], parts[2]); + } + } + else if (message.Length >= devicePeripheralReceivedWriteData.Length && message.Substring (0, devicePeripheralReceivedWriteData.Length) == devicePeripheralReceivedWriteData) + { + if (parts.Length >= 3) + OnPeripheralData (parts[1], parts[2]); + } + else if (message.Length >= deviceConnectedPeripheral.Length && message.Substring (0, deviceConnectedPeripheral.Length) == deviceConnectedPeripheral) + { + if (parts.Length >= 2 && ConnectedPeripheralAction != null) + ConnectedPeripheralAction (parts[1]); + } + else if (message.Length >= deviceDisconnectedPeripheral.Length && message.Substring (0, deviceDisconnectedPeripheral.Length) == deviceDisconnectedPeripheral) + { + if (parts.Length >= 2) + { + if (ConnectedDisconnectPeripheralAction != null) + ConnectedDisconnectPeripheralAction (parts[1]); + + if (DisconnectedPeripheralAction != null) + DisconnectedPeripheralAction (parts[1]); + } + } + else if (message.Length >= deviceDiscoveredService.Length && message.Substring (0, deviceDiscoveredService.Length) == deviceDiscoveredService) + { + if (parts.Length >= 3 && DiscoveredServiceAction != null) + DiscoveredServiceAction (parts[1], parts[2]); + } + else if (message.Length >= deviceDiscoveredCharacteristic.Length && message.Substring (0, deviceDiscoveredCharacteristic.Length) == deviceDiscoveredCharacteristic) + { + if (parts.Length >= 4 && DiscoveredCharacteristicAction != null) + DiscoveredCharacteristicAction (parts[1], parts[2], parts[3]); + } + else if (message.Length >= deviceDidWriteCharacteristic.Length && message.Substring (0, deviceDidWriteCharacteristic.Length) == deviceDidWriteCharacteristic) + { + if (parts.Length >= 2 && DidWriteCharacteristicAction != null) + DidWriteCharacteristicAction (parts[1]); + } + else if (message.Length >= deviceDidUpdateNotificationStateForCharacteristic.Length && message.Substring (0, deviceDidUpdateNotificationStateForCharacteristic.Length) == deviceDidUpdateNotificationStateForCharacteristic) + { + if (parts.Length >= 3) + { + if (DidUpdateNotificationStateForCharacteristicAction != null && DidUpdateNotificationStateForCharacteristicAction.ContainsKey (parts[1])) + { + var characteristicAction = DidUpdateNotificationStateForCharacteristicAction[parts[1]]; + if (characteristicAction != null && characteristicAction.ContainsKey (parts[2])) + { + var action = characteristicAction[parts[2]]; + if (action != null) + action (parts[2]); + } + } + + if (DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction != null && DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction.ContainsKey (parts[1])) + { + var characteristicAction = DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[parts[1]]; + if (characteristicAction != null && characteristicAction.ContainsKey (parts[2])) + { + var action = characteristicAction[parts[2]]; + if (action != null) + action (parts[1], parts[2]); + } + } + } + } + else if (message.Length >= deviceDidUpdateValueForCharacteristic.Length && message.Substring (0, deviceDidUpdateValueForCharacteristic.Length) == deviceDidUpdateValueForCharacteristic) + { + if (parts.Length >= 4) + OnBluetoothData (parts[1], parts[2], parts[3]); + } + else if (message.Length >= deviceRequestMtu.Length && message.Substring(0, deviceRequestMtu.Length) == deviceRequestMtu) + { + if (parts.Length >= 3) + { + if (RequestMtuAction != null) + { + int mtu = 0; + if (int.TryParse(parts[2], out mtu)) + RequestMtuAction(parts[1], mtu); + } + } + } + } + } + + public void OnBluetoothData (string base64Data) + { + OnBluetoothData ("", "", base64Data); + } + + public void OnBluetoothData (string deviceAddress, string characteristic, string base64Data) + { + if (base64Data != null) + { + byte[] bytes = System.Convert.FromBase64String (base64Data); + if (bytes.Length > 0) + { + deviceAddress = deviceAddress.ToUpper (); + characteristic = characteristic.ToUpper (); + +#if UNITY_IOS + if (BLEStandardUUIDs.ContainsKey(characteristic)) + characteristic = BLEStandardUUIDs[characteristic]; +#endif + + BluetoothLEHardwareInterface.Log ("Device: " + deviceAddress + " Characteristic Received: " + characteristic); + + string byteString = ""; + foreach (byte b in bytes) + byteString += string.Format ("{0:X2}", b); + + BluetoothLEHardwareInterface.Log (byteString); + + if (DidUpdateCharacteristicValueAction != null && DidUpdateCharacteristicValueAction.ContainsKey (deviceAddress)) + { + var characteristicAction = DidUpdateCharacteristicValueAction[deviceAddress]; +#if UNITY_ANDROID + characteristic = characteristic.ToLower (); +#endif + if (characteristicAction != null && characteristicAction.ContainsKey (characteristic)) + { + var action = characteristicAction[characteristic]; + if (action != null) + action (characteristic, bytes); + } + } + + if (DidUpdateCharacteristicValueWithDeviceAddressAction != null && DidUpdateCharacteristicValueWithDeviceAddressAction.ContainsKey (deviceAddress)) + { + var characteristicAction = DidUpdateCharacteristicValueWithDeviceAddressAction[deviceAddress]; +#if UNITY_ANDROID + characteristic = characteristic.ToLower (); +#endif + if (characteristicAction != null && characteristicAction.ContainsKey (characteristic)) + { + var action = characteristicAction[characteristic]; + if (action != null) + action (deviceAddress, characteristic, bytes); + } + } + } + } + } + + public void OnPeripheralData (string characteristic, string base64Data) + { + if (base64Data != null) + { + byte[] bytes = System.Convert.FromBase64String (base64Data); + if (bytes.Length > 0) + { + BluetoothLEHardwareInterface.Log ("Peripheral Received: " + characteristic); + + string byteString = ""; + foreach (byte b in bytes) + byteString += string.Format ("{0:X2}", b); + + BluetoothLEHardwareInterface.Log (byteString); + + if (PeripheralReceivedWriteDataAction != null) + PeripheralReceivedWriteDataAction (characteristic, bytes); + } + } + } + +#if UNITY_IOS + private void IncludeCoreLocationFramework() + { + // this method is here because Unity now only includes CoreLocation + // if there are methods in the .cs code that access it + Input.location.Stop (); + } +#endif +} diff --git a/Assets/Plugins/BluetoothDeviceScript.cs.meta b/Assets/Plugins/BluetoothDeviceScript.cs.meta new file mode 100644 index 00000000..e31cb904 --- /dev/null +++ b/Assets/Plugins/BluetoothDeviceScript.cs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b188ba3ac565e48f58fc50dd5db4818d +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: diff --git a/Assets/Plugins/BluetoothHardwareInterface.cs b/Assets/Plugins/BluetoothHardwareInterface.cs new file mode 100644 index 00000000..b7c11ed1 --- /dev/null +++ b/Assets/Plugins/BluetoothHardwareInterface.cs @@ -0,0 +1,998 @@ +#define EXPERIMENTAL_MACOS_EDITOR +/* + +This build includes an experimental implementation for the macOS editor of Unity +It is experiemental because of the way that the Unity editor hangs on to plugin +instances after leaving play mode. This causes this plugin to not free up its +resources and therefore can cause crashes in the Unity editor on macOS. + +Since Unity does not give plugins or apps a chance to do anything when the user +hits the play / stop button in the Editor there isn't a chance for the app to +deinitialize this plugin. + +What I have found in my own use of this is that if you put a button on your app +somewhere that you can press before hitting the stop button in the editor and +then in that button handler call this plugin's Deinitialize method it seems to +minimize how often the editor crashes. + +WARNING: using the macOS editor can cause the editor to crash an loose your work +and settings. Save often. You have been warned, so please don't contact me if +you have lost work becausee of this problem. This is experimental only. Use at +your own risk. + +*/ + +using UnityEngine; +using System; +using System.Runtime.InteropServices; +using System.Collections.Generic; + +#if UNITY_2018_3_OR_NEWER +#if UNITY_ANDROID +using UnityEngine.Android; +#endif +#endif + +public class BluetoothLEHardwareInterface +{ + public enum CBCharacteristicProperties + { + CBCharacteristicPropertyBroadcast = 0x01, + CBCharacteristicPropertyRead = 0x02, + CBCharacteristicPropertyWriteWithoutResponse = 0x04, + CBCharacteristicPropertyWrite = 0x08, + CBCharacteristicPropertyNotify = 0x10, + CBCharacteristicPropertyIndicate = 0x20, + CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40, + CBCharacteristicPropertyExtendedProperties = 0x80, + CBCharacteristicPropertyNotifyEncryptionRequired = 0x100, + CBCharacteristicPropertyIndicateEncryptionRequired = 0x200, + }; + + public enum ScanMode + { + LowPower = 0, + Balanced = 1, + LowLatency = 2 + } + + public enum ConnectionPriority + { + LowPower = 0, + Balanced = 1, + High = 2, + } + + public enum iOSProximity + { + Unknown = 0, + Immediate = 1, + Near = 2, + Far = 3, + } + + public struct iBeaconData + { + public string UUID; + public int Major; + public int Minor; + public int RSSI; + public int AndroidSignalPower; + public iOSProximity iOSProximity; + } + +#if UNITY_ANDROID + public enum CBAttributePermissions + { + CBAttributePermissionsReadable = 0x01, + CBAttributePermissionsWriteable = 0x10, + CBAttributePermissionsReadEncryptionRequired = 0x02, + CBAttributePermissionsWriteEncryptionRequired = 0x20, + }; +#else + public enum CBAttributePermissions + { + CBAttributePermissionsReadable = 0x01, + CBAttributePermissionsWriteable = 0x02, + CBAttributePermissionsReadEncryptionRequired = 0x04, + CBAttributePermissionsWriteEncryptionRequired = 0x08, + }; +#endif + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + + public delegate void UnitySendMessageCallbackDelegate (IntPtr objectName, IntPtr commandName, IntPtr commandData); + + [DllImport ("BluetoothLEOSX")] + private static extern void ConnectUnitySendMessageCallback ([MarshalAs (UnmanagedType.FunctionPtr)]UnitySendMessageCallbackDelegate callbackMethod); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLELog (string message); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEInitialize ([MarshalAs (UnmanagedType.Bool)]bool asCentral, [MarshalAs (UnmanagedType.Bool)]bool asPeripheral); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEDeInitialize (); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEPauseMessages (bool isPaused); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEScanForPeripheralsWithServices (string serviceUUIDsString, bool allowDuplicates, bool rssiOnly, bool clearPeripheralList); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLERetrieveListOfPeripheralsWithServices (string serviceUUIDsString); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEStopScan (); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEConnectToPeripheral (string name); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEDisconnectAll (); + + [DllImport("BluetoothLEOSX")] + private static extern void OSXBluetoothLERequestMtu (string name, int mtu); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEDisconnectPeripheral (string name); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEReadCharacteristic (string name, string service, string characteristic); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEWriteCharacteristic (string name, string service, string characteristic, byte[] data, int length, bool withResponse); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLESubscribeCharacteristic (string name, string service, string characteristic); + + [DllImport ("BluetoothLEOSX")] + private static extern void OSXBluetoothLEUnSubscribeCharacteristic (string name, string service, string characteristic); + +#endif + +#if UNITY_IOS || UNITY_TVOS + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLELog (string message); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEInitialize (bool asCentral, bool asPeripheral); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEDeInitialize (); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEPauseMessages (bool isPaused); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEScanForPeripheralsWithServices (string serviceUUIDsString, bool allowDuplicates, bool rssiOnly, bool clearPeripheralList); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLERetrieveListOfPeripheralsWithServices (string serviceUUIDsString); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEStopScan (); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEConnectToPeripheral (string name); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEDisconnectPeripheral (string name); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEReadCharacteristic (string name, string service, string characteristic); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEWriteCharacteristic (string name, string service, string characteristic, byte[] data, int length, bool withResponse); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLESubscribeCharacteristic (string name, string service, string characteristic); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEUnSubscribeCharacteristic (string name, string service, string characteristic); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEDisconnectAll (); + +#if !UNITY_TVOS + [DllImport("__Internal")] + private static extern void _iOSBluetoothLERequestMtu(string name, int mtu); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEScanForBeacons (string proximityUUIDsString); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEStopBeaconScan (); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEPeripheralName (string newName); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLECreateService (string uuid, bool primary); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLERemoveService (string uuid); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLERemoveServices (); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLECreateCharacteristic (string uuid, int properties, int permissions, byte[] data, int length); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLERemoveCharacteristic (string uuid); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLERemoveCharacteristics (); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEStartAdvertising (); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEStopAdvertising (); + + [DllImport ("__Internal")] + private static extern void _iOSBluetoothLEUpdateCharacteristicValue (string uuid, byte[] data, int length); +#endif +#elif UNITY_ANDROID + static AndroidJavaObject _android = null; +#endif + + + private static BluetoothDeviceScript bluetoothDeviceScript; + + public static void Log (string message) + { +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + Debug.Log(message); +#else + if (!Application.isEditor) + { +#if UNITY_IOS || UNITY_TVOS + _iOSBluetoothLELog (message); +#elif UNITY_ANDROID + if (_android == null) + { + AndroidJavaClass javaClass = new AndroidJavaClass ("com.shatalmic.unityandroidbluetoothlelib.UnityBluetoothLE"); + _android = javaClass.CallStatic ("getInstance"); + } + + if (_android != null) + _android.Call ("androidBluetoothLog", message); +#endif + } +#endif + } + + public static BluetoothDeviceScript Initialize (bool asCentral, bool asPeripheral, Action action, Action errorAction) + { + bluetoothDeviceScript = null; + +#if UNITY_2018_3_OR_NEWER +#if UNITY_ANDROID + if (!Permission.HasUserAuthorizedPermission (Permission.FineLocation)) + Permission.RequestUserPermission (Permission.FineLocation); +#endif +#endif + + GameObject bluetoothLEReceiver = GameObject.Find("BluetoothLEReceiver"); + if (bluetoothLEReceiver == null) + bluetoothLEReceiver = new GameObject ("BluetoothLEReceiver"); + + if (bluetoothLEReceiver != null) + { + bluetoothDeviceScript = bluetoothLEReceiver.GetComponent (); + if (bluetoothDeviceScript == null) + bluetoothDeviceScript = bluetoothLEReceiver.AddComponent (); + + if (bluetoothDeviceScript != null) + { + bluetoothDeviceScript.InitializedAction = action; + bluetoothDeviceScript.ErrorAction = errorAction; + } + } + + GameObject.DontDestroyOnLoad (bluetoothLEReceiver); + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + ConnectUnitySendMessageCallback ((objectName, commandName, commandData) => { + string name = Marshal.PtrToStringAuto (objectName); + string command = Marshal.PtrToStringAuto (commandName); + string data = Marshal.PtrToStringAuto (commandData); + + GameObject foundObject = GameObject.Find (name); + if (foundObject != null) + foundObject.SendMessage (command, data); + }); + + BluetoothLEHardwareInterface.OSXBluetoothLEInitialize (asCentral, asPeripheral); +#else + if (Application.isEditor) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.SendMessage ("OnBluetoothMessage", "Initialized"); + } + else + { +#if UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEInitialize (asCentral, asPeripheral); +#elif UNITY_ANDROID + if (_android == null) + { + AndroidJavaClass javaClass = new AndroidJavaClass ("com.shatalmic.unityandroidbluetoothlelib.UnityBluetoothLE"); + _android = javaClass.CallStatic ("getInstance"); + } + + if (_android != null) + _android.Call ("androidBluetoothInitialize", asCentral, asPeripheral); +#endif + } +#endif + + return bluetoothDeviceScript; + } + + public static void DeInitialize (Action action) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.DeinitializedAction = action; + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + BluetoothLEHardwareInterface.OSXBluetoothLEDeInitialize (); +#else + if (Application.isEditor) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.SendMessage ("OnBluetoothMessage", "DeInitialized"); + } + else + { +#if UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEDeInitialize (); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothDeInitialize"); +#endif + } +#endif + } + + public static void FinishDeInitialize () + { + GameObject bluetoothLEReceiver = GameObject.Find("BluetoothLEReceiver"); + if (bluetoothLEReceiver != null) + GameObject.Destroy(bluetoothLEReceiver); + } + + public static void BluetoothEnable (bool enable) + { + if (!Application.isEditor) + { +#if UNITY_IOS || UNITY_TVOS + //_iOSBluetoothLELog (message); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothEnable", enable); +#endif + } + } + + public static void BluetoothScanMode (ScanMode scanMode) + { + if (!Application.isEditor) + { +#if UNITY_IOS || UNITY_TVOS +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothScanMode", (int)scanMode); +#endif + } + } + + public static void BluetoothConnectionPriority (ConnectionPriority connectionPriority) + { + if (!Application.isEditor) + { +#if UNITY_IOS || UNITY_TVOS +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothConnectionPriority", (int)connectionPriority); +#endif + } + } + + public static void PauseMessages (bool isPaused) + { +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEPauseMessages (isPaused); +#else + if (!Application.isEditor) + { +#if UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEPauseMessages (isPaused); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothPause", isPaused); +#endif + } +#endif + } + + // scanning for beacons requires that you know the Proximity UUID + public static void ScanForBeacons (string[] proximityUUIDs, Action actionBeaconResponse) + { + if (proximityUUIDs != null && proximityUUIDs.Length >= 0) + { + if (!Application.isEditor) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.DiscoveredBeaconAction = actionBeaconResponse; + + string proximityUUIDsString = null; + + if (proximityUUIDs != null && proximityUUIDs.Length > 0) + { + proximityUUIDsString = ""; + + foreach (string proximityUUID in proximityUUIDs) + proximityUUIDsString += proximityUUID + "|"; + + proximityUUIDsString = proximityUUIDsString.Substring (0, proximityUUIDsString.Length - 1); + } + +#if UNITY_IOS + _iOSBluetoothLEScanForBeacons (proximityUUIDsString); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothScanForBeacons", proximityUUIDsString); +#endif + } + } + } + + public static void RequestMtu(string name, int mtu, Action action) + { + if (bluetoothDeviceScript != null) + { + bluetoothDeviceScript.RequestMtuAction = action; + } + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + if (mtu > 184) + mtu = 184; + OSXBluetoothLERequestMtu(name, mtu); +#elif UNITY_IOS || UNITY_TVOS + if (mtu > 180) + mtu = 180; + _iOSBluetoothLERequestMtu (name, mtu); +#elif UNITY_ANDROID + if (_android != null) + { + _android.Call ("androidBluetoothRequestMtu", name, mtu); + } +#endif + } + + public static void ScanForPeripheralsWithServices (string[] serviceUUIDs, Action action = null, Action actionAdvertisingInfo = null, bool rssiOnly = false, bool clearPeripheralList = true, int recordType = 0xFF) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + { + bluetoothDeviceScript.DiscoveredPeripheralAction = action; + bluetoothDeviceScript.DiscoveredPeripheralWithAdvertisingInfoAction = actionAdvertisingInfo; + + if (bluetoothDeviceScript.DiscoveredDeviceList != null) + bluetoothDeviceScript.DiscoveredDeviceList.Clear (); + } + + string serviceUUIDsString = null; + + if (serviceUUIDs != null && serviceUUIDs.Length > 0) + { + serviceUUIDsString = ""; + + foreach (string serviceUUID in serviceUUIDs) + serviceUUIDsString += serviceUUID + "|"; + + serviceUUIDsString = serviceUUIDsString.Substring (0, serviceUUIDsString.Length - 1); + } + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEScanForPeripheralsWithServices (serviceUUIDsString, (actionAdvertisingInfo != null), rssiOnly, clearPeripheralList); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEScanForPeripheralsWithServices (serviceUUIDsString, (actionAdvertisingInfo != null), rssiOnly, clearPeripheralList); +#elif UNITY_ANDROID + if (_android != null) + { + if (serviceUUIDsString == null) + serviceUUIDsString = ""; + + _android.Call ("androidBluetoothScanForPeripheralsWithServices", serviceUUIDsString, rssiOnly, recordType); + } +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void RetrieveListOfPeripheralsWithServices (string[] serviceUUIDs, Action action) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + { + bluetoothDeviceScript.RetrievedConnectedPeripheralAction = action; + + if (bluetoothDeviceScript.DiscoveredDeviceList != null) + bluetoothDeviceScript.DiscoveredDeviceList.Clear (); + } + + string serviceUUIDsString = serviceUUIDs.Length > 0 ? "" : null; + + foreach (string serviceUUID in serviceUUIDs) + serviceUUIDsString += serviceUUID + "|"; + + // strip the last delimeter + serviceUUIDsString = serviceUUIDsString.Substring (0, serviceUUIDsString.Length - 1); + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLERetrieveListOfPeripheralsWithServices (serviceUUIDsString); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLERetrieveListOfPeripheralsWithServices (serviceUUIDsString); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothRetrieveListOfPeripheralsWithServices", serviceUUIDsString); +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void StopScan () + { +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEStopScan (); +#else + if (!Application.isEditor) + { +#if UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEStopScan (); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothStopScan"); +#endif + } +#endif + } + + public static void StopBeaconScan () + { + if (!Application.isEditor) + { +#if UNITY_IOS + _iOSBluetoothLEStopBeaconScan (); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothStopBeaconScan"); +#endif + } + } + + public static void DisconnectAll () + { +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEDisconnectAll (); +#else + if (!Application.isEditor) + { +#if UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEDisconnectAll (); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothDisconnectAll"); +#endif + } +#endif + } + + public static void ConnectToPeripheral (string name, Action connectAction, Action serviceAction, Action characteristicAction, Action disconnectAction = null) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + { + bluetoothDeviceScript.ConnectedPeripheralAction = connectAction; + bluetoothDeviceScript.DiscoveredServiceAction = serviceAction; + bluetoothDeviceScript.DiscoveredCharacteristicAction = characteristicAction; + bluetoothDeviceScript.ConnectedDisconnectPeripheralAction = disconnectAction; + } + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEConnectToPeripheral (name); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEConnectToPeripheral (name); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothConnectToPeripheral", name); +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void DisconnectPeripheral (string name, Action action) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.DisconnectedPeripheralAction = action; + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEDisconnectPeripheral (name); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEDisconnectPeripheral (name); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidBluetoothDisconnectPeripheral", name); +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void ReadCharacteristic (string name, string service, string characteristic, Action action) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + { + if (!bluetoothDeviceScript.DidUpdateCharacteristicValueAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateCharacteristicValueAction[name] = new Dictionary>(); + +#if UNITY_IOS || UNITY_TVOS || (EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX)) + bluetoothDeviceScript.DidUpdateCharacteristicValueAction [name] [characteristic] = action; +#elif UNITY_ANDROID + bluetoothDeviceScript.DidUpdateCharacteristicValueAction [name] [FullUUID (characteristic).ToLower ()] = action; +#endif + } + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEReadCharacteristic (name, service, characteristic); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEReadCharacteristic (name, service, characteristic); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidReadCharacteristic", name, service, characteristic); +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void WriteCharacteristic (string name, string service, string characteristic, byte[] data, int length, bool withResponse, Action action) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.DidWriteCharacteristicAction = action; + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEWriteCharacteristic(name, service, characteristic, data, length, withResponse); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEWriteCharacteristic (name, service, characteristic, data, length, withResponse); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidWriteCharacteristic", name, service, characteristic, data, length, withResponse); +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void SubscribeCharacteristic (string name, string service, string characteristic, Action notificationAction, Action action) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + { + name = name.ToUpper (); + service = service.ToUpper (); + characteristic = characteristic.ToUpper (); + +#if UNITY_IOS || UNITY_TVOS || (EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX)) + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction [name] = new Dictionary> (); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction [name] [characteristic] = notificationAction; + + if (!bluetoothDeviceScript.DidUpdateCharacteristicValueAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateCharacteristicValueAction [name] = new Dictionary> (); + bluetoothDeviceScript.DidUpdateCharacteristicValueAction [name] [characteristic] = action; +#elif UNITY_ANDROID + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction [name] = new Dictionary> (); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction [name] [FullUUID (characteristic).ToLower ()] = notificationAction; + + if (!bluetoothDeviceScript.DidUpdateCharacteristicValueAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateCharacteristicValueAction [name] = new Dictionary> (); + bluetoothDeviceScript.DidUpdateCharacteristicValueAction [name] [FullUUID (characteristic).ToLower ()] = action; +#endif + } + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLESubscribeCharacteristic (name, service, characteristic); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLESubscribeCharacteristic (name, service, characteristic); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidSubscribeCharacteristic", name, service, characteristic); +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void SubscribeCharacteristicWithDeviceAddress (string name, string service, string characteristic, Action notificationAction, Action action) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + { + name = name.ToUpper (); + service = service.ToUpper (); + characteristic = characteristic.ToUpper (); + +#if UNITY_IOS || UNITY_TVOS || (EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX)) + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[name] = new Dictionary>(); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[name][characteristic] = notificationAction; + + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction[name] = new Dictionary>(); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction[name][characteristic] = null; + + if (!bluetoothDeviceScript.DidUpdateCharacteristicValueWithDeviceAddressAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateCharacteristicValueWithDeviceAddressAction[name] = new Dictionary>(); + bluetoothDeviceScript.DidUpdateCharacteristicValueWithDeviceAddressAction[name][characteristic] = action; +#elif UNITY_ANDROID + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[name] = new Dictionary>(); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[name][FullUUID (characteristic).ToLower ()] = notificationAction; + + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction.ContainsKey(name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction[name] = new Dictionary>(); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction[name][FullUUID (characteristic).ToLower ()] = null; + + if (!bluetoothDeviceScript.DidUpdateCharacteristicValueWithDeviceAddressAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateCharacteristicValueWithDeviceAddressAction[name] = new Dictionary>(); + bluetoothDeviceScript.DidUpdateCharacteristicValueWithDeviceAddressAction[name][FullUUID (characteristic).ToLower ()] = action; +#endif + } + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLESubscribeCharacteristic (name, service, characteristic); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLESubscribeCharacteristic (name, service, characteristic); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidSubscribeCharacteristic", name, service, characteristic); +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void UnSubscribeCharacteristic (string name, string service, string characteristic, Action action) + { +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + if (!Application.isEditor) + { +#endif + if (bluetoothDeviceScript != null) + { + name = name.ToUpper (); + service = service.ToUpper (); + characteristic = characteristic.ToUpper (); + +#if UNITY_IOS || UNITY_TVOS || (EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX)) + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[name] = new Dictionary>(); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[name][characteristic] = null; + + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction[name] = new Dictionary> (); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction[name][characteristic] = action; +#elif UNITY_ANDROID + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[name] = new Dictionary>(); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicWithDeviceAddressAction[name][FullUUID (characteristic).ToLower ()] = null; + + if (!bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction.ContainsKey (name)) + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction[name] = new Dictionary> (); + bluetoothDeviceScript.DidUpdateNotificationStateForCharacteristicAction[name][FullUUID (characteristic).ToLower ()] = action; +#endif + } + +#if EXPERIMENTAL_MACOS_EDITOR && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + OSXBluetoothLEUnSubscribeCharacteristic (name, service, characteristic); +#elif UNITY_IOS || UNITY_TVOS + _iOSBluetoothLEUnSubscribeCharacteristic (name, service, characteristic); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidUnsubscribeCharacteristic", name, service, characteristic); +#endif +#if !UNITY_EDITOR_OSX || !EXPERIMENTAL_MACOS_EDITOR + } +#endif + } + + public static void PeripheralName (string newName) + { + if (!Application.isEditor) + { +#if UNITY_IOS + _iOSBluetoothLEPeripheralName (newName); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidPeripheralName", newName); +#endif + } + } + + public static void CreateService (string uuid, bool primary, Action action) + { + if (!Application.isEditor) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.ServiceAddedAction = action; + +#if UNITY_IOS + _iOSBluetoothLECreateService (uuid, primary); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidCreateService", uuid, primary); +#endif + } + } + + public static void RemoveService (string uuid) + { + if (!Application.isEditor) + { +#if UNITY_IOS + _iOSBluetoothLERemoveService (uuid); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidRemoveService", uuid); +#endif + } + } + + public static void RemoveServices () + { + if (!Application.isEditor) + { +#if UNITY_IOS + _iOSBluetoothLERemoveServices (); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidRemoveServices"); +#endif + } + } + + public static void CreateCharacteristic (string uuid, CBCharacteristicProperties properties, CBAttributePermissions permissions, byte[] data, int length, Action action) + { + if (!Application.isEditor) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.PeripheralReceivedWriteDataAction = action; + +#if UNITY_IOS + _iOSBluetoothLECreateCharacteristic (uuid, (int)properties, (int)permissions, data, length); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidCreateCharacteristic", uuid, (int)properties, (int)permissions, data, length); +#endif + } + } + + public static void RemoveCharacteristic (string uuid) + { + if (!Application.isEditor) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.PeripheralReceivedWriteDataAction = null; + +#if UNITY_IOS + _iOSBluetoothLERemoveCharacteristic (uuid); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidRemoveCharacteristic", uuid); +#endif + } + } + + public static void RemoveCharacteristics () + { + if (!Application.isEditor) + { +#if UNITY_IOS + _iOSBluetoothLERemoveCharacteristics (); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidRemoveCharacteristics"); +#endif + } + } + + public static void StartAdvertising (Action action) + { + if (!Application.isEditor) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.StartedAdvertisingAction = action; + +#if UNITY_IOS + _iOSBluetoothLEStartAdvertising (); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidStartAdvertising"); +#endif + } + } + + public static void StopAdvertising (Action action) + { + if (!Application.isEditor) + { + if (bluetoothDeviceScript != null) + bluetoothDeviceScript.StoppedAdvertisingAction = action; + +#if UNITY_IOS + _iOSBluetoothLEStopAdvertising (); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidStopAdvertising"); +#endif + } + } + + public static void UpdateCharacteristicValue (string uuid, byte[] data, int length) + { + if (!Application.isEditor) + { +#if UNITY_IOS + _iOSBluetoothLEUpdateCharacteristicValue (uuid, data, length); +#elif UNITY_ANDROID + if (_android != null) + _android.Call ("androidUpdateCharacteristicValue", uuid, data, length); +#endif + } + } + + public static string FullUUID (string uuid) + { + if (uuid.Length == 4) + return "0000" + uuid + "-0000-1000-8000-00805F9B34FB"; + return uuid; + } +} diff --git a/Assets/Plugins/BluetoothHardwareInterface.cs.meta b/Assets/Plugins/BluetoothHardwareInterface.cs.meta new file mode 100644 index 00000000..f3b5a0a6 --- /dev/null +++ b/Assets/Plugins/BluetoothHardwareInterface.cs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b8496a9b1a1df40af9ada2311d1d6d09 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: diff --git a/Assets/Plugins/BluetoothLEOSX.bundle.meta b/Assets/Plugins/BluetoothLEOSX.bundle.meta new file mode 100644 index 00000000..7401d676 --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: 5383f7d08256547f6b36ee834b840062 +folderAsset: yes +timeCreated: 1553735174 +licenseType: Pro +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + data: + first: + Standalone: OSXIntel + second: + enabled: 1 + settings: {} + data: + first: + Standalone: OSXIntel64 + second: + enabled: 1 + settings: {} + data: + first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents.meta b/Assets/Plugins/BluetoothLEOSX.bundle/Contents.meta new file mode 100644 index 00000000..93308905 --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle/Contents.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 34c7c97d38c834839be439bcf96a0267 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents/Info.plist b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/Info.plist new file mode 100644 index 00000000..f1fbd9b3 --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/Info.plist @@ -0,0 +1,48 @@ + + + + + BuildMachineOSBuild + 19F101 + CFBundleDevelopmentRegion + en + CFBundleExecutable + BluetoothLEOSX + CFBundleIdentifier + com.shatalmic.BluetoothLEOSX + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + BluetoothLEOSX + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 11E608c + DTPlatformVersion + GM + DTSDKBuild + 19E258 + DTSDKName + macosx10.15 + DTXcode + 1150 + DTXcodeBuild + 11E608c + LSMinimumSystemVersion + 10.11 + NSHumanReadableCopyright + Copyright © 2016 Shatalmic. All rights reserved. + + diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents/Info.plist.meta b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/Info.plist.meta new file mode 100644 index 00000000..50ad560e --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/Info.plist.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8e7811284d1734fc2ba4b29fccd3ccf7 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS.meta b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS.meta new file mode 100644 index 00000000..bea35c1b --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4e9d68945c9234df3b7855f4e1006f0f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS/BluetoothLEOSX b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS/BluetoothLEOSX new file mode 100644 index 0000000000000000000000000000000000000000..e15d6a5b80b9324905fb648da8d60fab39cc98fa GIT binary patch literal 122464 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 literal 0 HcmV?d00001 diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS/BluetoothLEOSX.meta b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS/BluetoothLEOSX.meta new file mode 100644 index 00000000..34e31722 --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/MacOS/BluetoothLEOSX.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1a83baeb98e0141f587d978b89ecd37d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature.meta b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature.meta new file mode 100644 index 00000000..719d57c0 --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 673293518de2c4fbf913749ebdb36dd1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature/CodeResources b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature/CodeResources new file mode 100644 index 00000000..d5d0fd74 --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature/CodeResources @@ -0,0 +1,115 @@ + + + + + files + + files2 + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature/CodeResources.meta b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature/CodeResources.meta new file mode 100644 index 00000000..44cf6eb6 --- /dev/null +++ b/Assets/Plugins/BluetoothLEOSX.bundle/Contents/_CodeSignature/CodeResources.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2b362acd4e35f4bb380341cd3df2b9c0 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS.meta b/Assets/Plugins/iOS.meta new file mode 100644 index 00000000..b120dcb2 --- /dev/null +++ b/Assets/Plugins/iOS.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 2 +guid: 8d3303514acc04853a1fbd8393630e00 +folderAsset: yes +DefaultImporter: + userData: diff --git a/Assets/Plugins/iOS/UnityBluetoothLE.h b/Assets/Plugins/iOS/UnityBluetoothLE.h new file mode 100644 index 00000000..ce91cfc0 --- /dev/null +++ b/Assets/Plugins/iOS/UnityBluetoothLE.h @@ -0,0 +1,107 @@ +// +// UnityBluetoothLE.h +// Unity-iPhone +// +// Created by Tony Pitman on 03/05/2014. +// +// + +#import +#import + +#if !TARGET_OS_TV +#import + +@interface UnityBluetoothLE : NSObject +#else +@interface UnityBluetoothLE : NSObject +#endif + +{ + CBCentralManager *_centralManager; +#if !TARGET_OS_TV + CLLocationManager *_locationManager; +#endif + NSMutableDictionary *_peripherals; + +#if !TARGET_OS_TV + CBPeripheralManager *_peripheralManager; + + NSString *_peripheralName; + + NSMutableDictionary *_services; + NSMutableDictionary *_characteristics; +#endif + + NSMutableArray *_backgroundMessages; + BOOL _isPaused; + BOOL _alreadyNotified; + BOOL _isInitializing; + BOOL _rssiOnly; + int _recordType; + + long _mtu; + + unsigned char *_writeCharacteristicBytes; + long _writeCharacteristicLength; + long _writeCharacteristicPosition; + long _writeCharacteristicBytesToWrite; + CBCharacteristicWriteType _writeCharacteristicWithResponse; + int _writeCharacteristicRetries; +} + +@property (atomic, strong) NSMutableDictionary *_peripherals; +@property (atomic) BOOL _rssiOnly; + +- (void)initialize:(BOOL)asCentral asPeripheral:(BOOL)asPeripheral; +- (void)deInitialize; +- (void)scanForPeripheralsWithServices:(NSArray *)serviceUUIDs options:(NSDictionary *)options clearPeripheralList:(BOOL)clearPeripheralList recordType:(int)recordType; +- (void)stopScan; +- (void)retrieveListOfPeripheralsWithServices:(NSArray *)serviceUUIDs; +- (void)connectToPeripheral:(NSString *)name; +- (void)disconnectPeripheral:(NSString *)name; +- (CBCharacteristic *)getCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString; +- (void)readCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString; +- (void)writeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString data:(NSData *)data withResponse:(BOOL)withResponse; +- (void)subscribeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString; +- (void)unsubscribeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString; +- (void)writeCharactersticBytesReset; +- (void)writeCharactersticBytes:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic data:(NSData *)data withResponse:(CBCharacteristicWriteType)withResponse; +- (void)writeNextPacket:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic; + +#if !TARGET_OS_TV +- (void)requestMtu:(NSString *)name mtu:(int)mtu; +- (void)scanForBeacons:(NSArray *)beaconRegions; +- (void)stopBeaconScan; + +- (void)peripheralName:(NSString *)newName; +- (void)createService:(NSString *)uuid primary:(BOOL)primary; +- (void)removeService:(NSString *)uuid; +- (void)removeServices; +- (void)createCharacteristic:(NSString *)uuid properties:(CBCharacteristicProperties)properties permissions:(CBAttributePermissions)permissions value:(NSData *)value; +- (void)removeCharacteristic:(NSString *)uuid; +- (void)removeCharacteristics; +- (void)startAdvertising; +- (void)stopAdvertising; +- (void)updateCharacteristicValue:(NSString *)uuid value:(NSData *)value; +#endif + +- (void)pauseMessages:(BOOL)isPaused; +- (void)sendUnityMessage:(BOOL)isString message:(NSString *)message; + ++ (NSString *) base64StringFromData:(NSData *)data length:(int)length; + +@end + +@interface UnityMessage : NSObject + +{ + BOOL _isString; + NSString *_message; +} + +- (void)initialize:(BOOL)isString message:(NSString *)message; +- (void)deInitialize; +- (void)sendUnityMessage; + +@end diff --git a/Assets/Plugins/iOS/UnityBluetoothLE.h.meta b/Assets/Plugins/iOS/UnityBluetoothLE.h.meta new file mode 100644 index 00000000..4bab6c82 --- /dev/null +++ b/Assets/Plugins/iOS/UnityBluetoothLE.h.meta @@ -0,0 +1,95 @@ +fileFormatVersion: 2 +guid: 89fad22a839074ac08f9f7ffc9dbce4e +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + '': Linux + second: + enabled: 0 + settings: + CPU: x86 + - first: + '': OSXIntel + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + '': OSXIntel64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + iPhone: iOS + second: + enabled: 1 + settings: + CompileFlags: + FrameworkDependencies: + - first: + tvOS: tvOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/UnityBluetoothLE.mm b/Assets/Plugins/iOS/UnityBluetoothLE.mm new file mode 100644 index 00000000..7691553a --- /dev/null +++ b/Assets/Plugins/iOS/UnityBluetoothLE.mm @@ -0,0 +1,1316 @@ +// +// UnityBluetoothLE.h +// Unity-iPhone +// +// Created by Tony Pitman on 03/05/2014. +// +// + +#import "UnityBluetoothLE.h" + +const char _messageDelimeter = '~'; + +extern "C" { + + UnityBluetoothLE *_unityBluetoothLE = nil; + + void _iOSBluetoothLELogString (NSString *message) { + + NSLog (message); + } + + void _iOSBluetoothLELog (char *message) { + + _iOSBluetoothLELogString ([NSString stringWithFormat:@"%s", message]); + } + + void _iOSBluetoothLEInitialize (BOOL asCentral, BOOL asPeripheral) { + + _unityBluetoothLE = [UnityBluetoothLE new]; + [_unityBluetoothLE initialize:asCentral asPeripheral:asPeripheral]; + } + + void _iOSBluetoothLEDeInitialize () { + + if (_unityBluetoothLE != nil) { + + [_unityBluetoothLE deInitialize]; + [_unityBluetoothLE release]; + _unityBluetoothLE = nil; + + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", "DeInitialized"); + } + } + + void _iOSBluetoothLEPauseMessages (BOOL pause) { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE pauseMessages:pause]; + } + + void _iOSBluetoothLEScanForPeripheralsWithServices (char *serviceUUIDsStringRaw, bool allowDuplicates, bool rssiOnly, bool clearPeripheralList, int recordType) { + + if (_unityBluetoothLE != nil) + { + _unityBluetoothLE._rssiOnly = rssiOnly; + + NSMutableArray *actualUUIDs = nil; + + if (serviceUUIDsStringRaw != nil) + { + NSString *serviceUUIDsString = [NSString stringWithFormat:@"%s", serviceUUIDsStringRaw]; + NSArray *serviceUUIDs = [serviceUUIDsString componentsSeparatedByString:@"|"]; + + if (serviceUUIDs.count > 0) + { + actualUUIDs = [[NSMutableArray alloc] init]; + + for (NSString* sUUID in serviceUUIDs) + [actualUUIDs addObject:[CBUUID UUIDWithString:sUUID]]; + } + } + + NSDictionary *options = nil; + if (allowDuplicates) + options = @{ CBCentralManagerScanOptionAllowDuplicatesKey: @YES }; + + [_unityBluetoothLE scanForPeripheralsWithServices:actualUUIDs options:options clearPeripheralList:clearPeripheralList recordType:recordType]; + } + } + + void _iOSBluetoothLEStopScan () { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE stopScan]; + } + + void _iOSBluetoothLERetrieveListOfPeripheralsWithServices (char *serviceUUIDsStringRaw) { + + if (_unityBluetoothLE != nil) + { + NSMutableArray *actualUUIDs = nil; + + if (serviceUUIDsStringRaw != nil) + { + NSString *serviceUUIDsString = [NSString stringWithFormat:@"%s", serviceUUIDsStringRaw]; + NSArray *serviceUUIDs = [serviceUUIDsString componentsSeparatedByString:@"|"]; + + if (serviceUUIDs.count > 0) + { + actualUUIDs = [[NSMutableArray alloc] init]; + + for (NSString* sUUID in serviceUUIDs) + [actualUUIDs addObject:[CBUUID UUIDWithString:sUUID]]; + } + } + + [_unityBluetoothLE retrieveListOfPeripheralsWithServices:actualUUIDs]; + } + } + + void _iOSBluetoothLEConnectToPeripheral (char *name) { + + if (_unityBluetoothLE && name != nil) + [_unityBluetoothLE connectToPeripheral:[NSString stringWithFormat:@"%s", name]]; + } + + void _iOSBluetoothLEDisconnectPeripheral (char *name) { + + if (_unityBluetoothLE && name != nil) + [_unityBluetoothLE disconnectPeripheral:[NSString stringWithFormat:@"%s", name]]; + } + + void _iOSBluetoothLEReadCharacteristic (char *name, char *service, char *characteristic) { + + if (_unityBluetoothLE && name != nil && service != nil && characteristic != nil) + [_unityBluetoothLE readCharacteristic:[NSString stringWithFormat:@"%s", name] service:[NSString stringWithFormat:@"%s", service] characteristic:[NSString stringWithFormat:@"%s", characteristic]]; + } + + void _iOSBluetoothLEWriteCharacteristic (char *name, char *service, char *characteristic, unsigned char *data, int length, BOOL withResponse) { + + if (_unityBluetoothLE && name != nil && service != nil && characteristic != nil && data != nil && length > 0) + [_unityBluetoothLE writeCharacteristic:[NSString stringWithFormat:@"%s", name] service:[NSString stringWithFormat:@"%s", service] characteristic:[NSString stringWithFormat:@"%s", characteristic] data:[NSData dataWithBytes:data length:length] withResponse:withResponse]; + } + + void _iOSBluetoothLESubscribeCharacteristic (char *name, char *service, char *characteristic) { + + if (_unityBluetoothLE && name != nil && service != nil && characteristic != nil) + [_unityBluetoothLE subscribeCharacteristic:[NSString stringWithFormat:@"%s", name] service:[NSString stringWithFormat:@"%s", service] characteristic:[NSString stringWithFormat:@"%s", characteristic]]; + } + + void _iOSBluetoothLEUnSubscribeCharacteristic (char *name, char *service, char *characteristic) { + + if (_unityBluetoothLE && name != nil && service != nil && characteristic != nil) + [_unityBluetoothLE unsubscribeCharacteristic:[NSString stringWithFormat:@"%s", name] service:[NSString stringWithFormat:@"%s", service] characteristic:[NSString stringWithFormat:@"%s", characteristic]]; + } + + void _iOSBluetoothLEDisconnectAll () { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE disconnectAll]; + } + +#if !TARGET_OS_TV + void _iOSBluetoothLERequestMtu (char *name, int mtu) { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE requestMtu:[NSString stringWithFormat:@"%s", name] mtu:mtu]; + } + + void _iOSBluetoothLEScanForBeacons (char *proximityUUIDsStringRaw) { + + if (_unityBluetoothLE != nil) + { + NSMutableArray *actualUUIDs = nil; + + if (proximityUUIDsStringRaw != nil) + { + NSString *proximityUUIDsString = [NSString stringWithFormat:@"%s", proximityUUIDsStringRaw]; + NSArray *proximityUUIDs = [proximityUUIDsString componentsSeparatedByString:@"|"]; + + if (proximityUUIDs.count > 0) + { + NSMutableArray *beaconRegions = [[NSMutableArray alloc] init]; + + for (NSString* sUUID in proximityUUIDs) + { + NSArray *parts = [sUUID componentsSeparatedByString:@":"]; + if (parts.count == 2) + { + CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:parts[0]] identifier:parts[1]]; + [beaconRegions addObject:beaconRegion]; + + [_unityBluetoothLE scanForBeacons:beaconRegions]; + } + else + { + NSString *message = [NSString stringWithFormat:@"Error~iBeacon Scanning missing identifiers"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + } + } + else + { + NSString *message = [NSString stringWithFormat:@"Error~iBeacon Scanning requires proximity UUIDs"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + } + else + { + NSString *message = [NSString stringWithFormat:@"Error~iBeacon Scanning requires proximity UUIDs"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + } + } + + void _iOSBluetoothLEStopBeaconScan () { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE stopBeaconScan]; + } + + void _iOSBluetoothLEPeripheralName (char *newName) { + + if (_unityBluetoothLE != nil && newName != nil) + [_unityBluetoothLE peripheralName:[[NSString alloc] initWithUTF8String:newName]]; + } + + void _iOSBluetoothLECreateService (char *uuid, BOOL primary) { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE createService:[NSString stringWithFormat:@"%s", uuid] primary:primary]; + } + + void _iOSBluetoothLERemoveService (char *uuid) { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE removeService:[NSString stringWithFormat:@"%s", uuid]]; + } + + void _iOSBluetoothLERemoveServices () { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE removeServices]; + } + + void _iOSBluetoothLECreateCharacteristic (char *uuid, int properties, int permissions, unsigned char *data, int length) { + + if (_unityBluetoothLE != nil) { + + NSData *value = nil; + if (data != nil) + value = [[NSData alloc] initWithBytes:data length:length]; + + [_unityBluetoothLE createCharacteristic:[NSString stringWithFormat:@"%s", uuid] properties:properties permissions:permissions value:value]; + } + } + + void _iOSBluetoothLERemoveCharacteristic (char *uuid) { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE removeCharacteristic:[NSString stringWithFormat:@"%s", uuid]]; + } + + void _iOSBluetoothLERemoveCharacteristics () { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE removeCharacteristics]; + } + + void _iOSBluetoothLEStartAdvertising () { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE startAdvertising]; + } + + void _iOSBluetoothLEStopAdvertising () { + + if (_unityBluetoothLE != nil) + [_unityBluetoothLE stopAdvertising]; + } + + void _iOSBluetoothLEUpdateCharacteristicValue (char *uuid, unsigned char *data, int length) { + + if (_unityBluetoothLE != nil) { + + NSData *value = nil; + if (data != nil) + value = [[NSData alloc] initWithBytes:data length:length]; + + [_unityBluetoothLE updateCharacteristicValue:[NSString stringWithFormat:@"%s", uuid] value:value]; + } + } +#endif +} + +@implementation UnityBluetoothLE + +@synthesize _peripherals; +@synthesize _rssiOnly; + +- (void)initialize:(BOOL)asCentral asPeripheral:(BOOL)asPeripheral +{ + _mtu = 20; + + _isPaused = FALSE; + _isInitializing = TRUE; + + _centralManager = nil; +#if !TARGET_OS_TV + _peripheralManager = nil; + _services = nil; + _characteristics = nil; +#endif + if (asCentral) + _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; + +#if !TARGET_OS_TV + if (asPeripheral) + _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil]; + + _services = [[NSMutableDictionary alloc] init]; + _characteristics = [[NSMutableDictionary alloc] init]; +#endif + + _peripherals = [[NSMutableDictionary alloc] init]; +} + +- (void)deInitialize +{ + if (_backgroundMessages != nil) + { + for (UnityMessage *message in _backgroundMessages) + { + if (message != nil) + { + [message deInitialize]; + [message release]; + } + } + + [_backgroundMessages release]; + _backgroundMessages = nil; + } + +#if !TARGET_OS_TV + if (_peripheralManager != nil) + [self stopAdvertising]; + + [self removeCharacteristics]; + [self removeServices]; +#endif + + if (_centralManager != nil) + [self stopScan]; + + [_peripherals removeAllObjects]; +} + +- (void)pauseMessages:(BOOL)isPaused +{ + if (isPaused != _isPaused) { + + if (_backgroundMessages == nil) + _backgroundMessages = [[NSMutableArray alloc] init]; + + _isPaused = isPaused; + + // if we are not paused now since we know we changed state + // that means we were paused so we need to pump the saved + // messages to Unity + if (isPaused) { + + if (_backgroundMessages != nil) { + + for (UnityMessage *message in _backgroundMessages) { + + if (message != nil) { + + [message sendUnityMessage]; + [message deInitialize]; + [message release]; + } + } + + [_backgroundMessages removeAllObjects]; + } + } + } +} + +#if !TARGET_OS_TV +- (void)createService:(NSString *)uuid primary:(BOOL)primary +{ + CBUUID *cbuuid = [CBUUID UUIDWithString:uuid]; + CBMutableService *service = [[CBMutableService alloc] initWithType:cbuuid primary:primary]; + + NSMutableArray *characteristics = [[NSMutableArray alloc] init]; + + NSEnumerator *enumerator = [_characteristics keyEnumerator]; + id key; + while ((key = [enumerator nextObject])) + [characteristics addObject:[_characteristics objectForKey:key]]; + + service.characteristics = characteristics; + + [_services setObject:service forKey:cbuuid]; + + if (_peripheralManager != nil) + { + [_peripheralManager addService:service]; + } +} + +- (void)removeService:(NSString *)uuid +{ + if (_services != nil) + { + if (_peripheralManager != nil) + { + CBMutableService *service = [_services objectForKey:uuid]; + if (service != nil) + [_peripheralManager removeService:service]; + } + + [_services removeObjectForKey:uuid]; + } +} + +- (void)removeServices +{ + if (_services != nil) + { + [_services removeAllObjects]; + + if (_peripheralManager != nil) + [_peripheralManager removeAllServices]; + } +} + +- (void)peripheralName:(NSString *)newName +{ + _peripheralName = newName; +} + +- (void)createCharacteristic:(NSString *)uuid properties:(CBCharacteristicProperties)properties permissions:(CBAttributePermissions)permissions value:(NSData *)value +{ + CBUUID *cbuuid = [CBUUID UUIDWithString:uuid]; + CBCharacteristic *characteristic = [[CBMutableCharacteristic alloc] initWithType:cbuuid properties:properties value:value permissions:permissions]; + + [_characteristics setObject:characteristic forKey:cbuuid]; +} + +- (void)removeCharacteristic:(NSString *)uuid +{ + if (_characteristics != nil) + [_characteristics removeObjectForKey:uuid]; +} + +- (void)removeCharacteristics +{ + if (_characteristics != nil) + [_characteristics removeAllObjects]; +} + +- (void)startAdvertising +{ + if (_peripheralManager != nil && _services != nil) + { + NSMutableArray *services = [[NSMutableArray alloc] init]; + + NSEnumerator *enumerator = [_services keyEnumerator]; + id key; + while ((key = [enumerator nextObject])) + { + CBMutableService *service = [_services objectForKey:key]; + [services addObject:service.UUID]; + } + + if (_peripheralName == nil) + _peripheralName = @""; + + [_peripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey : services, CBAdvertisementDataLocalNameKey : _peripheralName }]; + } +} + +- (void)stopAdvertising +{ + if (_peripheralManager != nil) + { + [_peripheralManager stopAdvertising]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", "StoppedAdvertising"); + } +} + +- (void)updateCharacteristicValue:(NSString *)uuid value:(NSData *)value +{ + if (_characteristics != nil) + { + CBUUID *cbuuid = [CBUUID UUIDWithString:uuid]; + CBMutableCharacteristic *characteristic = [_characteristics objectForKey:cbuuid]; + if (characteristic != nil) + { + characteristic.value = value; + if (_peripheralManager != nil) + [_peripheralManager updateValue:value forCharacteristic:characteristic onSubscribedCentrals:nil]; + } + } +} + +- (void)requestMtu:(NSString *)name mtu:(int)mtu +{ + _mtu = mtu; + + NSString *message = [NSString stringWithFormat:@"MtuChanged~%@~%d", name, _mtu]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); +} + +- (void)scanForBeacons:(NSArray*)beaconRegions +{ + if ([CLLocationManager isRangingAvailable]) + { + _locationManager = [CLLocationManager new]; + _locationManager.delegate = self; + + [_locationManager requestWhenInUseAuthorization]; + + for (CLBeaconRegion *region in beaconRegions) + [_locationManager startRangingBeaconsInRegion:region]; + } + else + { + NSString *message = [NSString stringWithFormat:@"Error~iBeacon Ranging is not available"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } +} + +- (void) stopBeaconScan +{ + if (_locationManager != nil) + { + for (CLBeaconRegion *region in _locationManager.rangedRegions) + [_locationManager stopRangingBeaconsInRegion:region]; + + [_locationManager release]; + _locationManager = nil; + } +} + +// beacon delegate implementation +- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region +{ + NSString *message; + for (CLBeacon *beacon in beacons) + { + message = [NSString stringWithFormat:@"DiscoveredBeacon~%@~%ld~%ld~%ld~%ld~%ld", [beacon.proximityUUID.UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""], beacon.major.longValue, beacon.minor.longValue, (long)beacon.rssi, (long)0, (long)beacon.proximity]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + } +} +#endif + +// central delegate implementation +- (void)scanForPeripheralsWithServices:(NSArray *)serviceUUIDs options:(NSDictionary *)options clearPeripheralList:(BOOL)clearPeripheralList recordType:(int)recordType +{ + if (_centralManager != nil) + { + recordType = recordType; + if (clearPeripheralList && _peripherals != nil) + [_peripherals removeAllObjects]; + + [_centralManager scanForPeripheralsWithServices:serviceUUIDs options:options]; + } +} + +- (void) stopScan +{ + if (_centralManager != nil) + [_centralManager stopScan]; +} + +- (void)retrieveListOfPeripheralsWithServices:(NSArray *)serviceUUIDs +{ + if (_centralManager != nil) + { + if (_peripherals != nil) + [_peripherals removeAllObjects]; + + NSArray * list = [_centralManager retrieveConnectedPeripheralsWithServices:serviceUUIDs]; + if (list != nil) + { + for (int i = 0; i < list.count; ++i) + { + CBPeripheral *peripheral = [list objectAtIndex:i]; + if (peripheral != nil) + { + NSString *identifier = [[peripheral identifier] UUIDString]; + NSString *name = [peripheral name]; + + NSString *message = [NSString stringWithFormat:@"RetrievedConnectedPeripheral~%@~%@", identifier, name]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + + [_peripherals setObject:peripheral forKey:identifier]; + } + } + } + } +} + +- (void)connectToPeripheral:(NSString *)name +{ + if (_peripherals != nil && name != nil) + { + CBPeripheral *peripheral = [_peripherals objectForKey:name]; + if (peripheral != nil) + [_centralManager connectPeripheral:peripheral options:nil]; + } +} + +- (void)disconnectAll +{ + if (_peripherals != nil && [_peripherals count] > 0) + { + NSArray* keys = [_peripherals allKeys]; + for(NSString* key in keys) + { + CBPeripheral *peripheral = [_peripherals objectForKey:key]; + if (peripheral != nil) + [_centralManager cancelPeripheralConnection:peripheral]; + } + } +} + +- (void)disconnectPeripheral:(NSString *)name +{ + if (_peripherals != nil && name != nil) + { + CBPeripheral *peripheral = [_peripherals objectForKey:name]; + if (peripheral != nil) + { + [_centralManager cancelPeripheralConnection:peripheral]; + } + } +} + +- (CBCharacteristic *)getCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString +{ + CBCharacteristic *returnCharacteristic = nil; + + if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil) + { + CBPeripheral *peripheral = [_peripherals objectForKey:name]; + if (peripheral != nil) + { + CBUUID *serviceUUID = [CBUUID UUIDWithString:serviceString]; + CBUUID *characteristicUUID = [CBUUID UUIDWithString:characteristicString]; + + for (CBService *service in peripheral.services) + { + if ([service.UUID isEqual:serviceUUID]) + { + for (CBCharacteristic *characteristic in service.characteristics) + { + if ([characteristic.UUID isEqual:characteristicUUID]) + { + returnCharacteristic = characteristic; + } + } + } + } + } + } + + return returnCharacteristic; +} + +- (void)readCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString +{ + if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil) + { + CBPeripheral *peripheral = [_peripherals objectForKey:name]; + if (peripheral != nil) + { + CBCharacteristic *characteristic = [_unityBluetoothLE getCharacteristic:name service:serviceString characteristic:characteristicString]; + if (characteristic != nil) + [peripheral readValueForCharacteristic:characteristic]; + } + } +} + +- (void)writeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString data:(NSData *)data withResponse:(BOOL)withResponse +{ + if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil && data != nil) + { + CBPeripheral *peripheral = [_peripherals objectForKey:name]; + if (peripheral != nil) + { + CBCharacteristic *characteristic = [_unityBluetoothLE getCharacteristic:name service:serviceString characteristic:characteristicString]; + if (characteristic != nil) + { + CBCharacteristicWriteType type = CBCharacteristicWriteWithoutResponse; + if (withResponse) + type = CBCharacteristicWriteWithResponse; + + [self writeCharactersticBytes:peripheral characteristic:characteristic data:data withResponse:type]; + } + } + } +} + +- (void)subscribeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString +{ + if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil) + { + CBPeripheral *peripheral = [_peripherals objectForKey:name]; + if (peripheral != nil) + { + CBCharacteristic *characteristic = [_unityBluetoothLE getCharacteristic:name service:serviceString characteristic:characteristicString]; + if (characteristic != nil) + [peripheral setNotifyValue:YES forCharacteristic:characteristic]; + } + } +} + +- (void)unsubscribeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString +{ + if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil) + { + CBPeripheral *peripheral = [_peripherals objectForKey:name]; + if (peripheral != nil) + { + CBCharacteristic *characteristic = [_unityBluetoothLE getCharacteristic:name service:serviceString characteristic:characteristicString]; + if (characteristic != nil) + [peripheral setNotifyValue:NO forCharacteristic:characteristic]; + } + } +} + +- (void)centralManagerDidUpdateState:(CBCentralManager *)central +{ + switch (central.state) + { + case CBCentralManagerStateUnsupported: + { + NSLog(@"Central State: Unsupported"); + + NSString *message = [NSString stringWithFormat:@"Error~Bluetooth LE Not Supported"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } break; + + case CBCentralManagerStateUnauthorized: + { + NSLog(@"Central State: Unauthorized"); + + NSString *message = [NSString stringWithFormat:@"Error~Bluetooth LE Not Authorized"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } break; + + case CBCentralManagerStatePoweredOff: + { + NSLog(@"Central State: Powered Off"); + + NSString *message = [NSString stringWithFormat:@"Error~Bluetooth LE Powered Off"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } break; + + case CBCentralManagerStatePoweredOn: + { + NSLog(@"Central State: Powered On"); + if (_isInitializing) + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", "Initialized"); + _isInitializing = FALSE; + } break; + + case CBCentralManagerStateUnknown: + { + NSLog(@"Central State: Unknown"); + + NSString *message = [NSString stringWithFormat:@"Error~Bluetooth LE Unknown"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } break; + + default: + { + } + + } +} + +- (void)centralManager:(CBCentralManager *)central didRetrievePeripherals:(NSArray *)peripherals +{ + +} + +- (void)centralManager:(CBCentralManager *)central didRetrieveConnectedPeripherals:(NSArray *)peripherals +{ + +} + +- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error +{ + if (error) + { + NSString *message = [NSString stringWithFormat:@"Error~%@", error.description]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } +} + +- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI +{ + if (_peripherals != nil && peripheral != nil) + { + NSString *name = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey]; + if (name == nil) + name = peripheral.name; + if (name == nil) + name = @"No Name"; + + if (name != nil) + { + NSString *identifier = nil; + + NSString *foundPeripheral = [self findPeripheralName:peripheral]; + if (foundPeripheral == nil) + identifier = [[peripheral identifier] UUIDString]; + else + identifier = foundPeripheral; + + NSString *message = nil; + + if (advertisementData != nil && [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey] != nil) + { + NSData* bytes = [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey]; + message = [NSString stringWithFormat:@"DiscoveredPeripheral~%@~%@~%@~%@", identifier, name, RSSI, [UnityBluetoothLE base64StringFromData:bytes length:bytes.length]]; + } + else if (RSSI != 0 && _rssiOnly) + { + message = [NSString stringWithFormat:@"DiscoveredPeripheral~%@~%@~%@~", identifier, name, RSSI]; + } + else + { + message = [NSString stringWithFormat:@"DiscoveredPeripheral~%@~%@", identifier, name]; + } + + if (message != nil) + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + + [_peripherals setObject:peripheral forKey:identifier]; + } + } +} + +- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error +{ + if (_peripherals != nil) + { + NSString *foundPeripheral = [self findPeripheralName:peripheral]; + if (foundPeripheral != nil) + { + NSString *message = [NSString stringWithFormat:@"DisconnectedPeripheral~%@", foundPeripheral]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + } + } +} + +- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral +{ + NSString *foundPeripheral = [self findPeripheralName:peripheral]; + if (foundPeripheral != nil) + { + NSString *message = [NSString stringWithFormat:@"ConnectedPeripheral~%@", foundPeripheral]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + peripheral.delegate = self; + [peripheral discoverServices:nil]; + } +} + +- (CBPeripheral *) findPeripheralInList:(CBPeripheral*)peripheral +{ + CBPeripheral *foundPeripheral = nil; + + NSEnumerator *enumerator = [_peripherals keyEnumerator]; + id key; + while ((key = [enumerator nextObject])) + { + CBPeripheral *tempPeripheral = [_peripherals objectForKey:key]; + if ([tempPeripheral isEqual:peripheral]) + { + foundPeripheral = tempPeripheral; + break; + } + } + + return foundPeripheral; +} + +- (NSString *) findPeripheralName:(CBPeripheral*)peripheral +{ + NSString *foundPeripheral = nil; + + NSEnumerator *enumerator = [_peripherals keyEnumerator]; + id key; + while ((key = [enumerator nextObject])) + { + CBPeripheral *tempPeripheral = [_peripherals objectForKey:key]; + if ([tempPeripheral isEqual:peripheral]) + { + foundPeripheral = key; + break; + } + } + + return foundPeripheral; +} + +// central peripheral delegate implementation +- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error +{ + if (error) + { + NSString *message = [NSString stringWithFormat:@"Error~%@", error.description]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + else + { + NSString *foundPeripheral = [self findPeripheralName:peripheral]; + if (foundPeripheral != nil) + { + for (CBService *service in peripheral.services) + { + NSString *message = [NSString stringWithFormat:@"DiscoveredService~%@~%@", foundPeripheral, [service UUID]]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + + [peripheral discoverCharacteristics:nil forService:service]; + } + } + } +} + +- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error +{ + if (error) + { + NSString *message = [NSString stringWithFormat:@"Error~%@", error.description]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + else + { + NSString *foundPeripheral = [self findPeripheralName:peripheral]; + if (foundPeripheral != nil) + { + for (CBCharacteristic *characteristic in service.characteristics) + { + NSString *message = [NSString stringWithFormat:@"DiscoveredCharacteristic~%@~%@~%@", foundPeripheral, [service UUID], [characteristic UUID]]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + } + } + } +} + +- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error +{ + if (error) + { + NSString *message = [NSString stringWithFormat:@"Error~%@", error.description]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + else + { + NSString *foundPeripheral = [self findPeripheralName:peripheral]; + if (foundPeripheral != nil) + { + if (characteristic.value != nil) + { + NSString *message = [NSString stringWithFormat:@"DidUpdateValueForCharacteristic~%@~%@~%@", foundPeripheral, [characteristic UUID], [UnityBluetoothLE base64StringFromData:characteristic.value length:characteristic.value.length]]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + //NSString *message = [UnityBluetoothLE base64StringFromData:characteristic.value length:characteristic.value.length]; + //UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothData", [message UTF8String] ); + } + } + } +} + +- (void)writeCharactersticBytesReset +{ + _writeCharacteristicBytes = nil; + _writeCharacteristicLength = 0; + _writeCharacteristicPosition = 0; + _writeCharacteristicBytesToWrite = 0; + _writeCharacteristicWithResponse = CBCharacteristicWriteWithResponse; + _writeCharacteristicRetries = 3; +} + +- (void)writeCharactersticBytes:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic data:(NSData *)data withResponse:(CBCharacteristicWriteType)withResponse +{ + if (_writeCharacteristicBytes == nil && (_mtu == 0 || data.length > _mtu)) + { + _writeCharacteristicLength = data.length; + _writeCharacteristicBytes = (unsigned char*)malloc(_writeCharacteristicLength); + memcpy(_writeCharacteristicBytes, [data bytes], _writeCharacteristicLength); + _writeCharacteristicPosition = 0; + _writeCharacteristicWithResponse = withResponse; + if (_mtu == 0) + _writeCharacteristicBytesToWrite = _writeCharacteristicLength; + else + _writeCharacteristicBytesToWrite = _mtu; + } + + NSLog(@"write characteristic block"); + + if (_writeCharacteristicBytes != nil) + { + NSMutableData *newData = [NSMutableData dataWithCapacity:_writeCharacteristicBytesToWrite]; + [newData appendBytes:&_writeCharacteristicBytes[_writeCharacteristicPosition] length:_writeCharacteristicBytesToWrite]; + data = newData; + NSLog(@"data: %@", data); + } + + NSLog(@"writing %ld bytes, %ld with response", data.length, withResponse); + [peripheral writeValue:data forCharacteristic:characteristic type:withResponse]; + + if (withResponse == CBCharacteristicWriteWithoutResponse) + { + double delayInSeconds = 0.01; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ + [self writeNextPacket:peripheral characteristic:characteristic]; + }); + } +} + +- (void)writeNextPacket:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic +{ + if (_writeCharacteristicLength > _writeCharacteristicPosition + _writeCharacteristicBytesToWrite) + { + _writeCharacteristicPosition += _writeCharacteristicBytesToWrite; + if (_writeCharacteristicPosition + _writeCharacteristicBytesToWrite > _writeCharacteristicLength) + _writeCharacteristicBytesToWrite = _writeCharacteristicLength - _writeCharacteristicPosition; + + NSMutableData *data = [NSMutableData dataWithCapacity:_writeCharacteristicLength]; + [data appendBytes:_writeCharacteristicBytes length:_writeCharacteristicLength]; + [self writeCharactersticBytes:peripheral characteristic:characteristic data:data withResponse:_writeCharacteristicWithResponse]; + } + else + { + [self writeCharactersticBytesReset]; + NSString *message = [NSString stringWithFormat:@"DidWriteCharacteristic~%@", characteristic.UUID]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } +} + +- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error +{ + if (error) + { + NSString *message = [NSString stringWithFormat:@"Error~%@", error.description]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + else + { + NSLog(@"%ld bytes written, %ld position, %ld length, %ld with response", _writeCharacteristicBytesToWrite, _writeCharacteristicPosition, _writeCharacteristicLength, _writeCharacteristicWithResponse); + + if (_writeCharacteristicBytesToWrite > 0) + { + [self writeNextPacket:peripheral characteristic:characteristic]; + } + else + { + NSString *message = [NSString stringWithFormat:@"DidWriteCharacteristic~%@", characteristic.UUID]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + } +} + +- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error +{ + if (error) + { + NSString *message = [NSString stringWithFormat:@"Error~%@", error.description]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + else + { + NSString *foundPeripheral = [self findPeripheralName:peripheral]; + if (foundPeripheral != nil) + { + NSString *message = [NSString stringWithFormat:@"DidUpdateNotificationStateForCharacteristic~%@~%@", foundPeripheral, characteristic.UUID]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + } +} + +#if !TARGET_OS_TV +// peripheral manager delegate implementation +- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral +{ + _iOSBluetoothLELogString ([NSString stringWithFormat:@"Peripheral State Update: %d", (int)peripheral.state]); + if (_isInitializing && peripheral.state == CBPeripheralManagerStatePoweredOn) + { + _isInitializing = FALSE; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", "Initialized"); + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error +{ + if (error) + { + NSString *message = [NSString stringWithFormat:@"Error~%@", error.description]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + else + { + NSString *message = [NSString stringWithFormat:@"ServiceAdded~%@", service.UUID]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } +} + +- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error +{ + if (error) + { + NSString *message = [NSString stringWithFormat:@"Error~%@", error.description]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + else + { + NSString *message = [NSString stringWithFormat:@"StartedAdvertising"]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic +{ + +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic +{ + +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request +{ + BOOL success = FALSE; + + if (_peripheralManager != nil) + { + CBMutableCharacteristic *characteristic = [_characteristics objectForKey:request.characteristic.UUID]; + + if (characteristic != nil) + { + request.value = [characteristic.value subdataWithRange:NSMakeRange(request.offset, characteristic.value.length - request.offset)]; + [_peripheralManager respondToRequest:request withResult:CBATTErrorSuccess]; + + success = TRUE; + } + } + + if (!success) + [_peripheralManager respondToRequest:request withResult:CBATTErrorAttributeNotFound]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests +{ + BOOL success = FALSE; + + if (_peripheralManager != nil) + { + for (int i = 0; i < requests.count; ++i) + { + CBATTRequest *request = [requests objectAtIndex:i]; + if (request != nil) + { + CBMutableCharacteristic *characteristic = [_characteristics objectForKey:request.characteristic.UUID]; + + if (characteristic != nil) + { + characteristic.value = request.value; + + NSString *message = [NSString stringWithFormat:@"PeripheralReceivedWriteData~%@~%@", [characteristic UUID], [UnityBluetoothLE base64StringFromData:characteristic.value length:characteristic.value.length]]; + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]); + + success = TRUE; + } + else + { + success = FALSE; + break; + } + } + else + { + success = FALSE; + break; + } + } + } + + if (success) + [_peripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess]; + else + [_peripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorAttributeNotFound]; +} +#endif + +- (void)sendUnityMessage:(BOOL)isString message:(NSString *)message +{ + if (_isPaused) { + + if (_backgroundMessages != nil) { + + UnityMessage *unitymessage = [[UnityMessage alloc] init]; + if (unitymessage != nil) { + + [unitymessage initialize:isString message:message]; + [_backgroundMessages addObject:unitymessage]; + } + } + } + else { + + if (isString) + { + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] ); + } + else + { + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothData", [message UTF8String] ); + } + } +} + +static char base64EncodingTable[64] = +{ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' +}; + ++ (NSString *) base64StringFromData: (NSData *)data length: (int)length +{ + unsigned long ixtext, lentext; + long ctremaining; + unsigned char input[3], output[4]; + short i, charsonline = 0, ctcopy; + const unsigned char *raw; + NSMutableString *result; + + lentext = [data length]; + if (lentext < 1) + return @""; + result = [NSMutableString stringWithCapacity: lentext]; + raw = (const unsigned char *)[data bytes]; + ixtext = 0; + + while (true) { + ctremaining = lentext - ixtext; + if (ctremaining <= 0) + break; + for (i = 0; i < 3; i++) { + unsigned long ix = ixtext + i; + if (ix < lentext) + input[i] = raw[ix]; + else + input[i] = 0; + } + output[0] = (input[0] & 0xFC) >> 2; + output[1] = ((input[0] & 0x03) << 4) | ((input[1] & 0xF0) >> 4); + output[2] = ((input[1] & 0x0F) << 2) | ((input[2] & 0xC0) >> 6); + output[3] = input[2] & 0x3F; + ctcopy = 4; + switch (ctremaining) { + case 1: + ctcopy = 2; + break; + case 2: + ctcopy = 3; + break; + } + + for (i = 0; i < ctcopy; i++) + [result appendString: [NSString stringWithFormat: @"%c", base64EncodingTable[output[i]]]]; + + for (i = ctcopy; i < 4; i++) + [result appendString: @"="]; + + ixtext += 3; + charsonline += 4; + + if ((length > 0) && (charsonline >= length)) + charsonline = 0; + } + return result; +} + +#pragma mark Internal + +@end + +@implementation UnityMessage + +- (void)initialize:(BOOL)isString message:(NSString *)message +{ + _isString = isString; + _message = [message copy]; +} + +- (void)deInitialize +{ + if (_message != nil) + [_message release]; + _message = nil; +} + +- (void)sendUnityMessage +{ + if (_message != nil) { + + if (_isString) + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [_message UTF8String] ); + else + UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothData", [_message UTF8String] ); + } +} + +@end diff --git a/Assets/Plugins/iOS/UnityBluetoothLE.mm.meta b/Assets/Plugins/iOS/UnityBluetoothLE.mm.meta new file mode 100644 index 00000000..12a7a4c4 --- /dev/null +++ b/Assets/Plugins/iOS/UnityBluetoothLE.mm.meta @@ -0,0 +1,124 @@ +fileFormatVersion: 2 +guid: 6a40ca1fe2b7a48e5b257178dfdca41e +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + '': Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + '': Linux + second: + enabled: 0 + settings: + CPU: x86 + - first: + '': OSXIntel + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + '': OSXIntel64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Linux + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CompileFlags: -fno-objc-arc + FrameworkDependencies: CoreBluetooth; + - first: + tvOS: tvOS + second: + enabled: 1 + settings: + CompileFlags: -fno-objc-arc + FrameworkDependencies: CoreBluetooth; + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Devices/Ble/BleResponse.cs b/Assets/Scripts/Devices/Ble/BleResponse.cs index 0c15559d..707278a9 100644 --- a/Assets/Scripts/Devices/Ble/BleResponse.cs +++ b/Assets/Scripts/Devices/Ble/BleResponse.cs @@ -37,5 +37,10 @@ namespace Assets.Scripts.Devices.Ble { return base.ToString() + "\n" + this.Data; } - } + + public static implicit operator BleResponse(BleResponse> v) + { + throw new NotImplementedException(); + } + } } diff --git a/Assets/Scripts/Devices/Ble/mobile/BleMobileInterface.cs b/Assets/Scripts/Devices/Ble/mobile/BleMobileInterface.cs index dbbda567..2d45a78a 100644 --- a/Assets/Scripts/Devices/Ble/mobile/BleMobileInterface.cs +++ b/Assets/Scripts/Devices/Ble/mobile/BleMobileInterface.cs @@ -16,7 +16,7 @@ namespace Assets.Scripts.Ble public sealed class BleMobileInterface: IBleWinHwInterface { private static BleMobileInterface hwInterface; - private WclBleMainThread wclBleMainThread; + private BleMobileThread bleMobileThread; private Dictionary _pCache; public Dictionary pCache @@ -101,70 +101,71 @@ namespace Assets.Scripts.Ble private BleMobileInterface() { - wclBleMainThread = new WclBleMainThread(); - wclBleMainThread.ManagerStatusChanged += ManagerStatusChanged; - wclBleMainThread.ScanInfoReceived += WatcherScanInfoReceived; - //wclBleMainThread.gatt - wclBleMainThread.Start(); - + bleMobileThread = new BleMobileThread(); + bleMobileThread.ScanInfoReceived += WatcherScanInfoReceived; } - private void WatcherScanInfoReceived(WclBleMainThread sender, long address, string name, int rssi, CPPBridge.WclBleAdvertisementType packetType, Guid? service) + private void WatcherScanInfoReceived(BleMobileThread sender, string address, string name, int rssi,string type) { - //if(address != 224160707349234) + //if (address != 224160707349234) //{ - //return; + // return; //} - //Debug.Log($"address:{ address }, name:{ name }, service:{ (service == null ? "" : service.Value.ToString()) }"); - + SensorType sensor = SensorType.None; List services = new List(); - if (service != null) - { - services = new List { service.Value }; - foreach(var item in ServiceUuids.Services) - { - if(item.IdGuid != service.Value) - { - continue; - } - if(item.IdByteArray == ServiceUuids.Ftms) - { - sensor = SensorType.Trainer; - } - else if(item.IdByteArray == ServiceUuids.HeartRate) - { - sensor = SensorType.HeartRate; - } - else if(item.IdByteArray == ServiceUuids.CyclingPower) - { - sensor = SensorType.Power; - //sensor = SensorType.Trainer; - } - else if(item.IdByteArray == ServiceUuids.CyclingSpeedCadence) - { - sensor = SensorType.SpeedCadence; - } - else if(item.IdByteArray == ServiceUuids.TacxBle) - { - sensor = SensorType.Trainer; - } - } - }; - var addressStr = address.ToString("X12"); + if (type.Equals("trainer")) + { + sensor = SensorType.Trainer; + } + else + { + sensor = SensorType.HeartRate; + } + //if (service != null) + //{ + // services = new List { service.Value }; + // foreach (var item in ServiceUuids.Services) + // { + // if (item.IdGuid != service.Value) + // { + // continue; + // } + // if (item.IdByteArray == ServiceUuids.Ftms) + // { + // sensor = SensorType.Trainer; + // } + // else if (item.IdByteArray == ServiceUuids.HeartRate) + // { + // sensor = SensorType.HeartRate; + // } + // else if (item.IdByteArray == ServiceUuids.CyclingPower) + // { + // sensor = SensorType.Power; + // //sensor = SensorType.Trainer; + // } + // else if (item.IdByteArray == ServiceUuids.CyclingSpeedCadence) + // { + // sensor = SensorType.SpeedCadence; + // } + // else if (item.IdByteArray == ServiceUuids.TacxBle) + // { + // sensor = SensorType.Trainer; + // } + // } + //}; + + var addressStr = address; if (!pCache.ContainsKey(addressStr)) { var device = new BleAdvertisementInfo(new WinBlePeripheralInfo(addressStr, name), rssi, true, services, null, sensor); pCache.Add(addressStr, device); - - //WclBleGattThread gattClient = this.SetUpGattClient(device.Peripheral); - //this.ConnectInternal(gattClient); } if (!string.IsNullOrWhiteSpace(name)) { (pCache[addressStr].Peripheral as WinBlePeripheralInfo).SetName(name); } - if(sensor == SensorType.None) + if (sensor == SensorType.None) { return; } @@ -172,19 +173,19 @@ namespace Assets.Scripts.Ble pCache[addressStr].Rssi = rssi; //Debug.Log(sensor); //pCache[address.ToString()].SensorType = sensor; - - foreach (var item in services) - { - pCache[addressStr].TryAddService(item); - } - + + //foreach (var item in services) + //{ + // pCache[addressStr].TryAddService(item); + //} + pCache[addressStr].Index++; - if(pCache[addressStr].SensorType == SensorType.Power && pCache[addressStr].Services.Any(s=> s.Equals(ServiceUuids.Get(ServiceUuids.TacxBle).IdGuid))) + if (pCache[addressStr].SensorType == SensorType.Power && pCache[addressStr].Services.Any(s => s.Equals(ServiceUuids.Get(ServiceUuids.TacxBle).IdGuid))) { pCache[addressStr].SensorType = SensorType.Trainer; //Debug.Log("纠正为trainer, "+ pCache[address.ToString()].Index); } - + if (pCache[addressStr].SensorType == SensorType.Power) { @@ -193,51 +194,77 @@ namespace Assets.Scripts.Ble _discoveredCallback?.Invoke(pCache[addressStr]); } } - else if(pCache[addressStr].SensorType != SensorType.None) + else if (pCache[addressStr].SensorType != SensorType.None) { _discoveredCallback?.Invoke(pCache[addressStr]); } + this.BleState = BleState.On; } - private WclBleGattThread SetUpGattClient(BlePeripheralInfo peripheral) - { - WclBleGattThread wclBleGattThread = this.wclBleMainThread.CreateGattThread(peripheral); - wclBleGattThread.GattConnected += this.GattConnected; - wclBleGattThread.GattDisconnected += this.GattDisconnected; - wclBleGattThread.GattServicesDiscovered += this.GattServicesDiscovered; - wclBleGattThread.GattCharacteristicsDiscovered += this.GattCharacteristicsDiscovered; - wclBleGattThread.GattCharacteristicSubscribed += this.GattCharacteristicSubscribed; - wclBleGattThread.GattCharacteristicRead += this.GattCharacteristicRead; - wclBleGattThread.GattCharacteristicWrote += this.GattCharacteristicWrote; - wclBleGattThread.GattCharacteristicChanged += this.GattCharacteristicChanged; - wclBleGattThread.Start(); - return wclBleGattThread; - } + //private WclBleGattThread SetUpGattClient(BlePeripheralInfo peripheral) + //{ + // WclBleGattThread wclBleGattThread = this.wclBleMainThread.CreateGattThread(peripheral); + // wclBleGattThread.GattConnected += this.GattConnected; + // wclBleGattThread.GattDisconnected += this.GattDisconnected; + // wclBleGattThread.GattServicesDiscovered += this.GattServicesDiscovered; + // wclBleGattThread.GattCharacteristicsDiscovered += this.GattCharacteristicsDiscovered; + // wclBleGattThread.GattCharacteristicSubscribed += this.GattCharacteristicSubscribed; + // wclBleGattThread.GattCharacteristicRead += this.GattCharacteristicRead; + // wclBleGattThread.GattCharacteristicWrote += this.GattCharacteristicWrote; + // wclBleGattThread.GattCharacteristicChanged += this.GattCharacteristicChanged; + // wclBleGattThread.Start(); + // return wclBleGattThread; + //} + List servicelist = new List(); + List characteristilist = new List(); public void ConnectPeripheral(BlePeripheralInfo info, Action callback) { - this.callbacks.Add(info, callback); - WclBleGattThread wclBleGattThread = this.wclBleMainThread.GetGattThread(info); + //this.callbacks.Add(info, callback); + //WclBleGattThread wclBleGattThread = this.wclBleMainThread.GetGattThread(info); + + //if (wclBleGattThread == null) + //{ + + // wclBleGattThread = this.SetUpGattClient(info); + // this.ConnectInternal(wclBleGattThread); + // return; + //} + //else + //{ + // this.ConnectInternal(wclBleGattThread); + //}\ - if(wclBleGattThread == null) + BleResponse s = new BleResponse(); + s.IsSuccess = true; + s.Error = null; + var self = this; + BluetoothLEHardwareInterface.ConnectToPeripheral(info.Address, (address) => { + callback?.Invoke(self, info, s); + }, (address, service) => { + var l = servicelist.Where(c => c.Id.ToString().Equals(service)).ToList(); + if (l.Count == 0) + { + servicelist.Add(new WinBleServiceInfo(info,new Guid(service))); + } + }, (address, service, characteristic) => + { + var l = characteristilist.Where(c => c.Id.ToString().Equals(characteristic)).ToList(); + if (l.Count == 0) + { + //characteristilist.Add(new BleCharacteristicInfo new WinBleServiceInfo(info, new Guid(characteristic))); + } - wclBleGattThread = this.SetUpGattClient(info); - this.ConnectInternal(wclBleGattThread); - return; - } - else - { - this.ConnectInternal(wclBleGattThread); - } + }, null); } private void ConnectInternal(WclBleGattThread gattClient) { //Task.Run(() => //{ - int num = gattClient.Connect(); - Debug.Log("连接设备返回" + num); + //int num = gattClient.Connect(); + //Debug.Log("连接设备返回" + num); //}); @@ -245,16 +272,16 @@ namespace Assets.Scripts.Ble public void DisconnectPeripheral(BlePeripheralInfo peripheral, Action callback) { - var gattThread = this.wclBleMainThread.GetGattThread(peripheral); - if(gattThread != null && gattThread.CanLoadWork) - { - this.callbacks.Remove(peripheral); - this.servicesCallbacks.Remove(peripheral); - this.characteristicNotificationCallbacks.Remove(peripheral); + //var gattThread = this.wclBleMainThread.GetGattThread(peripheral); + //if(gattThread != null && gattThread.CanLoadWork) + //{ + // this.callbacks.Remove(peripheral); + // this.servicesCallbacks.Remove(peripheral); + // this.characteristicNotificationCallbacks.Remove(peripheral); - this.disconnectedCallback.Add(peripheral, callback); - gattThread.Discounect(); - } + // this.disconnectedCallback.Add(peripheral, callback); + // gattThread.Discounect(); + //} } @@ -272,29 +299,29 @@ namespace Assets.Scripts.Ble { pCache.Clear(); _discoveredCallback = discoveredCallBack; - this.wclBleMainThread.StartWatcher(); + bleMobileThread.StartWatcher(); } private void ManagerStatusChanged(WclBleMainThread sender, WclBleManagerStatus status) { - this.BleState = BleMobileInterface.StateFromNativeState(status); + //this.BleState = BleMobileInterface.StateFromNativeState(status); //Debug.Log("win hw:" + status); } private void GattConnected(WclBleGattThread gattClient, BleResponse response) { - Debug.Log($"gatt connected { response.ToString() }"); - if (!response.IsSuccess) - { - gattClient.Stop(); + //Debug.Log($"gatt connected { response.ToString() }"); + //if (!response.IsSuccess) + //{ + // gattClient.Stop(); - this.callbacks[gattClient.Peripheral].Invoke(this, gattClient.Peripheral, response); - this.callbacks.Remove(gattClient.Peripheral); - return; - } - this.callbacks[gattClient.Peripheral].Invoke(this, gattClient.Peripheral, response); - this.callbacks.Remove(gattClient.Peripheral); + // this.callbacks[gattClient.Peripheral].Invoke(this, gattClient.Peripheral, response); + // this.callbacks.Remove(gattClient.Peripheral); + // return; + //} + //this.callbacks[gattClient.Peripheral].Invoke(this, gattClient.Peripheral, response); + //this.callbacks.Remove(gattClient.Peripheral); } private void CleanUpPeripheral(BlePeripheralInfo peripheralInfo) @@ -304,41 +331,41 @@ namespace Assets.Scripts.Ble private void GattDisconnected(WclBleGattThread gattClient, BleResponse response) { - Debug.Log($"gatt disconnected { gattClient.Peripheral.Name }"); + //Debug.Log($"gatt disconnected { gattClient.Peripheral.Name }"); - this.callbacks.Remove(gattClient.Peripheral); - this.servicesCallbacks.Remove(gattClient.Peripheral); - this.characteristicNotificationCallbacks.Remove(gattClient.Peripheral); - //App.MainDeviceAdapter.PrintStatus(); + //this.callbacks.Remove(gattClient.Peripheral); + //this.servicesCallbacks.Remove(gattClient.Peripheral); + //this.characteristicNotificationCallbacks.Remove(gattClient.Peripheral); + ////App.MainDeviceAdapter.PrintStatus(); - var manualDisconnect = disconnectedCallback.ContainsKey(gattClient.Peripheral); - this.peripheralDisconnectedEvent(this, gattClient.Peripheral, response, manualDisconnect); + //var manualDisconnect = disconnectedCallback.ContainsKey(gattClient.Peripheral); + //this.peripheralDisconnectedEvent(this, gattClient.Peripheral, response, manualDisconnect); - //App.MainDeviceAdapter.PrintStatus(); - if (disconnectedCallback.ContainsKey(gattClient.Peripheral)) - { - disconnectedCallback[gattClient.Peripheral].Invoke(); - disconnectedCallback.Remove(gattClient.Peripheral); - } - //this.pCache.Remove(gattClient.Peripheral.Address); + ////App.MainDeviceAdapter.PrintStatus(); + //if (disconnectedCallback.ContainsKey(gattClient.Peripheral)) + //{ + // disconnectedCallback[gattClient.Peripheral].Invoke(); + // disconnectedCallback.Remove(gattClient.Peripheral); + //} + ////this.pCache.Remove(gattClient.Peripheral.Address); } private void GattServicesDiscovered(WclBleGattThread gattClient, BleResponse> response) { - //Debug.Log("services discovered"); - //this.callbacks[gattClient.Peripheral].Invoke(this, gattClient.Peripheral, response); + ////Debug.Log("services discovered"); + ////this.callbacks[gattClient.Peripheral].Invoke(this, gattClient.Peripheral, response); - //foreach (var item in response.Data) + ////foreach (var item in response.Data) + ////{ + //// Debug.Log(item.ToString()); + ////} + + //if (this.servicesCallbacks.ContainsKey(gattClient.Peripheral)) //{ - // Debug.Log(item.ToString()); + // this.servicesCallbacks[gattClient.Peripheral].Invoke(this, gattClient.Peripheral, response); //} - - if (this.servicesCallbacks.ContainsKey(gattClient.Peripheral)) - { - this.servicesCallbacks[gattClient.Peripheral].Invoke(this, gattClient.Peripheral, response); - } } private void GattCharacteristicsDiscovered(WclBleGattThread gattClient, BleServiceInfo service, BleResponse> response) @@ -353,23 +380,23 @@ namespace Assets.Scripts.Ble private void GattCharacteristicSubscribed(WclBleGattThread gattClient, BleCharacteristicInfo characteristic, BleResponse response) { - //Debug.Log("characteristics subscribed"); - if (this.characteristicNotificationCallbacks.ContainsKey(gattClient.Peripheral)) - { - this.characteristicNotificationCallbacks[gattClient.Peripheral].Invoke(this, characteristic, response); - } + ////Debug.Log("characteristics subscribed"); + //if (this.characteristicNotificationCallbacks.ContainsKey(gattClient.Peripheral)) + //{ + // this.characteristicNotificationCallbacks[gattClient.Peripheral].Invoke(this, characteristic, response); + //} } private void GattCharacteristicRead(WclBleGattThread gattClient, BleCharacteristicInfo characteristic, BleResponse response) { - Debug.Log("characteristic read"); + //Debug.Log("characteristic read"); - if (this.characteristicReadCallbacks.ContainsKey(characteristic)) - { - this.characteristicReadCallbacks[characteristic].Invoke(this, characteristic, response); - this.characteristicReadCallbacks.Remove(characteristic); - } + //if (this.characteristicReadCallbacks.ContainsKey(characteristic)) + //{ + // this.characteristicReadCallbacks[characteristic].Invoke(this, characteristic, response); + // this.characteristicReadCallbacks.Remove(characteristic); + //} - this.characteristicReadEvent.Invoke(this, characteristic, response); + //this.characteristicReadEvent.Invoke(this, characteristic, response); } private void GattCharacteristicWrote(WclBleGattThread gattClient, BleCharacteristicInfo characteristic, BleResponse response) @@ -378,119 +405,112 @@ namespace Assets.Scripts.Ble } private void GattCharacteristicChanged(WclBleGattThread gattClient, BleCharacteristicInfo characteristic, BleResponse response) { - //Debug.Log("characteristic changed"); - if(this.wclBleMainThread.GetGattThread(characteristic.Peripheral) != null) - { - this.characteristicReadEvent.Invoke(this, characteristic, response); - } + ////Debug.Log("characteristic changed"); + //if(this.wclBleMainThread.GetGattThread(characteristic.Peripheral) != null) + //{ + // this.characteristicReadEvent.Invoke(this, characteristic, response); + //} } public void DiscoverServices(BlePeripheralInfo peripheral, Action>> callback) { - WclBleGattThread gattThread = this.wclBleMainThread.GetGattThread(peripheral); - if(gattThread == null) - { - return; - } - this.servicesCallbacks.Add(peripheral, callback); - int num = gattThread.DiscoverServices(); - if (WclBleErrors.IsSuccessCode(num)) + if (servicelist.Count > 0) { + BleResponse> response = new BleResponse> + { + IsSuccess = true, + Error = null, + Data = (List)servicelist, + }; + + callback.Invoke(this, peripheral, response); return; } - - - BleResponse> response = new BleResponse> - { - IsSuccess = false, - Error = new BleHwInterfaceError(new int?(num), "WclBleGattClientErrorDomain", string.Format("Error discovering services - {0}", num)) - }; - this.GattServicesDiscovered(gattThread, response); } public void DiscoverCharacteristic(BleServiceInfo service, CharacteristicsDiscoveredCallback callback) { - WclBleGattThread gattThread = this.wclBleMainThread.GetGattThread(service.Peripheral); - if(gattThread == null) - { - return; - } - if (!this.characteristicsDiscoveredCallbacks.ContainsKey(service)) - { - this.characteristicsDiscoveredCallbacks.Add(service, callback); - } - int num = gattThread.DiscoverCharacteristics(service); - if (WclBleErrors.IsSuccessCode(num)) - { - return; - } - BleResponse> response = new BleResponse> - { - IsSuccess = false, - Error = new BleHwInterfaceError(new int?(num), "WclBleGattClientErrorDomain", string.Format("Error discovering characteristics - {0}", num)) - }; - this.GattCharacteristicsDiscovered(gattThread, service, response); + //WclBleGattThread gattThread = this.wclBleMainThread.GetGattThread(service.Peripheral); + //if(gattThread == null) + //{ + // return; + //} + //if (!this.characteristicsDiscoveredCallbacks.ContainsKey(service)) + //{ + // this.characteristicsDiscoveredCallbacks.Add(service, callback); + //} + //int num = gattThread.DiscoverCharacteristics(service); + //if (WclBleErrors.IsSuccessCode(num)) + //{ + // return; + //} + //BleResponse> response = new BleResponse> + //{ + // IsSuccess = false, + // Error = new BleHwInterfaceError(new int?(num), "WclBleGattClientErrorDomain", string.Format("Error discovering characteristics - {0}", num)) + //}; + //this.GattCharacteristicsDiscovered(gattThread, service, response); } public void SubscribeCharacteristic(BleCharacteristicInfo characteristic, Action callback) { - WclBleGattThread gattThread = this.wclBleMainThread.GetGattThread(characteristic.Peripheral); + //WclBleGattThread gattThread = this.wclBleMainThread.GetGattThread(characteristic.Peripheral); - this.characteristicNotificationCallbacks.Add(gattThread.Peripheral, callback); - int num = gattThread.SubscribeCharacteristic(characteristic); - if (WclBleErrors.IsSuccessCode(num)) - { - return; - } - BleResponse> response = new BleResponse> - { - IsSuccess = false, - Error = new BleHwInterfaceError(new int?(num), "WclBleGattClientErrorDomain", string.Format("Error subscribing characteristic - {0}", num)) - }; - this.GattCharacteristicSubscribed(gattThread, characteristic, response); + //this.characteristicNotificationCallbacks.Add(gattThread.Peripheral, callback); + //int num = gattThread.SubscribeCharacteristic(characteristic); + //if (WclBleErrors.IsSuccessCode(num)) + //{ + // return; + //} + //BleResponse> response = new BleResponse> + //{ + // IsSuccess = false, + // Error = new BleHwInterfaceError(new int?(num), "WclBleGattClientErrorDomain", string.Format("Error subscribing characteristic - {0}", num)) + //}; + //this.GattCharacteristicSubscribed(gattThread, characteristic, response); } public void WriteCharacteristic(BleCharacteristicInfo characteristic, byte[] data) { - var gattThread = this.wclBleMainThread.GetGattThread(characteristic.Peripheral); - if(gattThread == null) - { - return; - } + //var gattThread = this.wclBleMainThread.GetGattThread(characteristic.Peripheral); + //if(gattThread == null) + //{ + // return; + //} - int num = gattThread.WriteCharacteristic(characteristic, data); - if (WclBleErrors.IsSuccessCode(num)) - { - Debug.Log("设置命令成功"); - return; - } + //int num = gattThread.WriteCharacteristic(characteristic, data); + //if (WclBleErrors.IsSuccessCode(num)) + //{ + // Debug.Log("设置命令成功"); + // return; + //} } public void Dispose() { - this.wclBleMainThread.ManagerStatusChanged -= ManagerStatusChanged; - this.wclBleMainThread.ScanInfoReceived -= WatcherScanInfoReceived; - this.wclBleMainThread.Stop(); - this.wclBleMainThread = null; - hwInterface = null; - pCache.Clear(); + //this.wclBleMainThread.ManagerStatusChanged -= ManagerStatusChanged; + //this.wclBleMainThread.ScanInfoReceived -= WatcherScanInfoReceived; + //this.wclBleMainThread.Stop(); + //this.wclBleMainThread = null; + //hwInterface = null; + //pCache.Clear(); } public void ReadCharacteristic(BleCharacteristicInfo characteristic, CharacteristicReadCallback callback) { - WclBleGattThread gattThread = this.wclBleMainThread.GetGattThread(characteristic.Peripheral); - if(gattThread == null) - { - return; - } - this.characteristicReadCallbacks.Add(characteristic, callback); - int num = gattThread.ReadCharacteristicValue(characteristic); - if (WclBleErrors.IsSuccessCode(num)) - { - return; - } + //WclBleGattThread gattThread = this.wclBleMainThread.GetGattThread(characteristic.Peripheral); + //if(gattThread == null) + //{ + // return; + //} + //this.characteristicReadCallbacks.Add(characteristic, callback); + //int num = gattThread.ReadCharacteristicValue(characteristic); + //if (WclBleErrors.IsSuccessCode(num)) + //{ + // return; + //} } private static BleState StateFromNativeState(WclBleManagerStatus status) @@ -521,5 +541,21 @@ namespace Assets.Scripts.Ble base.Name = name; } } + + private class WinBleServiceInfo : BleServiceInfo + { + // Token: 0x06003F86 RID: 16262 RVA: 0x000EA27F File Offset: 0x000E847F + public WinBleServiceInfo(BlePeripheralInfo peripheral, Guid id) : base(id, peripheral) + { + } + } + private class WinBleCharacteristicInfo : BleCharacteristicInfo + { + // Token: 0x06003F86 RID: 16262 RVA: 0x000EA27F File Offset: 0x000E847F + public WinBleCharacteristicInfo(Guid id, BleServiceInfo service, BleCharacteristicProperties properties) : base(id,service, properties) + { + + } + } } } diff --git a/Assets/Scripts/Devices/Ble/mobile/BleMobileThread.cs b/Assets/Scripts/Devices/Ble/mobile/BleMobileThread.cs new file mode 100644 index 00000000..3274d7a5 --- /dev/null +++ b/Assets/Scripts/Devices/Ble/mobile/BleMobileThread.cs @@ -0,0 +1,66 @@ +using Assets.Scripts.Ble; +using System.Timers; + +namespace Assets.Scripts.Devices.Ble +{ + public class BleMobileThread + { + public delegate void WclAdvertisementPacketDelegate(BleMobileThread sender, string address, string name, int rssi, string type); + public BleMobileThread.WclAdvertisementPacketDelegate ScanInfoReceived; + private Timer timer { get; set; } + public BleMobileThread() { + //初始蓝牙 + BluetoothLEHardwareInterface.Initialize(true, false, () => { + + }, + (error) => { + + BluetoothLEHardwareInterface.Log("Error: " + error); + + if (error.Contains("Bluetooth LE Not Enabled")) + BluetoothLEHardwareInterface.BluetoothEnable(true); + }); + } + bool startScan = true; + public void StartWatcher() { + //if (timer == null) + //{ + // timer = new Timer(); + // timer.Interval = 500; + // timer.Elapsed += Timer_Elapsed; + //} + //if (!timer.Enabled) + //{ + // timer.Start(); + //} "0000180D-0000-1000-8000-00805F9B34FB" + var ss = new string[] {"00001826-0000-1000-8000-00805F9B34FB","0000180D-0000-1000-8000-00805F9B34FB", "6e40fec1-b5a3-f393-e0a9-e50e24dcca9e" }; + BluetoothLEHardwareInterface.ScanForPeripheralsWithServices(ss, (s,a)=> { + BluetoothLEHardwareInterface.Log(s); + }, (address, name, rssi, bytes) => + { + ScanInfoReceived?.Invoke(this, address, name, rssi,"trainer"); + }, true); + } + + public void StopWatcher() + { + if (timer.Enabled) + { + timer.Stop(); + } + } + + private void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + //if (startScan) + //{ + // startScan = false; + // BluetoothLEHardwareInterface.ScanForPeripheralsWithServices(null, null, null,(address, name, rssi, bytes) => + // { + + // ScanInfoReceived?.Invoke(this, address, name, rssi); + // }, true); + //} + } + } +} diff --git a/Assets/Scripts/Devices/Ble/mobile/BleMobileThread.cs.meta b/Assets/Scripts/Devices/Ble/mobile/BleMobileThread.cs.meta new file mode 100644 index 00000000..aaa1a572 --- /dev/null +++ b/Assets/Scripts/Devices/Ble/mobile/BleMobileThread.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cec5e50ac67fde54196ea58f5b1b790b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Devices/MainDeviceAdapter.cs b/Assets/Scripts/Devices/MainDeviceAdapter.cs index 8c953e93..a5f84d70 100644 --- a/Assets/Scripts/Devices/MainDeviceAdapter.cs +++ b/Assets/Scripts/Devices/MainDeviceAdapter.cs @@ -15,10 +15,12 @@ namespace Assets.Scripts.Devices private List adapters = new List(); public MainDeviceAdapter() { - #if !(UNITY_IOS || UNITY_ANDROID) - this.CreateAntAdapter(); +#if UNITY_IOS || UNITY_TVOS || UNITY_ANDROID this.CreateBleAdapter(); - #endif +#else + this.CreateBleAdapter(); + this.CreateAntAdapter(); +#endif } private void CreateAntAdapter() @@ -29,7 +31,7 @@ namespace Assets.Scripts.Devices private void CreateBleAdapter() { #if UNITY_IOS || UNITY_TVOS || UNITY_ANDROID - adapters.Add(new BleDeviceAdapter(BleWinHwInterface.GetInterface())); + adapters.Add(new BleDeviceAdapter(BleMobileInterface.GetInterface())); #else adapters.Add(new BleDeviceAdapter(BleWinHwInterface.GetInterface())); #endif diff --git a/Assets/Shatalmic.meta b/Assets/Shatalmic.meta new file mode 100644 index 00000000..22b01e25 --- /dev/null +++ b/Assets/Shatalmic.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fcef0f33b95c6fd4d9b2f5a0dcd0d564 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shatalmic/Demo.unity b/Assets/Shatalmic/Demo.unity new file mode 100644 index 00000000..87ccf60d --- /dev/null +++ b/Assets/Shatalmic/Demo.unity @@ -0,0 +1,439 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 10 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringMode: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ShowResolutionOverlay: 1 + m_LightingDataAsset: {fileID: 0} + m_UseShadowmask: 1 +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &53270293 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 53270296} + - component: {fileID: 53270295} + - component: {fileID: 53270294} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &53270294 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 53270293} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1077351063, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &53270295 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 53270293} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: -619905303, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &53270296 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 53270293} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &344612123 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 344612127} + - component: {fileID: 344612126} + - component: {fileID: 344612125} + - component: {fileID: 344612124} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &344612124 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 344612123} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &344612125 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 344612123} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!223 &344612126 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 344612123} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &344612127 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 344612123} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 601672577} + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &601672576 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 601672577} + - component: {fileID: 601672579} + - component: {fileID: 601672578} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &601672577 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 601672576} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 344612127} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &601672578 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 601672576} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 72 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 7 + m_MaxSize: 72 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Due to the new Unity asset publishing requirements you must follow the instructions + in the README file to unpack this plugin. +--- !u!222 &601672579 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 601672576} + m_CullTransparentMesh: 0 +--- !u!1 &1655223073 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1655223076} + - component: {fileID: 1655223075} + - component: {fileID: 1655223074} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1655223074 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1655223073} + m_Enabled: 1 +--- !u!20 &1655223075 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1655223073} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 1, g: 1, b: 1, a: 0} + m_projectionMatrixMode: 1 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_GateFitMode: 2 + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1655223076 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1655223073} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Assets/Shatalmic/Demo.unity.meta b/Assets/Shatalmic/Demo.unity.meta new file mode 100644 index 00000000..4f135db5 --- /dev/null +++ b/Assets/Shatalmic/Demo.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0149d806ed996492787b9cb44993fd5b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shatalmic/Editor.meta b/Assets/Shatalmic/Editor.meta new file mode 100644 index 00000000..6b9cdfe0 --- /dev/null +++ b/Assets/Shatalmic/Editor.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 2 +guid: 87247c18c42564aceaf454206a3ef873 +folderAsset: yes +DefaultImporter: + userData: diff --git a/Assets/Shatalmic/Editor/BluetoothPostProcessBuild.cs b/Assets/Shatalmic/Editor/BluetoothPostProcessBuild.cs new file mode 100644 index 00000000..847ca187 --- /dev/null +++ b/Assets/Shatalmic/Editor/BluetoothPostProcessBuild.cs @@ -0,0 +1,30 @@ +#if UNITY_IOS +using UnityEditor.Callbacks; +using UnityEditor; +using UnityEditor.iOS.Xcode; +using System.IO; + +public class BluetoothPostProcessBuild +{ + [PostProcessBuild] + public static void ChangeXcodePlist(BuildTarget buildTarget, string pathToBuiltProject) + { + if (buildTarget == BuildTarget.iOS) + { + // Get plist + string plistPath = pathToBuiltProject + "/Info.plist"; + PlistDocument plist = new PlistDocument(); + plist.ReadFromString(File.ReadAllText(plistPath)); + + // Get root + PlistElementDict rootDict = plist.root; + + rootDict.SetString("NSBluetoothPeripheralUsageDescription", "Uses BLE to communicate with devices."); + rootDict.SetString("NSBluetoothAlwaysUsageDescription", "Uses BLE to communicate with devices."); + + // Write to file + File.WriteAllText(plistPath, plist.WriteToString()); + } + } +} +#endif diff --git a/Assets/Shatalmic/Editor/BluetoothPostProcessBuild.cs.meta b/Assets/Shatalmic/Editor/BluetoothPostProcessBuild.cs.meta new file mode 100644 index 00000000..99d5856a --- /dev/null +++ b/Assets/Shatalmic/Editor/BluetoothPostProcessBuild.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 1695b640ed1354053a8cdd9e0186f57f +timeCreated: 1493664761 +licenseType: Pro +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shatalmic/Editor/PostprocessBuildPlayer b/Assets/Shatalmic/Editor/PostprocessBuildPlayer new file mode 100644 index 00000000..3c2e8ece --- /dev/null +++ b/Assets/Shatalmic/Editor/PostprocessBuildPlayer @@ -0,0 +1,93 @@ +#!/usr/bin/perl +use File::Copy; + +print ("\n*** Starting PostprocessBuildPlayer ***\n"); + +my $installPath = $ARGV[0]; + +#values to watch for already existing +my $requiredDeviceCapsString = "UIRequiredDeviceCapabilities"; +my $backgroundModesString = "UIBackgroundModes"; +my $bluetoothLEString = "bluetooth-le"; + +#go through the info.plist file line by line until you find this one: +my $endOfPlist = ""; + +# The type of player built: +# "dashboard", "standaloneWin32", "standaloneOSXIntel", "standaloneOSXPPC", "standaloneOSXUniversal", "webplayer", "iPhone" +my $target = $ARGV[1]; + +print ("\n*** PostprocessBuildPlayer - Building at '$installPath' with target: $target ***\n"); + +################################################################ +# This modifies info.plist to add the external accessory # +# entries # +################################################################ + +#open this file + +$oplistPath = $installPath."/Info.plist"; +$nplistPath = $installPath."/Info.plist.tmp"; + +open OLDPLIST, "<", $oplistPath or die("Cannot open Info.plist"); +open NEWPLIST, ">", $nplistPath or die("Cannot create new Info.plist"); + +my $nextLine = 0; +my $requiredDeviceCapsFound = 0; +my $backgroundModesFound = 0; + +while() +{ + if ($nextLine == 1) + { + if ($_ =~ m/$bluetoothLEString/) + { + } + else + { + $_ =~ s||\n\tbluetooth-le| + } + + $nextLine = 0; + } + + ################################################################ + # Check for required device caps string already exists # + ################################################################ + if ($_ =~ m/$requiredDeviceCapsString/ ) + { + $nextLine = 1; + } + + ################################################################ + # Check for background modes string already exists # + ################################################################ + if ($_ =~ m/$backgroundModesString/ ) + { + $backgroundModesFound = 1; + } + + ################################################################ + # Add any key/value pairs you want at the end of Info.plist # + ################################################################ + + if ($_ =~ m/$endOfPlist/ && $backgrounModesFound == 0) + { + my $keys = ""; + + $keys .= " UIBackgroundModes\n"; + $keys .= " \n"; + $keys .= " bluetooth-central\n"; + $keys .= " bluetooth-peripheral\n"; + $keys .= " \n"; + + $_ = $keys . $_; + } + + print NEWPLIST $_; +} + +close OLDPLIST; +close NEWPLIST; + +`mv \'$nplistPath\' \'$oplistPath\'`; diff --git a/Assets/Shatalmic/Editor/PostprocessBuildPlayer.meta b/Assets/Shatalmic/Editor/PostprocessBuildPlayer.meta new file mode 100644 index 00000000..7fceff06 --- /dev/null +++ b/Assets/Shatalmic/Editor/PostprocessBuildPlayer.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 3c726eecf85fe4a88a7e7d57812ff455 +timeCreated: 1515198229 +licenseType: Pro +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shatalmic/README.txt b/Assets/Shatalmic/README.txt new file mode 100644 index 00000000..ea8d3e7b --- /dev/null +++ b/Assets/Shatalmic/README.txt @@ -0,0 +1 @@ +You must double click on the unity package file called Plugin.unitypackage in order to unpack the plugin files. \ No newline at end of file diff --git a/Assets/Shatalmic/README.txt.meta b/Assets/Shatalmic/README.txt.meta new file mode 100644 index 00000000..56fc2dbd --- /dev/null +++ b/Assets/Shatalmic/README.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3465e0116537642f78e4b288b23c461c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shatalmic/plugin.unitypackage.meta b/Assets/Shatalmic/plugin.unitypackage.meta new file mode 100644 index 00000000..c11cbc4e --- /dev/null +++ b/Assets/Shatalmic/plugin.unitypackage.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1a70f37e2f13e4df0ac477f3c3eb451b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: