Kaynağa Gözat

添加蓝牙配置设备

moki 3 gün önce
ebeveyn
işleme
80ac98c280
5 değiştirilmiş dosya ile 252 ekleme ve 10 silme
  1. 18 0
      cmd/monitor/main.go
  2. 80 10
      docs/api.md
  3. 68 0
      internal/api/api.go
  4. 75 0
      internal/database/database.go
  5. 11 0
      internal/mqtt/mqtt.go

+ 18 - 0
cmd/monitor/main.go

@@ -10,6 +10,7 @@ import (
 	"path/filepath"
 	"strconv"
 	"strings"
+	"time"
 
 	"ai-status-light/internal/api"
 	"ai-status-light/internal/database"
@@ -116,6 +117,23 @@ func runServe(args []string) {
 	server.SetMQTTClient(mqttClient)
 	server.SetBLEStdin(bleStdin)
 
+	// 启动定时清理任务(每10分钟清理超过2小时的历史记录)
+	go func() {
+		ticker := time.NewTicker(10 * time.Minute)
+		defer ticker.Stop()
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case <-ticker.C:
+				if err := db.CleanOldStatusRecords(2); err != nil {
+					logger.Error("清理历史记录失败: %v", err)
+				}
+			}
+		}
+	}()
+	logger.Info("定时清理任务已启动(每10分钟清理超过2小时的历史记录)")
+
 	if *tls {
 		if err := api.EnsureSelfSignedCert(*tlsCert, *tlsKey); err != nil {
 			logger.Error("生成自签名证书失败: %v", err)

+ 80 - 10
docs/api.md

@@ -64,7 +64,77 @@ GET /api/health
 }
 ```
 
-### 3. 获取所有 MQTT 配置
+### 3. 获取连接状态
+
+```
+GET /api/status
+```
+
+**响应示例:**
+
+```json
+{
+  "code": 0,
+  "message": "ok",
+  "data": {
+    "mqtt": {
+      "connected": true,
+      "broker": "tcp://192.168.1.100:1883"
+    },
+    "ble": {
+      "running": true,
+      "device": "AI-Light"
+    }
+  }
+}
+```
+
+**说明:**
+
+- `mqtt.connected`: MQTT 客户端是否连接到 Broker
+- `mqtt.broker`: MQTT Broker 地址
+- `ble.running`: BLE 中继进程是否运行
+- `ble.device`: BLE 设备名称
+
+### 4. 获取状态历史
+
+```
+GET /api/history
+```
+
+**查询参数:**
+| 参数 | 类型 | 必填 | 默认值 | 说明 |
+|------|------|------|--------|------|
+| limit | integer | 否 | 100 | 返回记录数量 |
+
+**响应示例:**
+
+```json
+{
+  "code": 0,
+  "message": "ok",
+  "data": [
+    {
+      "id": 1,
+      "code": "busy",
+      "timestamp": "2026-06-27 14:30:00"
+    },
+    {
+      "id": 2,
+      "code": "idle",
+      "timestamp": "2026-06-27 14:25:00"
+    }
+  ]
+}
+```
+
+**说明:**
+
+- 记录按时间倒序返回
+- 默认保留最近2小时的历史记录
+- 超过2小时的记录会自动清理
+
+### 5. 获取所有 MQTT 配置
 
 ```
 GET /api/mqtt
@@ -89,7 +159,7 @@ GET /api/mqtt
 }
 ```
 
-### 4. 创建 MQTT 配置
+### 6. 创建 MQTT 配置
 
 ```
 POST /api/mqtt
@@ -133,7 +203,7 @@ POST /api/mqtt
 }
 ```
 
-### 5. 获取单个 MQTT 配置
+### 7. 获取单个 MQTT 配置
 
 ```
 GET /api/mqtt/:id
@@ -161,7 +231,7 @@ GET /api/mqtt/:id
 }
 ```
 
-### 6. 更新 MQTT 配置
+### 8. 更新 MQTT 配置
 
 ```
 PUT /api/mqtt/:id
