Bläddra i källkod

Merge branch 'NineAxisInfrared' of https://yuyekeji.cn/slambb/smart-bow-infrared into NineAxisInfrared

slambb 1 vecka sedan
förälder
incheckning
358afe5c7d

+ 8 - 0
Assets/AddressableAssetsData/Windows.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ef08063267f7ef0418336f1f3b6a200c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/AddressableAssetsData/Windows/addressables_content_state.bin


+ 7 - 0
Assets/AddressableAssetsData/Windows/addressables_content_state.bin.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a9d1f484409701045af0a59380bcecdf
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 26 - 0
Assets/AddressableAssetsData/link.xml

@@ -0,0 +1,26 @@
+<linker>
+  <assembly fullname="Unity.Addressables, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" preserve="all">
+    <type fullname="UnityEngine.AddressableAssets.Addressables" preserve="all" />
+  </assembly>
+  <assembly fullname="Unity.Localization, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+    <type fullname="UnityEngine.Localization.Locale" preserve="all" />
+    <type fullname="UnityEngine.Localization.Tables.SharedTableData" preserve="all" />
+    <type fullname="UnityEngine.Localization.Tables.StringTable" preserve="all" />
+    <type fullname="UnityEngine.Localization.LocaleIdentifier" preserve="nothing" serialized="true" />
+    <type fullname="UnityEngine.Localization.Metadata.MetadataCollection" preserve="nothing" serialized="true" />
+    <type fullname="UnityEngine.Localization.Tables.DistributedUIDGenerator" preserve="nothing" serialized="true" />
+    <type fullname="UnityEngine.Localization.Tables.SharedTableData/SharedTableEntry" preserve="nothing" serialized="true" />
+    <type fullname="UnityEngine.Localization.Metadata.SmartFormatTag" preserve="nothing" serialized="true" />
+    <type fullname="UnityEngine.Localization.Tables.TableEntryData" preserve="nothing" serialized="true" />
+  </assembly>
+  <assembly fullname="Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" preserve="all">
+    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider" preserve="all" />
+    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider" preserve="all" />
+    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider" preserve="all" />
+    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.LegacyResourcesProvider" preserve="all" />
+    <type fullname="UnityEngine.ResourceManagement.ResourceProviders.SceneProvider" preserve="all" />
+  </assembly>
+  <assembly fullname="UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
+    <type fullname="UnityEngine.Object" preserve="all" />
+  </assembly>
+</linker>

+ 7 - 0
Assets/AddressableAssetsData/link.xml.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 600e723bf87ad764c964f35d6e110e8f
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 1
Assets/BowArrow/Modules/InfraredGuider/InfraredGuider.cs

@@ -49,7 +49,8 @@ public class InfraredGuider : MonoBehaviour
     [Tooltip("异常问题时候文字提示步骤1")]
     public GameObject Step3;
     // Start is called before the first frame update
-
+    [Tooltip("异常问题时候文字提示第一个")]
+    public GameObject Step3_Tip1;
 
     void Start()
     {
@@ -64,7 +65,11 @@ public class InfraredGuider : MonoBehaviour
 
 #if  UNITY_STANDALONE_WIN || UNITY_EDITOR
         resulutionButton.gameObject.SetActive(false);
+        //不 修改分辨率,windows平台暂时不显示这个引导
+        Step3_Tip1.SetActive(false);
 #endif
+
+
         ////枪暂时不显示
         //if (BluetoothAim.ins.isMainConnectToGun()) {
         //    SetTipState(TipStep.None);
@@ -108,6 +113,9 @@ public class InfraredGuider : MonoBehaviour
     {
 #if UNITY_ANDROID || UNITY_IOS
         Instantiate(infraredLightGuider2);
+#endif
+#if UNITY_STANDALONE_WIN || UNITY_EDITOR
+        Instantiate(infraredLightGuider2);
 #endif
     }
     /// <summary>

+ 1 - 0
Assets/BowArrow/Modules/InfraredGuider/InfraredGuider.prefab

@@ -3843,6 +3843,7 @@ MonoBehaviour:
   Step1: {fileID: 3829436517265363255}
   Step2: {fileID: 5578052619716691282}
   Step3: {fileID: 6497584651364846767}
+  Step3_Tip1: {fileID: 3073941200769008743}
   cameraSensitivityBtn: {fileID: 7159374368309948006}
 --- !u!1 &3958523482832117442
 GameObject:

+ 1 - 1
Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs

@@ -1,4 +1,4 @@
-using ArduinoBluetoothAPI;
+using ArduinoBluetoothAPI;
 using System;
 using UnityEngine;
 using System.Collections.Generic;

+ 7 - 3
Assets/BowArrow/Scripts/CommonConfig.cs

@@ -34,7 +34,7 @@ public class CommonConfig
 #endif
 #if UNITY_STANDALONE_WIN
             //pc版
-            int index = 0;
+            int index = 1;
 #endif
             if (index == 0) return 0;
             else return 1;
@@ -157,8 +157,12 @@ public class CommonConfig
     /// <summary>
     /// 隐藏调试按钮
     /// </summary>
-    public static bool bHideInfraredDemoBtnSee { get; } = true;
 
+#if UNITY_EDITOR
+    public static bool bHideInfraredDemoBtnSee { get; } = false;
+#else
+    public static bool bHideInfraredDemoBtnSee { get; } = true;
+#endif
     //打包App 的端,比如打包B 端就设置 B
     public static OperatingPlatform OP { get; } = OperatingPlatform.C;
 
@@ -179,7 +183,7 @@ public class CommonConfig
     /// </summary>
     public static bool bDisplayTwoPlayerGames { get; } = true;
 
-    #endregion
+#endregion
 
 
     //单机版,B端,投币功能

+ 1 - 0
Assets/BowArrow/Scripts/View/Home/DeviceViewInfrared.cs

@@ -369,6 +369,7 @@ public class DeviceViewInfrared : JCUnityLib.ViewBase, MenuBackInterface
                     }
                     break;
                 case 4:
+                    Debug.Log("断开 连接!");
                     //切换设备前先断开蓝牙连接
                     if (selectDeviceViewItem)
                     {

BIN
Assets/Plugins/BleWinrtDll.dll


+ 26 - 1
Assets/SmartBow/SmartBowSDK/BleApi.cs

@@ -5,7 +5,15 @@ public class BleApi
     // dll calls
     public enum ScanStatus { PROCESSING, AVAILABLE, FINISHED };
 
-    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    /// <summary>与 BleWinrtDll SetBleScanMode 一致,须在 StartDeviceScan 前设置。</summary>
+    public enum BleScanMode : int
+    {
+        DeviceInformationOnly = 0,
+        AdvertisementWatcherFff0 = 1,
+        Combined = 2,
+    }
+
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 8)]
     public struct DeviceUpdate
     {
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)]
@@ -18,8 +26,18 @@ public class BleApi
         public string name;
         [MarshalAs(UnmanagedType.I1)]
         public bool nameUpdated;
+        /// <summary>与 BleWinrtDll DeviceUpdate::rssiDbm 对齐;仅当 rssiUpdated 时有效(System.Devices.Aep.SignalStrength,单位 dBm)。</summary>
+        public int rssiDbm;
+        [MarshalAs(UnmanagedType.I1)]
+        public bool rssiUpdated;
     }
 
+    [DllImport("BleWinrtDll.dll", EntryPoint = "SetBleScanMode")]
+    public static extern void SetBleScanMode(BleScanMode mode);
+
+    [DllImport("BleWinrtDll.dll", EntryPoint = "GetBleScanMode")]
+    public static extern BleScanMode GetBleScanMode();
+
     [DllImport("BleWinrtDll.dll", EntryPoint = "StartDeviceScan")]
     public static extern void StartDeviceScan();
 
@@ -78,6 +96,13 @@ public class BleApi
     [DllImport("BleWinrtDll.dll", EntryPoint = "PollData")]
     public static extern bool PollData(out BLEData data, bool block);
 
