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(不含密码):
{
"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 并重启:
{
"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 |
状态发布主题 |
注意:所有字段都是可选的,只更新提供的字段,未提供的字段保持原值。
恢复出厂设置
连续短按 BOOT 按钮 3 下(1.5 秒内)可恢复出厂设置,清除所有配置并重启。
Go 客户端示例
依赖
go mod init ailight-client
go get tinygo.org/x/bluetooth
完整代码
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 <mode>")
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")
}
使用方式
# 编译
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
封装为函数
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
}
调用示例
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 |
✅ 完整支持 |
推荐用于部署 |
注意事项
- 连接稳定性:每次发送指令需要重新扫描和连接,建议封装连接池
- 超时处理:扫描和连接都设置了 10 秒超时
- 并发控制:BLE 连接不支持并发,需要加锁
- 重连机制:建议实现自动重连逻辑