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
{
///
/// Windows连接BluetoothLE
/// 我的扫描逻辑默认了读写特征都在同一服务下
/// 扫描策略(对齐文档:广播含 16-bit 0xFFF0 + 名称含 “WF”;现网兼容旧名):
/// - 粗筛 0xFFF0:依赖 DLL 的 AdvertisementWatcher/Combined;纯 DeviceInformationOnly 无法在扫描期按广播 FFF0 过滤。
/// 连接后仍由 targetService(FFF0)GATT 校验。
/// - 精筛名称:默认在设备名中查找子串 (默认 “WF”),**默认不区分大小写**;勾选 后仅字面匹配(如仅 “WF” 不匹配 “wf”)。
/// 关键字可在 Inspector 改为如 “WF_” 以收紧前缀。另可选各 Aim 类型白名单全名(忽略大小写,兼容青凤鸾旧名)。
/// 关闭 则仅走关键字子串(无白名单全名)。
/// - 按 MAC 连接时以 MAC 为准,FFF0 仍后验。
/// - 若 已保存绑定 MAC(bInitMac),默认仅连接该 MAC,避免枪类白名单含 Bbow 时误连弓箭。
///
public class BleWinHelper : MonoBehaviour
{
/// 与文档 #define TARGET_SERVICE_UUID 0xFFF0 一致(Windows 扫描期不可见,仅文档与后验服务 UUID 对应)。
public const ushort BleAdvertisedServiceUuid16 = 0xFFF0;
/// 文档默认 FILTER_NAME_KEY; 留空时不做关键字子串匹配,仅白名单全名(若允许)。
public const string BleFilterNameKeyDefault = "WF";
[Tooltip("名称精筛子串,默认 WF;可改为 WF_ 等以收紧匹配。留空则仅能通过下方白名单全名命中(若允许)。")]
public string bleFilterNameKey = BleFilterNameKeyDefault;
[Tooltip("关=关键字不区分大小写(wf/WF/Wf 均可);开=严格按字面(如仅 WF 命中,wf 不命中)。")]
public bool bleFilterNameKeyCaseSensitive = false;
/// 扫描阶段最长时长(秒),到时停止枚举并由排序后的目标列表开始匹配。
public const float DeviceScanPhaseDurationSeconds = 10f;
/// 目标设备列表最多候选数,满则停止扫描。
public const int MaxScanTargetCandidates = 10;
/// 排序时该旧设备名固定排在列表末尾(与管道串中的名称一致)。
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";
/// 对应文档 16-bit 0xFFF0 的 128-bit GATT Service(连接后粗筛实质校验)。
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> deviceList = new Dictionary>();
private List serviceList = new List();
private List characteristicsList = new List();
private struct ScanCandidateEntry
{
public string DeviceId;
public string Name;
public int RssiDbm;
public bool RssiValid;
public bool IsConnectable;
}
private readonly Dictionary _scanCandidatesById = new Dictionary();
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 OnScanEnded;
private Action OnConnected;
///
/// 主动调用Disconnect()不会触发该委托
///
private Action OnConnectionFailed;
///
/// 注册window对象
///
/// 挂载对象
/// 关联的BluetoothWindows
/// 提示的log标签
///
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.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;
}
///
/// 设置心跳检测
/// 1.每次收到的蓝牙数据都视为心跳
/// 2.帮助触发蓝牙断开监听
///
/// 心跳检测间隔
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();
}
}
}
}
///
/// WinRT 设备 Id 末尾一般为真实 BLE MAC,例如 ...-d3:5c:89:57:68:de
///
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;
}
/// 无广播名时不打空引号,减少无效日志。
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" : "—";
///
/// 逐条 Poll 的详细日志(需 )。与 不同:后者仅在首次进入候选表时打印。
///
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}");
}
/// 某 deviceId 首次进入 时打印一行,便于对照「名称 + MAC + RSSI」。
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}");
}
/// 在 内、排序后打印完整候选列表(与日志中的顺序一致)。
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}");
}
}
/// 仅名称/可连有变化时必打;仅有 RSSI 且仍无广播名则不打,避免刷屏。
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;
}
///
/// 规范式列表:默认仅当「通过精筛」且名称或可连有更新时打印(不刷无广播名周边设备、不刷纯 RSSI)。
/// logAll=true 时退回详细模式(仍遵守 ShouldLogScanLine 对空名的 RSSI 抑制)。
///
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;
}
/// 精筛:名称含 (大小写由 );若 则另允许管道串中全名(忽略大小写,旧设备)。
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;
}
/// 无绑定 MAC 记录时不限制;有记录则 WinRT 设备尾段 MAC 须与存档一致(双向 Contains 兼容格式差异)。
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);
}
/// 存档 MAC 与 WinRT deviceId 尾段是否视为同一设备(双向 Contains);任一侧无法解析为十六进制则返回 false。
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);
}
///
/// 有绑定 MAC 且 :扫描中一旦 WinRT deviceId 尾段与存档 MAC 匹配且可连接,
/// 立即设置 并 (不经 / 候选表)。
/// 随后仍等待 ,由 进入 GATT。
///
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 {
{ "name", "" },
{ "isConnectable", "False" },
{ "rssi", "" },
{ "rssiValid", "False" }
};
return;
}
var d = deviceList[id];
if (!d.ContainsKey("rssi")) d["rssi"] = "";
if (!d.ContainsKey("rssiValid")) d["rssiValid"] = "False";
}
///
/// 将通过精筛的设备写入 (并镜像核心字段到 的维护过程在 Poll 上半段)。
/// 同一 deviceId 重复出现:更新名称 / 可连 / RSSI,不增加计数。
/// 新建条目若使总数达到 :调用 促使 DLL 结束扫描。
///
/// 是否为本轮扫描中首次加入该 deviceId(用于只打一次 )。
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;
}
/// 排序:名称 Ordinal; 置尾;同名按 RSSI 强到弱(dBm 越大越前)。
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";
}
///
/// 扫描已结束且未走快速重连选中:将 按 排序后,
/// 从前往后调用 (结合当前 Aim 类型白名单管道),直到第一个可连且仍过精筛的设备。
///
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;
}
}
///
/// DLL 返回 时调用(且仅处理一次,见 )。
/// 顺序:可选打印候选总表 → 若尚无 则从排序列表挑选 → 启动 (GATT 发现与订阅)→ 回调 。
///
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();
}
}
///
/// 入口:重置连接相关状态 → / → 记录扫描阶段起点与超时秒数。
/// 后续逻辑均在 的 Poll 循环与 中完成。
///
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;
}
}
}