+    // [合并自 dll文件] Win11 通信优化:订阅后由 DLL 推送线程投递数据,与 StopBlePushThread 成对使用
+    [DllImport("BleWinrtDll", EntryPoint = "StartBlePushThread")]
+    public static extern void StartBlePushThread();
+
+    [DllImport("BleWinrtDll", EntryPoint = "StopBlePushThread")]
+    public static extern void StopBlePushThread();
+
     [DllImport("BleWinrtDll.dll", EntryPoint = "SendData")]
     public static extern bool SendData(in BLEData data, bool block);
 

+ 503 - 129
Assets/SmartBow/SmartBowSDK/BleWinHelper.cs

@@ -1,6 +1,7 @@
-using System;
+using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using UnityEngine;
 using System.Runtime.InteropServices;
@@ -11,22 +12,83 @@ namespace SmartBowSDK_BleWinHelper
     /// <summary>
     /// Windows连接BluetoothLE
     /// 我的扫描逻辑默认了读写特征都在同一服务下
+    /// 扫描策略(对齐文档:广播含 16-bit 0xFFF0 + 名称含 “WF”;现网兼容旧名):
+    /// - 粗筛 0xFFF0:依赖 DLL <see cref="BleApi.BleScanMode"/> 的 AdvertisementWatcher/Combined;纯 DeviceInformationOnly 无法在扫描期按广播 FFF0 过滤。
+    ///   连接后仍由 targetService(FFF0)GATT 校验。
+    /// - 精筛名称:默认在设备名中查找子串 <see cref="bleFilterNameKey"/>(默认 “WF”),**默认不区分大小写**;勾选 <see cref="bleFilterNameKeyCaseSensitive"/> 后仅字面匹配(如仅 “WF” 不匹配 “wf”)。
+    ///   关键字可在 Inspector 改为如 “WF_” 以收紧前缀。另可选各 Aim 类型白名单全名(忽略大小写,兼容青凤鸾旧名)。
+    ///   关闭 <see cref="allowLegacyDeviceNameWhitelist"/> 则仅走关键字子串(无白名单全名)。
+    /// - 按 MAC 连接时以 MAC 为准,FFF0 仍后验。
+    /// - 若 <see cref="AimDeviceInfo"/> 已保存绑定 MAC(bInitMac),默认仅连接该 MAC,避免枪类白名单含 Bbow 时误连弓箭。
     /// </summary>
     public class BleWinHelper : MonoBehaviour
     {
+        /// <summary>与文档 #define TARGET_SERVICE_UUID 0xFFF0 一致(Windows 扫描期不可见,仅文档与后验服务 UUID 对应)。</summary>
+        public const ushort BleAdvertisedServiceUuid16 = 0xFFF0;
+
+        /// <summary>文档默认 FILTER_NAME_KEY;<see cref="bleFilterNameKey"/> 留空时不做关键字子串匹配,仅白名单全名(若允许)。</summary>
+        public const string BleFilterNameKeyDefault = "WF";
+
+        [Tooltip("名称精筛子串,默认 WF;可改为 WF_ 等以收紧匹配。留空则仅能通过下方白名单全名命中(若允许)。")]
+        public string bleFilterNameKey = BleFilterNameKeyDefault;
+
+        [Tooltip("关=关键字不区分大小写(wf/WF/Wf 均可);开=严格按字面(如仅 WF 命中,wf 不命中)。")]
+        public bool bleFilterNameKeyCaseSensitive = false;
+
+        /// <summary>扫描阶段最长时长(秒),到时停止枚举并由排序后的目标列表开始匹配。</summary>
+        public const float DeviceScanPhaseDurationSeconds = 10f;
+
+        /// <summary>目标设备列表最多候选数,满则停止扫描。</summary>
+        public const int MaxScanTargetCandidates = 10;
+
+        /// <summary>排序时该旧设备名固定排在列表末尾(与管道串中的名称一致)。</summary>
+        public const string LegacyDeviceNameSortLast = "Bbow_20210501";
+
+        [Header("BLE 扫描(文档:0xFFF0 广播 + 名称关键字;Windows 见类注释)")]
+        [Tooltip("开=名称含关键字(见 bleFilterNameKey)或白名单全名命中(忽略大小写,旧设备)。关=仅关键字子串(仍受大小写开关影响)。")]
+        public bool allowLegacyDeviceNameWhitelist = true;
+
+        [Tooltip("仅当 logBleVerbosePollStream 开启时生效:开=verbose 流里打印周边/未过精筛;关=verbose 流里只打印精筛候选。")]
+        public bool logAllBleAdvertisements = false;
+
+        [Tooltip("开=每次 Poll 按旧逻辑刷屏(ShouldEmitBleListStyleLog);关=不打印该类逐条日志(推荐)。")]
+        public bool logBleVerbosePollStream = false;
+
+        [Tooltip("开=每新增一台进「目标候选列表」打一行;扫描 FINISHED 再打一次完整列表。关=不打。")]
+        public bool logBleScanListEvents = true;
+
+        [Tooltip("须非 DeviceInformationOnly 才能在扫描期按广播 0xFFF0 过滤无关设备。扫描时长由本脚本 10s / 满 10 台控制。")]
+        public BleApi.BleScanMode bleDllScanMode = BleApi.BleScanMode.Combined;
+
+        [Tooltip("AimDeviceInfo 已保存绑定 MAC 时,仅匹配该 MAC,避免枪类白名单含 Bbow 时误连弓箭后再断开。")]
+        public bool restrictConnectToSavedMacWhenBonded = true;
+
+        [Tooltip("当存档存在绑定 MAC(bInitMac)时:扫描中一旦命中该 MAC 且可连接,立即选中并结束扫描,不经名称筛选与长等待;扫描总时长上限见 fastReconnectScanMaxSeconds。")]
+        public bool enableFastReconnectBySavedMac = true;
+
+        [Tooltip("仅当 enableFastReconnectBySavedMac 且存档有 MAC 时生效:扫描最长秒数(找不到则结束);一般 2~5。")]
+        public float fastReconnectScanMaxSeconds = 3f;
+
+        [Header("BLE 诊断(PollData)")]
+        [Tooltip("开=true:写入 SmartBowSDK.BleWinrtDllSharedInbox,打印每帧从原生队列排空的通知包数量(需工程引用含 BleWinrtDllSharedInbox 的 SmartBowSDK.dll)。")]
+        public bool logBleDllInboxVerbose = false;
+
+        [Tooltip("须与 logBleDllInboxVerbose 同时开启;每包一行 deviceId/len(极易刷屏)。")]
+        public bool logBleDllInboxEachPacket = false;
+
         public string LogTag = "BleWinHelper-Log: ";
-        public bool bDebug = false;
-        private  void Log(string text)
+        public bool bDebug = true;
+        private void Log(string text)
         {
-            if (bDebug)Debug.Log(LogTag + text);
+            if (bDebug) Debug.Log(LogTag + text);
         }
         private void Warn(string text)
         {
-            if (bDebug)Debug.LogWarning(LogTag + text);
+            if (bDebug) Debug.LogWarning(LogTag + text);
         }
-        private  void Error(string text)
+        private void Error(string text)
         {
-            if (bDebug)Debug.Log(LogTag + text);
+            if (bDebug) Debug.Log(LogTag + text);
         }
 
         private string targetDeviceNameAxis = "Bbow_20210501 | ARTEMIS | HOUYI | HOUYI Pro | ARTEMIS Pro";
@@ -35,6 +97,7 @@ namespace SmartBowSDK_BleWinHelper
         private string targetDeviceNameGun = "Pistol | Pistol M9 | Bbow_20210501";
         private string targetDeviceNameGun_M17 = "Pistol M17";
         private string targetDeviceNameGun_M416 = "Rifle M416";
+        /// <summary>对应文档 16-bit 0xFFF0 的 128-bit GATT Service(连接后粗筛实质校验)。</summary>
         private string targetService = "{0000fff0-0000-1000-8000-00805f9b34fb}";
         private string targetCharacteristicsNotify = "{0000fff1-0000-1000-8000-00805f9b34fb}";
         private string targetCharacteristicsWrite = "{0000fff2-0000-1000-8000-00805f9b34fb}";
@@ -54,17 +117,34 @@ namespace SmartBowSDK_BleWinHelper
         private List<string> serviceList = new List<string>();
         private List<string> characteristicsList = new List<string>();
 
+        private struct ScanCandidateEntry
+        {
+            public string DeviceId;
+            public string Name;
+            public int RssiDbm;
+            public bool RssiValid;
+            public bool IsConnectable;
+        }
+
+        private readonly Dictionary<string, ScanCandidateEntry> _scanCandidatesById = new Dictionary<string, ScanCandidateEntry>();
+        private float _deviceScanPhaseStartTime;
+        private float _scanPhaseTimeoutSeconds = DeviceScanPhaseDurationSeconds;
+        private bool _scanPhaseCompletionScheduled;
+
         private float _connectedTime = 0;
         private float _receiveDataTime = 0;
         private float _heartBeatInterval = 0;
 
 
+        // [合并-DLL Win11] 扫描结束回调(是否已选中设备)。DLL 版会接到 bluetoothWindows.OnScanEnded。
+        // 【冲突/对接】若你工程里的 BluetoothWindows 有 OnScanEnded,请在 RegisterTo 里取消下面注释的那一行赋值。
+        private Action<bool> OnScanEnded;
+
         private Action OnConnected;
         /// <summary>
         /// 主动调用Disconnect()不会触发该委托
         /// </summary>
         private Action OnConnectionFailed;
-        private static Action<string, byte[]> OnCharacteristicChanged;
 
         /// <summary>
         /// 注册window对象
@@ -73,17 +153,17 @@ namespace SmartBowSDK_BleWinHelper
         /// <param name="bluetoothWindows">关联的BluetoothWindows</param>
         /// <param name="logTip">提示的log标签</param>
         /// <returns></returns>
-        public static BleWinHelper RegisterTo(GameObject o,BluetoothWindows bluetoothWindows,string logTip = "first")
+        public static BleWinHelper RegisterTo(GameObject o, BluetoothWindows bluetoothWindows, string logTip = "first")
         {
             //if (_Instance)
             //{
             //    Error("Register fail, because only one can be registered.");
             //    return null;
             //}
-       
-            GameObject obj = new GameObject("BleWinHelper"+ logTip);
+
+            GameObject obj = new GameObject("BleWinHelper" + logTip);
             obj.transform.SetParent(o.transform);
-         
+
             BleWinHelper bleWinHelper = obj.AddComponent<BleWinHelper>();
             //日志名字
             bleWinHelper.LogTag = "BleWinHelper-" + logTip + ": ";
@@ -93,12 +173,13 @@ namespace SmartBowSDK_BleWinHelper
             bluetoothWindows.WriteByte = bleWinHelper.WriteByte;
             bleWinHelper.OnConnected = () => bluetoothWindows.OnConnected?.Invoke();
             bleWinHelper.OnConnectionFailed = () => bluetoothWindows.OnConnectionFailed?.Invoke();
-            //多个定义共用一个OnCharacteristicChanged
-            OnCharacteristicChanged += (deviceID,bytes) => {
-                if (deviceID == bleWinHelper.selectedDeviceId) { 
-                
+            bleWinHelper.OnScanEnded = (bool bSelectedDeviceId) => bluetoothWindows.OnScanEnded?.Invoke(bSelectedDeviceId);
+            BleWinrtDllSharedInbox.PacketReceived += (deviceID, bytes) =>
+            {
+                if (deviceID == bleWinHelper.selectedDeviceId && bleWinHelper.isSubscribed)
+                    bleWinHelper._receiveDataTime = Time.realtimeSinceStartup;
+                if (deviceID == bleWinHelper.selectedDeviceId)
                     bluetoothWindows.OnCharacteristicChanged?.Invoke(deviceID, bytes);
-                }
             };
             //bleWinHelper.bDebug = true;
             return bleWinHelper;
@@ -119,109 +200,97 @@ namespace SmartBowSDK_BleWinHelper
 
         void Awake()
         {
-           // _Instance = this;
+            // _Instance = this;
         }
 
         void OnDestroy()
         {
-          //  if (_Instance == this) _Instance = null;
+            //  if (_Instance == this) _Instance = null;
             BleApi.Quit();
         }
 
         void Update()
         {
+            BleWinrtDllSharedInbox.VerboseLogPollDispatch = logBleDllInboxVerbose;
+            BleWinrtDllSharedInbox.LogEachPacket = logBleDllInboxEachPacket;
+            BleWinrtDllSharedInbox.PumpOncePerUnityFrame();
+
             BleApi.ScanStatus status;
+            // -------------------------------------------------------------------------
+            // 设备扫描阶段(DLL:StartDeviceScan → PollDevice 循环 → Stop / FINISHED)
+            // 数据流概览:
+            //   ① DLL 侧(见 bleDllScanMode)可先做广播 0xFFF0 粗筛;② 本脚本做名称关键字 + 白名单精筛;
+            //   ③ 绑定 MAC 策略;④ 可选「存档 MAC 快速命中」;⑤ 其余设备进入排序候选表,扫描结束后再 TrySelect。
+            // -------------------------------------------------------------------------
             if (isScanningDevices)
             {
+                // 每次 Update 尽可能排空 DLL 队列:非阻塞 PollDevice,直到无 AVAILABLE 为止。
                 BleApi.DeviceUpdate res = new BleApi.DeviceUpdate();
                 do
                 {
+                    // block=false:无数据立即返回,避免卡死主线程。
                     status = BleApi.PollDevice(ref res, false);
                     if (status == BleApi.ScanStatus.AVAILABLE)
                     {
-                        if (!deviceList.ContainsKey(res.id))
-                            deviceList[res.id] = new Dictionary<string, string>() {
-                            { "name", "" },
-                            { "isConnectable", "False" }
-                        };
+                        // 已在前面某次 Poll 中选定了设备(快速重连或 FINISHED 前逻辑):跳过后续 AVAILABLE,等 FINISHED。
+                        if (selectedDeviceId != null)
+                            continue;
+
+                        // --- 合并 WinRT 推送的增量字段到 deviceList(按 deviceId 唯一键)---
+                        EnsureDeviceListEntry(res.id);
                         if (res.nameUpdated)
                             deviceList[res.id]["name"] = res.name;
                         if (res.isConnectableUpdated)
                             deviceList[res.id]["isConnectable"] = res.isConnectable.ToString();
+                        if (res.rssiUpdated)
+                        {
+                            deviceList[res.id]["rssi"] = res.rssiDbm.ToString();
+                            deviceList[res.id]["rssiValid"] = "True";
+                        }
 
-                        // 获取当前设备信息
                         string deviceName = deviceList[res.id]["name"];
+                        // 当前帧 isConnectable 或与历史缓存「或」:避免只收到旧缓存时误判不可连。
                         bool isConnectable = res.isConnectable || deviceList[res.id]["isConnectable"] == "True";
 
-                        var type = (AimDeviceType)AimHandler.ins.aimDeviceInfo.type;
-                        //Debug.Log("type:" + type);
-                        if (type == AimDeviceType.HOUYIPRO)
-                        {
-                            TrySelectDevice(targetDeviceNameHOUYIPro, deviceName, isConnectable, "HOUYIPro", res.id);
-                        }
-                        else if (type == AimDeviceType.ARTEMISPRO)
-                        {
-                            TrySelectDevice(targetDeviceName, deviceName, isConnectable, "ARTEMISPRO", res.id);
-                        }
-                        else if (type == AimDeviceType.Gun)
-                        {
-                            //Debug.Log(targetDeviceNameGun + "-----:" + deviceName);
-                            TrySelectDevice(targetDeviceNameGun, deviceName, isConnectable, "Pistol", res.id);
-                        }
-                        else if (type == AimDeviceType.PistolM17)
+                        // --- 分支 A:有存档 MAC 且开启快速重连时,优先只按 MAC 命中(跳过名称精筛与候选表)---
+                        if (TryFastSelectBySavedBondMac(res.id, isConnectable))
+                            continue;
+
+                        // --- 分支 B:名称精筛(关键字 + 可选旧名白名单)+ 绑定 MAC 一致性 ---
+                        // PickFineFilterPipeForCurrentType:按 Aim 类型取 Inspector 管道串,供白名单全名匹配。
+                        string finePipe = PickFineFilterPipeForCurrentType((AimDeviceType)AimHandler.ins.aimDeviceInfo.type);
+                        bool qualifies = DeviceNamePassesFineFilter(deviceName, finePipe)
+                            && WinRtDeviceIdMatchesSavedBondMac(res.id);
+                        // 可选:逐条 Poll 详细日志(易刷屏);仅当 logBleVerbosePollStream 开启时走 ShouldEmitBleListStyleLog。
+                        if (logBleVerbosePollStream && ShouldEmitBleListStyleLog(logAllBleAdvertisements, qualifies, res, deviceName))
+                            LogBleVerbosePollStreamLine(qualifies, deviceName, res.id, res);
+
+                        // --- 分支 C:通过精筛且有名 → 写入「目标候选」字典(最多 MaxScanTargetCandidates),满则请求 DLL 停扫 ---
+                        if (qualifies && !string.IsNullOrWhiteSpace(deviceName))
                         {
-                            TrySelectDevice(targetDeviceNameGun_M17, deviceName, isConnectable, "PistolM17", res.id);
+                            // MaybeUpsertScanCandidate 返回 true 表示本轮是「首次」加入该 deviceId,用于只打一次「新进」日志。
+                            if (MaybeUpsertScanCandidate(res.id, deviceName.Trim(), isConnectable, res) && logBleScanListEvents)
+                                LogBleScanNewCandidateLine(deviceName, res.id, res);
                         }
-                        else if (type == AimDeviceType.RifleM416)
-                        {
-                            TrySelectDevice(targetDeviceNameGun_M416, deviceName, isConnectable, "RifleM416", res.id);
-                        }
-                        else
-                        {
-                            //其余的九轴连接
-                            TrySelectDevice(targetDeviceNameAxis, deviceName, isConnectable, deviceName, res.id);
-                        }
-
-                        //if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.HOUYIPRO)
-                        //{   //需要判断是否是红外弓箭
-                        //    if (targetDeviceNameHOUYIPro.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                        //    {
-                        //        selectedDeviceId = res.id;
-                        //        StopDeviceScan();
-                        //    }
-                        //}
-                        //else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.ARTEMISPRO)
-                        //{   //需要判断是否是ARTEMISPRO弓箭
-                        //    if (targetDeviceName.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                        //    {
-                        //        selectedDeviceId = res.id;
-                        //        StopDeviceScan();
-                        //    }
-
-                        //}
-                        //else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.Gun|| AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.PistolM17|| AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.RifleM416) 
-                        //{
-                        //    //需要判断是否是枪
-                        //    if (targetDeviceNameGun.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                        //    {
-                        //        selectedDeviceId = res.id;
-                        //        StopDeviceScan();
-                        //    }
-                        //}
-                        //else
-                        //{   //其余的九轴连接
-                        //    if (targetDeviceName.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                        //    {
-                        //        selectedDeviceId = res.id;
-                        //        StopDeviceScan();
-                        //    }
-                        //}
                     }
                     else if (status == BleApi.ScanStatus.FINISHED)
                     {
-                        StartCoroutine(ScanServiceAndCharacteristicsToSubscribe());
+                        // DLL 宣布扫描结束:关标志位,再统一做「候选排序 + TrySelect + GATT 协程」。
+                        isScanningDevices = false;
+                        CompleteScanPhaseAndSubscribe();
+                        break;
                     }
                 } while (status == BleApi.ScanStatus.AVAILABLE);
+
+                // --- 主动停扫:候选已满或超过本阶段超时(有存档 MAC 时超时可能缩短为 fastReconnectScanMaxSeconds)---
+                // 仅 native Stop;isScanningDevices 仍为 true,直到随后某帧 Poll 到 FINISHED 再收尾。
+                if (isScanningDevices && selectedDeviceId == null)
+                {
+                    bool full = _scanCandidatesById.Count >= MaxScanTargetCandidates;
+                    bool timeout = Time.realtimeSinceStartup - _deviceScanPhaseStartTime >= _scanPhaseTimeoutSeconds;
+                    if (full || timeout)
+                        BleApi.StopDeviceScan();
+                }
             }
             if (isScanningServices)
             {
@@ -247,20 +316,8 @@ namespace SmartBowSDK_BleWinHelper
                     status = BleApi.PollCharacteristic(out res, false);
                     if (status == BleApi.ScanStatus.AVAILABLE)
                     {
-                        Log("res.userDescription:"+ res.userDescription+ ",res.uuid:" + res.uuid);
-                        if (res.userDescription == "no description available" ||  //旧设备
-                            res.userDescription == "SPP Write Channel" ||   //新设备
-                            res.userDescription == "SPP Read Channel")
-                        {
-                           
-                            characteristicsList.Add(res.uuid);
-                        }
-                        else {
-                            //跟着之前代码
-                            characteristicsList.Add(res.userDescription);
-                        }
-                        //string name = res.userDescription != "no description available" ? res.userDescription : res.uuid;
-                        //characteristicsList.Add(name);
+                        Log("res.userDescription:" + res.userDescription + ",res.uuid:" + res.uuid);
+                        characteristicsList.Add(res.uuid);
                     }
                     else if (status == BleApi.ScanStatus.FINISHED)
                     {
@@ -268,20 +325,6 @@ namespace SmartBowSDK_BleWinHelper
                     }
                 } while (status == BleApi.ScanStatus.AVAILABLE);
             }
-            if (isSubscribed)
-            {
-                BleApi.BLEData res;
-                while (BleApi.PollData(out res, false))
-                {
-                    //string text = BitConverter.ToString(res.buf, 0, res.size);
-                    // string text = Encoding.ASCII.GetString(res.buf, 0, res.size);
-                    byte[] bytes = new byte[res.size];
-                    Array.Copy(res.buf, bytes, res.size);
-                    _receiveDataTime = Time.realtimeSinceStartup;
-                    OnCharacteristicChanged?.Invoke(res.deviceId, bytes);
-
-                }
-            }
             {
                 BleApi.ErrorMessage res;
                 BleApi.GetError(out res);
@@ -297,23 +340,322 @@ namespace SmartBowSDK_BleWinHelper
                 }
             }
         }
+        /// <summary>
+        /// WinRT 设备 Id 末尾一般为真实 BLE MAC,例如 ...-d3:5c:89:57:68:de
+        /// </summary>
+        static string ExtractMacFromDeviceId(string deviceId)
+        {
+            if (string.IsNullOrEmpty(deviceId))
+                return null;
+            int lastDash = deviceId.LastIndexOf('-');
+            if (lastDash >= 0 && lastDash < deviceId.Length - 1)
+                return deviceId.Substring(lastDash + 1);
+            return deviceId;
+        }
+
+        /// <summary>无广播名时不打空引号,减少无效日志。</summary>
+        static string FormatScanDeviceNameForLog(string name) =>
+            string.IsNullOrWhiteSpace(name) ? "无广播名" : name.Trim();
+
+        string BleListMetaDescription()
+        {
+            string keyDisp = string.IsNullOrEmpty(bleFilterNameKey) ? "(无,仅白名单)" : bleFilterNameKey;
+            string caseDisp = bleFilterNameKeyCaseSensitive ? "敏感" : "不敏感";
+            return allowLegacyDeviceNameWhitelist
+                ? $"关键字「{keyDisp}」({caseDisp})或旧名"
+                : $"仅关键字「{keyDisp}」({caseDisp})";
+        }
+
+        static string FormatRssiForLog(BleApi.DeviceUpdate res) =>
+            res.rssiUpdated ? $"{res.rssiDbm} dBm" : "—";
+
+        /// <summary>
+        /// 逐条 Poll 的详细日志(需 <see cref="logBleVerbosePollStream"/>)。与 <see cref="LogBleScanNewCandidateLine"/> 不同:后者仅在首次进入候选表时打印。
+        /// </summary>
+        void LogBleVerbosePollStreamLine(bool qualifies, string deviceName, string deviceId, BleApi.DeviceUpdate res)
+        {
+            string macLog = ExtractMacFromDeviceId(deviceId) ?? deviceId;
+            string rssiLog = FormatRssiForLog(res);
+            string nameDisp = FormatScanDeviceNameForLog(deviceName);
+            string keyDisp = string.IsNullOrEmpty(bleFilterNameKey) ? "(无,仅白名单)" : bleFilterNameKey;
+            string caseDisp = bleFilterNameKeyCaseSensitive ? "敏感" : "不敏感";
+            if (logAllBleAdvertisements)
+                Log($"BLE | 粗:广播0x{BleAdvertisedServiceUuid16:X4}(DLL模式) | 精:{(qualifies ? "候选" : "否")}{(allowLegacyDeviceNameWhitelist ? "+白名单" : $"/仅关键字「{keyDisp}」({caseDisp})")} | 名:{nameDisp} | MAC:{macLog} | RSSI:{rssiLog}");
+            else
+                Log($"BLE列表 | 0x{BleAdvertisedServiceUuid16:X4} | {BleListMetaDescription()} | 名:{nameDisp} | MAC:{macLog} | RSSI:{rssiLog}");
+        }
+
+        /// <summary>某 deviceId 首次进入 <see cref="_scanCandidatesById"/> 时打印一行,便于对照「名称 + MAC + RSSI」。</summary>
+        void LogBleScanNewCandidateLine(string deviceName, string deviceId, BleApi.DeviceUpdate res)
+        {
+            string macLog = ExtractMacFromDeviceId(deviceId) ?? deviceId;
+            string rssiLog = FormatRssiForLog(res);
+            string nameDisp = FormatScanDeviceNameForLog(deviceName);
+            Log($"BLE列表·新进 | 0x{BleAdvertisedServiceUuid16:X4} | {BleListMetaDescription()} | 名:{nameDisp} | MAC:{macLog} | RSSI:{rssiLog}");
+        }
+
+        /// <summary>在 <see cref="CompleteScanPhaseAndSubscribe"/> 内、排序后打印完整候选列表(与日志中的顺序一致)。</summary>
+        void LogBleScanFinalSummary()
+        {
+            var list = _scanCandidatesById.Values.ToList();
+            list.Sort(CompareScanCandidates);
+            Log($"BLE列表·结束 | 共 {list.Count} 台({LegacyDeviceNameSortLast} 置尾 / 同名 RSSI 序)");
+            for (int i = 0; i < list.Count; i++)
+            {
+                var c = list[i];
+                string mac = ExtractMacFromDeviceId(c.DeviceId) ?? c.DeviceId;
+                string rssi = c.RssiValid ? $"{c.RssiDbm} dBm" : "—";
+                Log($"  [{i + 1}] 名:{FormatScanDeviceNameForLog(c.Name)} | MAC:{mac} | RSSI:{rssi} | 可连:{c.IsConnectable}");
+            }
+        }
+
+        /// <summary>仅名称/可连有变化时必打;仅有 RSSI 且仍无广播名则不打,避免刷屏。</summary>
+        static bool ShouldLogScanLine(BleApi.DeviceUpdate res, string deviceNameCached)
+        {
+            if (!(res.nameUpdated || res.isConnectableUpdated || res.rssiUpdated))
+                return false;
+            if (res.rssiUpdated && !res.nameUpdated && !res.isConnectableUpdated && string.IsNullOrWhiteSpace(deviceNameCached))
+                return false;
+            return true;
+        }
+
+        /// <summary>
+        /// 规范式列表:默认仅当「通过精筛」且名称或可连有更新时打印(不刷无广播名周边设备、不刷纯 RSSI)。
+        /// logAll=true 时退回详细模式(仍遵守 ShouldLogScanLine 对空名的 RSSI 抑制)。
+        /// </summary>
+        static bool ShouldEmitBleListStyleLog(bool logAll, bool passesFineFilter, BleApi.DeviceUpdate res, string deviceNameCached)
+        {
+            if (logAll)
+                return ShouldLogScanLine(res, deviceNameCached);
+            if (!passesFineFilter)
+                return false;
+            return res.nameUpdated || res.isConnectableUpdated;
+        }
+
+        /// <summary>精筛:名称含 <see cref="bleFilterNameKey"/>(大小写由 <see cref="bleFilterNameKeyCaseSensitive"/>);若 <see cref="allowLegacyDeviceNameWhitelist"/> 则另允许管道串中全名(忽略大小写,旧设备)。</summary>
+        bool DeviceNamePassesFineFilter(string deviceName, string pipeSeparatedLegacyNames)
+        {
+            if (string.IsNullOrWhiteSpace(deviceName))
+                return false;
+            string trimmed = deviceName.Trim();
+            var keyCmp = bleFilterNameKeyCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+            if (!string.IsNullOrEmpty(bleFilterNameKey) && trimmed.IndexOf(bleFilterNameKey, keyCmp) >= 0)
+                return true;
+            if (!allowLegacyDeviceNameWhitelist)
+                return false;
+            if (string.IsNullOrWhiteSpace(pipeSeparatedLegacyNames))
+                return false;
+            foreach (var part in pipeSeparatedLegacyNames.Split('|'))
+            {
+                if (string.Equals(part.Trim(), trimmed, StringComparison.OrdinalIgnoreCase))
+                    return true;
+            }
+            return false;
+        }
+
+        string PickFineFilterPipeForCurrentType(AimDeviceType type)
+        {
+            if (type == AimDeviceType.HOUYIPRO) return targetDeviceNameHOUYIPro;
+            if (type == AimDeviceType.ARTEMISPRO) return targetDeviceName;
+            if (type == AimDeviceType.Gun) return targetDeviceNameGun;
+            if (type == AimDeviceType.PistolM17) return targetDeviceNameGun_M17;
+            if (type == AimDeviceType.RifleM416) return targetDeviceNameGun_M416;
+            return targetDeviceNameAxis;
+        }
+
+        static string NormalizeMacHexDigits(string mac)
+        {
+            if (string.IsNullOrEmpty(mac)) return "";
+            return string.Concat(mac.Where(Uri.IsHexDigit)).ToUpperInvariant();
+        }
+
+        bool TryGetSavedBondMacFromAim(out string mac)
+        {
+            mac = null;
+            if (AimHandler.ins == null || AimHandler.ins.aimDeviceInfo == null) return false;
+            var info = AimHandler.ins.aimDeviceInfo;
+            if (!info.bInitMac || string.IsNullOrWhiteSpace(info.mac)) return false;
+            mac = info.mac;
+            return true;
+        }
+
+        /// <summary>无绑定 MAC 记录时不限制;有记录则 WinRT 设备尾段 MAC 须与存档一致(双向 Contains 兼容格式差异)。</summary>
+        bool WinRtDeviceIdMatchesSavedBondMac(string deviceId)
+        {
+            if (!restrictConnectToSavedMacWhenBonded) return true;
+            if (!TryGetSavedBondMacFromAim(out string saved)) return true;
+            string a = NormalizeMacHexDigits(ExtractMacFromDeviceId(deviceId));
+            string b = NormalizeMacHexDigits(saved);
+            if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return true;
+            return a.Contains(b) || b.Contains(a);
+        }
+
+        /// <summary>存档 MAC 与 WinRT deviceId 尾段是否视为同一设备(双向 Contains);任一侧无法解析为十六进制则返回 false。</summary>
+        static bool SavedMacMatchesWinRtDeviceId(string savedMac, string deviceId)
+        {
+            string a = NormalizeMacHexDigits(ExtractMacFromDeviceId(deviceId));
+            string b = NormalizeMacHexDigits(savedMac);
+            if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
+            return a.Contains(b) || b.Contains(a);
+        }
+
+        /// <summary>
+        /// 有绑定 MAC 且 <see cref="enableFastReconnectBySavedMac"/>:扫描中一旦 WinRT deviceId 尾段与存档 MAC 匹配且可连接,
+        /// 立即设置 <see cref="selectedDeviceId"/> 并 <see cref="BleApi.StopDeviceScan"/>(不经 <see cref="DeviceNamePassesFineFilter"/> / 候选表)。
+        /// 随后仍等待 <see cref="BleApi.ScanStatus.FINISHED"/>,由 <see cref="CompleteScanPhaseAndSubscribe"/> 进入 GATT。
+        /// </summary>
+        bool TryFastSelectBySavedBondMac(string deviceId, bool isConnectable)
+        {
+            if (!enableFastReconnectBySavedMac || !isConnectable)
+                return false;
+            if (!TryGetSavedBondMacFromAim(out string saved))
+                return false;
+            if (!SavedMacMatchesWinRtDeviceId(saved, deviceId))
+                return false;
+
+            selectedDeviceId = deviceId;
+            BleApi.StopDeviceScan();
+            if (logBleScanListEvents || bDebug)
+                Log($"BLE·快速重连 | 按存档 MAC 命中({NormalizeMacHexDigits(saved)}),跳过名称筛选;等待 FINISHED 后建链。");
+            return true;
+        }
+
+        float ResolveScanPhaseTimeoutSeconds()
+        {
+            if (enableFastReconnectBySavedMac && TryGetSavedBondMacFromAim(out _))
+                return Mathf.Min(DeviceScanPhaseDurationSeconds, Mathf.Max(0.5f, fastReconnectScanMaxSeconds));
+            return DeviceScanPhaseDurationSeconds;
+        }
+
         private bool TrySelectDevice(string filterNames, string deviceName, bool isConnectable, string typeName, string deviceId)
         {
-            //Log($"匹配设备 [{isConnectable}] :{string.IsNullOrWhiteSpace(filterNames)}");
-            if (!isConnectable || string.IsNullOrWhiteSpace(filterNames)) return false;
+            if (!isConnectable || string.IsNullOrWhiteSpace(deviceName)) return false;
+            if (!DeviceNamePassesFineFilter(deviceName, filterNames)) return false;
+            if (!WinRtDeviceIdMatchesSavedBondMac(deviceId)) return false;
+
+            selectedDeviceId = deviceId;
+            StopDeviceScan();
+            Log($"匹配设备 [{typeName}] {FormatScanDeviceNameForLog(deviceName)} | MAC:{ExtractMacFromDeviceId(deviceId) ?? deviceId}");
+            return true;
+        }
+
+        void EnsureDeviceListEntry(string id)
+        {
+            if (!deviceList.ContainsKey(id))
+            {
+                deviceList[id] = new Dictionary<string, string> {
+                    { "name", "" },
+                    { "isConnectable", "False" },
+                    { "rssi", "" },
+                    { "rssiValid", "False" }
+                };
+                return;
+            }
+            var d = deviceList[id];
+            if (!d.ContainsKey("rssi")) d["rssi"] = "";
+            if (!d.ContainsKey("rssiValid")) d["rssiValid"] = "False";
+        }
 
-            string[] filterArray = filterNames.Split('|'); // 支持多个名字
-            foreach (var f in filterArray)
+        /// <summary>
+        /// 将通过精筛的设备写入 <see cref="_scanCandidatesById"/>(并镜像核心字段到 <see cref="deviceList"/> 的维护过程在 Poll 上半段)。
+        /// 同一 deviceId 重复出现:更新名称 / 可连 / RSSI,不增加计数。
+        /// 新建条目若使总数达到 <see cref="MaxScanTargetCandidates"/>:调用 <see cref="BleApi.StopDeviceScan"/> 促使 DLL 结束扫描。
+        /// </summary>
+        /// <returns>是否为本轮扫描中<strong>首次</strong>加入该 deviceId(用于只打一次 <see cref="LogBleScanNewCandidateLine"/>)。</returns>
+        bool MaybeUpsertScanCandidate(string deviceId, string nameTrimmed, bool isConnectable, BleApi.DeviceUpdate res)
+        {
+            if (_scanCandidatesById.TryGetValue(deviceId, out var existing))
             {
-                if (string.Equals(f.Trim(), deviceName.Trim(), StringComparison.OrdinalIgnoreCase))
+                existing.Name = nameTrimmed;
+                existing.IsConnectable = isConnectable || existing.IsConnectable;
+                if (res.rssiUpdated)
                 {
-                    selectedDeviceId = deviceId;
-                    StopDeviceScan();
-                    Log($"匹配设备 [{typeName}] :{deviceName}");
-                    return true;
+                    existing.RssiDbm = res.rssiDbm;
+                    existing.RssiValid = true;
                 }
+                _scanCandidatesById[deviceId] = existing;
+                return false;
             }
-            return false;
+            if (_scanCandidatesById.Count >= MaxScanTargetCandidates)
+                return false;
+            _scanCandidatesById[deviceId] = new ScanCandidateEntry
+            {
+                DeviceId = deviceId,
+                Name = nameTrimmed,
+                IsConnectable = isConnectable,
+                RssiDbm = res.rssiDbm,
+                RssiValid = res.rssiUpdated
+            };
+            if (_scanCandidatesById.Count >= MaxScanTargetCandidates)
+                BleApi.StopDeviceScan();
+            return true;
+        }
+
+        /// <summary>排序:名称 Ordinal;<see cref="LegacyDeviceNameSortLast"/> 置尾;同名按 RSSI 强到弱(dBm 越大越前)。</summary>
+        static int CompareScanCandidates(ScanCandidateEntry a, ScanCandidateEntry b)
+        {
+            bool aBbow = string.Equals(a.Name?.Trim(), LegacyDeviceNameSortLast, StringComparison.Ordinal);
+            bool bBbow = string.Equals(b.Name?.Trim(), LegacyDeviceNameSortLast, StringComparison.Ordinal);
+            if (aBbow != bBbow)
+                return aBbow ? 1 : -1;
+
+            int nameCmp = string.Compare(a.Name, b.Name, StringComparison.Ordinal);
+            if (nameCmp != 0)
+                return nameCmp;
+
+            int ra = a.RssiValid ? a.RssiDbm : int.MinValue;
+            int rb = b.RssiValid ? b.RssiDbm : int.MinValue;
+            return rb.CompareTo(ra);
+        }
+
+        static string TypeNameForLog(AimDeviceType type)
+        {
+            if (type == AimDeviceType.HOUYIPRO) return "HOUYIPro";
+            if (type == AimDeviceType.ARTEMISPRO) return "ARTEMISPRO";
+            if (type == AimDeviceType.Gun) return "Pistol";
+            if (type == AimDeviceType.PistolM17) return "PistolM17";
+            if (type == AimDeviceType.RifleM416) return "RifleM416";
+            return "Axis";
+        }
+
+        /// <summary>
+        /// 扫描已结束且未走快速重连选中:将 <see cref="_scanCandidatesById"/> 按 <see cref="CompareScanCandidates"/> 排序后,
+        /// 从前往后调用 <see cref="TrySelectDevice"/>(结合当前 Aim 类型白名单管道),直到第一个可连且仍过精筛的设备。
+        /// </summary>
+        void TryPickDeviceFromSortedScanCandidates()
+        {
+            if (AimHandler.ins == null || AimHandler.ins.aimDeviceInfo == null)
+                return;
+            var type = (AimDeviceType)AimHandler.ins.aimDeviceInfo.type;
+            string pipe = PickFineFilterPipeForCurrentType(type);
+            string label = TypeNameForLog(type);
+            var list = _scanCandidatesById.Values.ToList();
+            list.Sort(CompareScanCandidates);
+            if (list.Count > 0)
+                Log($"扫描结束:从候选列表按序尝试连接 [{label}]({LegacyDeviceNameSortLast} 置尾 / 同名 RSSI)。");
+            foreach (var c in list)
+            {
+                if (TrySelectDevice(pipe, c.Name, c.IsConnectable, label, c.DeviceId))
+                    return;
+            }
+        }
+
+        /// <summary>
+        /// DLL 返回 <see cref="BleApi.ScanStatus.FINISHED"/> 时调用(且仅处理一次,见 <see cref="_scanPhaseCompletionScheduled"/>)。
+        /// 顺序:可选打印候选总表 → 若尚无 <see cref="selectedDeviceId"/> 则从排序列表挑选 → 启动 <see cref="ScanServiceAndCharacteristicsToSubscribe"/>(GATT 发现与订阅)→ 回调 <see cref="OnScanEnded"/>。
+        /// </summary>
+        void CompleteScanPhaseAndSubscribe()
+        {
+            if (_scanPhaseCompletionScheduled)
+                return;
+            _scanPhaseCompletionScheduled = true;
+            if (logBleScanListEvents)
+                LogBleScanFinalSummary();
+            if (selectedDeviceId == null)
+                TryPickDeviceFromSortedScanCandidates();
+            StartCoroutine(ScanServiceAndCharacteristicsToSubscribe());
+            Log(" ScanStatus FINISHED !");
+            OnScanEnded?.Invoke(selectedDeviceId != null);
         }
 
         void LateUpdate()
@@ -327,9 +669,13 @@ namespace SmartBowSDK_BleWinHelper
             {
                 HandleConnectFail();
             }
-              
+
         }
 
