package bridge import ( "context" "encoding/json" "log/slog" "sync" "github.com/AFASystems/presence/internal/pkg/bridge" "github.com/AFASystems/presence/internal/pkg/common/appcontext" "github.com/AFASystems/presence/internal/pkg/config" "github.com/AFASystems/presence/internal/pkg/kafkaclient" "github.com/AFASystems/presence/internal/pkg/logger" "github.com/AFASystems/presence/internal/pkg/model" mqtt "github.com/eclipse/paho.mqtt.golang" ) // BridgeApp holds dependencies for the bridge service (MQTT <-> Kafka). type BridgeApp struct { Cfg *config.Config KafkaManager *kafkaclient.KafkaManager AppState *appcontext.AppState MQTT *bridge.MQTTClient ChApi chan model.ApiUpdate ChAlert chan model.Alert ChMqtt chan []model.Tracker Cleanup func() wg sync.WaitGroup } // New creates a BridgeApp with Kafka readers (apibeacons, alert, mqtt), writer (rawbeacons), and MQTT client. func New(cfg *config.Config) (*BridgeApp, error) { appState := appcontext.NewAppState() kafkaManager := kafkaclient.InitKafkaManager() srvLogger, cleanup := logger.CreateLogger("bridge.log") slog.SetDefault(srvLogger) readerTopics := []string{"apibeacons", "alert", "mqtt"} writerTopics := []string{"rawbeacons"} kafkaManager.PopulateKafkaManager(cfg.KafkaURL, "bridge", readerTopics) kafkaManager.PopulateKafkaManager(cfg.KafkaURL, "", writerTopics) slog.Info("bridge service initialized", "readers", readerTopics, "writers", writerTopics) writer := kafkaManager.GetWriter("rawbeacons") mqttClient, err := bridge.NewMQTTClient(cfg, func(m mqtt.Message) { bridge.HandleMQTTMessage(m.Topic(), m.Payload(), appState, writer) }) if err != nil { cleanup() return nil, err } mqttClient.Subscribe() return &BridgeApp{ Cfg: cfg, KafkaManager: kafkaManager, AppState: appState, MQTT: mqttClient, ChApi: make(chan model.ApiUpdate, config.SMALL_CHANNEL_SIZE), ChAlert: make(chan model.Alert, config.SMALL_CHANNEL_SIZE), ChMqtt: make(chan []model.Tracker, config.SMALL_CHANNEL_SIZE), Cleanup: cleanup, }, nil } // Run starts Kafka consumers and the event loop until ctx is cancelled. func (a *BridgeApp) Run(ctx context.Context) { a.wg.Add(3) go kafkaclient.Consume(a.KafkaManager.GetReader("apibeacons"), a.ChApi, ctx, &a.wg) go kafkaclient.Consume(a.KafkaManager.GetReader("alert"), a.ChAlert, ctx, &a.wg) go kafkaclient.Consume(a.KafkaManager.GetReader("mqtt"), a.ChMqtt, ctx, &a.wg) for { select { case <-ctx.Done(): return case msg := <-a.ChApi: switch msg.Method { case "POST": a.AppState.AddBeaconToLookup(msg.MAC, msg.ID) slog.Info("beacon added to lookup", "id", msg.ID) case "DELETE": if msg.MAC == "all" { a.AppState.CleanLookup() slog.Info("lookup cleared") continue } a.AppState.RemoveBeaconFromLookup(msg.MAC) slog.Info("beacon removed from lookup", "mac", msg.MAC) } case msg := <-a.ChAlert: p, err := json.Marshal(msg) if err != nil { slog.Error("marshaling alert", "err", err) continue } a.MQTT.Client.Publish("/alerts", 0, true, p) case msg := <-a.ChMqtt: p, err := json.Marshal(msg) if err != nil { slog.Error("marshaling trackers", "err", err) continue } a.MQTT.Client.Publish("/trackers", 0, true, p) } } } // Shutdown disconnects MQTT, waits for consumers, and cleans up. func (a *BridgeApp) Shutdown() { a.wg.Wait() if a.MQTT != nil { a.MQTT.Disconnect() } a.KafkaManager.CleanKafkaReaders() a.KafkaManager.CleanKafkaWriters() if a.Cleanup != nil { a.Cleanup() } slog.Info("bridge service shutdown complete") }