@@ -201,7 +271,7 @@ PUT /api/mqtt/:id
 }
 ```
 
-### 7. 删除 MQTT 配置
+### 9. 删除 MQTT 配置
 
 ```
 DELETE /api/mqtt/:id
@@ -220,7 +290,7 @@ DELETE /api/mqtt/:id
 }
 ```
 
-### 8. 获取所有 BLE 配置
+### 10. 获取所有 BLE 配置
 
 ```
 GET /api/ble
@@ -243,7 +313,7 @@ GET /api/ble
 }
 ```
 
-### 9. 创建 BLE 配置
+### 11. 创建 BLE 配置
 
 ```
 POST /api/ble
@@ -281,7 +351,7 @@ POST /api/ble
 }
 ```
 
-### 10. 获取单个 BLE 配置
+### 12. 获取单个 BLE 配置
 
 ```
 GET /api/ble/:id
@@ -292,7 +362,7 @@ GET /api/ble/:id
 |------|------|------|
 | id | integer | 配置 ID |
 
-### 11. 更新 BLE 配置
+### 13. 更新 BLE 配置
 
 ```
 PUT /api/ble/:id
@@ -313,7 +383,7 @@ PUT /api/ble/:id
 }
 ```
 
-### 12. 删除 BLE 配置
+### 14. 删除 BLE 配置
 
 ```
 DELETE /api/ble/:id

+ 68 - 0
internal/api/api.go

@@ -58,6 +58,8 @@ func New(db *database.DB, addr string) *Server {
 	mux.HandleFunc("/api/ble", s.handleBLE)
 	mux.HandleFunc("/api/ble/", s.handleBLEByID)
 	mux.HandleFunc("/api/health", s.handleHealth)
+	mux.HandleFunc("/api/status", s.handleStatus)
+	mux.HandleFunc("/api/history", s.handleHistory)
 	mux.HandleFunc("/api/device/config", s.handleDeviceConfig)
 	mux.HandleFunc("/api/device/config/", s.handleDeviceConfigByID)
 	mux.HandleFunc("/api/device/config/push", s.handleDeviceConfigPush)
@@ -119,6 +121,67 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok"})
 }
 
+func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet {
+		writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "方法不允许"})
+		return
+	}
+
+	status := map[string]interface{}{
+		"mqtt": map[string]interface{}{
+			"connected": false,
+			"broker":    "",
+		},
+		"ble": map[string]interface{}{
+			"running": false,
+			"device":  "",
+		},
+	}
+
+	if s.mqttClient != nil {
+		status["mqtt"] = map[string]interface{}{
+			"connected": s.mqttClient.IsConnected(),
+			"broker":    s.mqttClient.GetBroker(),
+		}
+	}
+
+	if s.bleStdin != nil {
+		bleCfg, err := s.db.GetBLEConfig()
+		if err == nil && bleCfg != nil {
+			status["ble"] = map[string]interface{}{
+				"running": true,
+				"device":  bleCfg.DeviceName,
+			}
+		}
+	}
+
+	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok", Data: status})
+}
+
+func (s *Server) handleHistory(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet {
+		writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "方法不允许"})
+		return
+	}
+
+	limitStr := r.URL.Query().Get("limit")
+	limit := 100
+	if limitStr != "" {
+		if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
+			limit = l
+		}
+	}
+
+	records, err := s.db.GetStatusHistory(limit)
+	if err != nil {
+		logger.Error("查询状态历史失败: %v", err)
+		writeJSON(w, http.StatusInternalServerError, Response{Code: -1, Message: err.Error()})
+		return
+	}
+
+	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok", Data: records})
+}
+
 func (s *Server) handleEvent(w http.ResponseWriter, r *http.Request) {
 	if r.Method != http.MethodPost {
 		writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "方法不允许"})
@@ -161,6 +224,11 @@ func (s *Server) handleEvent(w http.ResponseWriter, r *http.Request) {
 	// 广播到 SSE 客户端
 	s.broadcastSSE(req.Code)
 
+	// 保存历史记录
+	if err := s.db.SaveStatusRecord(req.Code); err != nil {
+		logger.Error("保存状态历史失败: %v", err)
+	}
+
 	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok"})
 }
 

+ 75 - 0
internal/database/database.go

@@ -55,6 +55,12 @@ type DeviceConfig struct {
 	Enabled     bool   `json:"enabled"`
 }
 
+type StatusRecord struct {
+	ID        int    `json:"id"`
+	Code      string `json:"code"`
+	Timestamp string `json:"timestamp"`
+}
+
 func New(dbPath string) (*DB, error) {
 	dir := filepath.Dir(dbPath)
 	if err := os.MkdirAll(dir, 0755); err != nil {
@@ -150,6 +156,23 @@ func (d *DB) init() error {
 		return err
 	}
 
+	historyQuery := `
+	CREATE TABLE IF NOT EXISTS status_history (
+		id INTEGER PRIMARY KEY AUTOINCREMENT,
+		code TEXT NOT NULL,
+		timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
+	);
+	`
+	_, err = d.conn.ExecContext(context.Background(), historyQuery)
+	if err != nil {
+		return err
+	}
+
+	_, err = d.conn.ExecContext(context.Background(), "CREATE INDEX IF NOT EXISTS idx_status_history_timestamp ON status_history(timestamp)")
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -364,3 +387,55 @@ func (d *DB) ListDeviceConfigs() ([]DeviceConfig, error) {
 	logger.Debug("查询到 %d 条设备配置", len(configs))
 	return configs, nil
 }
+
+func (d *DB) SaveStatusRecord(code string) error {
+	query := "INSERT INTO status_history (code, timestamp) VALUES (?, datetime('now'))"
+	_, err := d.conn.ExecContext(context.Background(), query, code)
+	if err != nil {
+		logger.Error("保存状态记录失败: %v", err)
+	}
+	return err
+}
+
+func (d *DB) GetStatusHistory(limit int) ([]StatusRecord, error) {
+	if limit <= 0 {
+		limit = 100
+	}
+	query := "SELECT id, code, timestamp FROM status_history ORDER BY timestamp DESC LIMIT ?"
+	rows, err := d.conn.QueryContext(context.Background(), query, limit)
+	if err != nil {
+		logger.Error("查询状态历史失败: %v", err)
+		return nil, err
+	}
+	defer rows.Close()
+
+	var records []StatusRecord
+	for rows.Next() {
+		var r StatusRecord
+		if err := rows.Scan(&r.ID, &r.Code, &r.Timestamp); err != nil {
+			logger.Warn("扫描状态记录行失败: %v", err)
+			continue
+		}
+		records = append(records, r)
+	}
+	logger.Debug("查询到 %d 条状态记录", len(records))
+	return records, nil
+}
+
+func (d *DB) CleanOldStatusRecords(hours int) error {
+	if hours <= 0 {
+		hours = 2
+	}
+	query := "DELETE FROM status_history WHERE timestamp < datetime('now', ?)"
+	param := fmt.Sprintf("-%d hours", hours)
+	result, err := d.conn.ExecContext(context.Background(), query, param)
+	if err != nil {
+		logger.Error("清理旧状态记录失败: %v", err)
+		return err
+	}
+	rowsAffected, _ := result.RowsAffected()
+	if rowsAffected > 0 {
+		logger.Info("已清理 %d 条超过 %d 小时的状态记录", rowsAffected, hours)
+	}
+	return nil
+}

+ 11 - 0
internal/mqtt/mqtt.go

@@ -120,6 +120,17 @@ func (c *Client) GetTopic() string {
 	return c.topic
 }
 
+func (c *Client) GetBroker() string {
+	return c.broker
+}
+
+func (c *Client) IsConnected() bool {
+	if c.client == nil {
+		return false
+	}
+	return c.client.IsConnected()
+}
+
 func (c *Client) Disconnect() {
 	if c.client != nil && c.client.IsConnected() {
 		c.client.Disconnect(1000)