142 lines
3.5 KiB
Go
142 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|