CMDManager.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Threading;
  5. using UnityEngine;
  6. using UnityEngine.Android;
  7. namespace SmartBowSDK.CMD
  8. {
  9. public class CMDManager
  10. {
  11. // ---- GC 防止回调被销毁 ----
  12. private CMDScannerCallbackProxy scannerProxy;
  13. private CMDBleCallbackProxy bleCallbackProxy;
  14. private string pendingScanPattern = null;
  15. public event Action<string, string> OnCMDDeviceFound;
  16. public event Action<CMDScanState,string> OnCMDScanFailed;
  17. public event Action OnCMDBLEConnected;
  18. public event Action OnCMDBLEDisconnected;
  19. public event Action OnCMDBLEReady;
  20. public event Action<byte[]> OnCMDBLENotify;
  21. public event Action<string> OnPermissionDenied; // 某权限被拒绝
  22. public event Action<string> OnPermissionDontAsk; // 被永久拒绝
  23. //public int scanTimeout = 10; // 传超时 ,默认10秒
  24. // Android 12+
  25. private readonly string[] BLE_PERMISSIONS_31 =
  26. {
  27. "android.permission.BLUETOOTH_SCAN",
  28. "android.permission.BLUETOOTH_CONNECT"
  29. };
  30. // Android 6~11
  31. private readonly string[] BLE_PERMISSIONS =
  32. {
  33. Permission.FineLocation
  34. };
  35. // ----------- 统一复用对象 -----------
  36. private AndroidJavaObject unityActivity; // currentActivity
  37. private AndroidJavaClass bleManagerClass; // CMDBleManager.class
  38. private AndroidJavaObject bleManagerInst; // CMDBleManager.getInstance()
  39. private AndroidJavaClass transparentProxyActivityClass; // CMDBleManager.class
  40. // ========= 队列 & 后台解析线程 =========
  41. private readonly ConcurrentQueue<byte[]> notifyQueue = new ConcurrentQueue<byte[]>();
  42. private readonly AutoResetEvent notifyEvent = new AutoResetEvent(false);
  43. private Thread notifyWorker;
  44. private volatile bool notifyWorkerRunning = false;
  45. // 主线程派发节流:最多每 dispatchIntervalMs 毫秒派发一次到主线程
  46. private readonly int dispatchIntervalMs = 0; // 50 Hz 向主线程派发(可调)
  47. private long lastDispatchTime = 0;
  48. // -------- Constructor --------
  49. public CMDManager()
  50. {
  51. // 初始化统一复用对象 --------
  52. unityActivity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")
  53. .GetStatic<AndroidJavaObject>("currentActivity");
  54. //bleManagerClass = new AndroidJavaClass("com.ble.mycdmmanager.CMDBleManager");
  55. //bleManagerInst = bleManagerClass.CallStatic<AndroidJavaObject>("getInstance");
  56. bleManagerInst = new AndroidJavaObject(
  57. "com.ble.mycdmmanager.CMDBleManager",
  58. unityActivity // 传 context
  59. );
  60. transparentProxyActivityClass = new AndroidJavaClass("com.ble.mycdmmanager.CMDTransparentProxyActivity");
  61. // BLE callback
  62. bleCallbackProxy = new CMDBleCallbackProxy();
  63. bleCallbackProxy.OnBLEConnectedEvent += () =>
  64. OnCMDBLEConnected?.Invoke();
  65. bleCallbackProxy.OnBLEDisconnectedEvent += () =>
  66. OnCMDBLEDisconnected?.Invoke();
  67. bleCallbackProxy.OnBLEReadyEvent += () =>
  68. OnCMDBLEReady?.Invoke();
  69. bleCallbackProxy.OnBLENotifyEvent += (mergedDataObj) =>
  70. {
  71. //SmartBowLogger.LogError(this, "[OnBLENotifyEvent]:"+ BitConverter.ToString(data));
  72. //long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
  73. //SmartBowLogger.Log(this,$"[CMDBleManager→Unity] NotifyArrived | t={now} | size={data.Length}");
  74. //OnCMDBLENotify?.Invoke(data); // 实时调用
  75. // 快速入队(非阻塞)
  76. //EnqueueNotify(data);
  77. // --- 1. 使用 Buffer.BlockCopy 安全地将 JNI 数组转换为 C# byte[] ---
  78. if (!(mergedDataObj is Array sourceArray))
  79. {
  80. Debug.LogError($"[CMDManager] JNI 回调期望一个数组,但接收到: {mergedDataObj?.GetType()}");
  81. return;
  82. }
  83. int totalLen = sourceArray.Length;
  84. byte[] mergedData = new byte[totalLen];
  85. // BlockCopy 可以在 byte[] 和 sbyte[] 之间进行安全的原始内存复制
  86. System.Buffer.BlockCopy(sourceArray, 0, mergedData, 0, totalLen);
  87. // --- 2. 拆分多条数据 ---
  88. int index = 0;
  89. while (index + 2 <= mergedData.Length)
  90. {
  91. // 确保 Java 和 C# 的字节序一致(您的代码使用了小端序 for BitConverter.ToUInt16)
  92. ushort len = BitConverter.ToUInt16(mergedData, index);
  93. index += 2;
  94. if (index + len > mergedData.Length)
  95. {
  96. Debug.LogWarning("[CMDManager] 数据长度超出合并数组范围");
  97. break;
  98. }
  99. byte[] singleData = new byte[len];
  100. Array.Copy(mergedData, index, singleData, 0, len);
  101. index += len;
  102. // --- 3. 快速入队(非阻塞) ---
  103. EnqueueNotify(singleData); // 使用您的高性能 C# 队列和后台线程
  104. }
  105. };
  106. // 启动主线程 dispatcher(如果未存在)
  107. MainThreadDispatcher.EnsureCreated();
  108. // 启动后台线程
  109. StartNotifyWorker();
  110. //bleManagerClass.CallStatic("setGlobalCMDBleCallback", bleCallbackProxy);
  111. bleManagerInst.Call("setCmdBleCallback", bleCallbackProxy);
  112. }
  113. /// <summary>
  114. /// 设置服务特征值
  115. /// </summary>
  116. /// <param name="service"></param>
  117. /// <param name="write"></param>
  118. /// <param name="notify"></param>
  119. public void SetUUIDs(string service, string write, string notify) {
  120. bleManagerInst.Call("setUUIDs", service, write, notify );
  121. }
  122. // -------- 权限处理 --------
  123. private string[] GetRequiredPermissions() =>
  124. GetAndroidSDKInt() >= 31 ? BLE_PERMISSIONS_31 : BLE_PERMISSIONS;
  125. private bool CheckAllBLEPermissions()
  126. {
  127. foreach (var p in GetRequiredPermissions())
  128. {
  129. if (!Permission.HasUserAuthorizedPermission(p))
  130. return false;
  131. }
  132. return true;
  133. }
  134. private int GetAndroidSDKInt()
  135. {
  136. using (var v = new AndroidJavaClass("android.os.Build$VERSION"))
  137. return v.GetStatic<int>("SDK_INT");
  138. }
  139. private void RequestAllPermissions(Action onAllGranted)
  140. {
  141. var perms = GetRequiredPermissions();
  142. RequestPermissionRecursive(perms, 0, onAllGranted);
  143. }
  144. private void RequestPermissionRecursive(string[] perms, int index, Action onAllGranted)
  145. {
  146. if (index >= perms.Length)
  147. {
  148. onAllGranted?.Invoke();
  149. return;
  150. }
  151. string permission = perms[index];
  152. PermissionManager.Request(
  153. permission,
  154. onGranted: () =>
  155. {
  156. // 继续请求下一项权限
  157. RequestPermissionRecursive(perms, index + 1, onAllGranted);
  158. },
  159. onDenied: () =>
  160. {
  161. Debug.LogError($"[CMDManager] 权限被拒绝:{permission}");
  162. // ---- 调用事件 ----
  163. OnPermissionDenied?.Invoke(permission);
  164. },
  165. onDontAsk: () =>
  166. {
  167. Debug.LogError($"[CMDManager] 权限永久拒绝:{permission}");
  168. // ---- 调用事件 ----
  169. OnPermissionDontAsk?.Invoke(permission);
  170. }
  171. );
  172. }
  173. private void EnsureBLEPermissionThen(Action doWork)
  174. {
  175. if (CheckAllBLEPermissions())
  176. {
  177. doWork?.Invoke();
  178. }
  179. else
  180. {
  181. RequestAllPermissions(() =>
  182. {
  183. doWork?.Invoke();
  184. });
  185. }
  186. }
  187. // ---- 扫描入口 ----
  188. public void StartScan(string pattern)
  189. {
  190. pendingScanPattern = pattern;
  191. EnsureBLEPermissionThen(() =>
  192. {
  193. LaunchTransparentProxyScan(pendingScanPattern);
  194. });
  195. }
  196. // ---- 启动透明 Activity ----
  197. private void LaunchTransparentProxyScan(string pattern)
  198. {
  199. // Scanner Callback
  200. scannerProxy = new CMDScannerCallbackProxy();
  201. scannerProxy.OnDeviceFoundEvent = (name, mac) =>
  202. {
  203. //Debug.Log("[CMDManager] DeviceFound: " + name + " - " + mac);
  204. OnCMDDeviceFound?.Invoke(name, mac);
  205. };
  206. scannerProxy.OnScanFailedEvent = (state,reason) =>
  207. {
  208. //Debug.LogError("[CMDManager] ScanFailed: " + reason);
  209. OnCMDScanFailed?.Invoke(state,reason);
  210. };
  211. // ---- 设置静态回调 ----
  212. transparentProxyActivityClass.SetStatic("globalCMDScannerCallback", scannerProxy);
  213. // ---- 启动 Activity ----
  214. AndroidJavaObject intent = new AndroidJavaObject(
  215. "android.content.Intent",
  216. unityActivity,
  217. new AndroidJavaClass("com.ble.mycdmmanager.CMDTransparentProxyActivity")
  218. );
  219. //扫描设备名字字符串
  220. intent.Call<AndroidJavaObject>("putExtra", "pattern", pattern);
  221. //intent.Call<AndroidJavaObject>("putExtra", "timeout_sec", scanTimeout);
  222. //intent.Call<AndroidJavaObject>("addFlags", 0x10000000); // FLAG_ACTIVITY_NEW_TASK
  223. unityActivity.Call("startActivity", intent);
  224. }
  225. /// <summary>
  226. /// 通过 MAC 地址连接设备
  227. /// </summary>
  228. public bool ConnectMac(string mac)
  229. {
  230. if (string.IsNullOrEmpty(mac)) return false;
  231. bool result = false;
  232. EnsureBLEPermissionThen(() =>
  233. {
  234. result = bleManagerInst.Call<bool>("connectMac", mac);
  235. });
  236. return result;
  237. }
  238. /**
  239. * 断开cmd蓝牙连接
  240. */
  241. public void Disconnect()
  242. {
  243. try
  244. {
  245. bleManagerInst.Call("disconnect");
  246. //清空队列
  247. ClearNotifyQueue();
  248. SmartBowLogger.Log(this, "[CMDManager] Disconnect called (OK)");
  249. }
  250. catch (Exception e)
  251. {
  252. SmartBowLogger.LogError(this, "[CMDManager] Disconnect error: " + e);
  253. }
  254. }
  255. /**
  256. * 清空cmd manager
  257. */
  258. public void Cleanup()
  259. {
  260. try
  261. {
  262. SmartBowLogger.Log(this, "[CMDManager] <Disconnect> begin");
  263. // 1) 停 Worker
  264. StopNotifyWorker();
  265. // 2) 清空队列
  266. ClearNotifyQueue();
  267. // 3) 注销静态回调,避免旧 CMD 的 notify 残留
  268. //try
  269. //{
  270. // // 移除 Scanner 回调
  271. // transparentProxyActivityClass.SetStatic<AndroidJavaObject>("globalCMDScannerCallback", null);
  272. // // 移除 BLE 回调
  273. // bleManagerClass.CallStatic("setGlobalCMDBleCallback", null);
  274. // SmartBowLogger.Log(this, "[CMDManager] static callbacks cleared");
  275. //}
  276. //catch (Exception ex)
  277. //{
  278. // SmartBowLogger.LogError(this, "[CMDManager] clear static callback error:" + ex);
  279. //}
  280. // 4) 最后再断开 Android BLE
  281. bleManagerInst.Call("disconnect");
  282. SmartBowLogger.Log(this, "[CMDManager] Cleanup done");
  283. }
  284. catch (Exception e)
  285. {
  286. SmartBowLogger.LogError(this, "[CMDManager] Cleanup error: " + e);
  287. }
  288. }
  289. public bool SendCommand(string text)
  290. {
  291. if (string.IsNullOrWhiteSpace(text))
  292. return false;
  293. bool success;
  294. // 判断是否 HEX(01 02 03)
  295. if (System.Text.RegularExpressions.Regex.IsMatch(text, @"^([0-9A-Fa-f]{2}\s*)+$"))
  296. {
  297. byte[] bytes = ParseHexString(text);
  298. success = WriteBytes(bytes);
  299. }
  300. else
  301. {
  302. success = WriteString(text);
  303. }
  304. if (!success)
  305. {
  306. SmartBowLogger.LogError(this,"[CMDManager] Send failed, BLE not ready");
  307. return false;
  308. }
  309. SmartBowLogger.Log(this,"[CMDManager] Sent: " + text);
  310. return true;
  311. }
  312. public bool WriteString(string str)
  313. {
  314. try
  315. {
  316. return bleManagerInst.Call<bool>("writeString", str);
  317. }
  318. catch (System.Exception e)
  319. {
  320. SmartBowLogger.LogError(this,"[CMDManager] writeString error: " + e);
  321. return false;
  322. }
  323. }
  324. public bool WriteBytes(byte[] bytes)
  325. {
  326. try
  327. {
  328. return bleManagerInst.Call<bool>("writeBytes", bytes);
  329. }
  330. catch (System.Exception e)
  331. {
  332. SmartBowLogger.LogError(this,"[CMDManager] writeBytes error: " + e);
  333. return false;
  334. }
  335. }
  336. private byte[] ParseHexString(string hex)
  337. {
  338. try
  339. {
  340. string[] parts = hex.Trim().Split(' ');
  341. byte[] bytes = new byte[parts.Length];
  342. for (int i = 0; i < parts.Length; i++)
  343. {
  344. bytes[i] = System.Convert.ToByte(parts[i], 16);
  345. }
  346. return bytes;
  347. }
  348. catch
  349. {
  350. return null;
  351. }
  352. }
  353. // -------- 队列入队(由 CMDBleCallbackProxy 调用) --------
  354. internal void EnqueueNotify(byte[] data)
  355. {
  356. if (data == null) return;
  357. notifyQueue.Enqueue(data);
  358. notifyEvent.Set(); // 唤醒后台线程
  359. }
  360. private void StartNotifyWorker()
  361. {
  362. if (notifyWorkerRunning) return;
  363. notifyWorkerRunning = true;
  364. notifyWorker = new Thread(NotifyWorkerLoop)
  365. {
  366. IsBackground = true,
  367. Name = "CMDManager-NotifyWorker",
  368. Priority = System.Threading.ThreadPriority.AboveNormal // 提升优先级
  369. };
  370. notifyWorker.Start();
  371. }
  372. private void StopNotifyWorker()
  373. {
  374. notifyWorkerRunning = false;
  375. notifyEvent.Set();
  376. try { notifyWorker?.Join(500); } catch { }
  377. notifyWorker = null;
  378. }
  379. private void ClearNotifyQueue()
  380. {
  381. while (notifyQueue.TryDequeue(out _)) ;
  382. SmartBowLogger.Log(this, "[CMDManager] NotifyQueue cleared");
  383. }
  384. private void NotifyWorkerLoop()
  385. {
  386. // 后台线程不断消费队列,尽快完成解析工作
  387. while (notifyWorkerRunning)
  388. {
  389. try
  390. {
  391. // 等待直到有数据
  392. if (!notifyQueue.TryDequeue(out var data))
  393. {
  394. // 等待唤醒或超时
  395. notifyEvent.WaitOne(50);
  396. continue;
  397. }
  398. // 处理单条数据(在后台线程)
  399. ProcessNotifyBackground(data);
  400. // 批量消费:尽可能多地一次性取完队列,以减少唤醒次数
  401. while (notifyQueue.TryDequeue(out data))
  402. {
  403. ProcessNotifyBackground(data);
  404. }
  405. }
  406. catch (Exception ex)
  407. {
  408. Debug.LogError("[CMDManager] NotifyWorkerLoop error: " + ex);
  409. }
  410. }
  411. }
  412. private void ProcessNotifyBackground(byte[] bytes)
  413. {
  414. // 直接传引用,不做限频派发
  415. MainThreadDispatcher.Enqueue(() =>
  416. {
  417. OnCMDBLENotify?.Invoke(bytes);
  418. });
  419. // 后台处理完毕后,只做限频派发
  420. //DispatchToMainThreadWithThrottle(bytes);
  421. }
  422. // 将数据派发到主线程,但节流:每 dispatchIntervalMs 才发一次
  423. private void DispatchToMainThreadWithThrottle(byte[] processed)
  424. {
  425. long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
  426. if (now - lastDispatchTime >= dispatchIntervalMs)
  427. {
  428. lastDispatchTime = now;
  429. // 直接传引用,不做 Array.Copy
  430. MainThreadDispatcher.Enqueue(() =>
  431. {
  432. OnCMDBLENotify?.Invoke(processed);
  433. });
  434. }
  435. }
  436. }
  437. }