|
|
@@ -0,0 +1,523 @@
|
|
|
+using System;
|
|
|
+using System.Collections.Concurrent;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Threading;
|
|
|
+using UnityEngine;
|
|
|
+using UnityEngine.Android;
|
|
|
+
|
|
|
+namespace SmartBowSDK.CMD
|
|
|
+{
|
|
|
+ public class CMDManager
|
|
|
+ {
|
|
|
+ // ---- GC 防止回调被销毁 ----
|
|
|
+ private CMDScannerCallbackProxy scannerProxy;
|
|
|
+ private CMDBleCallbackProxy bleCallbackProxy;
|
|
|
+
|
|
|
+ private string pendingScanPattern = null;
|
|
|
+
|
|
|
+ public event Action<string, string> OnCMDDeviceFound;
|
|
|
+ public event Action<CMDScanState,string> OnCMDScanFailed;
|
|
|
+
|
|
|
+ public event Action OnCMDBLEConnected;
|
|
|
+ public event Action OnCMDBLEDisconnected;
|
|
|
+ public event Action OnCMDBLEReady;
|
|
|
+ public event Action<byte[]> OnCMDBLENotify;
|
|
|
+
|
|
|
+ public event Action<string> OnPermissionDenied; // 某权限被拒绝
|
|
|
+ public event Action<string> OnPermissionDontAsk; // 被永久拒绝
|
|
|
+
|
|
|
+ //public int scanTimeout = 10; // 传超时 ,默认10秒
|
|
|
+ // Android 12+
|
|
|
+ private readonly string[] BLE_PERMISSIONS_31 =
|
|
|
+ {
|
|
|
+ "android.permission.BLUETOOTH_SCAN",
|
|
|
+ "android.permission.BLUETOOTH_CONNECT"
|
|
|
+ };
|
|
|
+
|
|
|
+ // Android 6~11
|
|
|
+ private readonly string[] BLE_PERMISSIONS =
|
|
|
+ {
|
|
|
+ Permission.FineLocation
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ // ----------- 统一复用对象 -----------
|
|
|
+ private AndroidJavaObject unityActivity; // currentActivity
|
|
|
+ private AndroidJavaClass bleManagerClass; // CMDBleManager.class
|
|
|
+ private AndroidJavaObject bleManagerInst; // CMDBleManager.getInstance()
|
|
|
+ private AndroidJavaClass transparentProxyActivityClass; // CMDBleManager.class
|
|
|
+
|
|
|
+
|
|
|
+ // ========= 队列 & 后台解析线程 =========
|
|
|
+ private readonly ConcurrentQueue<byte[]> notifyQueue = new ConcurrentQueue<byte[]>();
|
|
|
+ private readonly AutoResetEvent notifyEvent = new AutoResetEvent(false);
|
|
|
+ private Thread notifyWorker;
|
|
|
+ private volatile bool notifyWorkerRunning = false;
|
|
|
+
|
|
|
+ // 主线程派发节流:最多每 dispatchIntervalMs 毫秒派发一次到主线程
|
|
|
+ private readonly int dispatchIntervalMs = 0; // 50 Hz 向主线程派发(可调)
|
|
|
+ private long lastDispatchTime = 0;
|
|
|
+
|
|
|
+ // -------- Constructor --------
|
|
|
+ public CMDManager()
|
|
|
+ {
|
|
|
+ // 初始化统一复用对象 --------
|
|
|
+ unityActivity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")
|
|
|
+ .GetStatic<AndroidJavaObject>("currentActivity");
|
|
|
+
|
|
|
+ //bleManagerClass = new AndroidJavaClass("com.ble.mycdmmanager.CMDBleManager");
|
|
|
+ //bleManagerInst = bleManagerClass.CallStatic<AndroidJavaObject>("getInstance");
|
|
|
+ bleManagerInst = new AndroidJavaObject(
|
|
|
+ "com.ble.mycdmmanager.CMDBleManager",
|
|
|
+ unityActivity // 传 context
|
|
|
+ );
|
|
|
+
|
|
|
+ transparentProxyActivityClass = new AndroidJavaClass("com.ble.mycdmmanager.CMDTransparentProxyActivity");
|
|
|
+
|
|
|
+
|
|
|
+ // BLE callback
|
|
|
+ bleCallbackProxy = new CMDBleCallbackProxy();
|
|
|
+
|
|
|
+ bleCallbackProxy.OnBLEConnectedEvent += () =>
|
|
|
+ OnCMDBLEConnected?.Invoke();
|
|
|
+
|
|
|
+ bleCallbackProxy.OnBLEDisconnectedEvent += () =>
|
|
|
+ OnCMDBLEDisconnected?.Invoke();
|
|
|
+
|
|
|
+ bleCallbackProxy.OnBLEReadyEvent += () =>
|
|
|
+ OnCMDBLEReady?.Invoke();
|
|
|
+
|
|
|
+ bleCallbackProxy.OnBLENotifyEvent += (mergedDataObj) =>
|
|
|
+ {
|
|
|
+ //SmartBowLogger.LogError(this, "[OnBLENotifyEvent]:"+ BitConverter.ToString(data));
|
|
|
+ //long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
+ //SmartBowLogger.Log(this,$"[CMDBleManager→Unity] NotifyArrived | t={now} | size={data.Length}");
|
|
|
+ //OnCMDBLENotify?.Invoke(data); // 实时调用
|
|
|
+ // 快速入队(非阻塞)
|
|
|
+ //EnqueueNotify(data);
|
|
|
+
|
|
|
+ // --- 1. 使用 Buffer.BlockCopy 安全地将 JNI 数组转换为 C# byte[] ---
|
|
|
+ if (!(mergedDataObj is Array sourceArray))
|
|
|
+ {
|
|
|
+ Debug.LogError($"[CMDManager] JNI 回调期望一个数组,但接收到: {mergedDataObj?.GetType()}");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int totalLen = sourceArray.Length;
|
|
|
+ byte[] mergedData = new byte[totalLen];
|
|
|
+ // BlockCopy 可以在 byte[] 和 sbyte[] 之间进行安全的原始内存复制
|
|
|
+ System.Buffer.BlockCopy(sourceArray, 0, mergedData, 0, totalLen);
|
|
|
+
|
|
|
+ // --- 2. 拆分多条数据 ---
|
|
|
+ int index = 0;
|
|
|
+ while (index + 2 <= mergedData.Length)
|
|
|
+ {
|
|
|
+ // 确保 Java 和 C# 的字节序一致(您的代码使用了小端序 for BitConverter.ToUInt16)
|
|
|
+ ushort len = BitConverter.ToUInt16(mergedData, index);
|
|
|
+ index += 2;
|
|
|
+
|
|
|
+ if (index + len > mergedData.Length)
|
|
|
+ {
|
|
|
+ Debug.LogWarning("[CMDManager] 数据长度超出合并数组范围");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ byte[] singleData = new byte[len];
|
|
|
+ Array.Copy(mergedData, index, singleData, 0, len);
|
|
|
+ index += len;
|
|
|
+
|
|
|
+ // --- 3. 快速入队(非阻塞) ---
|
|
|
+ EnqueueNotify(singleData); // 使用您的高性能 C# 队列和后台线程
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 启动主线程 dispatcher(如果未存在)
|
|
|
+ MainThreadDispatcher.EnsureCreated();
|
|
|
+
|
|
|
+ // 启动后台线程
|
|
|
+ StartNotifyWorker();
|
|
|
+
|
|
|
+ //bleManagerClass.CallStatic("setGlobalCMDBleCallback", bleCallbackProxy);
|
|
|
+ bleManagerInst.Call("setCmdBleCallback", bleCallbackProxy);
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+ /// <summary>
|
|
|
+ /// 设置服务特征值
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="service"></param>
|
|
|
+ /// <param name="write"></param>
|
|
|
+ /// <param name="notify"></param>
|
|
|
+ public void SetUUIDs(string service, string write, string notify) {
|
|
|
+
|
|
|
+ bleManagerInst.Call("setUUIDs", service, write, notify );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // -------- 权限处理 --------
|
|
|
+
|
|
|
+ private string[] GetRequiredPermissions() =>
|
|
|
+ GetAndroidSDKInt() >= 31 ? BLE_PERMISSIONS_31 : BLE_PERMISSIONS;
|
|
|
+
|
|
|
+ private bool CheckAllBLEPermissions()
|
|
|
+ {
|
|
|
+ foreach (var p in GetRequiredPermissions())
|
|
|
+ {
|
|
|
+ if (!Permission.HasUserAuthorizedPermission(p))
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int GetAndroidSDKInt()
|
|
|
+ {
|
|
|
+ using (var v = new AndroidJavaClass("android.os.Build$VERSION"))
|
|
|
+ return v.GetStatic<int>("SDK_INT");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void RequestAllPermissions(Action onAllGranted)
|
|
|
+ {
|
|
|
+ var perms = GetRequiredPermissions();
|
|
|
+ RequestPermissionRecursive(perms, 0, onAllGranted);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void RequestPermissionRecursive(string[] perms, int index, Action onAllGranted)
|
|
|
+ {
|
|
|
+ if (index >= perms.Length)
|
|
|
+ {
|
|
|
+ onAllGranted?.Invoke();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ string permission = perms[index];
|
|
|
+
|
|
|
+ PermissionManager.Request(
|
|
|
+ permission,
|
|
|
+ onGranted: () =>
|
|
|
+ {
|
|
|
+ // 继续请求下一项权限
|
|
|
+ RequestPermissionRecursive(perms, index + 1, onAllGranted);
|
|
|
+ },
|
|
|
+ onDenied: () =>
|
|
|
+ {
|
|
|
+ Debug.LogError($"[CMDManager] 权限被拒绝:{permission}");
|
|
|
+
|
|
|
+ // ---- 调用事件 ----
|
|
|
+ OnPermissionDenied?.Invoke(permission);
|
|
|
+ },
|
|
|
+ onDontAsk: () =>
|
|
|
+ {
|
|
|
+ Debug.LogError($"[CMDManager] 权限永久拒绝:{permission}");
|
|
|
+
|
|
|
+ // ---- 调用事件 ----
|
|
|
+ OnPermissionDontAsk?.Invoke(permission);
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+ private void EnsureBLEPermissionThen(Action doWork)
|
|
|
+ {
|
|
|
+ if (CheckAllBLEPermissions())
|
|
|
+ {
|
|
|
+ doWork?.Invoke();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ RequestAllPermissions(() =>
|
|
|
+ {
|
|
|
+ doWork?.Invoke();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- 扫描入口 ----
|
|
|
+
|
|
|
+ public void StartScan(string pattern)
|
|
|
+ {
|
|
|
+ pendingScanPattern = pattern;
|
|
|
+
|
|
|
+ EnsureBLEPermissionThen(() =>
|
|
|
+ {
|
|
|
+ LaunchTransparentProxyScan(pendingScanPattern);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // ---- 启动透明 Activity ----
|
|
|
+
|
|
|
+ private void LaunchTransparentProxyScan(string pattern)
|
|
|
+ {
|
|
|
+ // Scanner Callback
|
|
|
+ scannerProxy = new CMDScannerCallbackProxy();
|
|
|
+ scannerProxy.OnDeviceFoundEvent = (name, mac) =>
|
|
|
+ {
|
|
|
+ //Debug.Log("[CMDManager] DeviceFound: " + name + " - " + mac);
|
|
|
+ OnCMDDeviceFound?.Invoke(name, mac);
|
|
|
+ };
|
|
|
+
|
|
|
+ scannerProxy.OnScanFailedEvent = (state,reason) =>
|
|
|
+ {
|
|
|
+ //Debug.LogError("[CMDManager] ScanFailed: " + reason);
|
|
|
+ OnCMDScanFailed?.Invoke(state,reason);
|
|
|
+ };
|
|
|
+ // ---- 设置静态回调 ----
|
|
|
+ transparentProxyActivityClass.SetStatic("globalCMDScannerCallback", scannerProxy);
|
|
|
+ // ---- 启动 Activity ----
|
|
|
+ AndroidJavaObject intent = new AndroidJavaObject(
|
|
|
+ "android.content.Intent",
|
|
|
+ unityActivity,
|
|
|
+ new AndroidJavaClass("com.ble.mycdmmanager.CMDTransparentProxyActivity")
|
|
|
+ );
|
|
|
+ //扫描设备名字字符串
|
|
|
+ intent.Call<AndroidJavaObject>("putExtra", "pattern", pattern);
|
|
|
+
|
|
|
+ //intent.Call<AndroidJavaObject>("putExtra", "timeout_sec", scanTimeout);
|
|
|
+
|
|
|
+ //intent.Call<AndroidJavaObject>("addFlags", 0x10000000); // FLAG_ACTIVITY_NEW_TASK
|
|
|
+
|
|
|
+ unityActivity.Call("startActivity", intent);
|
|
|
+ }
|
|
|
+ /// <summary>
|
|
|
+ /// 通过 MAC 地址连接设备
|
|
|
+ /// </summary>
|
|
|
+ public bool ConnectMac(string mac)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(mac)) return false;
|
|
|
+
|
|
|
+ bool result = false;
|
|
|
+
|
|
|
+ EnsureBLEPermissionThen(() =>
|
|
|
+ {
|
|
|
+ result = bleManagerInst.Call<bool>("connectMac", mac);
|
|
|
+ });
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 断开cmd蓝牙连接
|
|
|
+ */
|
|
|
+ public void Disconnect()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ bleManagerInst.Call("disconnect");
|
|
|
+ //清空队列
|
|
|
+ ClearNotifyQueue();
|
|
|
+ SmartBowLogger.Log(this, "[CMDManager] Disconnect called (OK)");
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ SmartBowLogger.LogError(this, "[CMDManager] Disconnect error: " + e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 清空cmd manager
|
|
|
+ */
|
|
|
+ public void Cleanup()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ SmartBowLogger.Log(this, "[CMDManager] <Disconnect> begin");
|
|
|
+
|
|
|
+ // 1) 停 Worker
|
|
|
+ StopNotifyWorker();
|
|
|
+
|
|
|
+ // 2) 清空队列
|
|
|
+ ClearNotifyQueue();
|
|
|
+
|
|
|
+ // 3) 注销静态回调,避免旧 CMD 的 notify 残留
|
|
|
+ //try
|
|
|
+ //{
|
|
|
+ // // 移除 Scanner 回调
|
|
|
+ // transparentProxyActivityClass.SetStatic<AndroidJavaObject>("globalCMDScannerCallback", null);
|
|
|
+ // // 移除 BLE 回调
|
|
|
+ // bleManagerClass.CallStatic("setGlobalCMDBleCallback", null);
|
|
|
+
|
|
|
+ // SmartBowLogger.Log(this, "[CMDManager] static callbacks cleared");
|
|
|
+ //}
|
|
|
+ //catch (Exception ex)
|
|
|
+ //{
|
|
|
+ // SmartBowLogger.LogError(this, "[CMDManager] clear static callback error:" + ex);
|
|
|
+ //}
|
|
|
+
|
|
|
+ // 4) 最后再断开 Android BLE
|
|
|
+ bleManagerInst.Call("disconnect");
|
|
|
+
|
|
|
+ SmartBowLogger.Log(this, "[CMDManager] Cleanup done");
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ SmartBowLogger.LogError(this, "[CMDManager] Cleanup error: " + e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ public bool SendCommand(string text)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(text))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ bool success;
|
|
|
+
|
|
|
+ // 判断是否 HEX(01 02 03)
|
|
|
+ if (System.Text.RegularExpressions.Regex.IsMatch(text, @"^([0-9A-Fa-f]{2}\s*)+$"))
|
|
|
+ {
|
|
|
+ byte[] bytes = ParseHexString(text);
|
|
|
+ success = WriteBytes(bytes);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ success = WriteString(text);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!success)
|
|
|
+ {
|
|
|
+ SmartBowLogger.LogError(this,"[CMDManager] Send failed, BLE not ready");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ SmartBowLogger.Log(this,"[CMDManager] Sent: " + text);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public bool WriteString(string str)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return bleManagerInst.Call<bool>("writeString", str);
|
|
|
+ }
|
|
|
+ catch (System.Exception e)
|
|
|
+ {
|
|
|
+ SmartBowLogger.LogError(this,"[CMDManager] writeString error: " + e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public bool WriteBytes(byte[] bytes)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return bleManagerInst.Call<bool>("writeBytes", bytes);
|
|
|
+ }
|
|
|
+ catch (System.Exception e)
|
|
|
+ {
|
|
|
+ SmartBowLogger.LogError(this,"[CMDManager] writeBytes error: " + e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] ParseHexString(string hex)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ string[] parts = hex.Trim().Split(' ');
|
|
|
+ byte[] bytes = new byte[parts.Length];
|
|
|
+
|
|
|
+ for (int i = 0; i < parts.Length; i++)
|
|
|
+ {
|
|
|
+ bytes[i] = System.Convert.ToByte(parts[i], 16);
|
|
|
+ }
|
|
|
+
|
|
|
+ return bytes;
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // -------- 队列入队(由 CMDBleCallbackProxy 调用) --------
|
|
|
+ internal void EnqueueNotify(byte[] data)
|
|
|
+ {
|
|
|
+ if (data == null) return;
|
|
|
+ notifyQueue.Enqueue(data);
|
|
|
+ notifyEvent.Set(); // 唤醒后台线程
|
|
|
+ }
|
|
|
+
|
|
|
+ private void StartNotifyWorker()
|
|
|
+ {
|
|
|
+ if (notifyWorkerRunning) return;
|
|
|
+ notifyWorkerRunning = true;
|
|
|
+ notifyWorker = new Thread(NotifyWorkerLoop)
|
|
|
+ {
|
|
|
+ IsBackground = true,
|
|
|
+ Name = "CMDManager-NotifyWorker",
|
|
|
+ Priority = System.Threading.ThreadPriority.AboveNormal // 提升优先级
|
|
|
+ };
|
|
|
+ notifyWorker.Start();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void StopNotifyWorker()
|
|
|
+ {
|
|
|
+ notifyWorkerRunning = false;
|
|
|
+ notifyEvent.Set();
|
|
|
+ try { notifyWorker?.Join(500); } catch { }
|
|
|
+ notifyWorker = null;
|
|
|
+ }
|
|
|
+ private void ClearNotifyQueue()
|
|
|
+ {
|
|
|
+ while (notifyQueue.TryDequeue(out _)) ;
|
|
|
+ SmartBowLogger.Log(this, "[CMDManager] NotifyQueue cleared");
|
|
|
+ }
|
|
|
+ private void NotifyWorkerLoop()
|
|
|
+ {
|
|
|
+ // 后台线程不断消费队列,尽快完成解析工作
|
|
|
+ while (notifyWorkerRunning)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 等待直到有数据
|
|
|
+ if (!notifyQueue.TryDequeue(out var data))
|
|
|
+ {
|
|
|
+ // 等待唤醒或超时
|
|
|
+ notifyEvent.WaitOne(50);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理单条数据(在后台线程)
|
|
|
+ ProcessNotifyBackground(data);
|
|
|
+
|
|
|
+ // 批量消费:尽可能多地一次性取完队列,以减少唤醒次数
|
|
|
+ while (notifyQueue.TryDequeue(out data))
|
|
|
+ {
|
|
|
+ ProcessNotifyBackground(data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Debug.LogError("[CMDManager] NotifyWorkerLoop error: " + ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ProcessNotifyBackground(byte[] bytes)
|
|
|
+ {
|
|
|
+ // 直接传引用,不做限频派发
|
|
|
+ MainThreadDispatcher.Enqueue(() =>
|
|
|
+ {
|
|
|
+ OnCMDBLENotify?.Invoke(bytes);
|
|
|
+ });
|
|
|
+ // 后台处理完毕后,只做限频派发
|
|
|
+ //DispatchToMainThreadWithThrottle(bytes);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将数据派发到主线程,但节流:每 dispatchIntervalMs 才发一次
|
|
|
+ private void DispatchToMainThreadWithThrottle(byte[] processed)
|
|
|
+ {
|
|
|
+ long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
+ if (now - lastDispatchTime >= dispatchIntervalMs)
|
|
|
+ {
|
|
|
+ lastDispatchTime = now;
|
|
|
+ // 直接传引用,不做 Array.Copy
|
|
|
+ MainThreadDispatcher.Enqueue(() =>
|
|
|
+ {
|
|
|
+ OnCMDBLENotify?.Invoke(processed);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+}
|