1. 删除 ConfigAgent (与 MQTT 配置更新重叠且未启用) 2. OTA 完整安装流程: 解压tar.gz -> 校验ELF -> 替换二进制 -> systemctl重启 3. StreamManager stopAll 添加 Wait() 防止僵尸进程 4. InferClient socket 读写添加 timeout 防止永久阻塞 5. Heartbeat 集成 NPU 监控 (npu-smi + fallback 脚本) 6. 配置更新时动态重启 ffmpeg 以应用新 fps
178 lines
4.1 KiB
Go
178 lines
4.1 KiB
Go
package control
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"tianyan-edge/internal/config"
|
|
)
|
|
|
|
type Heartbeat struct {
|
|
cfg *config.Config
|
|
}
|
|
|
|
func NewHeartbeat(cfg *config.Config) *Heartbeat {
|
|
return &Heartbeat{cfg: cfg}
|
|
}
|
|
|
|
func (h *Heartbeat) Run(ctx context.Context) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
// Send first heartbeat immediately
|
|
h.send(client)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
h.send(client)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Heartbeat) send(client *http.Client) {
|
|
payload := map[string]any{
|
|
"edge_id": h.cfg.EdgeID,
|
|
"version": h.cfg.Version,
|
|
"ts": float64(time.Now().UnixMilli()) / 1000.0,
|
|
}
|
|
|
|
// Collect NPU stats if available
|
|
if npu := collectNPUStats(); npu != nil {
|
|
payload["npu"] = npu
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
url := fmt.Sprintf("%s/api/v1/edge/heartbeat", h.cfg.CloudURL)
|
|
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+h.cfg.EdgeToken)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
log.Printf("heartbeat: %v", err)
|
|
return
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
|
|
// NPUStats holds parsed NPU telemetry
|
|
type NPUStats struct {
|
|
TempC int `json:"temp_c"`
|
|
UtilPct int `json:"util_pct"`
|
|
MemUsedMB int `json:"mem_used_mb"`
|
|
MemTotalMB int `json:"mem_total_mb"`
|
|
MemPct int `json:"mem_pct"`
|
|
}
|
|
|
|
// collectNPUStats runs npu-smi and parses the output
|
|
func collectNPUStats() *NPUStats {
|
|
// Try npu-smi first (Ascend devices), fall back to the helper script
|
|
var commonOut, memOut []byte
|
|
var err error
|
|
|
|
commonOut, err = exec.Command("/usr/local/sbin/npu-smi", "info", "-t", "common", "-i", "0").CombinedOutput()
|
|
if err != nil {
|
|
// Fallback to shell script
|
|
commonOut, err = exec.Command("bash", "/opt/tianyan-edge/scripts/npu_info.sh").CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("npu: monitoring unavailable: %v", err)
|
|
return nil
|
|
}
|
|
// If script succeeded, it outputs Influx line protocol — parse that
|
|
line := strings.TrimSpace(string(commonOut))
|
|
return parseInfluxLine(line)
|
|
}
|
|
|
|
memOut, _ = exec.Command("/usr/local/sbin/npu-smi", "info", "-t", "memory", "-i", "0").CombinedOutput()
|
|
|
|
return parseNpuSmiOutput(string(commonOut), string(memOut))
|
|
}
|
|
|
|
func parseNpuSmiOutput(common, memory string) *NPUStats {
|
|
stats := &NPUStats{}
|
|
|
|
for _, line := range strings.Split(common, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.Contains(line, "Temperature") {
|
|
if v := extractLastNumber(line); v != 0 {
|
|
stats.TempC = v
|
|
}
|
|
}
|
|
if strings.Contains(line, "Aicore Usage Rate") {
|
|
if v := extractLastNumber(line); v != 0 {
|
|
stats.UtilPct = v
|
|
}
|
|
}
|
|
if strings.Contains(line, "Memory Usage Rate") {
|
|
if v := extractLastNumber(line); v != 0 {
|
|
stats.MemPct = v
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, line := range strings.Split(memory, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.Contains(line, "Capacity") {
|
|
if v := extractLastNumber(line); v != 0 {
|
|
stats.MemTotalMB = v
|
|
}
|
|
}
|
|
}
|
|
|
|
if stats.MemTotalMB > 0 && stats.MemPct > 0 {
|
|
stats.MemUsedMB = stats.MemTotalMB * stats.MemPct / 100
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
func parseInfluxLine(line string) *NPUStats {
|
|
// Format: npu_status,device=0 temp=X,utilization=Y,memory_used=Z,memory_total=W
|
|
stats := &NPUStats{}
|
|
parts := strings.Split(line, " ")
|
|
if len(parts) < 2 {
|
|
return nil
|
|
}
|
|
for _, field := range strings.Split(parts[1], ",") {
|
|
kv := strings.SplitN(field, "=", 2)
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
v, _ := strconv.Atoi(kv[1])
|
|
switch kv[0] {
|
|
case "temp":
|
|
stats.TempC = v
|
|
case "utilization":
|
|
stats.UtilPct = v
|
|
case "memory_used":
|
|
stats.MemUsedMB = v
|
|
case "memory_total":
|
|
stats.MemTotalMB = v
|
|
}
|
|
}
|
|
return stats
|
|
}
|
|
|
|
func extractLastNumber(line string) int {
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 0 {
|
|
return 0
|
|
}
|
|
// Last field might have units like "%" or "C"
|
|
raw := fields[len(fields)-1]
|
|
raw = strings.TrimRight(raw, "%C ")
|
|
v, _ := strconv.Atoi(raw)
|
|
return v
|
|
}
|