# AI-Light BLE 接口文档 ## 设备信息 | 项目 | 值 | |------|-----| | 设备名称 | `AI-Light` | | Service UUID | `b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001` | ### BLE 特征 | 特征 | UUID | 属性 | 说明 | |------|------|------|------| | Mode | `b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001` | READ / WRITE / NOTIFY | 灯效模式控制 | | Config | `b8b7e003-7a6b-4f4f-9a8b-11c0ffee0001` | READ / WRITE | WiFi/MQTT 配置 | ## Mode 特征 - 灯效控制 ### 支持的模式 | 模式 | 说明 | 灯效 | |------|------|------| | `init` | 初始化状态 | 三色一起呼吸 | | `thinking` | 思考中 | 绿→黄→红 连贯跑马灯 | | `ai` | AI 处理中 | 三色柔和波浪 | | `busy` | 忙碌状态 | 黄灯淡入淡出 | | `success` | 成功状态 | 绿灯常亮 | | `error` | 错误状态 | 红灯快速闪烁 | | `alarm` | 警告状态 | 红黄交替警灯 | | `traffic` | 交通灯模式 | 绿→黄→红 循环 | | `off` | 关闭 | 全灭 | | `red` | 红灯 | 红灯常亮 | | `yellow` | 黄灯 | 黄灯常亮 | | `green` | 绿灯 | 绿灯常亮 | | `idle` | 空闲(自动转为 traffic) | 同 traffic | ### 行为说明 - **重复模式忽略**:如果推送的模式与当前模式相同,设备会自动忽略,不做任何操作 - **超时规则**:普通模式运行 **5分钟** → 自动进入 `traffic`;`traffic` 运行 **10分钟** → 自动进入 `off` - **通知**:模式变化时会通过 NOTIFY 特征推送当前模式 ## Config 特征 - WiFi/MQTT 配置 ### 读取配置 读取 Config 特征返回当前配置 JSON(不含密码): ```json { "wifi_ssid": "MyNetwork", "mqtt_broker": "192.168.1.100", "mqtt_port": 1883, "mqtt_user": "user", "mqtt_client": "AI-Light", "mqtt_topic": "opencode/status", "mqtt_status": "openCodeLight/status", "comm_mode": 1 } ``` ### 写入配置 向 Config 特征写入 JSON 配置,设备自动保存到 NVS 并重启: ```json { "wifi_ssid": "MyNetwork", "wifi_pass": "password123", "mqtt_broker": "192.168.1.100", "mqtt_port": 1883, "mqtt_user": "user", "mqtt_pass": "pass", "mqtt_client": "AI-Light", "mqtt_topic": "opencode/status", "mqtt_status": "openCodeLight/status" } ``` | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | wifi_ssid | string | 否 | - | WiFi SSID | | wifi_pass | string | 否 | - | WiFi 密码 | | mqtt_broker | string | 否 | - | MQTT Broker 地址 | | mqtt_port | number | 否 | 1883 | MQTT 端口 | | mqtt_user | string | 否 | - | MQTT 用户名 | | mqtt_pass | string | 否 | - | MQTT 密码 | | mqtt_client | string | 否 | AI-Light | MQTT Client ID | | mqtt_topic | string | 否 | opencode/status | 订阅主题 | | mqtt_status | string | 否 | openCodeLight/status | 状态发布主题 | **注意**:所有字段都是可选的,只更新提供的字段,未提供的字段保持原值。 ## Go 客户端示例 ### 依赖 ```bash go mod init ailight-client go get tinygo.org/x/bluetooth ``` ### 完整代码 ```go package main import ( "fmt" "os" "time" "tinygo.org/x/bluetooth" ) var ( serviceUUID = bluetooth.NewUUID([16]byte{0xb8, 0xb7, 0xe0, 0x01, 0x7a, 0x6b, 0x4f, 0x4f, 0x9a, 0x8b, 0x11, 0xc0, 0xff, 0xee, 0x00, 0x01}) modeCharUUID = bluetooth.NewUUID([16]byte{0xb8, 0xb7, 0xe0, 0x02, 0x7a, 0x6b, 0x4f, 0x4f, 0x9a, 0x8b, 0x11, 0xc0, 0xff, 0xee, 0x00, 0x01}) configCharUUID = bluetooth.NewUUID([16]byte{0xb8, 0xb7, 0xe0, 0x03, 0x7a, 0x6b, 0x4f, 0x4f, 0x9a, 0x8b, 0x11, 0xc0, 0xff, 0xee, 0x00, 0x01}) ) func main() { if len(os.Args) < 2 { fmt.Println("Usage: ailight-client ") fmt.Println("Modes: init, thinking, ai, busy, success, error, alarm, traffic, off, red, yellow, green") os.Exit(1) } mode := os.Args[1] adapter := bluetooth.DefaultAdapter if err := adapter.Enable(); err != nil { fmt.Printf("Failed to enable BLE adapter: %v\n", err) os.Exit(1) } // 扫描并连接设备 fmt.Println("Scanning for AI-Light...") found := false var device bluetooth.Device done := make(chan struct{}) go func() { adapter.Scan(func(adapter *bluetooth.Adapter, scanResult bluetooth.ScanResult) { if scanResult.LocalName() == "AI-Light" { adapter.StopScan() found = true var err error device, err = adapter.Connect(scanResult.Address, bluetooth.ConnectionParams{}) if err != nil { fmt.Printf("Failed to connect: %v\n", err) os.Exit(1) } close(done) } }) }() select { case <-done: // 连接成功 case <-time.After(10 * time.Second): fmt.Println("Connection timeout") os.Exit(1) } if !found { fmt.Println("Device not found") os.Exit(1) } fmt.Println("Connected to AI-Light") // 发现服务和特征 services, err := device.DiscoverServices([]bluetooth.UUID{serviceUUID}) if err != nil { fmt.Printf("Failed to discover services: %v\n", err) os.Exit(1) } if len(services) == 0 { fmt.Println("Service not found") os.Exit(1) } characteristics, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{modeCharUUID}) if err != nil { fmt.Printf("Failed to discover characteristics: %v\n", err) os.Exit(1) } if len(characteristics) == 0 { fmt.Println("Characteristic not found") os.Exit(1) } // 写入模式 _, err = characteristics[0].WriteString(mode) if err != nil { fmt.Printf("Write failed: %v\n", err) os.Exit(1) } fmt.Printf("Sent mode: %s\n", mode) // 断开连接 device.Disconnect() fmt.Println("Disconnected") } ``` ### 使用方式 ```bash # 编译 go build -o ailight-client main.go # 切换模式 ./ailight-client thinking ./ailight-client busy ./ailight-client success ./ailight-client error ./ailight-client traffic ./ailight-client off ``` ### 封装为函数 ```go package ailight import ( "fmt" "time" "tinygo.org/x/bluetooth" ) var ( serviceUUID = bluetooth.NewUUID([16]byte{0xb8, 0xb7, 0xe0, 0x01, 0x7a, 0x6b, 0x4f, 0x4f, 0x9a, 0x8b, 0x11, 0xc0, 0xff, 0xee, 0x00, 0x01}) modeCharUUID = bluetooth.NewUUID([16]byte{0xb8, 0xb7, 0xe0, 0x02, 0x7a, 0x6b, 0x4f, 0x4f, 0x9a, 0x8b, 0x11, 0xc0, 0xff, 0xee, 0x00, 0x01}) configCharUUID = bluetooth.NewUUID([16]byte{0xb8, 0xb7, 0xe0, 0x03, 0x7a, 0x6b, 0x4f, 0x4f, 0x9a, 0x8b, 0x11, 0xc0, 0xff, 0xee, 0x00, 0x01}) ) // SetMode 设置灯效模式 func SetMode(mode string) error { adapter := bluetooth.DefaultAdapter if err := adapter.Enable(); err != nil { return fmt.Errorf("enable adapter: %w", err) } // 扫描设备 var device bluetooth.Device found := make(chan bool, 1) go func() { adapter.Scan(func(adapter *bluetooth.Adapter, scanResult bluetooth.ScanResult) { if scanResult.LocalName() == "AI-Light" { adapter.StopScan() var err error device, err = adapter.Connect(scanResult.Address, bluetooth.ConnectionParams{}) found <- err == nil } }) }() select { case ok := <-found: if !ok { return fmt.Errorf("connect failed") } case <-time.After(10 * time.Second): return fmt.Errorf("scan timeout") } defer device.Disconnect() // 发现服务 services, err := device.DiscoverServices([]bluetooth.UUID{serviceUUID}) if err != nil || len(services) == 0 { return fmt.Errorf("service not found") } // 发现特征 characteristics, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{modeCharUUID}) if err != nil || len(characteristics) == 0 { return fmt.Errorf("characteristic not found") } // 写入模式 _, err = characteristics[0].WriteString(mode) if err != nil { return fmt.Errorf("write failed: %w", err) } return nil } // SetConfig 写入 WiFi/MQTT 配置 func SetConfig(configJSON string) error { adapter := bluetooth.DefaultAdapter if err := adapter.Enable(); err != nil { return fmt.Errorf("enable adapter: %w", err) } var device bluetooth.Device found := make(chan bool, 1) go func() { adapter.Scan(func(adapter *bluetooth.Adapter, scanResult bluetooth.ScanResult) { if scanResult.LocalName() == "AI-Light" { adapter.StopScan() var err error device, err = adapter.Connect(scanResult.Address, bluetooth.ConnectionParams{}) found <- err == nil } }) }() select { case ok := <-found: if !ok { return fmt.Errorf("connect failed") } case <-time.After(10 * time.Second): return fmt.Errorf("scan timeout") } defer device.Disconnect() services, err := device.DiscoverServices([]bluetooth.UUID{serviceUUID}) if err != nil || len(services) == 0 { return fmt.Errorf("service not found") } characteristics, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{configCharUUID}) if err != nil || len(characteristics) == 0 { return fmt.Errorf("config characteristic not found") } _, err = characteristics[0].WriteString(configJSON) if err != nil { return fmt.Errorf("write failed: %w", err) } return nil } ``` ### 调用示例 ```go package main import ( "fmt" "ailight" ) func main() { // AI 开始思考 ailight.SetMode("thinking") // 执行任务... // 任务完成 ailight.SetMode("success") // 出错 ailight.SetMode("error") // 关闭 ailight.SetMode("off") // 配置 WiFi/MQTT ailight.SetConfig(`{ "wifi_ssid": "MyNetwork", "wifi_pass": "password123", "mqtt_broker": "192.168.1.100", "mqtt_port": 1883, "mqtt_client": "AI-Light", "mqtt_topic": "opencode/status" }`) } ``` ## 平台支持 | 平台 | 支持情况 | 备注 | |------|----------|------| | Linux | ✅ 完整支持 | 需要 BlueZ | | Windows | ⚠️ 部分支持 | 需要额外配置 | | Raspberry Pi | ✅ 完整支持 | 推荐用于部署 | ## 注意事项 1. **连接稳定性**:每次发送指令需要重新扫描和连接,建议封装连接池 2. **超时处理**:扫描和连接都设置了 10 秒超时 3. **并发控制**:BLE 连接不支持并发,需要加锁 4. **重连机制**:建议实现自动重连逻辑