Files
spore-gateway/internal/mqtt/mqtt.go
2025-10-26 18:37:07 +01:00

139 lines
3.7 KiB
Go

package mqtt
import (
"context"
"fmt"
"os"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
log "github.com/sirupsen/logrus"
)
// MQTTClient represents an MQTT client for the gateway
type MQTTClient struct {
client mqtt.Client
serverURL string
username string
password string
connected bool
logger *log.Logger
messageCallback func(topic string, data []byte)
}
// NewMQTTClient creates a new MQTT client instance
func NewMQTTClient(serverURL, username, password string) *MQTTClient {
return &MQTTClient{
serverURL: serverURL,
username: username,
password: password,
logger: log.New(),
}
}
// SetMessageCallback sets the callback function to be called when messages are received
func (mc *MQTTClient) SetMessageCallback(callback func(topic string, data []byte)) {
mc.messageCallback = callback
}
// Connect connects to the MQTT broker
func (mc *MQTTClient) Connect() error {
opts := mqtt.NewClientOptions()
opts.AddBroker(mc.serverURL)
opts.SetClientID(fmt.Sprintf("spore-gateway-%d", time.Now().Unix()))
opts.SetCleanSession(true)
opts.SetAutoReconnect(true)
opts.SetConnectRetry(true)
opts.SetConnectRetryInterval(10 * time.Second)
opts.SetKeepAlive(30 * time.Second)
opts.SetPingTimeout(10 * time.Second)
// Set credentials if provided
if mc.username != "" {
opts.SetUsername(mc.username)
}
if mc.password != "" {
opts.SetPassword(mc.password)
}
// Set connection callbacks
opts.SetOnConnectHandler(mc.onConnected)
opts.SetConnectionLostHandler(mc.onConnectionLost)
mc.client = mqtt.NewClient(opts)
mc.logger.WithFields(log.Fields{
"server": mc.serverURL,
"username": mc.username,
}).Info("Connecting to MQTT broker")
if token := mc.client.Connect(); token.Wait() && token.Error() != nil {
return fmt.Errorf("failed to connect to MQTT broker: %w", token.Error())
}
return nil
}
// onConnected is called when the client successfully connects to the broker
func (mc *MQTTClient) onConnected(client mqtt.Client) {
mc.logger.Info("Successfully connected to MQTT broker")
mc.connected = true
// Subscribe to all topics
if token := mc.client.Subscribe("#", 0, mc.handleMessage); token.Wait() && token.Error() != nil {
mc.logger.WithError(token.Error()).Error("Failed to subscribe to MQTT topics")
} else {
mc.logger.Info("Subscribed to all MQTT topics (#)")
}
}
// onConnectionLost is called when the connection to the broker is lost
func (mc *MQTTClient) onConnectionLost(client mqtt.Client, err error) {
mc.logger.WithError(err).Error("MQTT connection lost")
mc.connected = false
}
// handleMessage handles incoming MQTT messages
func (mc *MQTTClient) handleMessage(client mqtt.Client, msg mqtt.Message) {
topic := msg.Topic()
payload := msg.Payload()
mc.logger.WithFields(log.Fields{
"topic": topic,
"length": len(payload),
}).Debug("Received MQTT message")
// Call the callback if set
if mc.messageCallback != nil {
mc.messageCallback(topic, payload)
}
}
// Disconnect disconnects from the MQTT broker
func (mc *MQTTClient) Disconnect() {
if mc.client != nil && mc.connected {
mc.logger.Info("Disconnecting from MQTT broker")
mc.client.Disconnect(250)
mc.connected = false
}
}
// Shutdown gracefully shuts down the MQTT client
func (mc *MQTTClient) Shutdown(ctx context.Context) error {
mc.logger.Info("Shutting down MQTT client")
mc.Disconnect()
return nil
}
// IsConnected returns whether the client is currently connected
func (mc *MQTTClient) IsConnected() bool {
return mc.connected
}
// NewMQTTClientFromEnv creates a new MQTT client from environment variables
func NewMQTTClientFromEnv(serverURL string) *MQTTClient {
username := os.Getenv("MQTT_USER")
password := os.Getenv("MQTT_PASSWORD")
return NewMQTTClient(serverURL, username, password)
}