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 targets = new Dictionary() { { "K370", new DeviceTarget("K370", "LOGI K370 B", true) }, { "M240", new DeviceTarget("M240", "LOGI M240 B", false) } }; static ConcurrentDictionary liveRfMacs = new ConcurrentDictionary(); static ConcurrentDictionary uncollectedDevices = new ConcurrentDictionary(); 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(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(OnPairingRequested); var kinds = DevicePairingKinds.ConfirmOnly | DevicePairingKinds.ProvidePin | DevicePairingKinds.ConfirmPinMatch | DevicePairingKinds.DisplayPin; var opPair = customPairing.PairAsync(kinds, DevicePairingProtectionLevel.Default); var res = AwaitOp(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(opBle, 2000); if (newBle != null) { AttachPairedDevice(target, newBle); } } catch {} // 이전 구버전 MAC 인스턴스 정리 try { var opAll = DeviceInformation.FindAllAsync(BluetoothLEDevice.GetDeviceSelector()); var allDevs = AwaitOp(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(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(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(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(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 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; } } }