| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- 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);
- });
- }
- }
- }
- }
|