Files
Extrudex/backend/internal/clients/mqtt.go
hex-bot 90fd028bfc CUB-117: Port Moonraker + MQTT printer integrations to Go
- Add internal/clients/moonraker.go: HTTP client for Moonraker REST API
- Add internal/clients/mqtt.go: MQTT client via paho.mqtt.golang with TLS support
- Add internal/workers/moonraker_poller.go: background polling, usage logging, SSE broadcasts
- Add internal/workers/mqtt_subscriber.go: per-printer MQTT subscriber, Bambu telemetry parsing, SSE broadcasts
- Wire workers into cmd/server/main.go (Start/Stop with graceful shutdown)

Blocked at step 4: go toolchain not installed on build host
2026-05-10 16:14:35 -04:00

120 lines
3.4 KiB
Go

// 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",
}
}