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
This commit is contained in:
119
backend/internal/clients/mqtt.go
Normal file
119
backend/internal/clients/mqtt.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// 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",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user