package transport import ( "context" "errors" "fmt" "os" "sync" "time" "tinygo.org/x/bluetooth" ) func bleLog(msg string) { line := fmt.Sprintf("[BLE] %s | %s\n", time.Now().Format("15:04:05.000"), msg) fmt.Print(line) f, err := os.OpenFile("kvmote_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return } defer f.Close() f.WriteString(line) } var ( NusServiceUUID = bluetooth.NewUUID([16]byte{0x6e, 0x40, 0x00, 0x01, 0xb5, 0xa3, 0xf3, 0x93, 0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e}) NusRxUUID = bluetooth.NewUUID([16]byte{0x6e, 0x40, 0x00, 0x02, 0xb5, 0xa3, 0xf3, 0x93, 0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e}) ) type RealBleTransport struct { adapter *bluetooth.Adapter device bluetooth.Device rxChar bluetooth.DeviceCharacteristic connected bool mu sync.Mutex address bluetooth.Address found bool } func NewBleTransport() *RealBleTransport { return &RealBleTransport{ adapter: bluetooth.DefaultAdapter, } } func (t *RealBleTransport) Detect(ctx context.Context) (bool, error) { t.mu.Lock() if t.found { t.mu.Unlock() return true, nil } t.mu.Unlock() fmt.Println("[BLE] Iniciando Scan...") if err := t.adapter.Enable(); err != nil { return false, err } foundChan := make(chan bluetooth.Address, 1) go func() { t.adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) { hasUUID := result.HasServiceUUID(NusServiceUUID) bleLog(fmt.Sprintf("visto: nome=%q hasNUS=%v addr=%s", result.LocalName(), hasUUID, result.Address.String())) if result.LocalName() == "KVMote" || hasUUID { adapter.StopScan() select { case foundChan <- result.Address: default: } } }) }() select { case addr := <-foundChan: t.mu.Lock() t.address = addr t.found = true t.mu.Unlock() fmt.Println("[BLE] Encontrado!") return true, nil case <-ctx.Done(): t.adapter.StopScan() return false, ctx.Err() case <-time.After(5 * time.Second): t.adapter.StopScan() return false, errors.New("não encontrado") } } func (t *RealBleTransport) Connect(ctx context.Context) error { t.mu.Lock() addr := t.address t.mu.Unlock() var device bluetooth.Device var services []bluetooth.DeviceService var err error for cycle := 1; cycle <= 3; cycle++ { if cycle > 1 { bleLog(fmt.Sprintf("Reconectando (ciclo %d)...", cycle)) time.Sleep(1 * time.Second) } bleLog(fmt.Sprintf("Conectando (ciclo %d)...", cycle)) device, err = t.adapter.Connect(addr, bluetooth.ConnectionParams{}) if err != nil { bleLog(fmt.Sprintf(" Connect erro: %v", err)) continue } t.mu.Lock() t.device = device t.mu.Unlock() // Delay crucial para BT 4.0 estabilizar a conexão antes de pedir serviços bleLog("Aguardando estabilização (2s)...") time.Sleep(2 * time.Second) bleLog("DiscoverServices...") services, err = device.DiscoverServices(nil) if err != nil { bleLog(fmt.Sprintf(" DiscoverServices erro: %v", err)) // Tenta mais uma vez após um pequeno delay antes de desistir do ciclo time.Sleep(1 * time.Second) bleLog("DiscoverServices (retry)...") services, err = device.DiscoverServices(nil) if err != nil { bleLog(fmt.Sprintf(" DiscoverServices retry erro: %v", err)) go func() { defer func() { recover() }(); device.Disconnect() }() continue } } break } if err != nil { return fmt.Errorf("erro ao descobrir serviços: %w", err) } bleLog(fmt.Sprintf("Serviços encontrados: %d", len(services))) nusIdx := -1 for i, s := range services { bleLog(fmt.Sprintf(" [%d] %s", i, s.UUID().String())) if s.UUID() == NusServiceUUID { nusIdx = i } } if nusIdx < 0 { return errors.New("serviço NUS não encontrado") } bleLog("Buscando característica RX...") chars, err := services[nusIdx].DiscoverCharacteristics([]bluetooth.UUID{NusRxUUID}) if err != nil || len(chars) == 0 { return errors.New("característica não encontrada") } t.mu.Lock() t.rxChar = chars[0] t.connected = true t.mu.Unlock() fmt.Println("[BLE] PRONTO!") t.Send([]byte{'H'}) return nil } func (t *RealBleTransport) Disconnect() error { t.mu.Lock() defer t.mu.Unlock() if !t.connected { return nil } // Proteção contra crash no Windows go func() { defer func() { recover() }() t.device.Disconnect() }() t.connected = false return nil } func (t *RealBleTransport) IsConnected() bool { t.mu.Lock() defer t.mu.Unlock() return t.connected } func (t *RealBleTransport) Send(data []byte) error { t.mu.Lock() defer t.mu.Unlock() if !t.connected { return nil } _, err := t.rxChar.WriteWithoutResponse(data) return err } func (t *RealBleTransport) SendLossy(data []byte) error { return t.Send(data) } func (t *RealBleTransport) DeviceLabel() string { return "KVMote (BLE)" } func (t *RealBleTransport) ClipboardConfig() (int, int, bool) { return 65536, 5, true }