diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e506fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +logs/ +*.om +*.onnx +test.jpg +fusion_result.json +device.uuid diff --git a/cmd/edge-agent/main.go b/cmd/edge-agent/main.go index 664342d..c54e29a 100644 --- a/cmd/edge-agent/main.go +++ b/cmd/edge-agent/main.go @@ -21,13 +21,14 @@ func main() { flag.Parse() cfg := config.Load(*cfgPath) - log.Printf("edge-agent start id=%s cloud=%s", cfg.EdgeID, cfg.CloudURL) + log.Printf("edge-agent start id=%s uuid=%s cloud=%s mqtt=%s", + cfg.EdgeID, cfg.GetDeviceIdentity(), cfg.CloudURL, cfg.MqttBroker) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() - frames := make(chan stream.Frame, 50) - events := make(chan event.SuspectedEvent, 200) + frames := make(chan stream.Frame, 100) + events := make(chan event.SuspectedEvent, 500) var wg sync.WaitGroup run := func(fn func(context.Context)) { @@ -38,11 +39,23 @@ func main() { }() } - run(stream.NewIngestor(cfg, frames).Run) - run(infer.NewClient(cfg, frames, events).Run) + // 1. Cloud Communication + mqttMgr := control.NewMqttManager(cfg) + run(mqttMgr.Run) + + // 2. Stream Ingestion (Dynamic) + run(stream.NewStreamManager(cfg, frames, mqttMgr.GetUpdates()).Run) + + // 3. Inference (Dynamic Workers) + run(infer.NewClient(cfg, frames, events, mqttMgr.GetUpdates()).Run) + + // 4. Event Upload run(event.NewUploader(cfg, events).Run) + + // 5. Heartbeat run(control.NewHeartbeat(cfg).Run) - run(control.NewConfigAgent(cfg).Run) + + // 6. OTA Update run(control.NewOTAAgent(cfg).Run) wg.Wait() diff --git a/config/edge.yaml b/config/edge.yaml index 784978e..32b6093 100644 --- a/config/edge.yaml +++ b/config/edge.yaml @@ -1,13 +1,21 @@ edge_id: edge-demo-001 cloud_url: http://101.36.73.102:8004 +mqtt_broker: "tcp://101.36.73.102:1883" +mqtt_user: "" +mqtt_pass: "" edge_token: "" -# RTSP 流地址(留空使用演示模式) +# 视频流配置 rtsp_urls: [] +# 推理配置 infer_socket: /tmp/edge-infer.sock infer_fps: 5 infer_workers: 3 conf_threshold: 0.5 dedup_window_sec: 30 -version: 1.0.0 + +# OTA 配置 (可选: Hawkbit URL 或 Nginx URL) +ota_url: http://101.36.73.102:8087 + +version: 1.0.0 \ No newline at end of file diff --git a/config/telegraf.conf b/config/telegraf.conf new file mode 100644 index 0000000..49f871a --- /dev/null +++ b/config/telegraf.conf @@ -0,0 +1,32 @@ +[agent] + interval = "10s" + round_interval = true + metric_batch_size = 1000 + metric_buffer_limit = 10000 + flush_interval = "10s" + hostname = "edge-001" # 启动时替换为实际 edge_id + +# 输出到云端 InfluxDB +[[outputs.influxdb_v2]] + urls = ["http://101.36.73.102:18086"] + token = "my-super-secret-token" # 替换为实际 token + organization = "tianyan" + bucket = "edge_metrics" + +# 基础硬件监控 +[[inputs.cpu]] + percpu = false + totalcpu = true + +[[inputs.mem]] + +[[inputs.disk]] + ignore_fs = ["tmpfs", "devtmpfs", "devfs", "overlay", "aufs", "squashfs"] + +[[inputs.net]] + +# NPU 自定义监控 +[[inputs.exec]] + commands = ["/opt/tianyan-edge/scripts/npu_info.sh"] + timeout = "5s" + data_format = "influx" \ No newline at end of file diff --git a/go.mod b/go.mod index 4bb9890..bba98a7 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,15 @@ module tianyan-edge -go 1.22 +go 1.18 -require gopkg.in/yaml.v3 v3.0.1 +require ( + github.com/eclipse/paho.mqtt.golang v1.4.3 + gopkg.in/yaml.v3 v3.0.1 +) -require gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +require ( + github.com/gorilla/websocket v1.5.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sync v0.1.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum index 33d593e..084e15e 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,16 @@ +github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= +github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/config/config.go b/internal/config/config.go index ced1260..ea2b8d3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,14 +1,21 @@ package config import ( + "crypto/rand" + "encoding/hex" "os" + "path/filepath" "gopkg.in/yaml.v3" ) type Config struct { + DeviceUUID string `yaml:"device_uuid"` EdgeID string `yaml:"edge_id"` CloudURL string `yaml:"cloud_url"` + MqttBroker string `yaml:"mqtt_broker"` + MqttUser string `yaml:"mqtt_user"` + MqttPass string `yaml:"mqtt_pass"` EdgeToken string `yaml:"edge_token"` RTSPURLs []string `yaml:"rtsp_urls"` InferSocket string `yaml:"infer_socket"` @@ -16,12 +23,14 @@ type Config struct { InferWorkers int `yaml:"infer_workers"` ConfThreshold float64 `yaml:"conf_threshold"` DedupWindowSec int `yaml:"dedup_window_sec"` + OTAUrl string `yaml:"ota_url"` Version string `yaml:"version"` + configPath string `json:"-"` } func Load(path string) *Config { cfg := &Config{ - EdgeID: "edge-demo-001", + EdgeID: "edge-unknown", CloudURL: "http://localhost:8004", InferSocket: "/tmp/edge-infer.sock", InferFPS: 5, @@ -29,12 +38,15 @@ func Load(path string) *Config { ConfThreshold: 0.5, DedupWindowSec: 30, Version: "1.0.0", + configPath: path, } - data, err := os.ReadFile(path) - if err != nil { - return cfg + + cfg.loadUUID() + + if data, err := os.ReadFile(path); err == nil { + _ = yaml.Unmarshal(data, cfg) } - _ = yaml.Unmarshal(data, cfg) + if cfg.InferWorkers <= 0 { cfg.InferWorkers = 1 } @@ -43,3 +55,31 @@ func Load(path string) *Config { } return cfg } + +func (c *Config) loadUUID() { + uuidPath := "/opt/tianyan-edge/device.uuid" + if data, err := os.ReadFile(uuidPath); err == nil { + c.DeviceUUID = string(data) + } else { + b := make([]byte, 16) + rand.Read(b) + c.DeviceUUID = hex.EncodeToString(b) + os.MkdirAll(filepath.Dir(uuidPath), 0755) + os.WriteFile(uuidPath, []byte(c.DeviceUUID), 0644) + } +} + +func (c *Config) Save() error { + data, err := yaml.Marshal(c) + if err != nil { + return err + } + return os.WriteFile(c.configPath, data, 0644) +} + +func (c *Config) GetDeviceIdentity() string { + if c.DeviceUUID != "" { + return c.DeviceUUID + } + return c.EdgeID +} diff --git a/internal/control/mqtt_manager.go b/internal/control/mqtt_manager.go new file mode 100644 index 0000000..6c27de3 --- /dev/null +++ b/internal/control/mqtt_manager.go @@ -0,0 +1,123 @@ +package control + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" + "tianyan-edge/internal/config" +) + +type MqttManager struct { + client mqtt.Client + cfg *config.Config + updates chan *config.Config // Channel to broadcast config updates +} + +func NewMqttManager(cfg *config.Config) *MqttManager { + return &MqttManager{ + cfg: cfg, + updates: make(chan *config.Config, 10), + } +} + +func (m *MqttManager) Run(ctx context.Context) { + if m.cfg.MqttBroker == "" { + log.Println("mqtt: no broker configured, skipping") + return + } + + opts := mqtt.NewClientOptions() + opts.AddBroker(m.cfg.MqttBroker) + opts.SetClientID(m.cfg.GetDeviceIdentity()) + opts.SetUsername(m.cfg.MqttUser) + opts.SetPassword(m.cfg.MqttPass) + opts.SetAutoReconnect(true) + opts.SetMaxReconnectInterval(1 * time.Minute) + + // LWT + willTopic := fmt.Sprintf("tianyan/edge/%s/status", m.cfg.GetDeviceIdentity()) + opts.SetWill(willTopic, "offline", 1, true) + + opts.SetOnConnectHandler(func(c mqtt.Client) { + log.Println("mqtt: connected to broker") + c.Publish(willTopic, 1, true, "online") + configTopic := fmt.Sprintf("tianyan/edge/%s/config", m.cfg.GetDeviceIdentity()) + token := c.Subscribe(configTopic, 1, m.handleConfigUpdate) + token.Wait() + if token.Error() != nil { + log.Printf("mqtt: subscribe failed: %v", token.Error()) + } else { + log.Printf("mqtt: subscribed to %s", configTopic) + } + }) + + m.client = mqtt.NewClient(opts) + if token := m.client.Connect(); token.Wait() && token.Error() != nil { + log.Printf("mqtt: connect error: %v", token.Error()) + } + + <-ctx.Done() + m.client.Disconnect(1000) + log.Println("mqtt: disconnected") +} + +func (m *MqttManager) handleConfigUpdate(c mqtt.Client, msg mqtt.Message) { + log.Printf("mqtt: received config update on %s", msg.Topic()) + var newCfg map[string]interface{} + if err := json.Unmarshal(msg.Payload(), &newCfg); err != nil { + log.Printf("mqtt: config parse error: %v", err) + return + } + + // Update local config struct + if v, ok := newCfg["infer_fps"].(float64); ok { + m.cfg.InferFPS = int(v) + } + if v, ok := newCfg["conf_threshold"].(float64); ok { + m.cfg.ConfThreshold = v + } + if v, ok := newCfg["infer_workers"].(float64); ok { + m.cfg.InferWorkers = int(v) + } + if v, ok := newCfg["rtsp_urls"]; ok { + if urls, ok := v.([]interface{}); ok { + var s []string + for _, u := range urls { + if str, ok := u.(string); ok { + s = append(s, str) + } + } + m.cfg.RTSPURLs = s + } + } + + // Save to disk + if err := m.cfg.Save(); err != nil { + log.Printf("mqtt: failed to save config: %v", err) + } else { + log.Println("mqtt: config saved to disk") + } + + // Broadcast update to other components + select { + case m.updates <- m.cfg: + default: + log.Println("mqtt: update channel full, dropping update") + } +} + +func (m *MqttManager) GetUpdates() <-chan *config.Config { + return m.updates +} + +func (m *MqttManager) Publish(topic string, payload interface{}) { + if m.client == nil || !m.client.IsConnected() { + return + } + data, _ := json.Marshal(payload) + m.client.Publish(topic, 1, false, data) +} diff --git a/internal/infer/client.go b/internal/infer/client.go index 97b61a3..c1c2a9b 100644 --- a/internal/infer/client.go +++ b/internal/infer/client.go @@ -46,31 +46,75 @@ type dedupState struct { } type Client struct { - cfg *config.Config - frames <-chan stream.Frame - events chan<- event.SuspectedEvent - dedup *dedupState + cfg *config.Config + frames <-chan stream.Frame + events chan<- event.SuspectedEvent + dedup *dedupState + updates <-chan *config.Config } -func NewClient(cfg *config.Config, frames <-chan stream.Frame, events chan<- event.SuspectedEvent) *Client { +func NewClient(cfg *config.Config, frames <-chan stream.Frame, events chan<- event.SuspectedEvent, updates <-chan *config.Config) *Client { return &Client{ - cfg: cfg, - frames: frames, - events: events, - dedup: &dedupState{seen: make(map[string]float64)}, + cfg: cfg, + frames: frames, + events: events, + dedup: &dedupState{seen: make(map[string]float64)}, + updates: updates, } } func (c *Client) Run(ctx context.Context) { var wg sync.WaitGroup - for i := 0; i < c.cfg.InferWorkers; i++ { + + // Worker management loop + numWorkers := c.cfg.InferWorkers + activeWorkers := make([]context.CancelFunc, numWorkers) + + for i := 0; i < numWorkers; i++ { + workerCtx, cancel := context.WithCancel(ctx) + activeWorkers[i] = cancel wg.Add(1) - go func(workerID int) { + go func(id int, wCtx context.Context) { defer wg.Done() - c.workerLoop(ctx, workerID) - }(i) + c.workerLoop(wCtx, id) + }(i, workerCtx) + } + + for { + select { + case <-ctx.Done(): + for _, cancel := range activeWorkers { + cancel() + } + wg.Wait() + return + case newCfg := <-c.updates: + c.cfg = newCfg + target := c.cfg.InferWorkers + if target < 0 { + target = 0 + } + + // Scale up + for i := len(activeWorkers); i < target; i++ { + workerCtx, cancel := context.WithCancel(ctx) + activeWorkers = append(activeWorkers, cancel) + wg.Add(1) + go func(id int, wCtx context.Context) { + defer wg.Done() + c.workerLoop(wCtx, id) + }(i, workerCtx) + } + // Scale down + if target < len(activeWorkers) { + for i := target; i < len(activeWorkers); i++ { + activeWorkers[i]() + } + activeWorkers = activeWorkers[:target] + } + log.Printf("infer: workers scaled to %d", target) + } } - wg.Wait() } func (c *Client) workerLoop(ctx context.Context, workerID int) { @@ -89,10 +133,9 @@ func (c *Client) workerLoop(ctx context.Context, workerID int) { log.Printf("infer[w%d]: socket connected", workerID) if !c.loop(ctx, conn, workerID) { conn.Close() - return + time.Sleep(1 * time.Second) } conn.Close() - time.Sleep(500 * time.Millisecond) } } @@ -103,12 +146,10 @@ func (c *Client) loop(ctx context.Context, conn net.Conn, workerID int) bool { return false case f := <-c.frames: if err := c.send(conn, f); err != nil { - log.Printf("infer[w%d]: send error: %v", workerID, err) return true } res, err := c.recv(conn) if err != nil { - log.Printf("infer[w%d]: recv error: %v", workerID, err) return true } c.emitEvents(f, res) diff --git a/internal/stream/ingestor.go b/internal/stream/ingestor.go index 38825a1..5e55ebd 100644 --- a/internal/stream/ingestor.go +++ b/internal/stream/ingestor.go @@ -1,7 +1,6 @@ package stream import ( - "bytes" "context" "fmt" "io" @@ -10,6 +9,8 @@ import ( "sync" "time" + "bytes" + "tianyan-edge/internal/config" ) @@ -21,63 +22,109 @@ type Frame struct { TS float64 } -type Ingestor struct { - cfg *config.Config - frames chan<- Frame +type StreamManager struct { + cfg *config.Config + frames chan<- Frame + processes map[string]*exec.Cmd + mu sync.Mutex + updates <-chan *config.Config } -func NewIngestor(cfg *config.Config, frames chan<- Frame) *Ingestor { - return &Ingestor{cfg: cfg, frames: frames} -} - -func (ing *Ingestor) Run(ctx context.Context) { - if len(ing.cfg.RTSPURLs) == 0 { - log.Println("stream: no RTSP URLs configured, idle") - <-ctx.Done() - return +func NewStreamManager(cfg *config.Config, frames chan<- Frame, updates <-chan *config.Config) *StreamManager { + return &StreamManager{ + cfg: cfg, + frames: frames, + processes: make(map[string]*exec.Cmd), + updates: updates, } - var wg sync.WaitGroup - for i, url := range ing.cfg.RTSPURLs { - wg.Add(1) - go func(idx int, u string) { - defer wg.Done() - ing.streamLoop(ctx, idx, u) - }(i, url) - } - wg.Wait() } -func (ing *Ingestor) streamLoop(ctx context.Context, idx int, url string) { +func (sm *StreamManager) Run(ctx context.Context) { + // Initial start + sm.applyStreams(ctx, sm.cfg.RTSPURLs) + + for { + select { + case <-ctx.Done(): + sm.stopAll() + return + case newCfg := <-sm.updates: + log.Printf("stream: applying new config, urls=%d", len(newCfg.RTSPURLs)) + sm.applyStreams(ctx, newCfg.RTSPURLs) + } + } +} + +func (sm *StreamManager) applyStreams(ctx context.Context, urls []string) { + sm.mu.Lock() + defer sm.mu.Unlock() + + // Identify URLs to remove (present in sm.processes but not in new urls) + toRemove := map[string]*exec.Cmd{} + for url, cmd := range sm.processes { + found := false + for _, u := range urls { + if u == url { + found = true + break + } + } + if !found { + toRemove[url] = cmd + } + } + + // Stop removed streams + for url, cmd := range toRemove { + log.Printf("stream: stopping %s", url) + cmd.Process.Kill() + cmd.Wait() + delete(sm.processes, url) + } + + // Start new streams + for i, url := range urls { + if _, exists := sm.processes[url]; !exists { + go sm.streamLoop(ctx, i, url) + } + } +} + +func (sm *StreamManager) streamLoop(ctx context.Context, idx int, url string) { deviceID := fmt.Sprintf("cam-%03d", idx) - fps := ing.cfg.InferFPS - if fps <= 0 { - fps = 5 - } for { select { case <-ctx.Done(): return default: } + cmd := exec.CommandContext(ctx, "ffmpeg", - "-rtsp_transport", "tcp", "-i", url, - "-vf", fmt.Sprintf("fps=%d", fps), + "-i", url, + "-vf", fmt.Sprintf("fps=%d", sm.cfg.InferFPS), "-f", "image2pipe", "-vcodec", "mjpeg", "-q:v", "5", "-", ) stdout, err := cmd.StdoutPipe() if err != nil { - log.Printf("stream[%d] stdout pipe failed: %v, retry 10s", idx, err) - time.Sleep(10 * time.Second) + log.Printf("stream[%d] stdout pipe failed: %v", idx, err) + time.Sleep(5 * time.Second) continue } + + sm.mu.Lock() + sm.processes[url] = cmd + sm.mu.Unlock() + if err := cmd.Start(); err != nil { - log.Printf("stream[%d] ffmpeg start failed: %v, retry 10s", idx, err) - time.Sleep(10 * time.Second) + log.Printf("stream[%d] start failed: %v", idx, err) + time.Sleep(5 * time.Second) continue } + log.Printf("stream[%d] connected url=%s", idx, url) - ing.readFrames(ctx, stdout, idx, deviceID, url) + sm.readFrames(ctx, stdout, idx, deviceID, url) cmd.Wait() + select { case <-ctx.Done(): return @@ -88,7 +135,7 @@ func (ing *Ingestor) streamLoop(ctx context.Context, idx int, url string) { } } -func (ing *Ingestor) readFrames(ctx context.Context, r io.Reader, idx int, deviceID, url string) { +func (sm *StreamManager) readFrames(ctx context.Context, r io.Reader, idx int, deviceID, url string) { soi, eoi := []byte{0xFF, 0xD8}, []byte{0xFF, 0xD9} buf, tmp := make([]byte, 0, 1<<20), make([]byte, 32768) for { @@ -116,7 +163,7 @@ func (ing *Ingestor) readFrames(ctx context.Context, r io.Reader, idx int, devic copy(frame, buf[:end]) buf = buf[end:] select { - case ing.frames <- Frame{StreamID: idx, DeviceID: deviceID, URL: url, + case sm.frames <- Frame{StreamID: idx, DeviceID: deviceID, URL: url, JPEG: frame, TS: float64(time.Now().UnixMilli()) / 1000.0}: default: } @@ -127,3 +174,13 @@ func (ing *Ingestor) readFrames(ctx context.Context, r io.Reader, idx int, devic } } } + +func (sm *StreamManager) stopAll() { + sm.mu.Lock() + defer sm.mu.Unlock() + for url, cmd := range sm.processes { + log.Printf("stream: stopping all %s", url) + cmd.Process.Kill() + delete(sm.processes, url) + } +} diff --git a/model/model.om b/model/model.om index 90a2456..fd5912b 100644 Binary files a/model/model.om and b/model/model.om differ diff --git a/scripts/npu_info.sh b/scripts/npu_info.sh new file mode 100644 index 0000000..e3ad8de --- /dev/null +++ b/scripts/npu_info.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# 采集 NPU 状态并输出为 Influx Line Protocol + +COMMON_OUTPUT=$(/usr/local/sbin/npu-smi info -t common -i 0 2>/dev/null) +MEM_OUTPUT=$(/usr/local/sbin/npu-smi info -t memory -i 0 2>/dev/null) + +TEMP=$(echo "$COMMON_OUTPUT" | grep "Temperature(C)" | awk '{print $NF}') +UTIL=$(echo "$COMMON_OUTPUT" | grep "Aicore Usage Rate" | awk '{print $NF}') +MEM_TOTAL_MB=$(echo "$MEM_OUTPUT" | grep "Capacity(MB)" | awk '{print $NF}') +MEM_RATE=$(echo "$COMMON_OUTPUT" | grep "Memory Usage Rate" | awk '{print $NF}') + +# 计算实际使用量 (使用 awk 代替 bc) +if [ -n "$MEM_TOTAL_MB" ] && [ -n "$MEM_RATE" ]; then + MEM_USED_MB=$(awk "BEGIN {printf \"%d\", $MEM_TOTAL_MB * $MEM_RATE / 100}") +else + MEM_TOTAL_MB=0 + MEM_USED_MB=0 + UTIL=0 + TEMP=0 +fi + +# 默认值保护 +TEMP=${TEMP:-0} +UTIL=${UTIL:-0} + +echo "npu_status,device=0 temp=${TEMP},utilization=${UTIL},memory_used=${MEM_USED_MB:-0},memory_total=${MEM_TOTAL_MB:-0}"