BLE_API.md 9.8 KB

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分钟 → 自动进入 traffictraffic 运行 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 状态发布主题
factory_reset boolean false 恢复出厂设置,清除所有配置并重启

注意:所有字段都是可选的,只更新提供的字段,未提供的字段保持原值。

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"
	}`)
	
	// 恢复出厂设置
	ailight.SetConfig(`{"factory_reset": true}`)
}

平台支持

平台 支持情况 备注
Linux ✅ 完整支持 需要 BlueZ
Windows ⚠️ 部分支持 需要额外配置
Raspberry Pi ✅ 完整支持 推荐用于部署

注意事项

  1. 连接稳定性:每次发送指令需要重新扫描和连接,建议封装连接池
  2. 超时处理:扫描和连接都设置了 10 秒超时
  3. 并发控制:BLE 连接不支持并发,需要加锁
  4. 重连机制:建议实现自动重连逻辑