// Package clients provides third-party printer integrations. package clients import ( "context" "crypto/tls" "encoding/json" "fmt" "log/slog" "time" mqtt "github.com/eclipse/paho.mqtt.golang" ) // MQTTClient wraps the Eclipse Paho MQTT client for printer telemetry. type MQTTClient struct { client mqtt.Client } // MQTTConfig holds per-printer MQTT connection settings. type MQTTConfig struct { BrokerHost string BrokerPort int TopicPrefix string TLSEnabled bool ClientID string } // BambuPrintStatus is the known Bambu Lab print-status payload shape. type BambuPrintStatus struct { Print struct { GcodeFile string `json:"gcode_file,omitempty"` Stage int `json:"stage,omitempty"` SubTaskName string `json:"subtask_name,omitempty"` PrintType string `json:"print_type,omitempty"` FilamentUsedMm float64 `json:"mc_percent,omitempty"` // placeholder; real telemetry varies } `json:"print,omitempty"` } // NewMQTTClient creates an MQTT client connected to the given broker. func NewMQTTClient(cfg MQTTConfig) (*MQTTClient, error) { if cfg.BrokerPort == 0 { if cfg.TLSEnabled { cfg.BrokerPort = 8883 } else { cfg.BrokerPort = 1883 } } if cfg.ClientID == "" { cfg.ClientID = fmt.Sprintf("extrudex-%d", time.Now().Unix()) } opts := mqtt.NewClientOptions(). AddBroker(fmt.Sprintf("tcp://%s:%d", cfg.BrokerHost, cfg.BrokerPort)). SetClientID(cfg.ClientID). SetAutoReconnect(true). SetConnectTimeout(10 * time.Second). SetOrderMatters(false) if cfg.TLSEnabled { opts = opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: false}) } client := mqtt.NewClient(opts) token := client.Connect() if token.Wait() && token.Error() != nil { return nil, fmt.Errorf("mqtt connect failed: %w", token.Error()) } slog.Info("mqtt client connected", "broker", cfg.BrokerHost, "port", cfg.BrokerPort, "tls", cfg.TLSEnabled) return &MQTTClient{client: client}, nil } // Subscribe registers a callback for messages matching topic. func (c *MQTTClient) Subscribe(topic string, qos byte, callback func([]byte)) error { token := c.client.Subscribe(topic, qos, func(_ mqtt.Client, msg mqtt.Message) { callback(msg.Payload()) }) if token.Wait() && token.Error() != nil { return fmt.Errorf("mqtt subscribe failed: %w", token.Error()) } slog.Info("mqtt subscribed", "topic", topic, "qos", qos) return nil } // Unsubscribe removes a subscription. func (c *MQTTClient) Unsubscribe(topics ...string) error { token := c.client.Unsubscribe(topics...) if token.Wait() && token.Error() != nil { return fmt.Errorf("mqtt unsubscribe failed: %w", token.Error()) } return nil } // Disconnect cleanly disconnects the MQTT client. func (c *MQTTClient) Disconnect(quiesceMs uint) { c.client.Disconnect(quiesceMs) } // IsConnected returns whether the underlying client is connected. func (c *MQTTClient) IsConnected() bool { return c.client.IsConnected() } // ParseBambuTelemetry attempts to parse a Bambu Lab telemetry JSON payload. func ParseBambuTelemetry(payload []byte) (*BambuPrintStatus, error) { var msg BambuPrintStatus if err := json.Unmarshal(payload, &msg); err != nil { return nil, fmt.Errorf("parse bambu telemetry failed: %w", err) } return &msg, nil } // DefaultBambuTopics returns the default topic patterns for Bambu Lab printers. func DefaultBambuTopics(topicPrefix string) []string { return []string{ topicPrefix + "/report", } }