package main import ( "context" "os" "os/signal" "syscall" "time" "github.com/coreos/go-systemd/v22/daemon" log "github.com/sirupsen/logrus" ) const ( serviceName = "lutron-control" serviceDescription = "Bridges DMX (sACN/Art-Net) and MQTT control to Lutron GRAFIK Eye QS zones over serial or telnet" serviceVersion = "0.1.0" ) // App is the global application structure tying together configuration, the // Lutron devices, and the control sources that drive them. type App struct { flags *Flags config *Config devices []*Device sources []Source } var app *App func main() { app = new(App) app.ParseFlags() app.ReadConfig() // Discovery mode: query each device for its integration IDs and exit. if app.flags.Discover { runDiscovery(app.config.Devices) return } // Build a Lutron device for each configured interface, keyed by name so // sources can bind to their target. byName := make(map[string]*Device) for _, dc := range app.config.Devices { d := NewDevice(dc) app.devices = append(app.devices, d) byName[dc.Name] = d } // Build each control source and bind it to its target device. for _, sc := range app.config.Sources { dev := byName[sc.Device] s, err := NewSource(sc, dev) if err != nil { log.Fatalf("Source %q: %s", sc.Name, err) } app.sources = append(app.sources, s) } // Context cancelled on shutdown so every background loop can stop cleanly. ctx, cancel := context.WithCancel(context.Background()) // Start the sources before the devices. A source registers its feedback and // monitoring callbacks in Start (OnZoneFeedback, NoteMonitoring, etc.), which // the device's connect-time setup reads, so they must be in place before the // device connects. A frame arriving before the device loops run simply lands // in the device's target state and is flushed once the device starts. for _, s := range app.sources { if err := s.Start(ctx); err != nil { log.Errorf("Failed to start source %q: %s", s.Name(), err) } } for _, d := range app.devices { d.Start(ctx) } log.Infof("%s %s started with %d device(s) and %d source(s)", serviceName, serviceVersion, len(app.devices), len(app.sources)) // Notify systemd we're ready and begin feeding its watchdog. daemon.SdNotify(false, daemon.SdNotifyReady) daemon.SdNotify(false, "STATUS=Running") stopWatchdog := startSDWatchdog(ctx) // Wait for a termination signal. c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) <-c log.Info("Shutting down") daemon.SdNotify(false, daemon.SdNotifyStopping) stopWatchdog() cancel() for _, s := range app.sources { s.Stop() } for _, d := range app.devices { d.Stop() } } // startSDWatchdog periodically pings the systemd watchdog while running, using // the interval systemd advertises via $WATCHDOG_USEC. It returns a function that // stops the pinger. When systemd has not enabled the watchdog, it does nothing. func startSDWatchdog(ctx context.Context) func() { interval, err := daemon.SdWatchdogEnabled(false) if err != nil || interval <= 0 { return func() {} } // Ping at half the configured interval to stay comfortably within the deadline. interval /= 2 if interval <= 0 { return func() {} } stop := make(chan struct{}) go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-stop: return case <-ticker.C: daemon.SdNotify(false, daemon.SdNotifyWatchdog) } } }() var once bool return func() { if !once { once = true close(stop) } } }