+        /// <summary>
+        /// 入口:重置连接相关状态 → <see cref="BleApi.SetBleScanMode"/> / <see cref="BleApi.StartDeviceScan"/> → 记录扫描阶段起点与超时秒数。
+        /// 后续逻辑均在 <see cref="Update"/> 的 Poll 循环与 <see cref="CompleteScanPhaseAndSubscribe"/> 中完成。
+        /// </summary>
         private bool Connect()
         {
             if (isConnectLocking || isScanningDevices)
@@ -337,10 +683,23 @@ namespace SmartBowSDK_BleWinHelper
                 Warn("Connect Invalid, because is in connect.");
                 return false;
             }
+            // [合并-DLL Win11] 开始连接前重置状态,避免脏数据
+            ReinitAfterConnectFail();
+
+            if (bleDllScanMode == BleApi.BleScanMode.DeviceInformationOnly)
+                Warn("BleDllScanMode=DeviceInformationOnly:扫描期无法按广播 0xFFF0 过滤耳机/手环等,建议 Combined 或 AdvertisementWatcherFff0。");
+            BleApi.SetBleScanMode(bleDllScanMode);
             BleApi.StartDeviceScan();
             isConnectLocking = true;
             isScanningDevices = true;
-            Log("Start Connect");
+            _deviceScanPhaseStartTime = Time.realtimeSinceStartup;
+            _scanPhaseTimeoutSeconds = ResolveScanPhaseTimeoutSeconds();
+            string keyLog = string.IsNullOrEmpty(bleFilterNameKey) ? $"(无子串,仅白名单)" : bleFilterNameKey;
+            string caseLog = bleFilterNameKeyCaseSensitive ? "区分大小写" : "不区分大小写";
+            if (enableFastReconnectBySavedMac && TryGetSavedBondMacFromAim(out string savedMac))
+                Log($"开始扫描:检测到存档 MAC({NormalizeMacHexDigits(savedMac)}),优先按 MAC 快速匹配(≤{_scanPhaseTimeoutSeconds:0.#}s),命中即连;否则再走名称列表。");
+            else
+                Log($"开始扫描:广播 0x{BleAdvertisedServiceUuid16:X4}(DLL 模式)+ 名称子串「{keyLog}」({caseLog})/ 旧名白名单;最长 {_scanPhaseTimeoutSeconds:0.#}s,最多 {MaxScanTargetCandidates} 台候选,结束后按序匹配。");
             return true;
         }
 
