Files
logi_k370_monitor/BlePairer.cs
T

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;
}
}
}