morz-infoboard/player/agent/internal/mqttheartbeat/heartbeat.go
Jesko Anschütz d0137179e5 Fuege MQTT-Heartbeat zum Agent hinzu (kein Broker konfiguriert = skip)
- neues Paket mqttheartbeat: Publisher mit paho, topic signage/screen/<id>/heartbeat,
  payload {screen_id, ts, status, server_connectivity}, auto-reconnect bei Ausfall
- MORZ_INFOBOARD_MQTT_BROKER leer (Standard) -> MQTT komplett uebersprungen
- app.emitHeartbeat() publiziert bei jedem Tick per MQTT wenn Broker konfiguriert,
  loggt Fehler und laeuft weiter (kein Stop bei MQTT-Ausfall)
- mqtt.Close() bei context.Done()
- MQTTBroker-Default von tcp://127.0.0.1:1883 auf "" geaendert
- erste externe Dep: github.com/eclipse/paho.mqtt.golang v1.5.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:54:12 +01:00

78 lines
2.4 KiB
Go

package mqttheartbeat
import (
"encoding/json"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
// payload is the JSON structure published to the heartbeat topic.
type payload struct {
ScreenID string `json:"screen_id"`
Timestamp string `json:"ts"`
Status string `json:"status"`
ServerConnectivity string `json:"server_connectivity"`
}
// Publisher publishes MQTT heartbeats for a single screen.
// It connects with auto-reconnect enabled, so transient broker outages are
// handled transparently; individual Publish calls fail and should be logged
// by the caller.
type Publisher struct {
client mqtt.Client
screenID string
}
// New creates a Publisher and initiates a non-blocking connection to broker.
// paho retries the connection in the background if the broker is unreachable
// at startup, so New always succeeds. Publish calls will return errors until
// the connection is established.
func New(broker, screenID string) *Publisher {
opts := mqtt.NewClientOptions().
AddBroker(broker).
SetClientID("morz-agent-" + screenID).
SetCleanSession(true).
SetAutoReconnect(true).
SetConnectRetry(true).
SetConnectRetryInterval(10 * time.Second)
client := mqtt.NewClient(opts)
client.Connect() // non-blocking; paho retries in background
return &Publisher{client: client, screenID: screenID}
}
// Topic returns the MQTT topic for the screen's heartbeat.
func Topic(screenID string) string {
return "signage/screen/" + screenID + "/heartbeat"
}
// BuildPayload builds the JSON heartbeat payload. Exported for testing.
func BuildPayload(screenID, status, connectivity string, ts time.Time) ([]byte, error) {
return json.Marshal(payload{
ScreenID: screenID,
Timestamp: ts.UTC().Format(time.RFC3339),
Status: status,
ServerConnectivity: connectivity,
})
}
// SendHeartbeat publishes a heartbeat to the screen's topic.
// QoS 0, not retained. Returns an error if the broker is unreachable or
// the publish token fails; callers should log and continue.
func (p *Publisher) SendHeartbeat(status, connectivity string, ts time.Time) error {
data, err := BuildPayload(p.screenID, status, connectivity, ts)
if err != nil {
return err
}
topic := Topic(p.screenID)
token := p.client.Publish(topic, 0, false, data)
token.WaitTimeout(3 * time.Second)
return token.Error()
}
// Close disconnects from the broker gracefully.
func (p *Publisher) Close() {
p.client.Disconnect(250)
}