@@ -349,6 +708,7 @@ namespace SmartBowSDK_BleWinHelper
             if (!isScanningDevices) return;
             BleApi.StopDeviceScan();
             isScanningDevices = false;
+            Log($"Stop DeviceScan!");
         }
 
         private IEnumerator ScanServiceAndCharacteristicsToSubscribe()
@@ -358,6 +718,7 @@ namespace SmartBowSDK_BleWinHelper
             if (selectedDeviceId == null)
             {
                 HandleConnectFail();
+                // [合并-DLL 冲突/未接入] DLL 版: smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "选择设备 DeviceId 不存在!"); 本脚本无 SmartBowHelper 引用,请按需在上层处理
                 yield break;
             }
             Log("SelectedDeviceId OK");
@@ -383,6 +744,7 @@ namespace SmartBowSDK_BleWinHelper
             if (!findTargetService)
             {
                 HandleConnectFail();
+                // [合并-DLL 冲突/未接入] DLL 版: smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "目标设备服务 service 不存在!");
                 yield break;
             }
 
@@ -412,12 +774,16 @@ namespace SmartBowSDK_BleWinHelper
             if (!findTargetCharacteristicsNotify || !findTargetCharacteristicsWrite)
             {
                 HandleConnectFail();
+                // [合并-DLL 冲突/未接入] DLL 版: smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "目标设备特征值 Characteristics 不存在!");
                 yield break;
             }
 
             BleApi.SubscribeCharacteristic(selectedDeviceId, targetService, targetCharacteristicsNotify, false);
             isSubscribed = true;
             isSubscribing = false;
