| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using UnityEngine;
- using System.Runtime.InteropServices;
- using SmartBowSDK;
- 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 = true;
- private void Log(string text)
- {
- if (bDebug) Debug.Log(LogTag + text);
- }
- private void Warn(string text)
- {
- if (bDebug) Debug.LogWarning(LogTag + text);
- }
- private void Error(string text)
- {
- if (bDebug) Debug.Log(LogTag + text);
- }
- private string targetDeviceNameAxis = "Bbow_20210501 | ARTEMIS | HOUYI | HOUYI Pro | ARTEMIS Pro";
- private string targetDeviceName = "Bbow_20210501 | ARTEMIS Pro";
- private string targetDeviceNameHOUYIPro = "HOUYI Pro | Bbow_20210501";
- 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}";
- private bool isConnectLocking = false;
- private bool isScanningDevices = false;
- private bool isScanningServices = false;
- private bool isScanningCharacteristics = false;
- private bool isSubscribed = false;
- private bool isSubscribing = false;
- private string lastError = null;
- [SerializeField]
- private string selectedDeviceId = null;
- private Dictionary<string, Dictionary<string, string>> deviceList = new Dictionary<string, Dictionary<string, string>>();
- 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;
- /// <summary>
- /// 注册window对象
- /// </summary>
- /// <param name="o">挂载对象</param>
- /// <param name="bluetoothWindows">关联的BluetoothWindows</param>
- /// <param name="logTip">提示的log标签</param>
- /// <returns></returns>
- 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);
- obj.transform.SetParent(o.transform);
- BleWinHelper bleWinHelper = obj.AddComponent<BleWinHelper>();
- //日志名字
- bleWinHelper.LogTag = "BleWinHelper-" + logTip + ": ";
- bluetoothWindows.Connect = bleWinHelper.Connect;
- bluetoothWindows.Disconnect = bleWinHelper.Disconnect;
- bluetoothWindows.Write = bleWinHelper.Write;
- bluetoothWindows.WriteByte = bleWinHelper.WriteByte;
- bleWinHelper.OnConnected = () => bluetoothWindows.OnConnected?.Invoke();
- bleWinHelper.OnConnectionFailed = () => bluetoothWindows.OnConnectionFailed?.Invoke();
- 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;
- }
- /// <summary>
- /// 设置心跳检测
- /// 1.每次收到的蓝牙数据都视为心跳
- /// 2.帮助触发蓝牙断开监听
- /// </summary>
- /// <param name="interval">心跳检测间隔</param>
- public void SetHeartBeat(float interval)
- {
- _heartBeatInterval = interval;
- }
- //private static BleWinHelper _Instance;
- void Awake()
- {
- // _Instance = this;
- }
- void OnDestroy()
- {
- // 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)
- {
- // 已在前面某次 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";
- // --- 分支 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))
- {
- // MaybeUpsertScanCandidate 返回 true 表示本轮是「首次」加入该 deviceId,用于只打一次「新进」日志。
- if (MaybeUpsertScanCandidate(res.id, deviceName.Trim(), isConnectable, res) && logBleScanListEvents)
- LogBleScanNewCandidateLine(deviceName, res.id, res);
- }
- }
- else if (status == BleApi.ScanStatus.FINISHED)
- {
- // 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)
- {
- BleApi.Service res;
- do
- {
- status = BleApi.PollService(out res, false);
- if (status == BleApi.ScanStatus.AVAILABLE)
- {
- serviceList.Add(res.uuid);
- }
- else if (status == BleApi.ScanStatus.FINISHED)
- {
- isScanningServices = false;
- }
- } while (status == BleApi.ScanStatus.AVAILABLE);
- }
- if (isScanningCharacteristics)
- {
- BleApi.Characteristic res;
- do
- {
- status = BleApi.PollCharacteristic(out res, false);
- if (status == BleApi.ScanStatus.AVAILABLE)
- {
- Log("res.userDescription:" + res.userDescription + ",res.uuid:" + res.uuid);
- characteristicsList.Add(res.uuid);
- }
- else if (status == BleApi.ScanStatus.FINISHED)
- {
- isScanningCharacteristics = false;
- }
- } while (status == BleApi.ScanStatus.AVAILABLE);
- }
- {
- BleApi.ErrorMessage res;
- BleApi.GetError(out res);
- if (lastError != res.msg)
- {
- Error(res.msg);
- lastError = res.msg;
- //对应设备才断开
- if (lastError.Contains("SendDataAsync") && lastError.Contains(selectedDeviceId) && isSubscribed)
- {
- HandleConnectFail();
- }
- }
- }
- }
- /// <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)
- {
- 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";
- }
- /// <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))
- {
- existing.Name = nameTrimmed;
- existing.IsConnectable = isConnectable || existing.IsConnectable;
- if (res.rssiUpdated)
- {
- existing.RssiDbm = res.rssiDbm;
- existing.RssiValid = true;
- }
- _scanCandidatesById[deviceId] = existing;
- 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()
- {
- if (
- _heartBeatInterval > 0 &&
- isSubscribed &&
- Time.realtimeSinceStartup - _connectedTime >= _heartBeatInterval &&
- Time.realtimeSinceStartup - _receiveDataTime >= _heartBeatInterval
- )
- {
- HandleConnectFail();
- }
- }
- /// <summary>
- /// 入口:重置连接相关状态 → <see cref="BleApi.SetBleScanMode"/> / <see cref="BleApi.StartDeviceScan"/> → 记录扫描阶段起点与超时秒数。
- /// 后续逻辑均在 <see cref="Update"/> 的 Poll 循环与 <see cref="CompleteScanPhaseAndSubscribe"/> 中完成。
- /// </summary>
- private bool Connect()
- {
- if (isConnectLocking || isScanningDevices)
- {
- 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;
- _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;
- }
- private void StopDeviceScan()
- {
- if (!isScanningDevices) return;
- BleApi.StopDeviceScan();
- isScanningDevices = false;
- Log($"Stop DeviceScan!");
- }
- private IEnumerator ScanServiceAndCharacteristicsToSubscribe()
- {
- isSubscribing = true;
- if (selectedDeviceId == null)
- {
- HandleConnectFail();
- // [合并-DLL 冲突/未接入] DLL 版: smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "选择设备 DeviceId 不存在!"); 本脚本无 SmartBowHelper 引用,请按需在上层处理
- yield break;
- }
- Log("SelectedDeviceId OK");
- BleApi.ScanServices(selectedDeviceId);
- isScanningServices = true;
- serviceList.Clear();
- while (isScanningServices) yield return null;
- bool findTargetService = false;
- foreach (string service in serviceList)
- {
- if (service == targetService)
- {
- findTargetService = true;
- Log("FindTargetService OK");
- break;
- }
- }
- if (!findTargetService)
- {
- HandleConnectFail();
- // [合并-DLL 冲突/未接入] DLL 版: smartBowHelper.InvokeOnBluetoothError(BluetoothError.Unknown, "目标设备服务 service 不存在!");
- yield break;
- }
- BleApi.ScanCharacteristics(selectedDeviceId, targetService);
- isScanningCharacteristics = true;
- characteristicsList.Clear();
- while (isScanningCharacteristics) yield return null;
- bool findTargetCharacteristicsNotify = false;
- bool findTargetCharacteristicsWrite = false;
- foreach (string characteristics in characteristicsList)
- {
- if (characteristics == targetCharacteristicsNotify)
- {
- findTargetCharacteristicsNotify = true;
- Log("FindTargetCharacteristicsNotify OK");
- }
- else if (characteristics == targetCharacteristicsWrite)
- {
- findTargetCharacteristicsWrite = true;
- Log("FindTargetCharacteristicsWrite OK");
- }
- }
- 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;
- OnConnected?.Invoke();
- }
- private void HandleConnectFail()
- {
- // [合并-DLL Win11] 先停推送线程再断开
- BleApi.StopBlePushThread();
- if (selectedDeviceId != null) BleApi.Disconnect(selectedDeviceId);
- bool isLockBefore = isConnectLocking;
- ReinitAfterConnectFail();
- if (isLockBefore) OnConnectionFailed?.Invoke();
- }
- private void ReinitAfterConnectFail()
- {
- isConnectLocking = false;
- isScanningDevices = false;
- isScanningServices = false;
- isScanningCharacteristics = false;
- isSubscribed = false;
- isSubscribing = false;
- selectedDeviceId = null;
- _scanPhaseCompletionScheduled = false;
- _scanCandidatesById.Clear();
- deviceList.Clear();
- serviceList.Clear();
- characteristicsList.Clear();
- }
- private bool Write(string text)
- {
- if (!isSubscribed) return false;
- byte[] payload = Encoding.ASCII.GetBytes(text);
- BleApi.BLEData data = new BleApi.BLEData();
- data.buf = new byte[512];
- data.size = (short)payload.Length;
- data.deviceId = selectedDeviceId;
- data.serviceUuid = targetService;
- data.characteristicUuid = targetCharacteristicsWrite;
- for (int i = 0; i < payload.Length; i++)
- data.buf[i] = payload[i];
- // no error code available in non-blocking mode
- BleApi.SendData(in data, false);
- //Log("Write(" + text + ")");
- return true;
- }
- private bool WriteByte(byte[] payload)
- {
- if (!isSubscribed) return false;
- BleApi.BLEData data = new BleApi.BLEData();
- data.buf = new byte[512];
- data.size = (short)payload.Length;
- data.deviceId = selectedDeviceId;
- data.serviceUuid = targetService;
- data.characteristicUuid = targetCharacteristicsWrite;
- for (int i = 0; i < payload.Length; i++)
- data.buf[i] = payload[i];
- // no error code available in non-blocking mode
- BleApi.SendData(in data, false);
- Log("Write(byte[])");
- return true;
- }
- private bool Disconnect()
- {
- if (!isConnectLocking)
- {
- Warn("Disconnect Invalid, because not in connect.");
- return false;
- }
- if (isSubscribing)
- {
- Warn("Disconnect Invalid, because is subscribing.");
- return false;
- }
- // [合并-DLL Win11]
- BleApi.StopBlePushThread();
- if (isScanningDevices) StopDeviceScan();
- if (selectedDeviceId != null) BleApi.Disconnect(selectedDeviceId);
- ReinitAfterConnectFail();
- Log("Disconnect OK");
- return true;
- }
- }
- }
|