127 lines
3.0 KiB
Go
127 lines
3.0 KiB
Go
package mq
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
paho "github.com/eclipse/paho.mqtt.golang"
|
|
)
|
|
|
|
type MQTT struct {
|
|
client paho.Client
|
|
mu sync.RWMutex
|
|
closed bool
|
|
}
|
|
|
|
func NewMQTT(cfg Config) (*MQTT, error) {
|
|
if !cfg.Enabled {
|
|
return &MQTT{}, nil
|
|
}
|
|
if cfg.Broker == "" {
|
|
return nil, fmt.Errorf("mqtt broker is empty")
|
|
}
|
|
if cfg.KeepAlive <= 0 {
|
|
cfg.KeepAlive = 30 * time.Second
|
|
}
|
|
if cfg.ConnectTimeout <= 0 {
|
|
cfg.ConnectTimeout = 10 * time.Second
|
|
}
|
|
opts := paho.NewClientOptions().AddBroker(cfg.Broker).SetClientID(cfg.ClientID).SetUsername(cfg.Username).SetPassword(cfg.Password)
|
|
opts.SetKeepAlive(cfg.KeepAlive).SetCleanSession(cfg.CleanSession).SetConnectTimeout(cfg.ConnectTimeout).SetAutoReconnect(true)
|
|
opts.SetResumeSubs(true).SetOrderMatters(false)
|
|
c := &MQTT{}
|
|
token := paho.NewClient(opts)
|
|
connect := token.Connect()
|
|
if !connect.WaitTimeout(cfg.ConnectTimeout) {
|
|
return nil, fmt.Errorf("connect mqtt: %w", ErrUnavailable)
|
|
}
|
|
if err := connect.Error(); err != nil {
|
|
return nil, err
|
|
}
|
|
c.client = token
|
|
return c, nil
|
|
}
|
|
|
|
func (c *MQTT) Publish(ctx context.Context, topic string, payload []byte, qos byte, retain bool) error {
|
|
if topic == "" {
|
|
return fmt.Errorf("mqtt topic is empty")
|
|
}
|
|
if qos > ExactlyOnce {
|
|
return fmt.Errorf("invalid mqtt qos %d", qos)
|
|
}
|
|
if c == nil || c.client == nil || !c.client.IsConnected() {
|
|
return ErrUnavailable
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
return waitToken(ctx, c.client.Publish(topic, qos, retain, payload))
|
|
}
|
|
|
|
func (c *MQTT) Subscribe(ctx context.Context, topic string, qos byte, handler Handler) error {
|
|
if topic == "" {
|
|
return fmt.Errorf("mqtt topic is empty")
|
|
}
|
|
if qos > ExactlyOnce {
|
|
return fmt.Errorf("invalid mqtt qos %d", qos)
|
|
}
|
|
if c == nil || c.client == nil || !c.client.IsConnected() {
|
|
return ErrUnavailable
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
if handler == nil {
|
|
return fmt.Errorf("mqtt handler is nil")
|
|
}
|
|
return waitToken(ctx, c.client.Subscribe(topic, qos, func(_ paho.Client, msg paho.Message) {
|
|
handler(context.Background(), Message{Topic: msg.Topic(), Payload: append([]byte(nil), msg.Payload()...), QoS: msg.Qos(), Retain: msg.Retained()})
|
|
}))
|
|
}
|
|
|
|
func (c *MQTT) Unsubscribe(ctx context.Context, topics ...string) error {
|
|
if len(topics) == 0 {
|
|
return fmt.Errorf("mqtt topics are empty")
|
|
}
|
|
if c == nil || c.client == nil || !c.client.IsConnected() {
|
|
return ErrUnavailable
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
return waitToken(ctx, c.client.Unsubscribe(topics...))
|
|
}
|
|
|
|
func waitToken(ctx context.Context, token paho.Token) error {
|
|
if token == nil {
|
|
return ErrUnavailable
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-token.Done():
|
|
return token.Error()
|
|
}
|
|
}
|
|
func (c *MQTT) Connected() bool { return c != nil && c.client != nil && c.client.IsConnected() }
|
|
func (c *MQTT) Close() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.closed {
|
|
return nil
|
|
}
|
|
c.closed = true
|
|
if c.client != nil && c.client.IsConnected() {
|
|
c.client.Disconnect(250)
|
|
}
|
|
return nil
|
|
}
|