+
+            // [合并-DLL Win11] 订阅成功后启动 DLL 推送线程(与 HandleConnectFail/Disconnect 中 Stop 成对)
+            BleApi.StartBlePushThread();
             Log("SubscribeCharacteristicNotify OK");
 
             _connectedTime = Time.realtimeSinceStartup;
@@ -426,6 +792,9 @@ namespace SmartBowSDK_BleWinHelper
 
         private void HandleConnectFail()
         {
+            // [合并-DLL Win11] 先停推送线程再断开
+            BleApi.StopBlePushThread();
+
             if (selectedDeviceId != null) BleApi.Disconnect(selectedDeviceId);
             bool isLockBefore = isConnectLocking;
             ReinitAfterConnectFail();
@@ -441,6 +810,8 @@ namespace SmartBowSDK_BleWinHelper
             isSubscribed = false;
             isSubscribing = false;
             selectedDeviceId = null;
+            _scanPhaseCompletionScheduled = false;
+            _scanCandidatesById.Clear();
             deviceList.Clear();
             serviceList.Clear();
             characteristicsList.Clear();
@@ -460,7 +831,7 @@ namespace SmartBowSDK_BleWinHelper
                 data.buf[i] = payload[i];
             // no error code available in non-blocking mode
             BleApi.SendData(in data, false);
-            Log("Write(" + text + ")");
+            //Log("Write(" + text + ")");
             return true;
         }
 
