419 lines
17 KiB
C#
419 lines
17 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using Windows.Devices.Enumeration;
|
|
using Windows.Devices.Bluetooth;
|
|
using Windows.Devices.Bluetooth.Advertisement;
|
|
using Windows.Foundation;
|
|
|
|
class DeviceTarget {
|
|
public string Key; // "K370", "M240"
|
|
public string Name; // "LOGI K370 B", "LOGI M240 B"
|
|
public bool IsKeyboard;
|
|
public BluetoothLEDevice PairedBle;
|
|
public string Status = ""; // "CONNECTED", "PAIRED_WAITING", "SEARCHING_FOR_PAIR"
|
|
|
|
public DeviceTarget(string key, string name, bool isKeyboard) {
|
|
this.Key = key;
|
|
this.Name = name;
|
|
this.IsKeyboard = isKeyboard;
|
|
}
|
|
}
|
|
|
|
class BlePairer {
|
|
static MethodInfo asTaskGeneric = null;
|
|
static Dictionary<string, DeviceTarget> targets = new Dictionary<string, DeviceTarget>() {
|
|
{ "K370", new DeviceTarget("K370", "LOGI K370 B", true) },
|
|
{ "M240", new DeviceTarget("M240", "LOGI M240 B", false) }
|
|
};
|
|
|
|
static ConcurrentDictionary<string, DateTime> liveRfMacs = new ConcurrentDictionary<string, DateTime>();
|
|
static ConcurrentDictionary<string, DeviceInformation> uncollectedDevices = new ConcurrentDictionary<string, DeviceInformation>();
|
|
|
|
static object pairingLock = new object();
|
|
static bool isPairingInProgress = false;
|
|
static string activePairingKey = "";
|
|
|
|
static BluetoothLEAdvertisementWatcher advWatcher = null;
|
|
static DeviceWatcher devWatcher = null;
|
|
static bool isScanningActive = false;
|
|
static object scanLock = new object();
|
|
|
|
static T AwaitOp<T>(object asyncOp, int timeoutMs) {
|
|
if (asyncOp == null) return default(T);
|
|
if (asTaskGeneric == null) {
|
|
var methods = typeof(System.WindowsRuntimeSystemExtensions).GetMethods();
|
|
foreach (var m in methods) {
|
|
if (m.Name == "AsTask" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.Name == "IAsyncOperation`1") {
|
|
asTaskGeneric = m;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
var method = asTaskGeneric.MakeGenericMethod(typeof(T));
|
|
object task = method.Invoke(null, new object[] { asyncOp });
|
|
var waitMethod = task.GetType().GetMethod("Wait", new Type[] { typeof(int) });
|
|
bool finished = (bool)waitMethod.Invoke(task, new object[] { timeoutMs });
|
|
if (finished) {
|
|
var resultProp = task.GetType().GetProperty("Result");
|
|
return (T)resultProp.GetValue(task, null);
|
|
}
|
|
return default(T);
|
|
}
|
|
|
|
static string FormatMac(ulong rawAddr) {
|
|
string hex = rawAddr.ToString("X12").ToLower();
|
|
return string.Format("{0}:{1}:{2}:{3}:{4}:{5}",
|
|
hex.Substring(0, 2), hex.Substring(2, 2), hex.Substring(4, 2),
|
|
hex.Substring(6, 2), hex.Substring(8, 2), hex.Substring(10, 2));
|
|
}
|
|
|
|
static DeviceTarget GetTargetForName(string name) {
|
|
if (string.IsNullOrEmpty(name)) return null;
|
|
string upper = name.ToUpper();
|
|
if (upper.Contains("K370")) return targets["K370"];
|
|
if (upper.Contains("M240")) return targets["M240"];
|
|
return null;
|
|
}
|
|
|
|
static void SetDeviceStatus(DeviceTarget target, string newStatus) {
|
|
lock (scanLock) {
|
|
if (target.Status != newStatus) {
|
|
target.Status = newStatus;
|
|
Console.WriteLine("STATUS:" + target.Key + ":" + newStatus);
|
|
UpdateScannerState();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void UpdateScannerState() {
|
|
lock (scanLock) {
|
|
bool allConnected = true;
|
|
foreach (var t in targets.Values) {
|
|
if (t.Status != "CONNECTED") {
|
|
allConnected = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (allConnected) {
|
|
StopScanners();
|
|
} else {
|
|
StartScanners();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void StartScanners() {
|
|
lock (scanLock) {
|
|
if (isScanningActive) return;
|
|
|
|
try {
|
|
if (advWatcher != null) {
|
|
advWatcher.ScanningMode = BluetoothLEScanningMode.Passive; // Passive 모드: 전파 송출 없이 청취만 수행
|
|
advWatcher.Start();
|
|
}
|
|
if (devWatcher != null && devWatcher.Status != DeviceWatcherStatus.Started && devWatcher.Status != DeviceWatcherStatus.EnumerationCompleted) {
|
|
devWatcher.Start();
|
|
}
|
|
isScanningActive = true;
|
|
Console.WriteLine("[DEBUG] Scanners started (Passive mode, zero radio contention)");
|
|
} catch (Exception ex) {
|
|
Console.WriteLine("[DEBUG] StartScanners error: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void StopScanners() {
|
|
lock (scanLock) {
|
|
if (!isScanningActive) return;
|
|
|
|
try {
|
|
if (advWatcher != null) {
|
|
advWatcher.Stop();
|
|
}
|
|
if (devWatcher != null && (devWatcher.Status == DeviceWatcherStatus.Started || devWatcher.Status == DeviceWatcherStatus.EnumerationCompleted)) {
|
|
devWatcher.Stop();
|
|
}
|
|
isScanningActive = false;
|
|
Console.WriteLine("[DEBUG] Scanners stopped (All devices connected, radio fully released)");
|
|
} catch (Exception ex) {
|
|
Console.WriteLine("[DEBUG] StopScanners error: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void AttachPairedDevice(DeviceTarget target, BluetoothLEDevice ble) {
|
|
if (ble == null) return;
|
|
lock (scanLock) {
|
|
if (target.PairedBle != null && target.PairedBle.DeviceId != ble.DeviceId) {
|
|
try { target.PairedBle.Dispose(); } catch {}
|
|
}
|
|
target.PairedBle = ble;
|
|
target.PairedBle.ConnectionStatusChanged += (s, e) => {
|
|
Console.WriteLine("[DEBUG] " + target.Name + " ConnectionStatusChanged: " + s.ConnectionStatus);
|
|
if (s.ConnectionStatus == BluetoothConnectionStatus.Connected) {
|
|
SetDeviceStatus(target, "CONNECTED");
|
|
} else {
|
|
SetDeviceStatus(target, "PAIRED_WAITING");
|
|
}
|
|
};
|
|
|
|
Console.WriteLine("[DEBUG] Attached to " + target.Name + " (" + FormatMac(ble.BluetoothAddress) + "), Conn: " + ble.ConnectionStatus);
|
|
if (ble.ConnectionStatus == BluetoothConnectionStatus.Connected) {
|
|
SetDeviceStatus(target, "CONNECTED");
|
|
} else {
|
|
SetDeviceStatus(target, "PAIRED_WAITING");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void OnPairingRequested(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs eventArgs) {
|
|
Console.WriteLine("[DEBUG] PairingRequested fired! Mode: " + eventArgs.PairingKind);
|
|
switch (eventArgs.PairingKind) {
|
|
case DevicePairingKinds.ConfirmOnly:
|
|
eventArgs.Accept();
|
|
break;
|
|
case DevicePairingKinds.ProvidePin:
|
|
Console.WriteLine("[DEBUG] ProvidePin requested: using '0000'");
|
|
eventArgs.Accept("0000");
|
|
break;
|
|
case DevicePairingKinds.ConfirmPinMatch:
|
|
case DevicePairingKinds.DisplayPin:
|
|
if (!string.IsNullOrEmpty(eventArgs.Pin)) {
|
|
Console.WriteLine("PIN_REQUIRED:" + activePairingKey + ":" + eventArgs.Pin);
|
|
}
|
|
eventArgs.Accept();
|
|
break;
|
|
default:
|
|
eventArgs.Accept();
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void TryPairDevice(DeviceInformation dev, string mac, DeviceTarget target) {
|
|
lock (pairingLock) {
|
|
if (isPairingInProgress) return;
|
|
isPairingInProgress = true;
|
|
activePairingKey = target.Key;
|
|
}
|
|
|
|
try {
|
|
Console.WriteLine("DEVICE_DETECTED:" + target.Key + ":" + mac);
|
|
Console.WriteLine("[DEBUG] Initiating pairing with live " + target.Name + ": " + mac);
|
|
|
|
var customPairing = dev.Pairing.Custom;
|
|
customPairing.PairingRequested += new TypedEventHandler<DeviceInformationCustomPairing, DevicePairingRequestedEventArgs>(OnPairingRequested);
|
|
|
|
var kinds = DevicePairingKinds.ConfirmOnly |
|
|
DevicePairingKinds.ProvidePin |
|
|
DevicePairingKinds.ConfirmPinMatch |
|
|
DevicePairingKinds.DisplayPin;
|
|
|
|
var opPair = customPairing.PairAsync(kinds, DevicePairingProtectionLevel.Default);
|
|
var res = AwaitOp<DevicePairingResult>(opPair, 60000); // Wait up to 60s for PIN if keyboard
|
|
|
|
if (res != null && (res.Status == DevicePairingResultStatus.Paired || res.Status == DevicePairingResultStatus.AlreadyPaired)) {
|
|
Console.WriteLine("PAIRED_AND_CONNECTED:" + target.Key + ":" + mac);
|
|
|
|
// 새 장치로 BluetoothLEDevice 바인딩
|
|
try {
|
|
var opBle = BluetoothLEDevice.FromIdAsync(dev.Id);
|
|
var newBle = AwaitOp<BluetoothLEDevice>(opBle, 2000);
|
|
if (newBle != null) {
|
|
AttachPairedDevice(target, newBle);
|
|
}
|
|
} catch {}
|
|
|
|
// 이전 구버전 MAC 인스턴스 정리
|
|
try {
|
|
var opAll = DeviceInformation.FindAllAsync(BluetoothLEDevice.GetDeviceSelector());
|
|
var allDevs = AwaitOp<DeviceInformationCollection>(opAll, 3000);
|
|
if (allDevs != null) {
|
|
foreach (var d in allDevs) {
|
|
if (d.Id != dev.Id && GetTargetForName(d.Name) == target) {
|
|
var opOld = BluetoothLEDevice.FromIdAsync(d.Id);
|
|
var oldBle = AwaitOp<BluetoothLEDevice>(opOld, 2000);
|
|
if (oldBle != null) {
|
|
if (oldBle.DeviceInformation.Pairing.IsPaired) {
|
|
string oldMac = FormatMac(oldBle.BluetoothAddress);
|
|
if (oldMac != mac) {
|
|
Console.WriteLine("[DEBUG] Cleaning up stale paired instance: " + oldMac);
|
|
var unpairOp = oldBle.DeviceInformation.Pairing.UnpairAsync();
|
|
AwaitOp<DeviceUnpairingResult>(unpairOp, 3000);
|
|
}
|
|
}
|
|
oldBle.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch {}
|
|
} else {
|
|
Console.WriteLine("[DEBUG] Pairing result for " + target.Name + ": " + (res != null ? res.Status.ToString() : "Timeout"));
|
|
}
|
|
} catch (Exception ex) {
|
|
Console.WriteLine("[DEBUG] TryPairDevice exception: " + ex.Message);
|
|
} finally {
|
|
lock (pairingLock) {
|
|
isPairingInProgress = false;
|
|
activePairingKey = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CheckAndTriggerPair(DeviceInformation dev, string mac) {
|
|
if (dev == null || string.IsNullOrEmpty(mac)) return;
|
|
var target = GetTargetForName(dev.Name);
|
|
if (target == null) return;
|
|
|
|
DateTime lastSeen;
|
|
if (liveRfMacs.TryGetValue(mac, out lastSeen)) {
|
|
if ((DateTime.UtcNow - lastSeen).TotalSeconds < 10) {
|
|
new Thread(() => TryPairDevice(dev, mac, target)).Start();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void RefreshPairedDevices() {
|
|
try {
|
|
var op = DeviceInformation.FindAllAsync(BluetoothLEDevice.GetDeviceSelector());
|
|
var devices = AwaitOp<DeviceInformationCollection>(op, 2000);
|
|
if (devices != null) {
|
|
foreach (var dev in devices) {
|
|
var target = GetTargetForName(dev.Name);
|
|
if (target != null) {
|
|
var opBle = BluetoothLEDevice.FromIdAsync(dev.Id);
|
|
var ble = AwaitOp<BluetoothLEDevice>(opBle, 2000);
|
|
if (ble != null) {
|
|
if (ble.DeviceInformation.Pairing.IsPaired) {
|
|
if (target.PairedBle == null || target.PairedBle.DeviceId != ble.DeviceId) {
|
|
AttachPairedDevice(target, ble);
|
|
} else {
|
|
if (target.PairedBle.ConnectionStatus == BluetoothConnectionStatus.Connected) {
|
|
SetDeviceStatus(target, "CONNECTED");
|
|
} else {
|
|
SetDeviceStatus(target, "PAIRED_WAITING");
|
|
}
|
|
ble.Dispose();
|
|
}
|
|
} else {
|
|
ble.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception ex) {
|
|
Console.WriteLine("[DEBUG] RefreshPairedDevices error: " + ex.Message);
|
|
}
|
|
|
|
foreach (var t in targets.Values) {
|
|
if (t.PairedBle == null) {
|
|
SetDeviceStatus(t, "SEARCHING_FOR_PAIR");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void RunMonitorDaemon() {
|
|
Console.WriteLine("[DEBUG] Initializing BlePairer multi-device daemon (K370 Keyboard + M240 Mouse)...");
|
|
|
|
// 1. Passive Advertisement Watcher (무선 송신 없이 신호만 청취)
|
|
advWatcher = new BluetoothLEAdvertisementWatcher();
|
|
advWatcher.ScanningMode = BluetoothLEScanningMode.Passive;
|
|
advWatcher.Received += (s, e) => {
|
|
string rawMac = e.BluetoothAddress.ToString("X12").ToLower();
|
|
string macWithColons = FormatMac(e.BluetoothAddress);
|
|
DateTime now = DateTime.UtcNow;
|
|
liveRfMacs[macWithColons] = now;
|
|
liveRfMacs[rawMac] = now;
|
|
|
|
DeviceInformation pendingDev;
|
|
if (uncollectedDevices.TryGetValue(macWithColons, out pendingDev)) {
|
|
CheckAndTriggerPair(pendingDev, macWithColons);
|
|
}
|
|
};
|
|
|
|
// 2. DeviceWatcher
|
|
string[] requestedProps = {
|
|
"System.Devices.Aep.DeviceAddress",
|
|
"System.Devices.Aep.IsConnected",
|
|
"System.Devices.Aep.IsPresent",
|
|
"System.Devices.Aep.CanPair",
|
|
"System.Devices.Aep.IsPaired",
|
|
"System.Devices.Aep.SignalStrength"
|
|
};
|
|
string selector = "System.Devices.DevObjectType:=5 AND System.Devices.Aep.ProtocolId:=\"{BB7BB05E-5972-42B5-94FC-76EAA7084D49}\"";
|
|
devWatcher = DeviceInformation.CreateWatcher(selector, requestedProps, DeviceInformationKind.AssociationEndpoint);
|
|
|
|
Action<DeviceInformation> handleDevice = (dev) => {
|
|
if (dev == null || string.IsNullOrEmpty(dev.Name)) return;
|
|
var target = GetTargetForName(dev.Name);
|
|
if (target == null) return;
|
|
|
|
string addr = "";
|
|
if (dev.Properties.ContainsKey("System.Devices.Aep.DeviceAddress") && dev.Properties["System.Devices.Aep.DeviceAddress"] != null) {
|
|
addr = dev.Properties["System.Devices.Aep.DeviceAddress"].ToString().ToLower();
|
|
}
|
|
|
|
bool isPaired = dev.Pairing != null && dev.Pairing.IsPaired;
|
|
bool canPair = dev.Pairing != null && dev.Pairing.CanPair;
|
|
|
|
if (!isPaired && canPair && !string.IsNullOrEmpty(addr)) {
|
|
uncollectedDevices[addr] = dev;
|
|
CheckAndTriggerPair(dev, addr);
|
|
}
|
|
};
|
|
|
|
devWatcher.Added += (s, dev) => handleDevice(dev);
|
|
devWatcher.Updated += (s, upd) => {
|
|
foreach (var kv in uncollectedDevices) {
|
|
if (upd.Id.ToLower().Contains(kv.Key.Replace(":", ""))) {
|
|
CheckAndTriggerPair(kv.Value, kv.Key);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 초기 시스템 등록 상태 확인
|
|
RefreshPairedDevices();
|
|
|
|
// 메인 폴링 루프 (2초 주기): 이벤트 누락 방지용 가벼운 헬스체크
|
|
while (true) {
|
|
Thread.Sleep(2000);
|
|
|
|
foreach (var t in targets.Values) {
|
|
if (t.PairedBle != null) {
|
|
var connStatus = t.PairedBle.ConnectionStatus;
|
|
if (connStatus == BluetoothConnectionStatus.Connected) {
|
|
SetDeviceStatus(t, "CONNECTED");
|
|
} else {
|
|
SetDeviceStatus(t, "PAIRED_WAITING");
|
|
}
|
|
} else {
|
|
RefreshPairedDevices();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int Main(string[] args) {
|
|
try {
|
|
if (args.Length >= 1 && args[0].ToLower() == "monitor") {
|
|
RunMonitorDaemon();
|
|
return 0;
|
|
}
|
|
|
|
RefreshPairedDevices();
|
|
foreach (var t in targets.Values) {
|
|
Console.WriteLine(t.Key + ":" + t.Status);
|
|
}
|
|
return 0;
|
|
} catch (Exception ex) {
|
|
Console.WriteLine("[DEBUG] Main exception: " + ex.ToString());
|
|
return 99;
|
|
}
|
|
}
|
|
}
|