| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- package main
- import (
- "context"
- "flag"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "strconv"
- "strings"
- "ai-status-light/internal/api"
- "ai-status-light/internal/database"
- "ai-status-light/internal/logger"
- mqttcli "ai-status-light/internal/mqtt"
- )
- const defaultDBPath = "./data/config.db"
- var Version = "dev"
- func main() {
- if len(os.Args) < 2 {
- printUsage()
- return
- }
- switch os.Args[1] {
- case "serve":
- runServe(os.Args[2:])
- case "config":
- runConfig(os.Args[2:])
- case "version":
- fmt.Printf("ai-status-light %s\n", Version)
- default:
- printUsage()
- }
- }
- func printUsage() {
- fmt.Printf("ai-status-light %s\n\n", Version)
- fmt.Println("用法: ai-status-light <命令> [选项]")
- fmt.Println("")
- fmt.Println("命令:")
- fmt.Println(" serve 启动 API 服务(接收 OpenCode 插件事件)")
- fmt.Println(" config 管理 MQTT 和 BLE 配置")
- fmt.Println(" version 显示版本信息")
- fmt.Println("")
- fmt.Println("运行 'ai-status-light <命令> -h' 查看命令帮助")
- }
- func runServe(args []string) {
- fs := flag.NewFlagSet("serve", flag.ExitOnError)
- addr := fs.String("addr", ":8080", "监听地址")
- dbPath := fs.String("db", defaultDBPath, "数据库路径")
- logFile := fs.String("log-file", "./logs", "日志文件路径(默认 ./logs/monitor.log)")
- logLevel := fs.String("log-level", "info", "日志级别 (debug/info/warn/error)")
- tls := fs.Bool("tls", false, "启用 HTTPS (使用自签名证书)")
- tlsCert := fs.String("tls-cert", "./data/tls/cert.pem", "TLS 证书文件路径")
- tlsKey := fs.String("tls-key", "./data/tls/key.pem", "TLS 私钥文件路径")
- fs.Parse(args)
- logger.SetLevel(logger.ParseLevel(*logLevel))
- if err := logger.InitFileLog(*logFile); err != nil {
- fmt.Printf("初始化日志文件失败: %v\n", err)
- return
- }
- defer logger.Close()
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- db, err := database.New(*dbPath)
- if err != nil {
- logger.Error("打开数据库失败: %v", err)
- fmt.Printf("打开数据库失败: %v\n", err)
- return
- }
- defer db.Close()
- logger.Info("数据库已连接: %s", *dbPath)
- var mqttClient *mqttcli.Client
- cfg, err := db.GetMQTTConfig()
- if err != nil {
- logger.Error("读取 MQTT 配置失败: %v", err)
- fmt.Printf("读取 MQTT 配置失败: %v\n", err)
- } else if cfg != nil {
- mqttClient = mqttcli.NewFromConfig(cfg)
- if err := mqttClient.Connect(); err != nil {
- logger.Error("MQTT 连接失败: %v", err)
- fmt.Printf("MQTT 连接失败: %v\n", err)
- mqttClient = nil
- } else {
- defer mqttClient.Disconnect()
- logger.Info("MQTT 已连接: %s (主题: %s)", cfg.Broker, cfg.Topic)
- fmt.Printf("MQTT 已连接: %s (主题: %s)\n", cfg.Broker, cfg.Topic)
- }
- } else {
- logger.Info("未配置 MQTT,跳过 MQTT 连接")
- }
- var bleStdin io.WriteCloser
- bleCfg, err := db.GetBLEConfig()
- if err != nil {
- logger.Error("读取 BLE 配置失败: %v", err)
- fmt.Printf("读取 BLE 配置失败: %v\n", err)
- } else if bleCfg != nil {
- bleStdin = startBLERelay(bleCfg, ctx)
- } else {
- logger.Info("未配置 BLE,跳过 BLE 中继")
- }
- server := api.New(db, *addr)
- server.SetMQTTClient(mqttClient)
- server.SetBLEStdin(bleStdin)
- if *tls {
- if err := api.EnsureSelfSignedCert(*tlsCert, *tlsKey); err != nil {
- logger.Error("生成自签名证书失败: %v", err)
- fmt.Printf("生成自签名证书失败: %v\n", err)
- return
- }
- server.EnableTLS(*tlsCert, *tlsKey)
- logger.Info("HTTPS 已启用")
- fmt.Println("HTTPS 已启用 (自签名证书)")
- }
- fmt.Printf("API 服务已启动: %s\n", *addr)
- fmt.Println("Ctrl+C 停止")
- if err := server.Start(); err != nil {
- logger.Error("服务启动失败: %v", err)
- fmt.Printf("服务启动失败: %v\n", err)
- }
- }
- func runConfig(args []string) {
- if len(args) < 1 {
- printConfigUsage()
- return
- }
- dbPath := defaultDBPath
- logFile := "./logs"
- logLevel := "info"
- for i, arg := range args {
- if arg == "--db" && i+1 < len(args) {
- dbPath = args[i+1]
- }
- if arg == "--log-file" && i+1 < len(args) {
- logFile = args[i+1]
- }
- if arg == "--log-level" && i+1 < len(args) {
- logLevel = args[i+1]
- }
- }
- logger.SetLevel(logger.ParseLevel(logLevel))
- if err := logger.InitFileLog(logFile); err != nil {
- fmt.Printf("初始化日志文件失败: %v\n", err)
- return
- }
- defer logger.Close()
- // 过滤全局选项,避免传递给子命令的 FlagSet
- var filtered []string
- for i := 0; i < len(args); i++ {
- switch args[i] {
- case "--db", "--log-file", "--log-level":
- i++ // 跳过值
- default:
- filtered = append(filtered, args[i])
- }
- }
- args = filtered
- db, err := database.New(dbPath)
- if err != nil {
- logger.Error("打开数据库失败: %v", err)
- fmt.Printf("打开数据库失败: %v\n", err)
- return
- }
- defer db.Close()
- logger.Info("数据库已连接: %s", dbPath)
- switch args[0] {
- case "list":
- configs, err := db.ListMQTTConfigs()
- if err != nil {
- logger.Error("查询配置失败: %v", err)
- fmt.Printf("查询失败: %v\n", err)
- return
- }
- if len(configs) == 0 {
- fmt.Println("未配置 MQTT")
- return
- }
- logger.Info("查询到 %d 条 MQTT 配置", len(configs))
- for _, cfg := range configs {
- status := "禁用"
- if cfg.Enabled {
- status = "启用"
- }
- auth := ""
- if cfg.Username != "" {
- auth = fmt.Sprintf(" [认证: %s]", cfg.Username)
- }
- fmt.Printf("[%d] %s | %s | %s%s | %s\n", cfg.ID, cfg.Broker, cfg.ClientID, cfg.Topic, auth, status)
- }
- case "set":
- fs := flag.NewFlagSet("config set", flag.ExitOnError)
- broker := fs.String("broker", "", "MQTT Broker 地址 (如: tcp://127.0.0.1:1883)")
- clientID := fs.String("client-id", "opencode-monitor", "MQTT 客户端 ID")
- username := fs.String("username", "", "MQTT 用户名")
- password := fs.String("password", "", "MQTT 密码")
- topic := fs.String("topic", "opencode/status", "MQTT 主题")
- enabled := fs.Bool("enabled", true, "是否启用")
- fs.Parse(args[1:])
- if *broker == "" {
- fmt.Println("必须指定 --broker")
- return
- }
- cfg := &database.MQTTConfig{
- Broker: *broker,
- ClientID: *clientID,
- Username: *username,
- Password: *password,
- Topic: *topic,
- Enabled: *enabled,
- }
- if err := db.SaveMQTTConfig(cfg); err != nil {
- logger.Error("保存 MQTT 配置失败: %v", err)
- fmt.Printf("保存失败: %v\n", err)
- return
- }
- logger.Info("MQTT 配置已保存: %s (主题: %s)", cfg.Broker, cfg.Topic)
- fmt.Println("配置已保存")
- case "delete":
- if len(args) < 2 {
- fmt.Println("必须指定配置 ID")
- return
- }
- id, err := strconv.Atoi(args[1])
- if err != nil {
- logger.Warn("无效的配置 ID: %s", args[1])
- fmt.Println("无效的 ID")
- return
- }
- if err := db.DeleteMQTTConfig(id); err != nil {
- logger.Error("删除配置失败: id=%d, %v", id, err)
- fmt.Printf("删除失败: %v\n", err)
- return
- }
- logger.Info("MQTT 配置已删除: id=%d", id)
- fmt.Println("配置已删除")
- case "ble":
- runBleConfig(db, args[1:])
- case "device":
- runDeviceConfig(db, args[1:])
- default:
- printConfigUsage()
- }
- }
- func printConfigUsage() {
- fmt.Println("用法: ai-status-light config <子命令> [选项]")
- fmt.Println("")
- fmt.Println("子命令:")
- fmt.Println(" list 列出所有 MQTT 配置")
- fmt.Println(" set 设置 MQTT 配置")
- fmt.Println(" delete <id> 删除 MQTT 配置")
- fmt.Println(" ble list 列出所有 BLE 配置")
- fmt.Println(" ble set 设置 BLE 配置")
- fmt.Println(" ble delete <id> 删除 BLE 配置")
- fmt.Println("")
- fmt.Println("MQTT 选项:")
- fmt.Println(" --broker MQTT Broker 地址")
- fmt.Println(" --client-id MQTT 客户端 ID")
- fmt.Println(" --username MQTT 用户名")
- fmt.Println(" --password MQTT 密码")
- fmt.Println(" --topic MQTT 主题")
- fmt.Println(" --enabled 是否启用 (true/false)")
- fmt.Println("")
- fmt.Println("BLE 选项:")
- fmt.Println(" --device 蓝牙设备名称 (必填)")
- fmt.Println(" --service-uuid BLE 服务 UUID (必填)")
- fmt.Println(" --mode-char-uuid BLE 模式特征 UUID (必填)")
- fmt.Println(" --config-char-uuid BLE 配置特征 UUID (必填)")
- fmt.Println(" --enabled 是否启用 (true/false)")
- fmt.Println("")
- fmt.Println("全局选项:")
- fmt.Println(" --db 数据库路径")
- fmt.Println(" --log-file 日志文件路径(默认 ./logs/monitor.log)")
- fmt.Println(" --log-level 日志级别 (debug/info/warn/error)")
- }
- func startBLERelay(bleCfg *database.BLEConfig, ctx context.Context) io.WriteCloser {
- if len(embeddedBLERelay) == 0 {
- logger.Warn("BLE 中继未嵌入,请使用 make build-with-ble 构建")
- fmt.Println("警告: BLE 中继未嵌入,已跳过。请使用 make build-with-ble 构建")
- return nil
- }
- // 释放嵌入的 exe 到临时目录
- tmpDir := filepath.Join(os.TempDir(), "ai-status-light")
- if err := os.MkdirAll(tmpDir, 0755); err != nil {
- logger.Error("创建临时目录失败: %v", err)
- return nil
- }
- var tmpExe string
- if strings.Contains(strings.ToLower(os.Getenv("OS")), "windows") {
- tmpExe = filepath.Join(tmpDir, "ble_relay.exe")
- } else {
- tmpExe = filepath.Join(tmpDir, "ble_relay")
- }
- if err := os.WriteFile(tmpExe, embeddedBLERelay, 0755); err != nil {
- logger.Error("释放 BLE 中继失败: %v", err)
- return nil
- }
- logger.Debug("BLE 中继已释放到: %s", tmpExe)
- args := []string{
- "--device", bleCfg.DeviceName,
- "--service-uuid", bleCfg.ServiceUUID,
- "--mode-char-uuid", bleCfg.ModeCharUUID,
- "--config-char-uuid", bleCfg.ConfigCharUUID,
- }
- cmd := exec.CommandContext(ctx, tmpExe, args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- stdin, err := cmd.StdinPipe()
- if err != nil {
- logger.Error("创建 BLE 中继 stdin 管道失败: %v", err)
- return nil
- }
- if err := cmd.Start(); err != nil {
- logger.Error("启动 BLE 中继失败: %v", err)
- return nil
- }
- logger.Info("BLE 中继已启动: %s (PID: %d)", bleCfg.DeviceName, cmd.Process.Pid)
- fmt.Printf("BLE 中继已启动: %s (PID: %d)\n", bleCfg.DeviceName, cmd.Process.Pid)
- go func() {
- if err := cmd.Wait(); err != nil {
- logger.Error("BLE 中继退出: %v", err)
- }
- }()
- return stdin
- }
- func runBleConfig(db *database.DB, args []string) {
- if len(args) < 1 {
- printBleConfigUsage()
- return
- }
- switch args[0] {
- case "list":
- configs, err := db.ListBLEConfigs()
- if err != nil {
- logger.Error("查询 BLE 配置失败: %v", err)
- fmt.Printf("查询失败: %v\n", err)
- return
- }
- if len(configs) == 0 {
- fmt.Println("未配置 BLE")
- return
- }
- logger.Info("查询到 %d 条 BLE 配置", len(configs))
- for _, cfg := range configs {
- status := "禁用"
- if cfg.Enabled {
- status = "启用"
- }
- fmt.Printf("[%d] %s | %s | %s | %s | %s\n", cfg.ID, cfg.DeviceName, cfg.ServiceUUID, cfg.ModeCharUUID, cfg.ConfigCharUUID, status)
- }
- case "set":
- fs := flag.NewFlagSet("config ble set", flag.ExitOnError)
- deviceName := fs.String("device", "", "蓝牙设备名称 (必填)")
- serviceUUID := fs.String("service-uuid", "", "BLE 服务 UUID (必填)")
- modeCharUUID := fs.String("mode-char-uuid", "", "BLE 模式特征 UUID (必填)")
- configCharUUID := fs.String("config-char-uuid", "", "BLE 配置特征 UUID (必填)")
- enabled := fs.Bool("enabled", true, "是否启用")
- fs.Parse(args[1:])
- if *deviceName == "" || *serviceUUID == "" || *modeCharUUID == "" || *configCharUUID == "" {
- fmt.Println("错误: --device, --service-uuid, --mode-char-uuid, --config-char-uuid 为必填参数")
- fs.Usage()
- return
- }
- cfg := &database.BLEConfig{
- DeviceName: *deviceName,
- ServiceUUID: *serviceUUID,
- ModeCharUUID: *modeCharUUID,
- ConfigCharUUID: *configCharUUID,
- Enabled: *enabled,
- }
- if err := db.SaveBLEConfig(cfg); err != nil {
- logger.Error("保存 BLE 配置失败: %v", err)
- fmt.Printf("保存失败: %v\n", err)
- return
- }
- logger.Info("BLE 配置已保存: %s", cfg.DeviceName)
- fmt.Println("配置已保存")
- case "delete":
- if len(args) < 2 {
- fmt.Println("必须指定配置 ID")
- return
- }
- id, err := strconv.Atoi(args[1])
- if err != nil {
- logger.Warn("无效的配置 ID: %s", args[1])
- fmt.Println("无效的 ID")
- return
- }
- if err := db.DeleteBLEConfig(id); err != nil {
- logger.Error("删除 BLE 配置失败: id=%d, %v", id, err)
- fmt.Printf("删除失败: %v\n", err)
- return
- }
- logger.Info("BLE 配置已删除: id=%d", id)
- fmt.Println("配置已删除")
- default:
- printBleConfigUsage()
- }
- }
- func printBleConfigUsage() {
- fmt.Println("用法: ai-status-light config ble <子命令> [选项]")
- fmt.Println("")
- fmt.Println("子命令:")
- fmt.Println(" list 列出所有 BLE 配置")
- fmt.Println(" set 设置 BLE 配置")
- fmt.Println(" delete <id> 删除 BLE 配置")
- fmt.Println("")
- fmt.Println("选项:")
- fmt.Println(" --device 蓝牙设备名称 (必填)")
- fmt.Println(" --service-uuid BLE 服务 UUID (必填)")
- fmt.Println(" --mode-char-uuid BLE 模式特征 UUID (必填)")
- fmt.Println(" --config-char-uuid BLE 配置特征 UUID (必填)")
- fmt.Println(" --enabled 是否启用 (true/false)")
- }
- func runDeviceConfig(db *database.DB, args []string) {
- if len(args) < 1 {
- printDeviceConfigUsage()
- return
- }
- switch args[0] {
- case "list":
- configs, err := db.ListDeviceConfigs()
- if err != nil {
- logger.Error("查询设备配置失败: %v", err)
- fmt.Printf("查询失败: %v\n", err)
- return
- }
- if len(configs) == 0 {
- fmt.Println("未配置设备")
- return
- }
- logger.Info("查询到 %d 条设备配置", len(configs))
- for _, cfg := range configs {
- status := "禁用"
- if cfg.Enabled {
- status = "启用"
- }
- fmt.Printf("[%d] %s | %s | %s\n", cfg.ID, cfg.DeviceName, cfg.ConfigTopic, status)
- }
- case "set":
- fs := flag.NewFlagSet("config device set", flag.ExitOnError)
- deviceName := fs.String("device", "", "设备名称 (必填)")
- configTopic := fs.String("topic", "agent/status/config", "配置 topic")
- wifiSSID := fs.String("wifi-ssid", "", "WiFi SSID")
- wifiPass := fs.String("wifi-pass", "", "WiFi 密码")
- mqttBroker := fs.String("mqtt-broker", "", "MQTT Broker 地址")
- mqttPort := fs.Int("mqtt-port", 1883, "MQTT 端口")
- mqttUser := fs.String("mqtt-user", "", "MQTT 用户名")
- mqttPass := fs.String("mqtt-pass", "", "MQTT 密码")
- mqttClient := fs.String("mqtt-client", "", "MQTT 客户端 ID")
- mqttTopic := fs.String("mqtt-topic", "", "MQTT 状态 topic")
- mqttStatus := fs.String("mqtt-status", "", "MQTT 状态上报 topic")
- pinRed := fs.Int("pin-red", 4, "红灯引脚")
- pinGreen := fs.Int("pin-green", 3, "绿灯引脚")
- pinYellow := fs.Int("pin-yellow", 2, "黄灯引脚")
- enabled := fs.Bool("enabled", true, "是否启用")
- fs.Parse(args[1:)
- if *deviceName == "" {
- fmt.Println("错误: --device 为必填参数")
- fs.Usage()
- return
- }
- cfg := &database.DeviceConfig{
- DeviceName: *deviceName,
- ConfigTopic: *configTopic,
- WifiSSID: *wifiSSID,
- WifiPass: *wifiPass,
- MqttBroker: *mqttBroker,
- MqttPort: *mqttPort,
- MqttUser: *mqttUser,
- MqttPass: *mqttPass,
- MqttClient: *mqttClient,
- MqttTopic: *mqttTopic,
- MqttStatus: *mqttStatus,
- PinRed: *pinRed,
- PinGreen: *pinGreen,
- PinYellow: *pinYellow,
- Enabled: *enabled,
- }
- if err := db.SaveDeviceConfig(cfg); err != nil {
- logger.Error("保存设备配置失败: %v", err)
- fmt.Printf("保存失败: %v\n", err)
- return
- }
- logger.Info("设备配置已保存: %s", cfg.DeviceName)
- fmt.Println("配置已保存")
- case "delete":
- if len(args) < 2 {
- fmt.Println("必须指定配置 ID")
- return
- }
- id, err := strconv.Atoi(args[1])
- if err != nil {
- logger.Warn("无效的配置 ID: %s", args[1])
- fmt.Println("无效的 ID")
- return
- }
- if err := db.DeleteDeviceConfig(id); err != nil {
- logger.Error("删除设备配置失败: id=%d, %v", id, err)
- fmt.Printf("删除失败: %v\n", err)
- return
- }
- logger.Info("设备配置已删除: id=%d", id)
- fmt.Println("配置已删除")
- default:
- printDeviceConfigUsage()
- }
- }
- func printDeviceConfigUsage() {
- fmt.Println("用法: ai-status-light config device <子命令> [选项]")
- fmt.Println("")
- fmt.Println("子命令:")
- fmt.Println(" list 列出所有设备配置")
- fmt.Println(" set 设置设备配置")
- fmt.Println(" delete <id> 删除设备配置")
- fmt.Println("")
- fmt.Println("选项:")
- fmt.Println(" --device 设备名称 (必填)")
- fmt.Println(" --topic 配置 topic (默认: agent/status/config)")
- fmt.Println(" --wifi-ssid WiFi SSID")
- fmt.Println(" --wifi-pass WiFi 密码")
- fmt.Println(" --mqtt-broker MQTT Broker 地址")
- fmt.Println(" --mqtt-port MQTT 端口 (默认: 1883)")
- fmt.Println(" --mqtt-user MQTT 用户名")
- fmt.Println(" --mqtt-pass MQTT 密码")
- fmt.Println(" --mqtt-client MQTT 客户端 ID")
- fmt.Println(" --mqtt-topic MQTT 状态 topic")
- fmt.Println(" --mqtt-status MQTT 状态上报 topic")
- fmt.Println(" --pin-red 红灯引脚 (默认: 4)")
- fmt.Println(" --pin-green 绿灯引脚 (默认: 3)")
- fmt.Println(" --pin-yellow 黄灯引脚 (默认: 2)")
- fmt.Println(" --enabled 是否启用 (true/false)")
- }
|