@@ -498,6 +869,9 @@ namespace SmartBowSDK_BleWinHelper
                 Warn("Disconnect Invalid, because is subscribing.");
                 return false;
             }
+            // [合并-DLL Win11]
+            BleApi.StopBlePushThread();
+
             if (isScanningDevices) StopDeviceScan();
             if (selectedDeviceId != null) BleApi.Disconnect(selectedDeviceId);
             ReinitAfterConnectFail();
@@ -505,5 +879,5 @@ namespace SmartBowSDK_BleWinHelper
             return true;
         }
     }
-   
+
 }

BIN
Assets/SmartBow/SmartBowSDK/SmartBowSDK.dll


+ 2 - 2
ProjectSettings/ProjectSettings.asset

@@ -13,7 +13,7 @@ PlayerSettings:
   useOnDemandResources: 0
   accelerometerFrequency: 60
   companyName: JssF
-  productName: "WONDERFITTER \u8FD0\u52A8"
+  productName: WONDERFITTER
   defaultCursor: {fileID: 0}
   cursorHotspot: {x: 0, y: 0}
   m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}
@@ -285,7 +285,7 @@ PlayerSettings:
   - m_BuildTarget: 
     m_Icons:
     - serializedVersion: 2
-      m_Icon: {fileID: 2800000, guid: e1485575cd9962247a6d7cc46fd6d6a9, type: 3}
+      m_Icon: {fileID: 2800000, guid: 5a86b450fbbf31f4d9269beac3ccd441, type: 3}
       m_Width: 128
       m_Height: 128
       m_Kind: 0

