package main import ( "bufio" "io" "log" "os" "os/exec" "path/filepath" "strings" "syscall" "time" ) var ( user32 = syscall.NewLazyDLL("user32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll") procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow") procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId") procShowWindow = user32.NewProc("ShowWindow") procSetForegroundWindow = user32.NewProc("SetForegroundWindow") procSetWindowPos = user32.NewProc("SetWindowPos") procGetAncestor = user32.NewProc("GetAncestor") procIsIconic = user32.NewProc("IsIconic") procBringWindowToTop = user32.NewProc("BringWindowToTop") procGetForegroundWindow = user32.NewProc("GetForegroundWindow") procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId") procAttachThreadInput = user32.NewProc("AttachThreadInput") ) const ( SW_RESTORE = 9 SW_SHOW = 5 GA_ROOTOWNER = 3 SWP_NOMOVE = 0x0002 SWP_NOSIZE = 0x0001 SWP_SHOWWINDOW = 0x0040 ) func getTargetWindow() uintptr { hwnd, _, _ := procGetConsoleWindow.Call() if hwnd == 0 { return 0 } root, _, _ := procGetAncestor.Call(hwnd, GA_ROOTOWNER) if root != 0 { return root } return hwnd } func bringWindowToTopmost(topmost bool) { hwnd := getTargetWindow() if hwnd == 0 { return } // 1. 최소화되어 있는 경우 창 복원 isMin, _, _ := procIsIconic.Call(hwnd) if isMin != 0 { procShowWindow.Call(hwnd, SW_RESTORE) } else { procShowWindow.Call(hwnd, SW_SHOW) } // 2. HWND_TOPMOST (-1) 또는 HWND_NOTOPMOST (-2) 적용 var insertAfter uintptr if topmost { insertAfter = ^uintptr(0) // -1 (HWND_TOPMOST) } else { insertAfter = ^uintptr(1) // -2 (HWND_NOTOPMOST) } procSetWindowPos.Call(hwnd, insertAfter, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE|SWP_SHOWWINDOW) // 3. 포커스 전환 및 최상위 활성화 fgWnd, _, _ := procGetForegroundWindow.Call() if fgWnd != 0 && fgWnd != hwnd { fgThread, _, _ := procGetWindowThreadProcessId.Call(fgWnd, 0) curThread, _, _ := procGetCurrentThreadId.Call() if fgThread != curThread { procAttachThreadInput.Call(curThread, fgThread, 1) procSetForegroundWindow.Call(hwnd) procBringWindowToTop.Call(hwnd) procAttachThreadInput.Call(curThread, fgThread, 0) } else { procSetForegroundWindow.Call(hwnd) procBringWindowToTop.Call(hwnd) } } else { procSetForegroundWindow.Call(hwnd) procBringWindowToTop.Call(hwnd) } } func getPairerExePath() string { exePath, err := os.Executable() if err != nil { return "BlePairer.exe" } dir := filepath.Dir(exePath) return filepath.Join(dir, "BlePairer.exe") } func getDeviceDisplayName(key string) string { switch key { case "K370": return "LOGI K370 B" case "M240": return "LOGI M240 B" default: return key } } func runMonitor() { pairerExe := getPairerExePath() for { log.Println("Starting Logitech Bluetooth monitor daemon (LOGI K370 B + LOGI M240 B)...") cmd := exec.Command(pairerExe, "monitor") cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} stdout, err := cmd.StdoutPipe() if err != nil { log.Printf("Stdout 파이프 연결 오류: %v", err) time.Sleep(2 * time.Second) continue } stderr, err := cmd.StderrPipe() if err == nil { go io.Copy(os.Stderr, stderr) } if err := cmd.Start(); err != nil { log.Printf("BlePairer 실행 실패: %v", err) time.Sleep(2 * time.Second) continue } scanner := bufio.NewScanner(stdout) lastStatusMap := make(map[string]string) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } if strings.HasPrefix(line, "[DEBUG]") { log.Println(line) } else if strings.HasPrefix(line, "STATUS:") { parts := strings.Split(strings.TrimPrefix(line, "STATUS:"), ":") if len(parts) == 2 { key := parts[0] status := parts[1] devName := getDeviceDisplayName(key) if lastStatusMap[key] != status { switch status { case "CONNECTED": log.Printf("[%s] ★ 정상 연결되었습니다 (동작 중)", devName) case "PAIRED_WAITING": log.Printf("[%s] 연결 해제됨 -> 대기 모드 (재연결 또는 페어링 신호 대기)", devName) case "SEARCHING_FOR_PAIR": if key == "K370" { log.Printf("[%s] 등록 대기 중 (채널 버튼을 3초간 길게 누르세요)", devName) } else { log.Printf("[%s] 등록 대기 중 (마우스 전원/페어링 버튼을 누르세요)", devName) } default: log.Printf("[%s] 상태 변경: %s", devName, status) } lastStatusMap[key] = status } } } else if strings.HasPrefix(line, "DEVICE_DETECTED:") { parts := strings.Split(strings.TrimPrefix(line, "DEVICE_DETECTED:"), ":") if len(parts) >= 2 { key := parts[0] mac := parts[1] devName := getDeviceDisplayName(key) if key == "K370" { log.Printf("[%s] ★ 키보드 신호 포착! 창을 최상위(Topmost)로 띄웁니다... (%s)", devName, mac) bringWindowToTopmost(true) } else { log.Printf("[%s] ★ 마우스 신호 포착! 자동 페어링을 진행합니다... (%s)", devName, mac) } } } else if strings.HasPrefix(line, "PIN_REQUIRED:") { parts := strings.Split(strings.TrimPrefix(line, "PIN_REQUIRED:"), ":") if len(parts) >= 2 { key := parts[0] pin := parts[1] devName := getDeviceDisplayName(key) bringWindowToTopmost(true) log.Printf("======================================================") log.Printf("★ [%s] 페어링 번호 감지! 키보드에서 [%s] 입력 후 [Enter]를 누르세요!", devName, pin) log.Printf("======================================================") } } else if strings.HasPrefix(line, "PAIRED_AND_CONNECTED:") { parts := strings.Split(strings.TrimPrefix(line, "PAIRED_AND_CONNECTED:"), ":") if len(parts) >= 2 { key := parts[0] mac := parts[1] devName := getDeviceDisplayName(key) log.Printf("======================================================") log.Printf("[%s] ★ 블루투스 페어링 & 연결 완료! (%s)", devName, mac) log.Printf("======================================================") if key == "K370" { go func() { time.Sleep(3 * time.Second) bringWindowToTopmost(false) }() } } } else { log.Println(line) } } cmd.Wait() log.Printf("BlePairer 데몬 종료됨. 2초 후 재시작합니다...") time.Sleep(2 * time.Second) } } func main() { runMonitor() }