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) }