+ 171 - 0
docs/0x5A与BLE规范对照说明.md

@@ -0,0 +1,171 @@
+# PC 工程:0x5A 授权流程说明
+
+本文档**以 Unity PC 工程 `smart-bow-pc` 为主**,说明应用侧何时发起 `0x5A` 授权轮询、如何写特征、如何解析 Notify。  
+7 字节组包、累加和与加解密算法在 **`BluetoothDecryptor`** 中实现(源码位于同仓库 **`smart-bow-sdk-dll`**,PC 端通过程序集或同步脚本引用同一逻辑)。
+
+---
+
+## 是否有此行为(结论)
+
+**有。** 在 PC 端当 **`CommonConfig.EnableDecryption`** 与 **`NeedDecryption`** 同时为真时,连接成功后会启动 **`PollingCoroutine`**,约每秒调用 **`SendEncrypt` → `WriteByteData`**,向外设下发 **`0x5A`~`0x5D` 的 7 字节授权请求**;收到首字节为 **`0x5a`** 的 Notify 后走 **`AUTHOR_Decrypt`**,成功后 **`StopEncrypt()`** 并 **`InitWhenConenct()`** 进入业务数据。
+
+---
+
+## PC 端简要流程(用户可读)
+
+1. 用户在 PC 用户端发起蓝牙连接并成功。  
+2. 若未开启上述两个配置,则直接 **`InitWhenConenct()`**,**不发送**授权帧。  
+3. 若已开启:PC 端约 **每 1 秒** 向写特征写入 **7 字节**(`0x5A` + 4 字节密文 + 校验 + `0x5D`)。  
+4. 外设解密后通过 Notify 回 **7 字节**(`0x5A` + 4 字节明文 + 校验 + `0x5D`),算法与规范附件 1、2 一致。  
+5. PC 校验通过后停止轮询,进入后续业务;失败时按 **`BluetoothAim`** 内逻辑可能重连。
+
+```mermaid
+sequenceDiagram
+    participant PC as PC 用户端 Unity
+    participant D as 蓝牙外设
+    PC->>D: BLE 连接成功
+    alt EnableDecryption && NeedDecryption
+        loop 约每秒,直至授权成功
+            PC->>D: Write 7 字节授权请求
+        end
+        D->>PC: Notify 7 字节应答
+        PC->>PC: StopEncrypt,InitWhenConenct
+    else 未开启授权
+        PC->>PC: 直接 InitWhenConenct
+    end
+```
+
+---
+
+## PC 工程代码路径与职责
+
+| 职责 | PC 工程内路径 |
+|------|----------------|
+| 连接成功是否进入授权 | `Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs`:`OnConnected`(约 399~404 行)、`OnConnected_windows1`(约 1595~1600 行)、`OnCMDBLEReady`(约 1970~1974 行) |
+| 每秒轮询与下发 | 同上:`PollingCoroutine`、`SendEncrypt`、`StopEncrypt` |
+| 收到 Notify 后校验授权 | 同上:`OnCharacteristicChanged`(约 1625~1659 行),首字节 `0x5a` 时调用 `BluetoothDecryptor.AUTHOR_Decrypt` |
+| Win 单路 BLE 回调绑定 | 同上:`firstBluetoothWindows.OnConnected = OnConnected_windows1` 等(搜索 `OnConnected_windows1`) |
+| SmartBow CMD 通道 Notify | 同上:`OnCMDBLENotify` → `OnCharacteristicChanged` |
+
+---
+
+## PC 端关键代码节选
+
+**入口:连接成功后启动轮询(通用 BluetoothHelper)**
+
+```399:404:Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs
+                if (CommonConfig.EnableDecryption && NeedDecryption)
+                {
+                    // 这里验证指令,开始请求授权
+                    // 启动轮询协程
+                    StartCoroutine(PollingCoroutine());
+                }
+```
+
+**入口:Windows 单路 BLE 连接成功**
+
+```1595:1600:Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs
+        if (CommonConfig.EnableDecryption && NeedDecryption)
+        {
+            // 这里验证指令,开始请求授权
+            // 启动轮询协程
+            StartCoroutine(PollingCoroutine());
+        }
+```
+
+**入口:SmartBow CMD BLE 特征就绪**
+
+```1970:1974:Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs
+        if (CommonConfig.EnableDecryption && NeedDecryption)
+        {
+            // 这里验证指令,开始请求授权
+            // 启动轮询协程
+            StartCoroutine(PollingCoroutine());
+        }
+```
+
+**轮询与写入写特征**
+
+```553:580:Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs
+    private IEnumerator PollingCoroutine()
+    {
+        // 发送请求
+        // SendEncrypt();
+        // 设置轮询标志
+        isPolling = true;
+        PollingCoroutineCount = 4;
+        uint systemTick = (uint)DateTime.Now.Ticks;
+        while (isPolling)
+        {
+            // 等待一秒
+            yield return new WaitForSeconds(1f);
+            SendEncrypt(systemTick);
+            PollingCoroutineCount--;
+        }
+    }
+    /// <summary>
+    /// 1、加密字节由系统生成的随机码加密而成;
+    /// 2、蓝牙每次断开并重新连接后,会重新生成随机加密值;
+    /// 3、当设备未发送正确的解密信息时,APP会每秒发送1次请求,直到解密成功。
+    /// </summary>
+    /// <param name="systemTick"></param>
+    private void SendEncrypt(uint systemTick)
+    {
+        byte[] sendByte = BluetoothDecryptor.AUTHOR_SendReq(systemTick);
+        Debug.Log("请求sendByte:" + BitConverter.ToString(sendByte));
+        WriteByteData(sendByte);
+    }
+```
+
+**Notify:授权未通过时只处理 `0x5a` 帧**
+
+```1625:1659:Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs
+        if (CommonConfig.EnableDecryption && NeedDecryption)
+        {
+            //Pc 版本先走校验流程
+            if (!BluetoothDecryptor.AUTHOR_IsDecrypt())
+            {
+                if (value[0] == 0x5a)
+                {
+                    // 从硬件读取数据
+                    if (value != null && value.Length > 0)
+                    {
+                        Debug.Log("接收到数据:" + BitConverter.ToString(value));
+                        BluetoothDecryptor.AUTHOR_Decrypt(value);
+                        if (BluetoothDecryptor.AUTHOR_IsDecrypt())
+                        {
+                            Debug.Log("解密成功!");
+                            //解密成功后
+                            StopEncrypt();
+                            //开始连接其他信息
+                            InitWhenConenct();
+                        }
+                        else
+                        {
+                            Debug.Log("解密失败!");
+                            //SideTipView.ShowTip("设备通信失败,断开连接", Color.yellow);
+                            //断开连接等操作
+                            if (PollingCoroutineCount <= 0)
+                            {
+                                StopEncrypt();
+                                DoConnect();
+                            }
+                        }
+                    }
+                }
+                return;
+            }
+        }
+```
+
+---
+
+## 与 SDK 的对应关系(供对照)
+
+PC 端仅调用 **`BluetoothDecryptor.AUTHOR_SendReq` / `AUTHOR_Decrypt` / `AUTHOR_IsDecrypt`**,具体算法与 7 字节布局见:
+
+`smart-bow-sdk-dll/SmartBowSDK/BluetoothDecryptor.cs`(`AUTHOR_SendReq`、`AUTHOR_Decrypt`、`CheckSum`、`AUTHOR_ImprovedEncrypt`、`AUTHOR_ImprovedDecrypt`)。
+
+---
+
+*详细帧格式与附件 1、2 以《蓝牙 BLE 外设设计与通信规范》为准;本文档路径:`smart-bow-pc/docs/0x5A与BLE规范对照说明.md`。*