//go:build linux package ble import ( "fmt" "strings" "sync" "time" "github.com/muka/go-bluetooth/api" "github.com/muka/go-bluetooth/bluez" "github.com/muka/go-bluetooth/bluez/profile/adapter" "github.com/muka/go-bluetooth/bluez/profile/device" "github.com/muka/go-bluetooth/bluez/profile/gatt" ) type Client struct { deviceName string modeChan chan string stopChan chan struct{} mu sync.Mutex connected bool currentMode string } func New(deviceName string) *Client { c := &Client{ deviceName: deviceName, modeChan: make(chan string, 10), stopChan: make(chan struct{}), } go c.run() return c } func (c *Client) SetMode(mode string) { if mode == "" { return } c.mu.Lock() if mode == c.currentMode { c.mu.Unlock() return } c.mu.Unlock() select { case c.modeChan <- mode: default: } } func (c *Client) Close() { close(c.stopChan) } func (c *Client) run() { retryDelay := time.Second maxRetryDelay := 30 * time.Second for { select { case <-c.stopChan: return default: } char, cleanup, err := c.connect() if err != nil { logWarn("BLE 连接失败: %v, %v后重试", err, retryDelay) select { case <-c.stopChan: return case <-time.After(retryDelay): } retryDelay *= 2 if retryDelay > maxRetryDelay { retryDelay = maxRetryDelay } continue } retryDelay = time.Second c.mu.Lock() c.connected = true c.mu.Unlock() logInfo("BLE 已连接: %s", c.deviceName) disconnected := c.processLoop(char) c.mu.Lock() c.connected = false c.mu.Unlock() if cleanup != nil { cleanup() } if disconnected { logWarn("BLE 断开连接, 1秒后重连") select { case <-c.stopChan: return case <-time.After(time.Second): } } } } func (c *Client) processLoop(char *gatt.GattCharacteristic1) bool { for { select { case <-c.stopChan: return false case mode := <-c.modeChan: if err := c.writeMode(char, mode); err != nil { logError("BLE 写入失败: %v", err) return true } c.mu.Lock() c.currentMode = mode c.mu.Unlock() logDebug("BLE 模式已设置: %s", mode) } } } func (c *Client) connect() (*gatt.GattCharacteristic1, func(), error) { a, err := api.GetDefaultAdapter() if err != nil { return nil, nil, fmt.Errorf("获取蓝牙适配器失败: %w", err) } adapterProps, err := adapter.NewAdapter1FromAdapterID(string(a.Path())) if err != nil { return nil, nil, fmt.Errorf("初始化适配器失败: %w", err) } powered, err := adapterProps.GetPowered() if err != nil { return nil, nil, fmt.Errorf("获取适配器状态失败: %w", err) } if !powered { if err := adapterProps.SetPowered(true); err != nil { return nil, nil, fmt.Errorf("启动蓝牙适配器失败: %w", err) } } dev, cancel, err := c.scanAndConnect(a) if err != nil { return nil, nil, err } char, err := c.findCharacteristic(dev) if err != nil { cancel() dev.Disconnect() return nil, nil, err } cleanup := func() { dev.Disconnect() cancel() } return char, cleanup, nil } func (c *Client) scanAndConnect(a *adapter.Adapter1) (*device.Device1, func(), error) { discover, cancel, err := api.Discover(a, nil) if err != nil { return nil, nil, fmt.Errorf("开始扫描失败: %w", err) } timeout := time.After(10 * time.Second) for { select { case ev := <-discover: if ev == nil { continue } dev, err := device.NewDevice1(ev.Path) if err != nil { continue } name := dev.Properties.Name if name == c.deviceName { if err := dev.Connect(); err != nil { cancel() return nil, nil, fmt.Errorf("连接设备失败: %w", err) } return dev, cancel, nil } case <-timeout: cancel() return nil, nil, fmt.Errorf("扫描超时,未找到设备 %s", c.deviceName) case <-c.stopChan: cancel() return nil, nil, fmt.Errorf("扫描已取消") } } } func (c *Client) findCharacteristic(dev *device.Device1) (*gatt.GattCharacteristic1, error) { om, err := bluez.GetObjectManager() if err != nil { return nil, fmt.Errorf("获取 ObjectManager 失败: %w", err) } objects, err := om.GetManagedObjects() if err != nil { return nil, fmt.Errorf("获取管理对象失败: %w", err) } devPath := string(dev.Path()) var servicePath string for path, ifaces := range objects { pathStr := string(path) if !strings.HasPrefix(pathStr, devPath+"/") { continue } if _, ok := ifaces[gatt.GattService1Interface]; ok { svc, err := gatt.NewGattService1(path) if err != nil { continue } uuid, err := svc.GetUUID() if err != nil { continue } if uuid == serviceUUID { servicePath = pathStr break } } } if servicePath == "" { return nil, fmt.Errorf("服务 %s 未找到", serviceUUID) } for path, ifaces := range objects { pathStr := string(path) if !strings.HasPrefix(pathStr, servicePath+"/") { continue } if _, ok := ifaces[gatt.GattCharacteristic1Interface]; ok { char, err := gatt.NewGattCharacteristic1(path) if err != nil { continue } uuid, err := char.GetUUID() if err != nil { continue } if uuid == charUUID { return char, nil } } } return nil, fmt.Errorf("特征 %s 未找到", charUUID) } func (c *Client) writeMode(char *gatt.GattCharacteristic1, mode string) error { options := map[string]interface{}{ "type": "request", } return char.WriteValue([]byte(mode), options) } const ( serviceUUID = "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001" charUUID = "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001" )