BleWinHelper.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using UnityEngine;
  7. using System.Runtime.InteropServices;
  8. using SmartBowSDK;
  9. namespace SmartBowSDK_BleWinHelper
  10. {
  11. /// <summary>
  12. /// Windows连接BluetoothLE
  13. /// 我的扫描逻辑默认了读写特征都在同一服务下
  14. /// 扫描策略(对齐文档:广播含 16-bit 0xFFF0 + 名称含 “WF”;现网兼容旧名):
  15. /// - 粗筛 0xFFF0:依赖 DLL <see cref="BleApi.BleScanMode"/> 的 AdvertisementWatcher/Combined;纯 DeviceInformationOnly 无法在扫描期按广播 FFF0 过滤。
  16. /// 连接后仍由 targetService(FFF0)GATT 校验。
  17. /// - 精筛名称:默认在设备名中查找子串 <see cref="bleFilterNameKey"/>(默认 “WF”),**默认不区分大小写**;勾选 <see cref="bleFilterNameKeyCaseSensitive"/> 后仅字面匹配(如仅 “WF” 不匹配 “wf”)。
  18. /// 关键字可在 Inspector 改为如 “WF_” 以收紧前缀。另可选各 Aim 类型白名单全名(忽略大小写,兼容青凤鸾旧名)。
  19. /// 关闭 <see cref="allowLegacyDeviceNameWhitelist"/> 则仅走关键字子串(无白名单全名)。
  20. /// - 按 MAC 连接时以 MAC 为准,FFF0 仍后验。
  21. /// - 若 <see cref="AimDeviceInfo"/> 已保存绑定 MAC(bInitMac),默认仅连接该 MAC,避免枪类白名单含 Bbow 时误连弓箭。
  22. /// </summary>
  23. public class BleWinHelper : MonoBehaviour
  24. {
  25. /// <summary>与文档 #define TARGET_SERVICE_UUID 0xFFF0 一致(Windows 扫描期不可见,仅文档与后验服务 UUID 对应)。</summary>
  26. public const ushort BleAdvertisedServiceUuid16 = 0xFFF0;
  27. /// <summary>文档默认 FILTER_NAME_KEY;<see cref="bleFilterNameKey"/> 留空时不做关键字子串匹配,仅白名单全名(若允许)。</summary>
  28. public const string BleFilterNameKeyDefault = "WF";
  29. [Tooltip("名称精筛子串,默认 WF;可改为 WF_ 等以收紧匹配。留空则仅能通过下方白名单全名命中(若允许)。")]
  30. public string bleFilterNameKey = BleFilterNameKeyDefault;
  31. [Tooltip("关=关键字不区分大小写(wf/WF/Wf 均可);开=严格按字面(如仅 WF 命中,wf 不命中)。")]
  32. public bool bleFilterNameKeyCaseSensitive = false;
  33. /// <summary>扫描阶段最长时长(秒),到时停止枚举并由排序后的目标列表开始匹配。</summary>
  34. public const float DeviceScanPhaseDurationSeconds = 10f;
  35. /// <summary>目标设备列表最多候选数,满则停止扫描。</summary>
  36. public const int MaxScanTargetCandidates = 10;
  37. /// <summary>排序时该旧设备名固定排在列表末尾(与管道串中的名称一致)。</summary>
  38. public const string LegacyDeviceNameSortLast = "Bbow_20210501";
  39. [Header("BLE 扫描(文档:0xFFF0 广播 + 名称关键字;Windows 见类注释)")]
  40. [Tooltip("开=名称含关键字(见 bleFilterNameKey)或白名单全名命中(忽略大小写,旧设备)。关=仅关键字子串(仍受大小写开关影响)。")]
  41. public bool allowLegacyDeviceNameWhitelist = true;
  42. [Tooltip("仅当 logBleVerbosePollStream 开启时生效:开=verbose 流里打印周边/未过精筛;关=verbose 流里只打印精筛候选。")]
  43. public bool logAllBleAdvertisements = false;
  44. [Tooltip("开=每次 Poll 按旧逻辑刷屏(ShouldEmitBleListStyleLog);关=不打印该类逐条日志(推荐)。")]
  45. public bool logBleVerbosePollStream = false;
  46. [Tooltip("开=每新增一台进「目标候选列表」打一行;扫描 FINISHED 再打一次完整列表。关=不打。")]
  47. public bool logBleScanListEvents = true;
  48. [Tooltip("须非 DeviceInformationOnly 才能在扫描期按广播 0xFFF0 过滤无关设备。扫描时长由本脚本 10s / 满 10 台控制。")]
  49. public BleApi.BleScanMode bleDllScanMode = BleApi.BleScanMode.Combined;
  50. [Tooltip("AimDeviceInfo 已保存绑定 MAC 时,仅匹配该 MAC,避免枪类白名单含 Bbow 时误连弓箭后再断开。")]
  51. public bool restrictConnectToSavedMacWhenBonded = true;
  52. [Tooltip("当存档存在绑定 MAC(bInitMac)时:扫描中一旦命中该 MAC 且可连接,立即选中并结束扫描,不经名称筛选与长等待;扫描总时长上限见 fastReconnectScanMaxSeconds。")]
  53. public bool enableFastReconnectBySavedMac = true;
  54. [Tooltip("仅当 enableFastReconnectBySavedMac 且存档有 MAC 时生效:扫描最长秒数(找不到则结束);一般 2~5。")]
  55. public float fastReconnectScanMaxSeconds = 3f;
  56. [Header("BLE 诊断(PollData)")]
  57. [Tooltip("开=true:写入 SmartBowSDK.BleWinrtDllSharedInbox,打印每帧从原生队列排空的通知包数量(需工程引用含 BleWinrtDllSharedInbox 的 SmartBowSDK.dll)。")]
  58. public bool logBleDllInboxVerbose = false;
  59. [Tooltip("须与 logBleDllInboxVerbose 同时开启;每包一行 deviceId/len(极易刷屏)。")]
  60. public bool logBleDllInboxEachPacket = false;
  61. public string LogTag = "BleWinHelper-Log: ";
  62. public bool bDebug = true;
  63. private void Log(string text)
  64. {
  65. if (bDebug) Debug.Log(LogTag + text);
  66. }
  67. private void Warn(string text)
  68. {
  69. if (bDebug) Debug.LogWarning(LogTag + text);
  70. }
  71. private void Error(string text)
  72. {
  73. if (bDebug) Debug.Log(LogTag + text);
  74. }
  75. private string targetDeviceNameAxis = "Bbow_20210501 | ARTEMIS | HOUYI | HOUYI Pro | ARTEMIS Pro";
  76. private string targetDeviceName = "Bbow_20210501 | ARTEMIS Pro";
  77. private string targetDeviceNameHOUYIPro = "HOUYI Pro | Bbow_20210501";
  78. private string targetDeviceNameGun = "Pistol | Pistol M9 | Bbow_20210501";
  79. private string targetDeviceNameGun_M17 = "Pistol M17";
  80. private string targetDeviceNameGun_M416 = "Rifle M416";
  81. /// <summary>对应文档 16-bit 0xFFF0 的 128-bit GATT Service(连接后粗筛实质校验)。</summary>
  82. private string targetService = "{0000fff0-0000-1000-8000-00805f9b34fb}";
  83. private string targetCharacteristicsNotify = "{0000fff1-0000-1000-8000-00805f9b34fb}";
  84. private string targetCharacteristicsWrite = "{0000fff2-0000-1000-8000-00805f9b34fb}";
  85. private bool isConnectLocking = false;
  86. private bool isScanningDevices = false;
  87. private bool isScanningServices = false;
  88. private bool isScanningCharacteristics = false;
  89. private bool isSubscribed = false;
  90. private bool isSubscribing = false;
  91. private string lastError = null;
  92. [SerializeField]
  93. private string selectedDeviceId = null;
  94. private Dictionary<string, Dictionary<string, string>> deviceList = new Dictionary<string, Dictionary<string, string>>();
  95. private List<string> serviceList = new List<string>();
  96. private List<string> characteristicsList = new List<string>();
  97. private struct ScanCandidateEntry
  98. {
  99. public string DeviceId;
  100. public string Name;
  101. public int RssiDbm;
  102. public bool RssiValid;
  103. public bool IsConnectable;
  104. }
  105. private readonly Dictionary<string, ScanCandidateEntry> _scanCandidatesById = new Dictionary<string, ScanCandidateEntry>();
  106. private float _deviceScanPhaseStartTime;
  107. private float _scanPhaseTimeoutSeconds = DeviceScanPhaseDurationSeconds;
  108. private bool _scanPhaseCompletionScheduled;
  109. private float _connectedTime = 0;
  110. private float _receiveDataTime = 0;
  111. private float _heartBeatInterval = 0;
  112. // [合并-DLL Win11] 扫描结束回调(是否已选中设备)。DLL 版会接到 bluetoothWindows.OnScanEnded。
  113. // 【冲突/对接】若你工程里的 BluetoothWindows 有 OnScanEnded,请在 RegisterTo 里取消下面注释的那一行赋值。
  114. private Action<bool> OnScanEnded;
  115. private Action OnConnected;
  116. /// <summary>
  117. /// 主动调用Disconnect()不会触发该委托
  118. /// </summary>
  119. private Action OnConnectionFailed;
  120. /// <summary>
  121. /// 注册window对象
  122. /// </summary>
  123. /// <param name="o">挂载对象</param>
  124. /// <param name="bluetoothWindows">关联的BluetoothWindows</param>
  125. /// <param name="logTip">提示的log标签</param>
  126. /// <returns></returns>
  127. public static BleWinHelper RegisterTo(GameObject o, BluetoothWindows bluetoothWindows, string logTip = "first")
  128. {
  129. //if (_Instance)
  130. //{
  131. // Error("Register fail, because only one can be registered.");
  132. // return null;
  133. //}
  134. GameObject obj = new GameObject("BleWinHelper" + logTip);
  135. obj.transform.SetParent(o.transform);
  136. BleWinHelper bleWinHelper = obj.AddComponent<BleWinHelper>();
  137. //日志名字
  138. bleWinHelper.LogTag = "BleWinHelper-" + logTip + ": ";
  139. bluetoothWindows.Connect = bleWinHelper.Connect;
  140. bluetoothWindows.Disconnect = bleWinHelper.Disconnect;
  141. bluetoothWindows.Write = bleWinHelper.Write;
  142. bluetoothWindows.WriteByte = bleWinHelper.WriteByte;
  143. bleWinHelper.OnConnected = () => bluetoothWindows.OnConnected?.Invoke();
  144. bleWinHelper.OnConnectionFailed = () => bluetoothWindows.OnConnectionFailed?.Invoke();
  145. bleWinHelper.OnScanEnded = (bool bSelectedDeviceId) => bluetoothWindows.OnScanEnded?.Invoke(bSelectedDeviceId);
  146. BleWinrtDllSharedInbox.PacketReceived += (deviceID, bytes) =>
  147. {
  148. if (deviceID == bleWinHelper.selectedDeviceId && bleWinHelper.isSubscribed)
  149. bleWinHelper._receiveDataTime = Time.realtimeSinceStartup;
  150. if (deviceID == bleWinHelper.selectedDeviceId)
  151. bluetoothWindows.OnCharacteristicChanged?.Invoke(deviceID, bytes);
  152. };
  153. //bleWinHelper.bDebug = true;
  154. return bleWinHelper;
  155. }
  156. /// <summary>
  157. /// 设置心跳检测
  158. /// 1.每次收到的蓝牙数据都视为心跳
  159. /// 2.帮助触发蓝牙断开监听
  160. /// </summary>
  161. /// <param name="interval">心跳检测间隔</param>
  162. public void SetHeartBeat(float interval)
  163. {
  164. _heartBeatInterval = interval;
  165. }
  166. //private static BleWinHelper _Instance;
  167. void Awake()
  168. {
  169. // _Instance = this;
  170. }
  171. void OnDestroy()
  172. {
  173. // if (_Instance == this) _Instance = null;
  174. BleApi.Quit();
  175. }
  176. void Update()
  177. {
  178. BleWinrtDllSharedInbox.VerboseLogPollDispatch = logBleDllInboxVerbose;
  179. BleWinrtDllSharedInbox.LogEachPacket = logBleDllInboxEachPacket;
  180. BleWinrtDllSharedInbox.PumpOncePerUnityFrame();
  181. BleApi.ScanStatus status;
  182. // -------------------------------------------------------------------------
  183. // 设备扫描阶段(DLL:StartDeviceScan → PollDevice 循环 → Stop / FINISHED)
  184. // 数据流概览:
  185. // ① DLL 侧(见 bleDllScanMode)可先做广播 0xFFF0 粗筛;② 本脚本做名称关键字 + 白名单精筛;
  186. // ③ 绑定 MAC 策略;④ 可选「存档 MAC 快速命中」;⑤ 其余设备进入排序候选表,扫描结束后再 TrySelect。
  187. // -------------------------------------------------------------------------
  188. if (isScanningDevices)
  189. {
  190. // 每次 Update 尽可能排空 DLL 队列:非阻塞 PollDevice,直到无 AVAILABLE 为止。
  191. BleApi.DeviceUpdate res = new BleApi.DeviceUpdate();
  192. do
  193. {
  194. // block=false:无数据立即返回,避免卡死主线程。
  195. status = BleApi.PollDevice(ref res, false);
  196. if (status == BleApi.ScanStatus.AVAILABLE)
  197. {
  198. // 已在前面某次 Poll 中选定了设备(快速重连或 FINISHED 前逻辑):跳过后续 AVAILABLE,等 FINISHED。
  199. if (selectedDeviceId != null)
  200. continue;
  201. // --- 合并 WinRT 推送的增量字段到 deviceList(按 deviceId 唯一键)---
  202. EnsureDeviceListEntry(res.id);
  203. if (res.nameUpdated)
  204. deviceList[res.id]["name"] = res.name;
  205. if (res.isConnectableUpdated)
  206. deviceList[res.id]["isConnectable"] = res.isConnectable.ToString();
  207. if (res.rssiUpdated)
  208. {
  209. deviceList[res.id]["rssi"] = res.rssiDbm.ToString();
  210. deviceList[res.id]["rssiValid"] = "True";
  211. }
  212. string deviceName = deviceList[res.id]["name"];
  213. // 当前帧 isConnectable 或与历史缓存「或」:避免只收到旧缓存时误判不可连。
  214. bool isConnectable = res.isConnectable || deviceList[res.id]["isConnectable"] == "True";
  215. // --- 分支 A:有存档 MAC 且开启快速重连时,优先只按 MAC 命中(跳过名称精筛与候选表)---
  216. if (TryFastSelectBySavedBondMac(res.id, isConnectable))
  217. continue;
  218. // --- 分支 B:名称精筛(关键字 + 可选旧名白名单)+ 绑定 MAC 一致性 ---
  219. // PickFineFilterPipeForCurrentType:按 Aim 类型取 Inspector 管道串,供白名单全名匹配。
  220. string finePipe = PickFineFilterPipeForCurrentType((AimDeviceType)AimHandler.ins.aimDeviceInfo.type);
  221. bool qualifies = DeviceNamePassesFineFilter(deviceName, finePipe)
  222. && WinRtDeviceIdMatchesSavedBondMac(res.id);
  223. // 可选:逐条 Poll 详细日志(易刷屏);仅当 logBleVerbosePollStream 开启时走 ShouldEmitBleListStyleLog。
  224. if (logBleVerbosePollStream && ShouldEmitBleListStyleLog(logAllBleAdvertisements, qualifies, res, deviceName))
  225. LogBleVerbosePollStreamLine(qualifies, deviceName, res.id, res);
  226. // --- 分支 C:通过精筛且有名 → 写入「目标候选」字典(最多 MaxScanTargetCandidates),满则请求 DLL 停扫 ---
  227. if (qualifies && !string.IsNullOrWhiteSpace(deviceName))
  228. {
  229. // MaybeUpsertScanCandidate 返回 true 表示本轮是「首次」加入该 deviceId,用于只打一次「新进」日志。
  230. if (MaybeUpsertScanCandidate(res.id, deviceName.Trim(), isConnectable, res) && logBleScanListEvents)
  231. LogBleScanNewCandidateLine(deviceName, res.id, res);
  232. }
  233. }
  234. else if (status == BleApi.ScanStatus.FINISHED)
  235. {
  236. // DLL 宣布扫描结束:关标志位,再统一做「候选排序 + TrySelect + GATT 协程」。
  237. isScanningDevices = false;
  238. CompleteScanPhaseAndSubscribe();
  239. break;
  240. }
  241. } while (status == BleApi.ScanStatus.AVAILABLE);
  242. // --- 主动停扫:候选已满或超过本阶段超时(有存档 MAC 时超时可能缩短为 fastReconnectScanMaxSeconds)---
  243. // 仅 native Stop;isScanningDevices 仍为 true,直到随后某帧 Poll 到 FINISHED 再收尾。
  244. if (isScanningDevices && selectedDeviceId == null)
  245. {
  246. bool full = _scanCandidatesById.Count >= MaxScanTargetCandidates;
  247. bool timeout = Time.realtimeSinceStartup - _deviceScanPhaseStartTime >= _scanPhaseTimeoutSeconds;
  248. if (full || timeout)
  249. BleApi.StopDeviceScan();
  250. }
  251. }
  252. if (isScanningServices)
  253. {
  254. BleApi.Service res;
  255. do
  256. {
  257. status = BleApi.PollService(out res, false);
  258. if (status == BleApi.ScanStatus.AVAILABLE)
  259. {
  260. serviceList.Add(res.uuid);
  261. }
  262. else if (status == BleApi.ScanStatus.FINISHED)
  263. {
  264. isScanningServices = false;
  265. }
  266. } while (status == BleApi.ScanStatus.AVAILABLE);
  267. }
  268. if (isScanningCharacteristics)
  269. {
  270. BleApi.Characteristic res;
  271. do
  272. {
  273. status = BleApi.PollCharacteristic(out res, false);
  274. if (status == BleApi.ScanStatus.AVAILABLE)
  275. {
  276. Log("res.userDescription:" + res.userDescription + ",res.uuid:" + res.uuid);
  277. characteristicsList.Add(res.uuid);
  278. }
  279. else if (status == BleApi.ScanStatus.FINISHED)
  280. {
  281. isScanningCharacteristics = false;
  282. }
  283. } while (status == BleApi.ScanStatus.AVAILABLE);
  284. }
  285. {
  286. BleApi.ErrorMessage res;
  287. BleApi.GetError(out res);
  288. if (lastError != res.msg)
  289. {
  290. Error(res.msg);
  291. lastError = res.msg;
  292. //对应设备才断开
  293. if (lastError.Contains("SendDataAsync") && lastError.Contains(selectedDeviceId) && isSubscribed)
  294. {
  295. HandleConnectFail();
  296. }
  297. }
  298. }
  299. }
  300. /// <summary>
  301. /// WinRT 设备 Id 末尾一般为真实 BLE MAC,例如 ...-d3:5c:89:57:68:de
  302. /// </summary>
  303. static string ExtractMacFromDeviceId(string deviceId)
  304. {
  305. if (string.IsNullOrEmpty(deviceId))
  306. return null;
  307. int lastDash = deviceId.LastIndexOf('-');
  308. if (lastDash >= 0 && lastDash < deviceId.Length - 1)
  309. return deviceId.Substring(lastDash + 1);
  310. return deviceId;
  311. }
  312. /// <summary>无广播名时不打空引号,减少无效日志。</summary>
  313. static string FormatScanDeviceNameForLog(string name) =>
  314. string.IsNullOrWhiteSpace(name) ? "无广播名" : name.Trim();
  315. string BleListMetaDescription()
  316. {
  317. string keyDisp = string.IsNullOrEmpty(bleFilterNameKey) ? "(无,仅白名单)" : bleFilterNameKey;
  318. string caseDisp = bleFilterNameKeyCaseSensitive ? "敏感" : "不敏感";
  319. return allowLegacyDeviceNameWhitelist
  320. ? $"关键字「{keyDisp}」({caseDisp})或旧名"
  321. : $"仅关键字「{keyDisp}」({caseDisp})";
  322. }
  323. static string FormatRssiForLog(BleApi.DeviceUpdate res) =>
  324. res.rssiUpdated ? $"{res.rssiDbm} dBm" : "—";
  325. /// <summary>
  326. /// 逐条 Poll 的详细日志(需 <see cref="logBleVerbosePollStream"/>)。与 <see cref="LogBleScanNewCandidateLine"/> 不同:后者仅在首次进入候选表时打印。
  327. /// </summary>
  328. void LogBleVerbosePollStreamLine(bool qualifies, string deviceName, string deviceId, BleApi.DeviceUpdate res)
  329. {
  330. string macLog = ExtractMacFromDeviceId(deviceId) ?? deviceId;
  331. string rssiLog = FormatRssiForLog(res);
  332. string nameDisp = FormatScanDeviceNameForLog(deviceName);
  333. string keyDisp = string.IsNullOrEmpty(bleFilterNameKey) ? "(无,仅白名单)" : bleFilterNameKey;
  334. string caseDisp = bleFilterNameKeyCaseSensitive ? "敏感" : "不敏感";
  335. if (logAllBleAdvertisements)
  336. Log($"BLE | 粗:广播0x{BleAdvertisedServiceUuid16:X4}(DLL模式) | 精:{(qualifies ? "候选" : "否")}{(allowLegacyDeviceNameWhitelist ? "+白名单" : $"/仅关键字「{keyDisp}」({caseDisp})")} | 名:{nameDisp} | MAC:{macLog} | RSSI:{rssiLog}");
  337. else
  338. Log($"BLE列表 | 0x{BleAdvertisedServiceUuid16:X4} | {BleListMetaDescription()} | 名:{nameDisp} | MAC:{macLog} | RSSI:{rssiLog}");
  339. }
  340. /// <summary>某 deviceId 首次进入 <see cref="_scanCandidatesById"/> 时打印一行,便于对照「名称 + MAC + RSSI」。</summary>
  341. void LogBleScanNewCandidateLine(string deviceName, string deviceId, BleApi.DeviceUpdate res)
  342. {
  343. string macLog = ExtractMacFromDeviceId(deviceId) ?? deviceId;
  344. string rssiLog = FormatRssiForLog(res);
  345. string nameDisp = FormatScanDeviceNameForLog(deviceName);
  346. Log($"BLE列表·新进 | 0x{BleAdvertisedServiceUuid16:X4} | {BleListMetaDescription()} | 名:{nameDisp} | MAC:{macLog} | RSSI:{rssiLog}");
  347. }
  348. /// <summary>在 <see cref="CompleteScanPhaseAndSubscribe"/> 内、排序后打印完整候选列表(与日志中的顺序一致)。</summary>
  349. void LogBleScanFinalSummary()
  350. {
  351. var list = _scanCandidatesById.Values.ToList();
  352. list.Sort(CompareScanCandidates);
  353. Log($"BLE列表·结束 | 共 {list.Count} 台({LegacyDeviceNameSortLast} 置尾 / 同名 RSSI 序)");
  354. for (int i = 0; i < list.Count; i++)
  355. {
  356. var c = list[i];
  357. string mac = ExtractMacFromDeviceId(c.DeviceId) ?? c.DeviceId;
  358. string rssi = c.RssiValid ? $"{c.RssiDbm} dBm" : "—";
  359. Log($" [{i + 1}] 名:{FormatScanDeviceNameForLog(c.Name)} | MAC:{mac} | RSSI:{rssi} | 可连:{c.IsConnectable}");
  360. }
  361. }
  362. /// <summary>仅名称/可连有变化时必打;仅有 RSSI 且仍无广播名则不打,避免刷屏。</summary>
  363. static bool ShouldLogScanLine(BleApi.DeviceUpdate res, string deviceNameCached)
  364. {
  365. if (!(res.nameUpdated || res.isConnectableUpdated || res.rssiUpdated))
  366. return false;
  367. if (res.rssiUpdated && !res.nameUpdated && !res.isConnectableUpdated && string.IsNullOrWhiteSpace(deviceNameCached))
  368. return false;
  369. return true;
  370. }
  371. /// <summary>
  372. /// 规范式列表:默认仅当「通过精筛」且名称或可连有更新时打印(不刷无广播名周边设备、不刷纯 RSSI)。
  373. /// logAll=true 时退回详细模式(仍遵守 ShouldLogScanLine 对空名的 RSSI 抑制)。
  374. /// </summary>
  375. static bool ShouldEmitBleListStyleLog(bool logAll, bool passesFineFilter, BleApi.DeviceUpdate res, string deviceNameCached)
  376. {
  377. if (logAll)
  378. return ShouldLogScanLine(res, deviceNameCached);
  379. if (!passesFineFilter)
  380. return false;
  381. return res.nameUpdated || res.isConnectableUpdated;
  382. }
  383. /// <summary>精筛:名称含 <see cref="bleFilterNameKey"/>(大小写由 <see cref="bleFilterNameKeyCaseSensitive"/>);若 <see cref="allowLegacyDeviceNameWhitelist"/> 则另允许管道串中全名(忽略大小写,旧设备)。</summary>
  384. bool DeviceNamePassesFineFilter(string deviceName, string pipeSeparatedLegacyNames)
  385. {
  386. if (string.IsNullOrWhiteSpace(deviceName))
  387. return false;
  388. string trimmed = deviceName.Trim();
  389. var keyCmp = bleFilterNameKeyCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
  390. if (!string.IsNullOrEmpty(bleFilterNameKey) && trimmed.IndexOf(bleFilterNameKey, keyCmp) >= 0)
  391. return true;
  392. if (!allowLegacyDeviceNameWhitelist)
  393. return false;
  394. if (string.IsNullOrWhiteSpace(pipeSeparatedLegacyNames))
  395. return false;
  396. foreach (var part in pipeSeparatedLegacyNames.Split('|'))
  397. {
  398. if (string.Equals(part.Trim(), trimmed, StringComparison.OrdinalIgnoreCase))
  399. return true;
  400. }
  401. return false;
  402. }
  403. string PickFineFilterPipeForCurrentType(AimDeviceType type)
  404. {
  405. if (type == AimDeviceType.HOUYIPRO) return targetDeviceNameHOUYIPro;
  406. if (type == AimDeviceType.ARTEMISPRO) return targetDeviceName;
  407. if (type == AimDeviceType.Gun) return targetDeviceNameGun;
  408. if (type == AimDeviceType.PistolM17) return targetDeviceNameGun_M17;
  409. if (type == AimDeviceType.RifleM416) return targetDeviceNameGun_M416;
  410. return targetDeviceNameAxis;
  411. }
  412. static string NormalizeMacHexDigits(string mac)
  413. {
  414. if (string.IsNullOrEmpty(mac)) return "";
  415. return string.Concat(mac.Where(Uri.IsHexDigit)).ToUpperInvariant();
  416. }
  417. bool TryGetSavedBondMacFromAim(out string mac)
  418. {
  419. mac = null;
  420. if (AimHandler.ins == null || AimHandler.ins.aimDeviceInfo == null) return false;
  421. var info = AimHandler.ins.aimDeviceInfo;
  422. if (!info.bInitMac || string.IsNullOrWhiteSpace(info.mac)) return false;
  423. mac = info.mac;
  424. return true;
  425. }
  426. /// <summary>无绑定 MAC 记录时不限制;有记录则 WinRT 设备尾段 MAC 须与存档一致(双向 Contains 兼容格式差异)。</summary>
  427. bool WinRtDeviceIdMatchesSavedBondMac(string deviceId)
  428. {
  429. if (!restrictConnectToSavedMacWhenBonded) return true;
  430. if (!TryGetSavedBondMacFromAim(out string saved)) return true;
  431. string a = NormalizeMacHexDigits(ExtractMacFromDeviceId(deviceId));
  432. string b = NormalizeMacHexDigits(saved);
  433. if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return true;
  434. return a.Contains(b) || b.Contains(a);
  435. }
  436. /// <summary>存档 MAC 与 WinRT deviceId 尾段是否视为同一设备(双向 Contains);任一侧无法解析为十六进制则返回 false。</summary>
  437. static bool SavedMacMatchesWinRtDeviceId(string savedMac, string deviceId)
  438. {
  439. string a = NormalizeMacHexDigits(ExtractMacFromDeviceId(deviceId));
  440. string b = NormalizeMacHexDigits(savedMac);
  441. if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
  442. return a.Contains(b) || b.Contains(a);
  443. }
  444. /// <summary>
  445. /// 有绑定 MAC 且 <see cref="enableFastReconnectBySavedMac"/>:扫描中一旦 WinRT deviceId 尾段与存档 MAC 匹配且可连接,
  446. /// 立即设置 <see cref="selectedDeviceId"/> 并 <see cref="BleApi.StopDeviceScan"/>(不经 <see cref="DeviceNamePassesFineFilter"/> / 候选表)。
  447. /// 随后仍等待 <see cref="BleApi.ScanStatus.FINISHED"/>,由 <see cref="CompleteScanPhaseAndSubscribe"/> 进入 GATT。
  448. /// </summary>
  449. bool TryFastSelectBySavedBondMac(string deviceId, bool isConnectable)
  450. {
  451. if (!enableFastReconnectBySavedMac || !isConnectable)
  452. return false;
  453. if (!TryGetSavedBondMacFromAim(out string saved))
  454. return false;
  455. if (!SavedMacMatchesWinRtDeviceId(saved, deviceId))
  456. return false;
  457. selectedDeviceId = deviceId;
  458. BleApi.StopDeviceScan();
  459. if (logBleScanListEvents || bDebug)
  460. Log($"BLE·快速重连 | 按存档 MAC 命中({NormalizeMacHexDigits(saved)}),跳过名称筛选;等待 FINISHED 后建链。");
  461. return true;
  462. }
  463. float ResolveScanPhaseTimeoutSeconds()
  464. {
  465. if (enableFastReconnectBySavedMac && TryGetSavedBondMacFromAim(out _))
  466. return Mathf.Min(DeviceScanPhaseDurationSeconds, Mathf.Max(0.5f, fastReconnectScanMaxSeconds));
  467. return DeviceScanPhaseDurationSeconds;
  468. }
  469. private bool TrySelectDevice(string filterNames, string deviceName, bool isConnectable, string typeName, string deviceId)
  470. {
  471. if (!isConnectable || string.IsNullOrWhiteSpace(deviceName)) return false;
  472. if (!DeviceNamePassesFineFilter(deviceName, filterNames)) return false;
  473. if (!WinRtDeviceIdMatchesSavedBondMac(deviceId)) return false;
  474. selectedDeviceId = deviceId;
  475. StopDeviceScan();
  476. Log($"匹配设备 [{typeName}] {FormatScanDeviceNameForLog(deviceName)} | MAC:{ExtractMacFromDeviceId(deviceId) ?? deviceId}");
  477. return true;
  478. }
  479. void EnsureDeviceListEntry(string id)
  480. {
  481. if (!deviceList.ContainsKey(id))
  482. {
  483. deviceList[id] = new Dictionary<string, string> {
  484. { "name", "" },
  485. { "isConnectable", "False" },
  486. { "rssi", "" },
  487. { "rssiValid", "False" }
  488. };
  489. return;
  490. }
  491. var d = deviceList[id];
  492. if (!d.ContainsKey("rssi")) d["rssi"] = "";
  493. if (!d.ContainsKey("rssiValid")) d["rssiValid"] = "False";
  494. }
  495. /// <summary>
  496. /// 将通过精筛的设备写入 <see cref="_scanCandidatesById"/>(并镜像核心字段到 <see cref="deviceList"/> 的维护过程在 Poll 上半段)。
  497. /// 同一 deviceId 重复出现:更新名称 / 可连 / RSSI,不增加计数。
  498. /// 新建条目若使总数达到 <see cref="MaxScanTargetCandidates"/>:调用 <see cref="BleApi.StopDeviceScan"/> 促使 DLL 结束扫描。
  499. /// </summary>
  500. /// <returns>是否为本轮扫描中<strong>首次</strong>加入该 deviceId(用于只打一次 <see cref="LogBleScanNewCandidateLine"/>)。</returns>
  501. bool MaybeUpsertScanCandidate(string deviceId, string nameTrimmed, bool isConnectable, BleApi.DeviceUpdate res)
  502. {
  503. if (_scanCandidatesById.TryGetValue(deviceId, out var existing))
  504. {
  505. existing.Name = nameTrimmed;
  506. existing.IsConnectable = isConnectable || existing.IsConnectable;
  507. if (res.rssiUpdated)
  508. {
  509. existing.RssiDbm = res.rssiDbm;
  510. existing.RssiValid = true;
  511. }
  512. _scanCandidatesById[deviceId] = existing;
  513. return false;
  514. }
  515. if (_scanCandidatesById.Count >= MaxScanTargetCandidates)
  516. return false;
  517. _scanCandidatesById[deviceId] = new ScanCandidateEntry
  518. {
  519. DeviceId = deviceId,
  520. Name = nameTrimmed,
  521. IsConnectable = isConnectable,
  522. RssiDbm = res.rssiDbm,
  523. RssiValid = res.rssiUpdated
  524. };
  525. if (_scanCandidatesById.Count >= MaxScanTargetCandidates)
  526. BleApi.StopDeviceScan();
  527. return true;
  528. }
  529. /// <summary>排序:名称 Ordinal;<see cref="LegacyDeviceNameSortLast"/> 置尾;同名按 RSSI 强到弱(dBm 越大越前)。</summary>
  530. static int CompareScanCandidates(ScanCandidateEntry a, ScanCandidateEntry b)
  531. {
  532. bool aBbow = string.Equals(a.Name?.Trim(), LegacyDeviceNameSortLast, StringComparison.Ordinal);
  533. bool bBbow = string.Equals(b.Name?.Trim(), LegacyDeviceNameSortLast, StringComparison.Ordinal);
  534. if (aBbow != bBbow)
  535. return aBbow ? 1 : -1;
  536. int nameCmp = string.Compare(a.Name, b.Name, StringComparison.Ordinal);
  537. if (nameCmp != 0)
  538. return nameCmp;
  539. int ra = a.RssiValid ? a.RssiDbm : int.MinValue;
  540. int rb = b.RssiValid ? b.RssiDbm : int.MinValue;
  541. return rb.CompareTo(ra);
  542. }
  543. static string TypeNameForLog(AimDeviceType type)
  544. {
  545. if (type == AimDeviceType.HOUYIPRO) return "HOUYIPro";
  546. if (type == AimDeviceType.ARTEMISPRO) return "ARTEMISPRO";
  547. if (type == AimDeviceType.Gun) return "Pistol";
  548. if (type == AimDeviceType.PistolM17) return "PistolM17";
  549. if (type == AimDeviceType.RifleM416) return "RifleM416";
  550. return "Axis";
  551. }
  552. /// <summary>
  553. /// 扫描已结束且未走快速重连选中:将 <see cref="_scanCandidatesById"/> 按 <see cref="CompareScanCandidates"/> 排序后,
  554. /// 从前往后调用 <see cref="TrySelectDevice"/>(结合当前 Aim 类型白名单管道),直到第一个可连且仍过精筛的设备。
  555. /// </summary>
  556. void TryPickDeviceFromSortedScanCandidates()
  557. {
  558. if (AimHandler.ins == null || AimHandler.ins.aimDeviceInfo == null)
  559. return;
  560. var type = (AimDeviceType)AimHandler.ins.aimDeviceInfo.type;
  561. string pipe = PickFineFilterPipeForCurrentType(type);
  562. string label = TypeNameForLog(type);
  563. var list = _scanCandidatesById.Values.ToList();
  564. list.Sort(CompareScanCandidates);
  565. if (list.Count > 0)
  566. Log($"扫描结束:从候选列表按序尝试连接 [{label}]({LegacyDeviceNameSortLast} 置尾 / 同名 RSSI)。");
  567. foreach (var c in list)
  568. {
  569. if (TrySelectDevice(pipe, c.Name, c.IsConnectable, label, c.DeviceId))
  570. return;
  571. }
  572. }
  573. /// <summary>
  574. /// DLL 返回 <see cref="BleApi.ScanStatus.FINISHED"/> 时调用(且仅处理一次,见 <see cref="_scanPhaseCompletionScheduled"/>)。
  575. /// 顺序:可选打印候选总表 → 若尚无 <see cref="selectedDeviceId"/> 则从排序列表挑选 → 启动 <see cref="ScanServiceAndCharacteristicsToSubscribe"/>(GATT 发现与订阅)→ 回调 <see cref="OnScanEnded"/>。
  576. /// </summary>
  577. void CompleteScanPhaseAndSubscribe()
  578. {
  579. if (_scanPhaseCompletionScheduled)
  580. return;
  581. _scanPhaseCompletionScheduled = true;
  582. if (logBleScanListEvents)
  583. LogBleScanFinalSummary();
  584. if (selectedDeviceId == null)
  585. TryPickDeviceFromSortedScanCandidates();
  586. StartCoroutine(ScanServiceAndCharacteristicsToSubscribe());
  587. Log(" ScanStatus FINISHED !");
  588. OnScanEnded?.Invoke(selectedDeviceId != null);
  589. }
  590. void LateUpdate()
  591. {
  592. if (
  593. _heartBeatInterval > 0 &&
  594. isSubscribed &&
  595. Time.realtimeSinceStartup - _connectedTime >= _heartBeatInterval &&
  596. Time.realtimeSinceStartup - _receiveDataTime >= _heartBeatInterval
  597. )
  598. {
  599. HandleConnectFail();
  600. }
  601. }
  602. /// <summary>
  603. /// 入口:重置连接相关状态 → <see cref="BleApi.SetBleScanMode"/> / <see cref="BleApi.StartDeviceScan"/> → 记录扫描阶段起点与超时秒数。
  604. /// 后续逻辑均在 <see cref="Update"/> 的 Poll 循环与 <see cref="CompleteScanPhaseAndSubscribe"/> 中完成。
  605. /// </summary>
  606. private bool Connect()
  607. {
  608. if (isConnectLocking || isScanningDevices)
  609. {
  610. Warn("Connect Invalid, because is in connect.");
  611. return false;
  612. }
  613. // [合并-DLL Win11] 开始连接前重置状态,避免脏数据
  614. ReinitAfterConnectFail();
  615. if (bleDllScanMode == BleApi.BleScanMode.DeviceInformationOnly)
  616. Warn("BleDllScanMode=DeviceInformationOnly:扫描期无法按广播 0xFFF0 过滤耳机/手环等,建议 Combined 或 AdvertisementWatcherFff0。");
  617. BleApi.SetBleScanMode(bleDllScanMode);
  618. BleApi.StartDeviceScan();
  619. isConnectLocking = true;
  620. isScanningDevices = true;
  621. _deviceScanPhaseStartTime = Time.realtimeSinceStartup;
  622. _scanPhaseTimeoutSeconds = ResolveScanPhaseTimeoutSeconds();
  623. string keyLog = string.IsNullOrEmpty(bleFilterNameKey) ? $"(无子串,仅白名单)" : bleFilterNameKey;
  624. string caseLog = bleFilterNameKeyCaseSensitive ? "区分大小写" : "不区分大小写";
  625. if (enableFastReconnectBySavedMac && TryGetSavedBondMacFromAim(out string savedMac))
  626. Log($"开始扫描:检测到存档 MAC({NormalizeMacHexDigits(savedMac)}),优先按 MAC 快速匹配(≤{_scanPhaseTimeoutSeconds:0.#}s),命中即连;否则再走名称列表。");
  627. else
  628. Log($"开始扫描:广播 0x{BleAdvertisedServiceUuid16:X4}(DLL 模式)+ 名称子串「{keyLog}」({caseLog})/ 旧名白名单;最长 {_scanPhaseTimeoutSeconds:0.#}s,最多 {MaxScanTargetCandidates} 台候选,结束后按序匹配。");
  629. return true;
  630. }
  631. private void StopDeviceScan()
  632. {
  633. if (!isScanningDevices) return;
  634. BleApi.StopDeviceScan();
  635. isScanningDevices = false;
  636. Log($"Stop DeviceScan!");
  637. }
  638. private IEnumerator ScanServiceAndCharacteristicsToSubscribe()
  639. {
  640. isSubscribing = true;
  641. if (selectedDeviceId == null)
  642. {
  643. HandleConnectFail();
  644. // [合并-DLL 冲突/未接入] DLL 版: smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "选择设备 DeviceId 不存在!"); 本脚本无 SmartBowHelper 引用,请按需在上层处理
  645. yield break;
  646. }
  647. Log("SelectedDeviceId OK");
  648. BleApi.ScanServices(selectedDeviceId);
  649. isScanningServices = true;
  650. serviceList.Clear();
  651. while (isScanningServices) yield return null;
  652. bool findTargetService = false;
  653. foreach (string service in serviceList)
  654. {
  655. if (service == targetService)
  656. {
  657. findTargetService = true;
  658. Log("FindTargetService OK");
  659. break;
  660. }
  661. }
  662. if (!findTargetService)
  663. {
  664. HandleConnectFail();
  665. // [合并-DLL 冲突/未接入] DLL 版: smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "目标设备服务 service 不存在!");
  666. yield break;
  667. }
  668. BleApi.ScanCharacteristics(selectedDeviceId, targetService);
  669. isScanningCharacteristics = true;
  670. characteristicsList.Clear();
  671. while (isScanningCharacteristics) yield return null;
  672. bool findTargetCharacteristicsNotify = false;
  673. bool findTargetCharacteristicsWrite = false;
  674. foreach (string characteristics in characteristicsList)
  675. {
  676. if (characteristics == targetCharacteristicsNotify)
  677. {
  678. findTargetCharacteristicsNotify = true;
  679. Log("FindTargetCharacteristicsNotify OK");
  680. }
  681. else if (characteristics == targetCharacteristicsWrite)
  682. {
  683. findTargetCharacteristicsWrite = true;
  684. Log("FindTargetCharacteristicsWrite OK");
  685. }
  686. }
  687. if (!findTargetCharacteristicsNotify || !findTargetCharacteristicsWrite)
  688. {
  689. HandleConnectFail();
  690. // [合并-DLL 冲突/未接入] DLL 版: smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "目标设备特征值 Characteristics 不存在!");
  691. yield break;
  692. }
  693. BleApi.SubscribeCharacteristic(selectedDeviceId, targetService, targetCharacteristicsNotify, false);
  694. isSubscribed = true;
  695. isSubscribing = false;
  696. // [合并-DLL Win11] 订阅成功后启动 DLL 推送线程(与 HandleConnectFail/Disconnect 中 Stop 成对)
  697. BleApi.StartBlePushThread();
  698. Log("SubscribeCharacteristicNotify OK");
  699. _connectedTime = Time.realtimeSinceStartup;
  700. OnConnected?.Invoke();
  701. }
  702. private void HandleConnectFail()
  703. {
  704. // [合并-DLL Win11] 先停推送线程再断开
  705. BleApi.StopBlePushThread();
  706. if (selectedDeviceId != null) BleApi.Disconnect(selectedDeviceId);
  707. bool isLockBefore = isConnectLocking;
  708. ReinitAfterConnectFail();
  709. if (isLockBefore) OnConnectionFailed?.Invoke();
  710. }
  711. private void ReinitAfterConnectFail()
  712. {
  713. isConnectLocking = false;
  714. isScanningDevices = false;
  715. isScanningServices = false;
  716. isScanningCharacteristics = false;
  717. isSubscribed = false;
  718. isSubscribing = false;
  719. selectedDeviceId = null;
  720. _scanPhaseCompletionScheduled = false;
  721. _scanCandidatesById.Clear();
  722. deviceList.Clear();
  723. serviceList.Clear();
  724. characteristicsList.Clear();
  725. }
  726. private bool Write(string text)
  727. {
  728. if (!isSubscribed) return false;
  729. byte[] payload = Encoding.ASCII.GetBytes(text);
  730. BleApi.BLEData data = new BleApi.BLEData();
  731. data.buf = new byte[512];
  732. data.size = (short)payload.Length;
  733. data.deviceId = selectedDeviceId;
  734. data.serviceUuid = targetService;
  735. data.characteristicUuid = targetCharacteristicsWrite;
  736. for (int i = 0; i < payload.Length; i++)
  737. data.buf[i] = payload[i];
  738. // no error code available in non-blocking mode
  739. BleApi.SendData(in data, false);
  740. //Log("Write(" + text + ")");
  741. return true;
  742. }
  743. private bool WriteByte(byte[] payload)
  744. {
  745. if (!isSubscribed) return false;
  746. BleApi.BLEData data = new BleApi.BLEData();
  747. data.buf = new byte[512];
  748. data.size = (short)payload.Length;
  749. data.deviceId = selectedDeviceId;
  750. data.serviceUuid = targetService;
  751. data.characteristicUuid = targetCharacteristicsWrite;
  752. for (int i = 0; i < payload.Length; i++)
  753. data.buf[i] = payload[i];
  754. // no error code available in non-blocking mode
  755. BleApi.SendData(in data, false);
  756. Log("Write(byte[])");
  757. return true;
  758. }
  759. private bool Disconnect()
  760. {
  761. if (!isConnectLocking)
  762. {
  763. Warn("Disconnect Invalid, because not in connect.");
  764. return false;
  765. }
  766. if (isSubscribing)
  767. {
  768. Warn("Disconnect Invalid, because is subscribing.");
  769. return false;
  770. }
  771. // [合并-DLL Win11]
  772. BleApi.StopBlePushThread();
  773. if (isScanningDevices) StopDeviceScan();
  774. if (selectedDeviceId != null) BleApi.Disconnect(selectedDeviceId);
  775. ReinitAfterConnectFail();
  776. Log("Disconnect OK");
  777. return true;
  778. }
  779. }
  780. }