diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..7a4886b --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,84 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/golang:1.12 + steps: + - checkout + - setup_remote_docker: + docker_layer_caching: true + + - run: + name: run tests + command: go test -v ./... + + - run: + name: gometalinter + command: | + curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.16.0 + golangci-lint run + + + publish: + docker: + - image: circleci/golang:1.12 + working_directory: /go/src/github.com/StageAutoControl/controller + + steps: + - checkout + - setup_remote_docker: + docker_layer_caching: true + + - run: + name: install dependencies + command: | + curl https://glide.sh/get | sh + glide install --strip-vendor + + sudo apt-get install -y libportmidi-dev portaudio19-dev + + - deploy: + name: push docker images + command: | + [ "${CIRCLE_BRANCH}" != "master" ] && [ -z "${CIRCLE_TAG}" ] && exit 0 + + export DOCKER_REPO=$(echo "${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" | awk '{print tolower($0)}') + + docker login -u ${DOCKER_USER} -p "${DOCKER_PASS}" https://index.docker.io/v1/ + docker build -t ${DOCKER_REPO} . + + if [ "${CIRCLE_BRANCH}" == "master" ]; then + docker push ${DOCKER_REPO} + fi + + if [ -n "${CIRCLE_TAG}" ]; then + docker tag ${DOCKER_REPO} ${DOCKER_REPO}:${CIRCLE_TAG} + docker push ${DOCKER_REPO}:${CIRCLE_TAG} + fi + + - deploy: + name: push artifacts + command: | + [ -z "${CIRCLE_TAG}" ] && exit 0 + + go get github.com/tcnksm/ghr + go build -o bin/controller . + ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${CIRCLE_TAG} ./bin + +workflows: + version: 2 + build: + jobs: + - build: + filters: + tags: + only: /.*/ + branches: + only: /.*/ + - publish: + context: docker + requires: + - build + filters: + tags: + only: /.*/ diff --git a/.gitignore b/.gitignore index 134ad23..254e7ab 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ bin .envrc /controller tmp +/storage .glide diff --git a/Dockerfile b/Dockerfile index 32d55e5..f9486e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,3 @@ -FROM scalify/glide:0.13.0 as dependencies -WORKDIR /go/src/github.com/StageAutoControl/controller/ - -COPY glide.yaml glide.lock ./ -RUN glide install --strip-vendor - FROM golang:1.10 as builder RUN apt-get update \ @@ -13,12 +7,12 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* WORKDIR /go/src/github.com/StageAutoControl/controller/ -COPY --from=dependencies /go/src/github.com/StageAutoControl/controller/vendor vendor -COPY . ./ -RUN go test ./... +COPY go.mod go.sum ./ +RUN go mod download -RUN go build -o bin/controller_amd64 . +COPY . ./ +RUN go build -o bin/controller . FROM ubuntu @@ -29,6 +23,6 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* WORKDIR /root/ -COPY --from=builder /go/src/github.com/StageAutoControl/controller/bin/controller_amd64 ./controller +COPY --from=builder /go/src/github.com/StageAutoControl/controller/bin/controller ./controller RUN chmod +x ./controller ENTRYPOINT ["./controller"] diff --git a/Makefile b/Makefile index a939016..727fc95 100644 --- a/Makefile +++ b/Makefile @@ -19,36 +19,12 @@ lint: test: go test -v $(PACKAGES) -start-playback-visualizer: build-darwin - ./bin/controller_darwin playback song "${SONG}" \ - --data-dir "${SAC_DATA_DIR}" \ - --transport visualizer \ - --visualizer-endpoint localhost:1337 - -start-playback-stream: build-darwin - ./bin/controller_darwin playback song "${SONG}" \ - --data-dir "${SAC_DATA_DIR}" \ - --transport stream - -start-playback-none: build-darwin - ./bin/controller_darwin playback song "${SONG}" \ - --data-dir "${SAC_DATA_DIR}" - -start-playback-artnet: build-darwin - ./bin/controller_darwin playback song "${SONG}" \ - --data-dir "${SAC_DATA_DIR}" \ - --transport artnet - -start-api: build-darwin - ./bin/controller_darwin api \ - --data-dir "${SAC_DATA_DIR}" - build-all: build-darwin build-arm build-linux build-docker build-darwin: go build -o bin/controller_darwin . -build-linux: +build-linux: GOOS=linux CGO_ENABLED=0 go build -a -ldflags '-s' -installsuffix cgo -o bin/controller_linux . build-arm: diff --git a/cmd/README.md b/cmd/README.md deleted file mode 100644 index 4a1a2a8..0000000 --- a/cmd/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Command examples - -## Playback - -```bash -./controller playback (song|setlist) song-or-setlist-uuid \ - --data-dir /var/controller/defintions/ \ - --wait-for-trigger audio \ - --trigger-audio-freq 15000 \ - --transport midi \ - --midi-device-id 1 \ - --transport artnet -``` - - -## Art-Net - -## MIDI - -## API diff --git a/cmd/api.go b/cmd/api.go deleted file mode 100644 index d66c33b..0000000 --- a/cmd/api.go +++ /dev/null @@ -1,42 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/StageAutoControl/controller/cmd/internal" - "github.com/StageAutoControl/controller/pkg/api" - "github.com/StageAutoControl/controller/pkg/storage" - "github.com/spf13/cobra" -) - -// apiCmd represents the api command -var apiCmd = &cobra.Command{ - Use: "api", - Short: "Opens the RPC API to manage the data and control the processes", - Long: ``, - Run: func(cmd *cobra.Command, args []string) { - logger := Logger.WithField("module", "api") - - store := storage.New("/var/controller/data") - server, err := api.NewServer(logger, store) - if err != nil { - logger.Fatal(err) - } - - port, err := cmd.Flags().GetUint16("port") - if err != nil { - logger.Fatal(err) - } - - ctx := internal.NewExitHandlerContext(logger.Logger) - if err := server.Run(ctx, fmt.Sprintf("0.0.0.0:%d", port)); err != nil { - logger.Fatal(err) - } - }, -} - - -func init() { - RootCmd.AddCommand(apiCmd) - - apiCmd.Flags().Uint16P("port", "p", 8080, "TCP port the API should listen on") -} diff --git a/cmd/artnet/artnet.go b/cmd/artnet/artnet.go deleted file mode 100644 index d2dbd0a..0000000 --- a/cmd/artnet/artnet.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2017 Alexander Pinnecke -// - -package artnet - -import ( - "github.com/StageAutoControl/controller/cmd" - "github.com/spf13/cobra" -) - -// ArtNetCmd represents the ArtNetTest command namespaces -var ArtNetCmd = &cobra.Command{ - Use: "artnet", - Short: "ArtNet commands to work with ArtNet devices", - Long: ``, -} - -func init() { - cmd.RootCmd.AddCommand(ArtNetCmd) -} diff --git a/cmd/artnet/listen.go b/cmd/artnet/listen.go deleted file mode 100644 index 086609c..0000000 --- a/cmd/artnet/listen.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright © 2017 Alexander Pinnecke -// - -package artnet - -import ( - "net" - "os" - "os/signal" - "runtime" - "strings" - "sync" - "syscall" - "time" - - root "github.com/StageAutoControl/controller/cmd" - artnetTransport "github.com/StageAutoControl/controller/pkg/cntl/transport/artnet" - "github.com/jsimonetti/go-artnet" - "github.com/spf13/cobra" -) - -// Listen represents the ArtNetTest command -var Listen = &cobra.Command{ - Use: "listen", - Short: "ArtNet server to listen for devices and print them", - Long: ``, - Run: func(cmd *cobra.Command, args []string) { - var ip net.IP - var err error - - root.Logger.Info("InterfaceName is empty, searching for suitable one ...") - ip, err = artnetTransport.FindArtNetIP() - if err != nil { - root.Logger.Fatal(err) - } - - root.Logger.Infof("Using interface with IP %s", ip.String()) - - if len(ip) == 0 { - root.Logger.Fatal("No IP found") - } - - host, err := os.Hostname() - if err != nil { - panic(err) - } - c := artnet.NewController(host, ip, artnet.NewLogger(root.Logger)) - var wg sync.WaitGroup - - go func() { - wg.Add(1) - if err := c.Start(); err != nil { - root.Logger.Fatal(err) - } - - wg.Done() - }() - - time.Sleep(10 * time.Second) - - cancel := make(chan os.Signal, 2) - signal.Notify(cancel, syscall.SIGTERM, syscall.SIGKILL) - var builder strings.Builder - - LOOP: - for { - select { - case <-cancel: - break LOOP - default: - } - - for _, n := range c.Nodes { - builder.WriteString(artnetTransport.NodeToString(n)) - } - - root.Logger.Infof(builder.String()) - builder.Reset() - - time.Sleep(10 * time.Second) - } - - c.Stop() - wg.Wait() - - root.Logger.Infof("num: %d", runtime.NumGoroutine()) - }, -} - -func init() { - ArtNetCmd.AddCommand(Listen) -} diff --git a/cmd/artnet/node.go b/cmd/artnet/node.go deleted file mode 100644 index 4dd90f2..0000000 --- a/cmd/artnet/node.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright © 2017 Alexander Pinnecke -// - -package artnet - -import ( - "log" - "net" - "os" - "os/signal" - - root "github.com/StageAutoControl/controller/cmd" - artnetTransport "github.com/StageAutoControl/controller/pkg/cntl/transport/artnet" - "github.com/jsimonetti/go-artnet" - "github.com/jsimonetti/go-artnet/packet/code" - "github.com/spf13/cobra" -) - -// Node represents the ArtNetTest command -var Node = &cobra.Command{ - Use: "node", - Short: "ArtNet node to test network communication", - Long: ``, - Run: func(cmd *cobra.Command, args []string) { - var ip net.IP - var err error - - log.Println("InterfaceName is empty, searching for suitable one ...") - ip, err = artnetTransport.FindArtNetIP() - if err != nil { - log.Fatal(err) - } - - log.Printf("Using interface with IP %s", ip.String()) - - if len(ip) == 0 { - log.Fatal("No IP found") - } - - host, err := os.Hostname() - if err != nil { - panic(err) - } - n := artnet.NewNode(host, code.StNode, ip, artnet.NewLogger(root.Logger)) - - if err := n.Start(); err != nil { - log.Fatal(err) - } - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, os.Kill) - <-c - - log.Println("Stopping node ...") - n.Stop() - }, -} - -func init() { - ArtNetCmd.AddCommand(Node) -} diff --git a/cmd/artnet/send.go b/cmd/artnet/send.go deleted file mode 100644 index ad6cde9..0000000 --- a/cmd/artnet/send.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright © 2017 Alexander Pinnecke -// - -package artnet - -import ( - "bufio" - "fmt" - "net" - "os" - "runtime" - "strconv" - "strings" - "sync" - "time" - - root "github.com/StageAutoControl/controller/cmd" - "github.com/StageAutoControl/controller/pkg/cntl" - "github.com/StageAutoControl/controller/pkg/cntl/transport" - artnetTransport "github.com/StageAutoControl/controller/pkg/cntl/transport/artnet" - "github.com/jsimonetti/go-artnet" - "github.com/spf13/cobra" -) - -const ( - form = " ..." -) - -// Send represents the ArtNetTest command -var Send = &cobra.Command{ - Use: "send", - Short: "Send dmx command to a artnet device (shell mode)", - Long: ``, - Run: func(cmd *cobra.Command, args []string) { - - var ip net.IP - var err error - - root.Logger.Info("InterfaceName is empty, searching for suitable one ...") - ip, err = artnetTransport.FindArtNetIP() - if err != nil { - root.Logger.Fatal(err) - } - - root.Logger.Infof("Using interface with IP %s", ip.String()) - - if len(ip) == 0 { - root.Logger.Fatal("No IP found") - } - - host, err := os.Hostname() - if err != nil { - panic(err) - } - c := artnet.NewController(host, ip, artnet.NewLogger(root.Logger)) - var wg sync.WaitGroup - - go func() { - wg.Add(1) - if err := c.Start(); err != nil { - root.Logger.Fatalf("Error during sending: %v", err) - } - - wg.Done() - }() - - root.Logger.Info("Waiting 5sec for nodes to register") - time.Sleep(5 * time.Second) - - root.Logger.Infof("Entering interactive mode. Please enter the lines in the form %s", form) - reader := bufio.NewReader(os.Stdin) - var universe, channel, value uint64 - - state := artnetTransport.NewState() - - for { - fmt.Print("> ") - text, _ := reader.ReadString('\n') - text = strings.Replace(text, "\n", "", -1) - - params := strings.Split(strings.TrimSpace(text), " ") - if len(params) != 3 { - root.Logger.Errorf("Please enter the form %s", form) - continue - } - - if universe, err = strconv.ParseUint(params[0], 10, 16); err != nil { - root.Logger.Errorf("Unable to parse universe: %v", err) - continue - } - if channel, err = strconv.ParseUint(params[1], 10, 16); err != nil { - root.Logger.Errorf("Unable to parse channel: %v", err) - continue - } - if value, err = strconv.ParseUint(params[2], 10, 8); err != nil { - root.Logger.Errorf("Unable to parse value: %v", err) - continue - } - - // root.Logger.Infof("Sending u=%d, c=%d, v=%d", universe, channel, value) - - state.Set(uint16(universe), uint8(channel), uint8(value)) - - for u, dmx := range state { - c.SendDMXToAddress(dmx, transport.UniverseToAddress(cntl.DMXUniverse(u))) - } - } - - c.Stop() - wg.Wait() - - root.Logger.Infof("num: %d", runtime.NumGoroutine()) - }, -} - -func init() { - ArtNetCmd.AddCommand(Send) -} diff --git a/cmd/artnet/server.go b/cmd/artnet/server.go deleted file mode 100644 index 3a68989..0000000 --- a/cmd/artnet/server.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright © 2017 Alexander Pinnecke -// - -package artnet - -import ( - "net" - "os" - "runtime" - "sync" - "time" - - root "github.com/StageAutoControl/controller/cmd" - artnetTransport "github.com/StageAutoControl/controller/pkg/cntl/transport/artnet" - "github.com/jsimonetti/go-artnet" - "github.com/spf13/cobra" -) - -// Server represents the ArtNetTest command -var Server = &cobra.Command{ - Use: "server", - Short: "ArtNet server to test network communication", - Long: ``, - Run: func(cmd *cobra.Command, args []string) { - var ip net.IP - var err error - - root.Logger.Info("InterfaceName is empty, searching for suitable one ...") - ip, err = artnetTransport.FindArtNetIP() - if err != nil { - root.Logger.Fatal(err) - } - - root.Logger.Infof("Using interface with IP %s", ip.String()) - - if len(ip) == 0 { - root.Logger.Fatal("No IP found") - } - - host, err := os.Hostname() - if err != nil { - panic(err) - } - c := artnet.NewController(host, ip, artnet.NewLogger(root.Logger)) - var wg sync.WaitGroup - - go func() { - wg.Add(1) - if err := c.Start(); err != nil { - root.Logger.Fatal(err) - } - - wg.Done() - }() - - time.Sleep(10 * time.Second) - c.SendDMXToAddress([512]byte{0x00, 0xff, 0x00, 0xff, 0x00}, artnet.Address{Net: 0, SubUni: 0}) - time.Sleep(2 * time.Second) - c.SendDMXToAddress([512]byte{0xff, 0x00, 0x00, 0xff, 0x00}, artnet.Address{Net: 0, SubUni: 0}) - time.Sleep(2 * time.Second) - c.SendDMXToAddress([512]byte{0x00, 0x00, 0xff, 0xff, 0x00}, artnet.Address{Net: 0, SubUni: 0}) - time.Sleep(2 * time.Second) - c.SendDMXToAddress([512]byte{}, artnet.Address{Net: 0, SubUni: 0}) - time.Sleep(2 * time.Second) - - c.Stop() - wg.Wait() - - root.Logger.Infof("num: %d", runtime.NumGoroutine()) - }, -} - -func init() { - ArtNetCmd.AddCommand(Server) -} diff --git a/cmd/audio/audio.go b/cmd/audio/audio.go old mode 100644 new mode 100755 diff --git a/cmd/audio/const.go b/cmd/audio/const.go old mode 100644 new mode 100755 diff --git a/cmd/audio/dump_input.go b/cmd/audio/dump_input.go old mode 100644 new mode 100755 index f3908a2..6dd6628 --- a/cmd/audio/dump_input.go +++ b/cmd/audio/dump_input.go @@ -5,6 +5,7 @@ package audio import ( "fmt" + "log" "os" "os/signal" @@ -24,26 +25,29 @@ var DumpInputCmd = &cobra.Command{ Short: "Dumps the audio input of a device to console", Long: ``, Run: func(cmd *cobra.Command, args []string) { - if err := portaudio.Initialize(); err != nil { - panic(err) - } - defer portaudio.Terminate() - buf := make([]float32, averageSamples) s, err := portaudio.OpenDefaultStream(1, 0, sampleRate, len(buf), buf) if err != nil { panic(err) } - defer s.Close() + defer func() { + if err := s.Close(); err != nil { + log.Fatal(err) + } + }() if err := s.Start(); err != nil { panic(err) } - defer s.Stop() + defer func() { + if err := s.Stop(); err != nil { + log.Fatal(err) + } + }() var frame int64 c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, os.Kill) + signal.Notify(c, os.Interrupt, os.Interrupt) fmt.Println("Started listener, dumping input.") diff --git a/cmd/audio/list_devices.go b/cmd/audio/list_devices.go old mode 100644 new mode 100755 index 05820e9..a543799 --- a/cmd/audio/list_devices.go +++ b/cmd/audio/list_devices.go @@ -17,12 +17,6 @@ var DeviceCmd = &cobra.Command{ Short: "Prints info about all devices", Long: ``, Run: func(cmd *cobra.Command, args []string) { - if err := portaudio.Initialize(); err != nil { - fmt.Println(err) - os.Exit(1) - } - defer portaudio.Terminate() - devices, err := portaudio.Devices() if err != nil { fmt.Println(err) diff --git a/cmd/audio/sine.go b/cmd/audio/sine.go old mode 100644 new mode 100755 index f6ae357..225c404 --- a/cmd/audio/sine.go +++ b/cmd/audio/sine.go @@ -4,6 +4,7 @@ package audio import ( + "log" "math" "time" @@ -22,16 +23,21 @@ var SineCmd = &cobra.Command{ Short: "Creates a sin curved audio", Long: ``, Run: func(cmd *cobra.Command, args []string) { - portaudio.Initialize() - defer portaudio.Terminate() - s := newStereoSine(float64(frequency), sampleRate) - defer s.Close() + defer func() { + if err := s.Close(); err != nil { + log.Fatal(err) + } + }() if err := s.Start(); err != nil { panic(err) } - defer s.Stop() + defer func() { + if err := s.Stop(); err != nil { + log.Fatal(err) + } + }() time.Sleep(time.Duration(length) * time.Millisecond) }, diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..fefff8a --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "os" + "path/filepath" + + "github.com/StageAutoControl/controller/pkg/artnet" + "github.com/StageAutoControl/controller/pkg/disk" + "github.com/sirupsen/logrus" +) + +func createLogger(logLevel string) *logrus.Entry { + logger := logrus.New() + level, err := logrus.ParseLevel(logLevel) + if err != nil { + logger.Panicf("Unable to parse log level %q: %v\n", logLevel, err) + os.Exit(1) + } + + logger.Infof("Using log level %s", logLevel) + + logger.SetLevel(level) + return logger.WithFields(logrus.Fields{}) +} + +func createStorage(logger *logrus.Entry, storagePath string) *disk.Storage { + if filepath.IsAbs(storagePath) { + return disk.New(storagePath) + } + + cwd, err := os.Getwd() + if err != nil { + logger.Fatal(err) + } + storagePath = filepath.Clean(filepath.Join(cwd, storagePath)) + if err != nil { + logger.Fatal(err) + } + + return disk.New(storagePath) +} + +func createController(logger *logrus.Entry, disable bool) artnet.Controller { + if disable { + logger.Warn("ArtNet controller is disabled, so no playback or playground will be possible!") + return nil + } + + c, err := artnet.NewController(logger.WithField("module", "controller")) + if err != nil { + logger.Fatal(err) + } + + return c +} diff --git a/cmd/internal/exit.go b/cmd/internal/exit.go deleted file mode 100644 index c6a8faa..0000000 --- a/cmd/internal/exit.go +++ /dev/null @@ -1,25 +0,0 @@ -package internal - -import ( - "context" - "os" - "os/signal" - "syscall" - - "github.com/sirupsen/logrus" -) - -// NewExitHandlerContext creates a trap for termination signals -func NewExitHandlerContext(logger *logrus.Logger) context.Context { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGKILL, os.Interrupt) - ctx, cancel := context.WithCancel(context.Background()) - - go func() { - <-c - defer cancel() - logger.Info("shutting down") - }() - - return ctx -} diff --git a/cmd/midi/devices.go b/cmd/midi/devices.go index 5b6b6dc..477c720 100644 --- a/cmd/midi/devices.go +++ b/cmd/midi/devices.go @@ -5,6 +5,7 @@ package midi import ( "fmt" + "log" "os" "github.com/rakyll/portmidi" @@ -21,7 +22,11 @@ var MidiDeviceCmd = &cobra.Command{ fmt.Println(err) os.Exit(1) } - defer portmidi.Terminate() + defer func() { + if err := portmidi.Terminate(); err != nil { + log.Fatal(err) + } + }() num := portmidi.CountDevices() fmt.Printf("Found %d devices. \n", num) diff --git a/cmd/midi/dump.go b/cmd/midi/dump.go index 5bad297..4674ee6 100644 --- a/cmd/midi/dump.go +++ b/cmd/midi/dump.go @@ -5,6 +5,7 @@ package midi import ( "fmt" + "log" "os" "strconv" @@ -26,7 +27,11 @@ var MidiDumpCmd = &cobra.Command{ fmt.Println(err) os.Exit(1) } - defer portmidi.Terminate() + defer func() { + if err := portmidi.Terminate(); err != nil { + log.Fatal(err) + } + }() var d portmidi.DeviceID if deviceID == "" { diff --git a/cmd/playback.go b/cmd/playback.go index 52178d7..b938274 100644 --- a/cmd/playback.go +++ b/cmd/playback.go @@ -6,29 +6,24 @@ import ( "fmt" "os" - "github.com/StageAutoControl/controller/cmd/internal" + "github.com/apinnecke/go-exitcontext" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/StageAutoControl/controller/pkg/artnet" "github.com/StageAutoControl/controller/pkg/cntl" "github.com/StageAutoControl/controller/pkg/cntl/playback" "github.com/StageAutoControl/controller/pkg/cntl/transport" "github.com/StageAutoControl/controller/pkg/cntl/waiter" - "github.com/StageAutoControl/controller/pkg/enhance" - "github.com/StageAutoControl/controller/pkg/loader/files" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" + "github.com/StageAutoControl/controller/pkg/visualizer" ) const ( playbackTypeSong = "song" playbackTypeSetList = "setlist" - - directoryLoader = "directory" - databaseLoader = "database" ) var ( - loaders = []string{directoryLoader, databaseLoader} - loaderType string - transportTypes = []string{ transport.TypeStream, transport.TypeVisualizer, @@ -38,8 +33,7 @@ var ( usedTransports []string viualizerEndpoint string - midiDeviceID string - dataDir string + midiDeviceID int8 waiterTypes = []string{ waiter.TypeNone, @@ -52,87 +46,60 @@ var ( // playbackCmd represents the playback command var playbackCmd = &cobra.Command{ Use: "playback [song|setlist] song-valid-uuid-1", - Short: "Plays a given Song or SetList by id", + Short: "Plays a given Process or SetList by id", Long: ``, Run: func(cmd *cobra.Command, args []string) { logrus.SetLevel(logrus.WarnLevel) if len(args) != 2 { - cmd.Usage() + if err := cmd.Usage(); err != nil { + logrus.Fatal(err) + } os.Exit(1) } - var loader cntl.Loader - switch loaderType { - case directoryLoader: - Logger.Infof("Loading data directory %q ...", dataDir) - loader = files.New(dataDir) - - case databaseLoader: - //loader = database.New(), - Logger.Fatal("Database loader is not yet supported.") - - default: - Logger.Fatalf("Loader %q is not supported. Choose one of %s", loader, loaders) - } - data, err := loader.Load() if err != nil { - Logger.Fatalf("Failed to load data from %q: %v", loaderType, err) - } - - Logger.Print("enhancing data ...") - for _, e := range enhance.Enhancers { - if es := e.Enhance(data); len(es) > 0 { - for _, e := range es { - Logger.Error(e) - } - Logger.Fatalf("Errors occurred enhancing data store") - } + logrus.Fatal(err) } - Logger.Print("Done. No errors found.") var writers []playback.TransportWriter for _, transportType := range usedTransports { switch transportType { case transport.TypeStream: - writers = append(writers, transport.NewStream(Logger.WithField(cntl.LoggerFieldTransport, transport.TypeStream), os.Stdout)) - break + writers = append(writers, transport.NewStream(logger.WithField(cntl.LoggerFieldTransport, transport.TypeStream), os.Stdout)) case transport.TypeBarLogger: - writers = append(writers, transport.NewBarLogger(Logger.WithField(cntl.LoggerFieldTransport, transport.TypeBarLogger))) - break + writers = append(writers, transport.NewBarLogger(logger.WithField(cntl.LoggerFieldTransport, transport.TypeBarLogger))) case transport.TypeVisualizer: - w, err := transport.NewVisualizer(Logger.WithField(cntl.LoggerFieldTransport, transport.TypeVisualizer), viualizerEndpoint) - if err != nil { - Logger.Fatalf("Unable to connect to the visualizer: %v", err) - } - + w := visualizer.NewServer(logger.WithField(cntl.LoggerFieldTransport, transport.TypeVisualizer)) writers = append(writers, w) - break case transport.TypeArtNet: - w, err := transport.NewArtNet(Logger.WithField(cntl.LoggerFieldTransport, transport.TypeArtNet), "stage-auto-control") + controller, err := artnet.NewController(logger.WithField(cntl.LoggerFieldTransport, transport.TypeArtNet)) if err != nil { - Logger.Fatalf("Unable to connect to the visualizer: %v", err) + logger.Fatal(err) + } + + w, err := transport.NewArtNet(controller) + if err != nil { + logger.Fatalf("Unable to open art net controller: %v", err) } writers = append(writers, w) - break case transport.TypeMidi: - w, err := transport.NewMIDI(Logger.WithField(cntl.LoggerFieldTransport, transport.TypeMidi), midiDeviceID) + w, err := transport.NewMIDI(logger.WithField(cntl.LoggerFieldTransport, transport.TypeMidi), midiDeviceID) if err != nil { - Logger.Fatalf("Unable to connect to midi device: %v", err) + logger.Fatalf("Unable to connect to midi device: %v", err) } writers = append(writers, w) - break default: - Logger.Fatalf("Transport %q is not supported.", transportType) + logger.Fatalf("Transport %q is not supported", transportType) } } @@ -140,37 +107,28 @@ var playbackCmd = &cobra.Command{ for _, waiterType := range usedWaiters { switch waiterType { case waiter.TypeNone: - waiters = append(waiters, waiter.NewNone(Logger.WithField(cntl.LoggerFieldWaiter, waiter.TypeNone))) - - break + waiters = append(waiters, waiter.NewNone(logger.WithField(cntl.LoggerFieldWaiter, waiter.TypeNone))) case waiter.TypeAudio: - a, err := waiter.NewAudio(Logger.WithField(cntl.LoggerFieldWaiter, waiter.TypeAudio), audioWaiterThreshold) - if err != nil { - Logger.Fatal(err) - } - - waiters = append(waiters, a) + waiters = append(waiters, waiter.NewAudio(logger.WithField(cntl.LoggerFieldWaiter, waiter.TypeAudio), audioWaiterThreshold)) - break } } - ctx := internal.NewExitHandlerContext(Logger.Logger) - player := playback.NewPlayer(Logger.Logger.WithField("player", "default"), data, writers, waiters) + ctx := exitcontext.New() + player := playback.NewPlayer(logger.Logger.WithField("player", "default"), data, writers, waiters) switch args[0] { case playbackTypeSong: songID := args[1] if err = player.PlaySong(ctx, songID); err != nil { - Logger.Fatal(err) + logger.Fatal(err) } - break case playbackTypeSetList: setListID := args[1] if err = player.PlaySetList(ctx, setListID); err != nil { - Logger.Fatal(err) + logger.Fatal(err) } } }, @@ -181,9 +139,7 @@ func init() { playbackCmd.Flags().StringSliceVarP(&usedTransports, "transport", "t", []string{}, fmt.Sprintf("Which usedTransports to use from %s.", transportTypes)) playbackCmd.Flags().StringVar(&viualizerEndpoint, "visualizer-endpoint", "localhost:1337", "Endpoint of the visualizer backend if visualizer transport is chosen.") - playbackCmd.Flags().StringVarP(&midiDeviceID, "midi-device-id", "m", "", "DeviceID of MIDI output to use (On empty string the default device is used)") + playbackCmd.Flags().Int8VarP(&midiDeviceID, "midi-device-id", "m", -1, "DeviceID of MIDI output to use (On empty string the default device is used)") playbackCmd.Flags().StringSliceVarP(&usedWaiters, "wait-for", "w", []string{waiter.TypeNone}, fmt.Sprintf("Wait for a specific signal before playing a song (required to be used on stage, otherwise the next song would start immediately), one of %s", waiterTypes)) playbackCmd.Flags().Float32Var(&audioWaiterThreshold, "audio-waiter-threshold", 0.9, "Threshold frequency for audio waiter to trigger a signal") - playbackCmd.Flags().StringVarP(&dataDir, "data-dir", "d", "", "Data directory to load (when loader is set to directory)") - playbackCmd.Flags().StringVar(&loaderType, "loader", directoryLoader, fmt.Sprintf("Which loader to use %s.", loaders)) } diff --git a/cmd/root.go b/cmd/root.go index 8c8057e..1148dda 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,17 +3,28 @@ package cmd import ( + "context" "fmt" "os" + "github.com/apinnecke/go-exitcontext" + "github.com/gordonklaus/portaudio" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + + "github.com/StageAutoControl/controller/pkg/artnet" + "github.com/StageAutoControl/controller/pkg/disk" ) var ( - // The Logger used by the whole application - Logger *logrus.Entry - logLevel string + logLevel string + logger *logrus.Entry + storagePath string + storage *disk.Storage + loader *disk.Loader + controller artnet.Controller + disableController bool + ctx context.Context ) // RootCmd represents the base command when called without any subcommands @@ -22,18 +33,29 @@ var RootCmd = &cobra.Command{ Short: "Stage automatic controlling, triggering state changes.", Long: `Automatic stage controlling, including midi and DMX, by analyzing audio signals and pre defined light scenes`, PersistentPreRun: func(cmd *cobra.Command, args []string) { - logger := logrus.New() - level, err := logrus.ParseLevel(logLevel) - if err != nil { - logger.Panicf("Unable to parse log level %q: %v\n", logLevel, err) - os.Exit(1) - } + ctx = exitcontext.New() + logger = createLogger(logLevel) + storage = createStorage(logger, storagePath) + controller = createController(logger, disableController) + loader = disk.NewLoader(storage) - logger.Infof("Using log level %s", logLevel) - - logger.SetLevel(level) - Logger = logger.WithFields(logrus.Fields{}) + if err := portaudio.Initialize(); err != nil { + logger.Fatalf("failed to initialize portaudio: %v", err) + } + go func() { + <-ctx.Done() + terminateAudio() + }() }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + terminateAudio() + }, +} + +func terminateAudio() { + if err := portaudio.Terminate(); err != nil { + logger.Errorf("failed to terminate portaudio: %v", err) + } } // Execute adds all child commands to the root command sets flags appropriately. @@ -47,4 +69,6 @@ func Execute() { func init() { RootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Which log level to use") + RootCmd.PersistentFlags().BoolVar(&disableController, "disable-controller", false, "Disable the controller, e.g. when not on an artnet network") + RootCmd.PersistentFlags().StringVarP(&storagePath, "storage-path", "s", "/var/controller/data", "path where the storage should store the data") } diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..81c4329 --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,59 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + apiServer "github.com/StageAutoControl/controller/pkg/api/server" + "github.com/StageAutoControl/controller/pkg/cntl/playback" + "github.com/StageAutoControl/controller/pkg/disk" + "github.com/StageAutoControl/controller/pkg/process" + "github.com/StageAutoControl/controller/pkg/visualizer" +) + +// serverCmd represents the server command +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Opens the RPC API to manage the data and control the processes", + Run: func(cmd *cobra.Command, args []string) { + pm := process.NewManager(ctx, logger) + visualizer := visualizer.NewServer(logger.WithField("module", "visualizer")) + + server, err := apiServer.New(logger.WithField("module", "api"), storage, loader, controller, pm, visualizer) + if err != nil { + logger.Fatal(err) + } + + port, err := cmd.Flags().GetUint16("port") + if err != nil { + logger.Fatal(err) + } + + endpoint := fmt.Sprintf("0.0.0.0:%d", port) + loader := disk.NewLoader(storage) + + if !disableController { + if err := playback.EnsureDefaultConfig(storage); err != nil { + logger.Fatal(err) + } + if err := pm.AddProcess(playback.ProcessName, playback.NewProcess(loader, storage, controller, visualizer), true); err != nil { + logger.Fatal(err) + } + if err := controller.Start(ctx); err != nil { + logger.Fatal(err) + } + logger.Info("Started ArtNet Controller") + } + + if err := server.Run(ctx, endpoint); err != nil { + logger.Fatal(err) + } + }, +} + +func init() { + RootCmd.AddCommand(serverCmd) + + serverCmd.Flags().Uint16P("port", "p", 8080, "TCP port the API should listen on") +} diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 67f1869..0000000 --- a/glide.lock +++ /dev/null @@ -1,50 +0,0 @@ -hash: 983effd3e9ab87af045e04b98c0772d1100c2fc7b15f24264dd851be2464082a -updated: 2019-02-07T21:22:24.230622+01:00 -imports: -- name: github.com/creasty/go-easing - version: 0cfd96d3a544aad2e643739e4a4f6c081b12cda0 -- name: github.com/google/btree - version: 4030bb1f1f0c35b30ca7009e9ebd06849dd45306 -- name: github.com/gordonklaus/portaudio - version: 00e7307ccd93051979a933c6fd5ead641eba5686 -- name: github.com/gorilla/handlers - version: 7e0847f9db758cdebd26c149d0ae9d5d0b9c98ce -- name: github.com/gorilla/rpc - version: 22c016f3df3febe0c1f6727598b6389507e03a18 - subpackages: - - v2 - - v2/json -- name: github.com/inconshreveable/mousetrap - version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 -- name: github.com/jsimonetti/go-artnet - version: f0b9cc962f904f793900f27ba65ffc04b4a942eb - subpackages: - - packet - - packet/code - - version -- name: github.com/konsorten/go-windows-terminal-sequences - version: 5c8c8bd35d3832f5d134ae1e1e375b69a4d25242 -- name: github.com/peterbourgon/diskv - version: 5f041e8faa004a95c88a202771f4cc3e991971e6 -- name: github.com/rakyll/portmidi - version: 1246dd47c56089ea2ae791266edb7f4c6b90c045 -- name: github.com/satori/go.uuid - version: f58768cc1a7a7e77a3bd49e98cdd21419399b6a3 -- name: github.com/sirupsen/logrus - version: 4ea4861398d99a2d05be29675c5b74caf7bea95e -- name: github.com/spf13/cobra - version: 7547e83b2d85fd1893c7d76916f67689d761fecb -- name: github.com/spf13/pflag - version: 24fa6976df40757dce6aea913e7b81ade90530e1 -- name: github.com/spf13/viper - version: 6d33b5a963d922d182c91e8a1c88d81fd150cfd4 -- name: golang.org/x/crypto - version: b8fe1690c61389d7d2a8074a507d1d40c5d30448 - subpackages: - - ssh/terminal -- name: golang.org/x/sys - version: 41f3e6584952bb034a481797859f6ab34b6803bd - subpackages: - - unix - - windows -testImports: [] diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index 3903179..0000000 --- a/glide.yaml +++ /dev/null @@ -1,25 +0,0 @@ -package: github.com/StageAutoControl/controller -import: - - package: github.com/sirupsen/logrus - - package: github.com/gordonklaus/portaudio - - package: github.com/gorilla/handlers - version: ^1.2.1 - - package: github.com/gorilla/rpc - version: ^1.1.0 - subpackages: - - v2 - - v2/json - - package: github.com/jsimonetti/go-artnet - # repo: https://github.com/StageAutoControl/go-artnet.git - subpackages: - - packet/code - - package: github.com/rakyll/portmidi - version: 1246dd47c56089ea2ae791266edb7f4c6b90c045 - - package: github.com/satori/go.uuid - version: ^1.1.0 - - package: github.com/spf13/cobra - - package: github.com/spf13/viper - version: ^1.0.0 - - package: github.com/creasty/go-easing - - package: github.com/peterbourgon/diskv - version: ^2.0.1 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..33acad9 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/StageAutoControl/controller + +go 1.13 + +require ( + github.com/apinnecke/go-exitcontext v0.0.0-20190131152654-06015046a58d + github.com/creasty/go-easing v0.0.0-20161107103139-0cfd96d3a544 + github.com/google/btree v1.0.1-0.20190326150332-20236160a414 // indirect + github.com/gordonklaus/portaudio v0.0.0-20180817120803-00e7307ccd93 + github.com/gorilla/handlers v1.4.2 + github.com/gorilla/rpc v1.2.0 + github.com/gorilla/websocket v1.4.1 + github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a + github.com/jsimonetti/go-artnet v0.0.0-20200117113556-db828ac108e3 + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible + github.com/rakyll/portmidi v0.0.0-20191102002215-74e95e8bc9b1 + github.com/satori/go.uuid v1.2.0 + github.com/sirupsen/logrus v1.4.2 + github.com/spf13/cobra v0.0.5 + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6e333e1 --- /dev/null +++ b/go.sum @@ -0,0 +1,92 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/apinnecke/go-exitcontext v0.0.0-20190131152654-06015046a58d h1:IasnhCMwdWF2hSn/wzAfDStDrCLl4vvTA96w4VSM9Hc= +github.com/apinnecke/go-exitcontext v0.0.0-20190131152654-06015046a58d/go.mod h1:AJk9IN8JW08Lq45W1qsMjrwBu1lqqo5sOW+gE8Iuab0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creasty/go-easing v0.0.0-20161107103139-0cfd96d3a544 h1:dSAtlq3+gqQHnEe3ZJdRU9Ur9VnpAqHViAGAH8oWX24= +github.com/creasty/go-easing v0.0.0-20161107103139-0cfd96d3a544/go.mod h1:jK8jBccxnmSEDHLVmvYdZ2RNusC5wW9/iWCNNvvnGsA= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/google/btree v1.0.1-0.20190326150332-20236160a414 h1:BciqlM+jKfylRZv6hNtP39myqwtoz6JhgpxurLd8dTY= +github.com/google/btree v1.0.1-0.20190326150332-20236160a414/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/gordonklaus/portaudio v0.0.0-20180817120803-00e7307ccd93 h1:TSG+DyZBnazM22ZHyHLeUkzM34ClkJRjIWHTq4btvek= +github.com/gordonklaus/portaudio v0.0.0-20180817120803-00e7307ccd93/go.mod h1:HfYnZi/ARQKG0dwH5HNDmPCHdLiFiBf+SI7DbhW7et4= +github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= +github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= +github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3 h1:sHsPfNMAG70QAvKbddQ0uScZCHQoZsT5NykGRCeeeIs= +github.com/jinzhu/copier v0.0.0-20180308034124-7e38e58719c3/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= +github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= +github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= +github.com/jsimonetti/go-artnet v0.0.0-20191008094654-e099a3930c68 h1:F+kPtehe174cTiBrkF6P5d31/kWUUaIEb+ueFJ+XxEE= +github.com/jsimonetti/go-artnet v0.0.0-20191008094654-e099a3930c68/go.mod h1:N8sFzz7wHYGJd0Hu30NVGINiKthg2EVQW9E2xBVp9VM= +github.com/jsimonetti/go-artnet v0.0.0-20200117113556-db828ac108e3 h1:gsImleLF/P0sgU8zyXPW6GXPfr/u4ul3VD041tKmzoA= +github.com/jsimonetti/go-artnet v0.0.0-20200117113556-db828ac108e3/go.mod h1:N8sFzz7wHYGJd0Hu30NVGINiKthg2EVQW9E2xBVp9VM= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rakyll/portmidi v0.0.0-20170716032345-1246dd47c560 h1:LxQoLCD5lQzG48j4BYI6CnKNSbnXAL5KL1oZfempXcE= +github.com/rakyll/portmidi v0.0.0-20170716032345-1246dd47c560/go.mod h1:tO1ylmFo6+hnYFvj/fd92q30wkNQwgWC/8mcHq0KkQU= +github.com/rakyll/portmidi v0.0.0-20191102002215-74e95e8bc9b1 h1:ayxM9WkC6QRX0QnfrbZa98pfFqrNq1jo+58rWL1qjZs= +github.com/rakyll/portmidi v0.0.0-20191102002215-74e95e8bc9b1/go.mod h1:xKffaBd7e1YUoLpR2azvJqkxEdUDNGNDMlsUNdv6Bcs= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.4 h1:S0tLZ3VOKl2Te0hpq8+ke0eSJPfCnNTPiDlsfwi1/NE= +github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40 h1:2gwxRRQ5I+FcDbxGtkIC9kWD7EFBewHjQqD8rDQAVQA= +github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index c92edf9..55d47b1 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ package main import ( "github.com/StageAutoControl/controller/cmd" - _ "github.com/StageAutoControl/controller/cmd/artnet" _ "github.com/StageAutoControl/controller/cmd/audio" _ "github.com/StageAutoControl/controller/cmd/midi" ) diff --git a/pkg/api/controller.go b/pkg/api/controller.go deleted file mode 100644 index 78afe62..0000000 --- a/pkg/api/controller.go +++ /dev/null @@ -1,104 +0,0 @@ -package api - -import ( - "encoding/json" - "errors" - "fmt" - "github.com/satori/go.uuid" - "github.com/sirupsen/logrus" - "net/http" - "reflect" -) - -type Controller struct { - logger *logrus.Entry - storage storage - controlledType interface{} -} - -//type Arg json.RawMessage - -type Response struct { - value interface{} -} - -type listResponse struct { - value []interface{} -} - -func newController(logger *logrus.Entry, storage storage, controlledType interface{}) *Controller { - return &Controller{ - logger: logger, - storage: storage, - controlledType: controlledType, - } -} - -func (c *Controller) Create(r *http.Request, args *json.RawMessage, reply *Response) error { - if args == nil || len(*args) == 0 { - return errors.New("no parameter given") - } - - t := reflect.ValueOf(c.controlledType).Type() - target := reflect.New(t).Elem() - if err := json.Unmarshal(*args, &target); err != nil { - return fmt.Errorf("failed to unmarshal json content: %v", err) - } - - id, err := c.getID(&target) - if err != nil { - return err - } - - if err := c.storage.Write(id, &target); err != nil { - return fmt.Errorf("failed to write to disk: %v", err) - } - - reply.value = target - return nil -} - -func (c *Controller) checkType(value interface{}) error { - t := reflect.TypeOf(value) - if t.Kind() != reflect.Ptr { - return fmt.Errorf("parameter %s of type %s is no pointer", t.Name(), t.Kind()) - } - - s := t.Elem() - if s.Kind() != reflect.Struct { - return fmt.Errorf("parameter pointer %v of type %v is no struct", s.Name(), s.Kind()) - } - - expected := reflect.TypeOf(c.controlledType) - if s.Name() != expected.Name() { - return fmt.Errorf("expected to get value of type %v, got type %v", expected.Name(), s.Name()) - } - - return nil -} - -// getID expects a parameter "value" which is a pointer to a struct, which has a field called ID. -// if the ID field is set the value is returned, otherwise a new UUID v4 is generated and set as field value. -func (c *Controller) getID(value interface{}) (string, error) { - v := reflect.ValueOf(value).Elem() - field := v.FieldByName("ID") - if !field.IsValid() { - return "", fmt.Errorf("field ID of struct %s is not valid", v.Kind()) - } - - if !field.CanSet() { - return "", fmt.Errorf("field ID of struct %s is not settable", v.Kind()) - } - - if field.Kind() != reflect.String { - return "", fmt.Errorf("field ID of struct %s is not a string", v.Kind()) - } - - id := field.String() - if id == "" { - id = uuid.NewV4().String() - field.SetString(id) - } - - return id, nil -} diff --git a/pkg/api/datastore/dmx_animation_controller.go b/pkg/api/datastore/dmx_animation_controller.go new file mode 100644 index 0000000..9799883 --- /dev/null +++ b/pkg/api/datastore/dmx_animation_controller.go @@ -0,0 +1,105 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// DMXAnimationController controls the DMXAnimation entity +type DMXAnimationController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewDMXAnimationController returns a new DMXAnimationController instance +func NewDMXAnimationController(logger *logrus.Entry, storage api.Storage) *DMXAnimationController { + return &DMXAnimationController{ + logger: logger, + storage: storage, + } +} + +// Create a new DMXAnimation +func (c *DMXAnimationController) Create(r *http.Request, entity *cntl.DMXAnimation, reply *cntl.DMXAnimation) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new DMXAnimation +func (c *DMXAnimationController) Update(r *http.Request, entity *cntl.DMXAnimation, reply *cntl.DMXAnimation) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a DMXAnimation +func (c *DMXAnimationController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.DMXAnimation) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXAnimation{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of DMXAnimation +func (c *DMXAnimationController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.DMXAnimation) error { + *reply = []*cntl.DMXAnimation{} + for _, id := range c.storage.List(&cntl.DMXAnimation{}) { + entity := &cntl.DMXAnimation{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a DMXAnimation +func (c *DMXAnimationController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXAnimation{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.DMXAnimation{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/dmx_animation_controller_test.go b/pkg/api/datastore/dmx_animation_controller_test.go new file mode 100644 index 0000000..b3a228a --- /dev/null +++ b/pkg/api/datastore/dmx_animation_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestDMXAnimationController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXAnimationController(logger, store) + key := "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" + entity := ds.DMXAnimations[key] + + createReply := &cntl.DMXAnimation{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXAnimationController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXAnimationController(logger, store) + key := "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" + entity := ds.DMXAnimations[key] + + createEntity := &cntl.DMXAnimation{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.DMXAnimation{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXAnimationController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXAnimationController(logger, store) + key := "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" + + reply := &cntl.DMXAnimation{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXAnimationController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXAnimationController(logger, store) + key := "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" + entity := ds.DMXAnimations[key] + + createReply := &cntl.DMXAnimation{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXAnimation{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestDMXAnimationController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXAnimationController(logger, store) + key := "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" + entity := ds.DMXAnimations[key] + + reply := &cntl.DMXAnimation{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXAnimationController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXAnimationController(logger, store) + key := "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" + entity := ds.DMXAnimations[key] + + createReply := &cntl.DMXAnimation{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXAnimation{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestDMXAnimationController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXAnimationController(logger, store) + key := "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXAnimationController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXAnimationController(logger, store) + key := "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" + entity := ds.DMXAnimations[key] + + createReply := &cntl.DMXAnimation{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/dmx_color_variable_controller.go b/pkg/api/datastore/dmx_color_variable_controller.go new file mode 100644 index 0000000..3b582d4 --- /dev/null +++ b/pkg/api/datastore/dmx_color_variable_controller.go @@ -0,0 +1,105 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// DMXColorVariableController controls the DMXColorVariable entity +type DMXColorVariableController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewDMXColorVariableController returns a new MXColorVariableController instance +func NewDMXColorVariableController(logger *logrus.Entry, storage api.Storage) *DMXColorVariableController { + return &DMXColorVariableController{ + logger: logger, + storage: storage, + } +} + +// Create a new DMXColorVariable +func (c *DMXColorVariableController) Create(r *http.Request, entity *cntl.DMXColorVariable, reply *cntl.DMXColorVariable) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new DMXColorVariable +func (c *DMXColorVariableController) Update(r *http.Request, entity *cntl.DMXColorVariable, reply *cntl.DMXColorVariable) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a DMXColorVariable +func (c *DMXColorVariableController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.DMXColorVariable) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXColorVariable{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of DMXColorVariable +func (c *DMXColorVariableController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.DMXColorVariable) error { + *reply = []*cntl.DMXColorVariable{} + for _, id := range c.storage.List(&cntl.DMXColorVariable{}) { + entity := &cntl.DMXColorVariable{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a DMXColorVariable +func (c *DMXColorVariableController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXColorVariable{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.DMXColorVariable{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/dmx_color_variable_controller_test.go b/pkg/api/datastore/dmx_color_variable_controller_test.go new file mode 100644 index 0000000..6282b4d --- /dev/null +++ b/pkg/api/datastore/dmx_color_variable_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestDMXColorVariableController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXColorVariableController(logger, store) + key := "4b848ea8-5094-4509-a067-09a0e568220d" + entity := ds.DMXColorVariables[key] + + createReply := &cntl.DMXColorVariable{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXColorVariableController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXColorVariableController(logger, store) + key := "4b848ea8-5094-4509-a067-09a0e568220d" + entity := ds.DMXColorVariables[key] + + createEntity := &cntl.DMXColorVariable{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.DMXColorVariable{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXColorVariableController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXColorVariableController(logger, store) + key := "4b848ea8-5094-4509-a067-09a0e568220d" + + reply := &cntl.DMXColorVariable{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXColorVariableController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXColorVariableController(logger, store) + key := "4b848ea8-5094-4509-a067-09a0e568220d" + entity := ds.DMXColorVariables[key] + + createReply := &cntl.DMXColorVariable{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXColorVariable{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestDMXColorVariableController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXColorVariableController(logger, store) + key := "4b848ea8-5094-4509-a067-09a0e568220d" + entity := ds.DMXColorVariables[key] + + reply := &cntl.DMXColorVariable{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXColorVariableController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXColorVariableController(logger, store) + key := "4b848ea8-5094-4509-a067-09a0e568220d" + entity := ds.DMXColorVariables[key] + + createReply := &cntl.DMXColorVariable{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXColorVariable{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestDMXColorVariableController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXColorVariableController(logger, store) + key := "4b848ea8-5094-4509-a067-09a0e568220d" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXColorVariableController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXColorVariableController(logger, store) + key := "4b848ea8-5094-4509-a067-09a0e568220d" + entity := ds.DMXColorVariables[key] + + createReply := &cntl.DMXColorVariable{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/dmx_device_controller.go b/pkg/api/datastore/dmx_device_controller.go new file mode 100644 index 0000000..f94b331 --- /dev/null +++ b/pkg/api/datastore/dmx_device_controller.go @@ -0,0 +1,125 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// DMXDeviceController controls the DMXDevice entity +type DMXDeviceController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewDMXDeviceController returns a new DMXDeviceController instance +func NewDMXDeviceController(logger *logrus.Entry, storage api.Storage) *DMXDeviceController { + return &DMXDeviceController{ + logger: logger, + storage: storage, + } +} + +func (c *DMXDeviceController) validate(entity *cntl.DMXDevice) error { + if entity.Tags == nil { + entity.Tags = make([]cntl.Tag, 0) + } + + if !c.storage.Has(entity.TypeID, &cntl.DMXDeviceType{}) { + return fmt.Errorf("cannot save DMXDevice with non-existing DMXDeviceType %q", entity.TypeID) + } + + return nil +} + +// Create a new DMXDevice +func (c *DMXDeviceController) Create(r *http.Request, entity *cntl.DMXDevice, reply *cntl.DMXDevice) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.validate(entity); err != nil { + return fmt.Errorf("failed to validate entity: %v", err) + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new DMXDevice +func (c *DMXDeviceController) Update(r *http.Request, entity *cntl.DMXDevice, reply *cntl.DMXDevice) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.validate(entity); err != nil { + return fmt.Errorf("failed to validate entity: %v", err) + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a DMXDevice +func (c *DMXDeviceController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.DMXDevice) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXDevice{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of DMXDevice +func (c *DMXDeviceController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.DMXDevice) error { + *reply = []*cntl.DMXDevice{} + for _, id := range c.storage.List(&cntl.DMXDevice{}) { + entity := &cntl.DMXDevice{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a DMXDevice +func (c *DMXDeviceController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXDevice{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.DMXDevice{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/dmx_device_controller_test.go b/pkg/api/datastore/dmx_device_controller_test.go new file mode 100644 index 0000000..b258460 --- /dev/null +++ b/pkg/api/datastore/dmx_device_controller_test.go @@ -0,0 +1,179 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func createDeviceType(t *testing.T) { + err := store.Write("1555d67e-1187-11e7-8135-9b41038b5b75", ds.DMXDeviceTypes["1555d67e-1187-11e7-8135-9b41038b5b75"]) + if err != nil { + t.Fatalf("failed to create DMXDeviceType: %v", err) + } +} + +func TestDMXDeviceController_Create_WithID(t *testing.T) { + createDeviceType(t) + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceController(logger, store) + key := "35cae00a-0b17-11e7-8bca-bbf30c56f20e" + entity := ds.DMXDevices[key] + + createReply := &cntl.DMXDevice{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXDeviceController_Create_WithoutID(t *testing.T) { + createDeviceType(t) + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceController(logger, store) + key := "35cae00a-0b17-11e7-8bca-bbf30c56f20e" + entity := ds.DMXDevices[key] + + createEntity := &cntl.DMXDevice{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.DMXDevice{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXDeviceController_Get_NotExisting(t *testing.T) { + createDeviceType(t) + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceController(logger, store) + key := "35cae00a-0b17-11e7-8bca-bbf30c56f20e" + + reply := &cntl.DMXDevice{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceController_Get_Existing(t *testing.T) { + createDeviceType(t) + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceController(logger, store) + key := "35cae00a-0b17-11e7-8bca-bbf30c56f20e" + entity := ds.DMXDevices[key] + + createReply := &cntl.DMXDevice{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXDevice{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestDMXDeviceController_Update_NotExisting(t *testing.T) { + createDeviceType(t) + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceController(logger, store) + key := "35cae00a-0b17-11e7-8bca-bbf30c56f20e" + entity := ds.DMXDevices[key] + + reply := &cntl.DMXDevice{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceController_Update_Existing(t *testing.T) { + createDeviceType(t) + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceController(logger, store) + key := "35cae00a-0b17-11e7-8bca-bbf30c56f20e" + entity := ds.DMXDevices[key] + + createReply := &cntl.DMXDevice{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXDevice{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestDMXDeviceController_Delete_NotExisting(t *testing.T) { + createDeviceType(t) + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceController(logger, store) + key := "35cae00a-0b17-11e7-8bca-bbf30c56f20e" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceController_Delete_Existing(t *testing.T) { + createDeviceType(t) + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceController(logger, store) + key := "35cae00a-0b17-11e7-8bca-bbf30c56f20e" + entity := ds.DMXDevices[key] + + createReply := &cntl.DMXDevice{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/dmx_device_group_controller.go b/pkg/api/datastore/dmx_device_group_controller.go new file mode 100644 index 0000000..8ee55eb --- /dev/null +++ b/pkg/api/datastore/dmx_device_group_controller.go @@ -0,0 +1,105 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// DMXDeviceGroupController controls the DMXDeviceGroup entity +type DMXDeviceGroupController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewDMXDeviceGroupController returns a new DMXDeviceGroupController instance +func NewDMXDeviceGroupController(logger *logrus.Entry, storage api.Storage) *DMXDeviceGroupController { + return &DMXDeviceGroupController{ + logger: logger, + storage: storage, + } +} + +// Create a new DMXDeviceGroup +func (c *DMXDeviceGroupController) Create(r *http.Request, entity *cntl.DMXDeviceGroup, reply *cntl.DMXDeviceGroup) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new DMXDeviceGroup +func (c *DMXDeviceGroupController) Update(r *http.Request, entity *cntl.DMXDeviceGroup, reply *cntl.DMXDeviceGroup) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a DMXDeviceGroup +func (c *DMXDeviceGroupController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.DMXDeviceGroup) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXDeviceGroup{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of DMXDeviceGroup +func (c *DMXDeviceGroupController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.DMXDeviceGroup) error { + *reply = []*cntl.DMXDeviceGroup{} + for _, id := range c.storage.List(&cntl.DMXDeviceGroup{}) { + entity := &cntl.DMXDeviceGroup{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a DMXDeviceGroup +func (c *DMXDeviceGroupController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXDeviceGroup{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.DMXDeviceGroup{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/dmx_device_group_controller_test.go b/pkg/api/datastore/dmx_device_group_controller_test.go new file mode 100644 index 0000000..86ebe66 --- /dev/null +++ b/pkg/api/datastore/dmx_device_group_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestDMXDeviceGroupController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceGroupController(logger, store) + key := "475b71a0-0b16-11e7-9406-e3f678e8b788" + entity := ds.DMXDeviceGroups[key] + + createReply := &cntl.DMXDeviceGroup{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXDeviceGroupController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceGroupController(logger, store) + key := "475b71a0-0b16-11e7-9406-e3f678e8b788" + entity := ds.DMXDeviceGroups[key] + + createEntity := &cntl.DMXDeviceGroup{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.DMXDeviceGroup{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXDeviceGroupController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceGroupController(logger, store) + key := "475b71a0-0b16-11e7-9406-e3f678e8b788" + + reply := &cntl.DMXDeviceGroup{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceGroupController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceGroupController(logger, store) + key := "475b71a0-0b16-11e7-9406-e3f678e8b788" + entity := ds.DMXDeviceGroups[key] + + createReply := &cntl.DMXDeviceGroup{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXDeviceGroup{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestDMXDeviceGroupController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceGroupController(logger, store) + key := "475b71a0-0b16-11e7-9406-e3f678e8b788" + entity := ds.DMXDeviceGroups[key] + + reply := &cntl.DMXDeviceGroup{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceGroupController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceGroupController(logger, store) + key := "475b71a0-0b16-11e7-9406-e3f678e8b788" + entity := ds.DMXDeviceGroups[key] + + createReply := &cntl.DMXDeviceGroup{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXDeviceGroup{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestDMXDeviceGroupController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceGroupController(logger, store) + key := "475b71a0-0b16-11e7-9406-e3f678e8b788" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceGroupController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceGroupController(logger, store) + key := "475b71a0-0b16-11e7-9406-e3f678e8b788" + entity := ds.DMXDeviceGroups[key] + + createReply := &cntl.DMXDeviceGroup{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/dmx_device_type_controller.go b/pkg/api/datastore/dmx_device_type_controller.go new file mode 100644 index 0000000..e673f60 --- /dev/null +++ b/pkg/api/datastore/dmx_device_type_controller.go @@ -0,0 +1,121 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// DMXDeviceTypeController controls the DMXDeviceType entity +type DMXDeviceTypeController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewDMXDeviceTypeController returns a new DMXDeviceTypeController instance +func NewDMXDeviceTypeController(logger *logrus.Entry, storage api.Storage) *DMXDeviceTypeController { + return &DMXDeviceTypeController{ + logger: logger, + storage: storage, + } +} + +func (c *DMXDeviceTypeController) validate(entity *cntl.DMXDeviceType) error { + if entity.LEDs == nil { + entity.LEDs = make([]cntl.LED, 0) + } + + return nil +} + +// Create a new DMXDeviceType +func (c *DMXDeviceTypeController) Create(r *http.Request, entity *cntl.DMXDeviceType, reply *cntl.DMXDeviceType) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.validate(entity); err != nil { + return fmt.Errorf("failed to validate entity: %v", err) + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new DMXDeviceType +func (c *DMXDeviceTypeController) Update(r *http.Request, entity *cntl.DMXDeviceType, reply *cntl.DMXDeviceType) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.validate(entity); err != nil { + return fmt.Errorf("failed to validate entity: %v", err) + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a DMXDeviceType +func (c *DMXDeviceTypeController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.DMXDeviceType) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXDeviceType{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of DMXDeviceType +func (c *DMXDeviceTypeController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.DMXDeviceType) error { + *reply = []*cntl.DMXDeviceType{} + for _, id := range c.storage.List(&cntl.DMXDeviceType{}) { + entity := &cntl.DMXDeviceType{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a DMXDeviceType +func (c *DMXDeviceTypeController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXDeviceType{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.DMXDeviceType{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/dmx_device_type_controller_test.go b/pkg/api/datastore/dmx_device_type_controller_test.go new file mode 100644 index 0000000..1ee2bac --- /dev/null +++ b/pkg/api/datastore/dmx_device_type_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestDMXDeviceTypeController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceTypeController(logger, store) + key := "628fc3ea-1188-11e7-8824-5f72d80c17b6" + entity := ds.DMXDeviceTypes[key] + + createReply := &cntl.DMXDeviceType{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXDeviceTypeController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceTypeController(logger, store) + key := "628fc3ea-1188-11e7-8824-5f72d80c17b6" + entity := ds.DMXDeviceTypes[key] + + createEntity := &cntl.DMXDeviceType{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.DMXDeviceType{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXDeviceTypeController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceTypeController(logger, store) + key := "628fc3ea-1188-11e7-8824-5f72d80c17b6" + + reply := &cntl.DMXDeviceType{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceTypeController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceTypeController(logger, store) + key := "628fc3ea-1188-11e7-8824-5f72d80c17b6" + entity := ds.DMXDeviceTypes[key] + + createReply := &cntl.DMXDeviceType{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXDeviceType{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestDMXDeviceTypeController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceTypeController(logger, store) + key := "628fc3ea-1188-11e7-8824-5f72d80c17b6" + entity := ds.DMXDeviceTypes[key] + + reply := &cntl.DMXDeviceType{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceTypeController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceTypeController(logger, store) + key := "628fc3ea-1188-11e7-8824-5f72d80c17b6" + entity := ds.DMXDeviceTypes[key] + + createReply := &cntl.DMXDeviceType{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXDeviceType{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestDMXDeviceTypeController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceTypeController(logger, store) + key := "628fc3ea-1188-11e7-8824-5f72d80c17b6" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXDeviceTypeController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXDeviceTypeController(logger, store) + key := "628fc3ea-1188-11e7-8824-5f72d80c17b6" + entity := ds.DMXDeviceTypes[key] + + createReply := &cntl.DMXDeviceType{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/dmx_preset_controller.go b/pkg/api/datastore/dmx_preset_controller.go new file mode 100644 index 0000000..9f5fb21 --- /dev/null +++ b/pkg/api/datastore/dmx_preset_controller.go @@ -0,0 +1,105 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// DMXPresetController controls the DMXPreset entity +type DMXPresetController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewDMXPresetController returns a new DMXPresetController instance +func NewDMXPresetController(logger *logrus.Entry, storage api.Storage) *DMXPresetController { + return &DMXPresetController{ + logger: logger, + storage: storage, + } +} + +// Create a new DMXPreset +func (c *DMXPresetController) Create(r *http.Request, entity *cntl.DMXPreset, reply *cntl.DMXPreset) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new DMXPreset +func (c *DMXPresetController) Update(r *http.Request, entity *cntl.DMXPreset, reply *cntl.DMXPreset) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a DMXPreset +func (c *DMXPresetController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.DMXPreset) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXPreset{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of DMXPreset +func (c *DMXPresetController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.DMXPreset) error { + *reply = []*cntl.DMXPreset{} + for _, id := range c.storage.List(&cntl.DMXPreset{}) { + entity := &cntl.DMXPreset{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a DMXPreset +func (c *DMXPresetController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXPreset{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.DMXPreset{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/dmx_preset_controller_test.go b/pkg/api/datastore/dmx_preset_controller_test.go new file mode 100644 index 0000000..39075af --- /dev/null +++ b/pkg/api/datastore/dmx_preset_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestDMXPresetController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXPresetController(logger, store) + key := "0de258e0-0e7b-11e7-afd4-ebf6036983dc" + entity := ds.DMXPresets[key] + + createReply := &cntl.DMXPreset{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXPresetController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXPresetController(logger, store) + key := "0de258e0-0e7b-11e7-afd4-ebf6036983dc" + entity := ds.DMXPresets[key] + + createEntity := &cntl.DMXPreset{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.DMXPreset{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXPresetController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXPresetController(logger, store) + key := "0de258e0-0e7b-11e7-afd4-ebf6036983dc" + + reply := &cntl.DMXPreset{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXPresetController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXPresetController(logger, store) + key := "0de258e0-0e7b-11e7-afd4-ebf6036983dc" + entity := ds.DMXPresets[key] + + createReply := &cntl.DMXPreset{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXPreset{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestDMXPresetController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXPresetController(logger, store) + key := "0de258e0-0e7b-11e7-afd4-ebf6036983dc" + entity := ds.DMXPresets[key] + + reply := &cntl.DMXPreset{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXPresetController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXPresetController(logger, store) + key := "0de258e0-0e7b-11e7-afd4-ebf6036983dc" + entity := ds.DMXPresets[key] + + createReply := &cntl.DMXPreset{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXPreset{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestDMXPresetController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXPresetController(logger, store) + key := "0de258e0-0e7b-11e7-afd4-ebf6036983dc" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get errNotExists, but got %v", err) + } +} + +func TestDMXPresetController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXPresetController(logger, store) + key := "0de258e0-0e7b-11e7-afd4-ebf6036983dc" + entity := ds.DMXPresets[key] + + createReply := &cntl.DMXPreset{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/dmx_scene_controller.go b/pkg/api/datastore/dmx_scene_controller.go new file mode 100644 index 0000000..867ebc1 --- /dev/null +++ b/pkg/api/datastore/dmx_scene_controller.go @@ -0,0 +1,105 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// DMXSceneController controls the DMXScene entity +type DMXSceneController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewDMXSceneController returns a new DMXSceneController instance +func NewDMXSceneController(logger *logrus.Entry, storage api.Storage) *DMXSceneController { + return &DMXSceneController{ + logger: logger, + storage: storage, + } +} + +// Create a new DMXScene +func (c *DMXSceneController) Create(r *http.Request, entity *cntl.DMXScene, reply *cntl.DMXScene) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new DMXScene +func (c *DMXSceneController) Update(r *http.Request, entity *cntl.DMXScene, reply *cntl.DMXScene) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a DMXScene +func (c *DMXSceneController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.DMXScene) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXScene{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of DMXScene +func (c *DMXSceneController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.DMXScene) error { + *reply = []*cntl.DMXScene{} + for _, id := range c.storage.List(&cntl.DMXScene{}) { + entity := &cntl.DMXScene{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a DMXScene +func (c *DMXSceneController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXScene{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.DMXScene{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/dmx_scene_controller_test.go b/pkg/api/datastore/dmx_scene_controller_test.go new file mode 100644 index 0000000..29ee69c --- /dev/null +++ b/pkg/api/datastore/dmx_scene_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestDMXSceneController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXSceneController(logger, store) + key := "492cef2e-0b14-11e7-be89-c3fa25f9cabb" + entity := ds.DMXScenes[key] + + createReply := &cntl.DMXScene{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXSceneController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXSceneController(logger, store) + key := "492cef2e-0b14-11e7-be89-c3fa25f9cabb" + entity := ds.DMXScenes[key] + + createEntity := &cntl.DMXScene{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.DMXScene{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXSceneController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXSceneController(logger, store) + key := "492cef2e-0b14-11e7-be89-c3fa25f9cabb" + + reply := &cntl.DMXScene{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXSceneController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXSceneController(logger, store) + key := "492cef2e-0b14-11e7-be89-c3fa25f9cabb" + entity := ds.DMXScenes[key] + + createReply := &cntl.DMXScene{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXScene{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestDMXSceneController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXSceneController(logger, store) + key := "492cef2e-0b14-11e7-be89-c3fa25f9cabb" + entity := ds.DMXScenes[key] + + reply := &cntl.DMXScene{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXSceneController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXSceneController(logger, store) + key := "492cef2e-0b14-11e7-be89-c3fa25f9cabb" + entity := ds.DMXScenes[key] + + createReply := &cntl.DMXScene{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXScene{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestDMXSceneController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXSceneController(logger, store) + key := "492cef2e-0b14-11e7-be89-c3fa25f9cabb" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXSceneController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXSceneController(logger, store) + key := "492cef2e-0b14-11e7-be89-c3fa25f9cabb" + entity := ds.DMXScenes[key] + + createReply := &cntl.DMXScene{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/dmx_transition_controller.go b/pkg/api/datastore/dmx_transition_controller.go new file mode 100644 index 0000000..37db5d5 --- /dev/null +++ b/pkg/api/datastore/dmx_transition_controller.go @@ -0,0 +1,105 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// DMXTransitionController controls the DMXTransition entity +type DMXTransitionController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewDMXTransitionController returns a new DMXTransitionController instance +func NewDMXTransitionController(logger *logrus.Entry, storage api.Storage) *DMXTransitionController { + return &DMXTransitionController{ + logger: logger, + storage: storage, + } +} + +// Create a new DMXTransition +func (c *DMXTransitionController) Create(r *http.Request, entity *cntl.DMXTransition, reply *cntl.DMXTransition) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new DMXTransition +func (c *DMXTransitionController) Update(r *http.Request, entity *cntl.DMXTransition, reply *cntl.DMXTransition) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a DMXTransition +func (c *DMXTransitionController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.DMXTransition) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXTransition{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of DMXTransition +func (c *DMXTransitionController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.DMXTransition) error { + *reply = []*cntl.DMXTransition{} + for _, id := range c.storage.List(&cntl.DMXTransition{}) { + entity := &cntl.DMXTransition{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a DMXTransition +func (c *DMXTransitionController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.DMXTransition{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.DMXTransition{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/dmx_transition_controller_test.go b/pkg/api/datastore/dmx_transition_controller_test.go new file mode 100644 index 0000000..de2fa08 --- /dev/null +++ b/pkg/api/datastore/dmx_transition_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestDMXTransitionController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXTransitionController(logger, store) + key := "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21" + entity := ds.DMXTransitions[key] + + createReply := &cntl.DMXTransition{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXTransitionController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXTransitionController(logger, store) + key := "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21" + entity := ds.DMXTransitions[key] + + createEntity := &cntl.DMXTransition{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.DMXTransition{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestDMXTransitionController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXTransitionController(logger, store) + key := "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21" + + reply := &cntl.DMXTransition{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXTransitionController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXTransitionController(logger, store) + key := "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21" + entity := ds.DMXTransitions[key] + + createReply := &cntl.DMXTransition{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXTransition{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestDMXTransitionController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXTransitionController(logger, store) + key := "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21" + entity := ds.DMXTransitions[key] + + reply := &cntl.DMXTransition{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXTransitionController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXTransitionController(logger, store) + key := "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21" + entity := ds.DMXTransitions[key] + + createReply := &cntl.DMXTransition{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.DMXTransition{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestDMXTransitionController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXTransitionController(logger, store) + key := "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestDMXTransitionController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewDMXTransitionController(logger, store) + key := "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21" + entity := ds.DMXTransitions[key] + + createReply := &cntl.DMXTransition{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/set_list_controller.go b/pkg/api/datastore/set_list_controller.go new file mode 100644 index 0000000..5b655fc --- /dev/null +++ b/pkg/api/datastore/set_list_controller.go @@ -0,0 +1,105 @@ +package datastore + +import ( + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// SetListController controls the SetList entity +type SetListController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewSetListController returns a new SetListController instance +func NewSetListController(logger *logrus.Entry, storage api.Storage) *SetListController { + return &SetListController{ + logger: logger, + storage: storage, + } +} + +// Create a new SetList +func (c *SetListController) Create(r *http.Request, entity *cntl.SetList, reply *cntl.SetList) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new SetList +func (c *SetListController) Update(r *http.Request, entity *cntl.SetList, reply *cntl.SetList) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a SetList +func (c *SetListController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.SetList) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.SetList{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of SetList +func (c *SetListController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.SetList) error { + *reply = []*cntl.SetList{} + for _, id := range c.storage.List(&cntl.SetList{}) { + entity := &cntl.SetList{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a SetList +func (c *SetListController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.SetList{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.SetList{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/set_list_controller_test.go b/pkg/api/datastore/set_list_controller_test.go new file mode 100644 index 0000000..e4ffdc0 --- /dev/null +++ b/pkg/api/datastore/set_list_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestSetListController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSetListController(logger, store) + key := "f5b4be8a-0b18-11e7-b837-4bac99d86956" + entity := ds.SetLists[key] + + createReply := &cntl.SetList{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestSetListController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSetListController(logger, store) + key := "f5b4be8a-0b18-11e7-b837-4bac99d86956" + entity := ds.SetLists[key] + + createEntity := &cntl.SetList{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.SetList{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestSetListController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSetListController(logger, store) + key := "f5b4be8a-0b18-11e7-b837-4bac99d86956" + + reply := &cntl.SetList{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestSetListController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSetListController(logger, store) + key := "f5b4be8a-0b18-11e7-b837-4bac99d86956" + entity := ds.SetLists[key] + + createReply := &cntl.SetList{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.SetList{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestSetListController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSetListController(logger, store) + key := "f5b4be8a-0b18-11e7-b837-4bac99d86956" + entity := ds.SetLists[key] + + reply := &cntl.SetList{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestSetListController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSetListController(logger, store) + key := "f5b4be8a-0b18-11e7-b837-4bac99d86956" + entity := ds.SetLists[key] + + createReply := &cntl.SetList{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.SetList{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestSetListController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSetListController(logger, store) + key := "f5b4be8a-0b18-11e7-b837-4bac99d86956" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestSetListController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSetListController(logger, store) + key := "f5b4be8a-0b18-11e7-b837-4bac99d86956" + entity := ds.SetLists[key] + + createReply := &cntl.SetList{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/song_controller.go b/pkg/api/datastore/song_controller.go new file mode 100644 index 0000000..76be2c1 --- /dev/null +++ b/pkg/api/datastore/song_controller.go @@ -0,0 +1,131 @@ +package datastore + +import ( + "errors" + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/cntl/song" + "github.com/jinzhu/copier" + "github.com/satori/go.uuid" + "github.com/sirupsen/logrus" +) + +// SongController controls the Song entity +type SongController struct { + logger *logrus.Entry + storage api.Storage +} + +// NewSongController returns a new SongController instance +func NewSongController(logger *logrus.Entry, storage api.Storage) *SongController { + return &SongController{ + logger: logger, + storage: storage, + } +} + +func (c *SongController) validate(entity *cntl.Song) error { + if entity.MIDICommands == nil { + entity.MIDICommands = make([]cntl.MIDICommand, 0) + } + + if entity.BarChanges == nil { + return errors.New("song needs to have at least one BarChange") + } + + if err := song.ValidateBarChanges(song.StreamlineBarChanges(entity)); err != nil { + return fmt.Errorf("failed to validate bar changes: %v", err) + } + + return nil +} + +// Create a new Song +func (c *SongController) Create(r *http.Request, entity *cntl.Song, reply *cntl.Song) error { + if entity.ID == "" { + entity.ID = uuid.NewV4().String() + } + + if c.storage.Has(entity.ID, entity) { + return api.ErrExists + } + + if err := c.validate(entity); err != nil { + return fmt.Errorf("failed to validate entity: %v", err) + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to write to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Update a new Song +func (c *SongController) Update(r *http.Request, entity *cntl.Song, reply *cntl.Song) error { + if !c.storage.Has(entity.ID, entity) { + return api.ErrNotExists + } + + if err := c.validate(entity); err != nil { + return fmt.Errorf("failed to validate entity: %v", err) + } + + if err := c.storage.Write(entity.ID, entity); err != nil { + return fmt.Errorf("failed to update to disk: %v", err) + } + + return copier.Copy(reply, entity) +} + +// Get a Song +func (c *SongController) Get(r *http.Request, idReq *api.IDBody, reply *cntl.Song) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.Song{}) { + return api.ErrNotExists + } + + if err := c.storage.Read(idReq.ID, reply); err != nil { + return fmt.Errorf("failed to read entity: %v", err) + } + + return nil +} + +// GetAll returns all entities of Song +func (c *SongController) GetAll(r *http.Request, idReq *api.Empty, reply *[]*cntl.Song) error { + *reply = []*cntl.Song{} + for _, id := range c.storage.List(&cntl.Song{}) { + entity := &cntl.Song{} + if err := c.storage.Read(id, entity); err != nil { + return fmt.Errorf("failed to read entity %s: %v", id, err) + } + *reply = append(*reply, entity) + } + + return nil +} + +// Delete a Song +func (c *SongController) Delete(r *http.Request, idReq *api.IDBody, reply *api.SuccessResponse) error { + if idReq.ID == "" { + return api.ErrNoIDGiven + } + + if !c.storage.Has(idReq.ID, &cntl.Song{}) { + return api.ErrNotExists + } + + if err := c.storage.Delete(idReq.ID, &cntl.Song{}); err != nil { + return fmt.Errorf("failed to delete entity: %v", err) + } + + reply.Success = true + return nil +} diff --git a/pkg/api/datastore/song_controller_test.go b/pkg/api/datastore/song_controller_test.go new file mode 100644 index 0000000..d354ebb --- /dev/null +++ b/pkg/api/datastore/song_controller_test.go @@ -0,0 +1,164 @@ +package datastore + +import ( + "testing" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" + "github.com/jinzhu/copier" +) + +func TestSongController_Create_WithID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSongController(logger, store) + key := "3c1065c8-0b14-11e7-96eb-5b134621c411" + entity := ds.Songs[key] + + createReply := &cntl.Song{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestSongController_Create_WithoutID(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSongController(logger, store) + key := "3c1065c8-0b14-11e7-96eb-5b134621c411" + entity := ds.Songs[key] + + createEntity := &cntl.Song{} + if err := copier.Copy(createEntity, entity); err != nil { + t.Fatal(err) + } + + createEntity.ID = "" + + createReply := &cntl.Song{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } +} + +func TestSongController_Get_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSongController(logger, store) + key := "3c1065c8-0b14-11e7-96eb-5b134621c411" + + reply := &cntl.Song{} + + idReq := &api.IDBody{ID: key} + if err := controller.Get(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestSongController_Get_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSongController(logger, store) + key := "3c1065c8-0b14-11e7-96eb-5b134621c411" + entity := ds.Songs[key] + + createReply := &cntl.Song{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.Song{} + idReq := &api.IDBody{ID: key} + t.Log("idReq has ID:", idReq.ID) + if err := controller.Get(req, idReq, reply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} + +func TestSongController_Update_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSongController(logger, store) + key := "3c1065c8-0b14-11e7-96eb-5b134621c411" + entity := ds.Songs[key] + + reply := &cntl.Song{} + + if err := controller.Update(req, entity, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestSongController_Update_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSongController(logger, store) + key := "3c1065c8-0b14-11e7-96eb-5b134621c411" + entity := ds.Songs[key] + + createReply := &cntl.Song{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &cntl.Song{} + if err := controller.Update(req, entity, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if reply.ID != key { + t.Errorf("Expected reply to have id %s, but has %s", key, reply.ID) + } +} +func TestSongController_Delete_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSongController(logger, store) + key := "3c1065c8-0b14-11e7-96eb-5b134621c411" + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != api.ErrNotExists { + t.Errorf("expected to get api.ErrNotExists, but got %v", err) + } +} + +func TestSongController_Delete_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + controller := NewSongController(logger, store) + key := "3c1065c8-0b14-11e7-96eb-5b134621c411" + entity := ds.Songs[key] + + createReply := &cntl.Song{} + if err := controller.Create(req, entity, createReply); err != nil { + t.Errorf("failed to call apiController: %v", err) + } + + if createReply.ID != key { + t.Errorf("Expected createReply to have id %s, but has %s", key, createReply.ID) + } + + reply := &api.SuccessResponse{} + idReq := &api.IDBody{ID: key} + if err := controller.Delete(req, idReq, reply); err != nil { + t.Errorf("expected to get no error, but got %v", err) + } + + if !reply.Success { + t.Error("Expected to get result true, but got false") + } +} diff --git a/pkg/api/datastore/types_test.go b/pkg/api/datastore/types_test.go new file mode 100644 index 0000000..197e4b2 --- /dev/null +++ b/pkg/api/datastore/types_test.go @@ -0,0 +1,31 @@ +package datastore + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/disk" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" + "github.com/sirupsen/logrus" +) + +var ( + logger *logrus.Entry + path string + store api.Storage + ds = fixtures.DataStore() + req = httptest.NewRequest(http.MethodPost, api.RPCPath, nil) +) + +func init() { + var err error + logger = logrus.New().WithFields(logrus.Fields{}) + path, err = ioutil.TempDir("", "api_test") + if err != nil { + panic(err) + } + + store = disk.New(path) +} diff --git a/pkg/api/playback/playback.go b/pkg/api/playback/playback.go new file mode 100644 index 0000000..830f0a4 --- /dev/null +++ b/pkg/api/playback/playback.go @@ -0,0 +1,87 @@ +package playback + +import ( + "errors" + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/cntl/playback" + "github.com/StageAutoControl/controller/pkg/process" +) + +var ( + errSongSetListNeedToBeDistinct = errors.New("either a SetList ID or a Song ID must be given, neither both nor none") +) + +// Controller handles management of playback processes +type Controller struct { + pm process.Manager +} + +// NewController returns a new playback controller instance +func NewController(pm process.Manager) *Controller { + return &Controller{ + pm: pm, + } +} + +// Status Response of the playback process +type Status struct { + Process process.Status `json:"process"` + Params playback.Params `json:"params"` +} + +// Start a playback with either a song or a setlist +func (c *Controller) Start(r *http.Request, req *playback.Params, res *Status) error { + if (req.Song.ID != "" && req.SetList.ID != "") || (req.Song.ID == "" && req.SetList.ID == "") { + return errSongSetListNeedToBeDistinct + } + + p, _, err := c.pm.GetProcess(playback.ProcessName) + if err != nil { + return fmt.Errorf("failed to fetch playback process: %v", err) + } + p.(*playback.Process).SetParams(*req) + + s, err := c.pm.Start(playback.ProcessName) + if err != nil { + return fmt.Errorf("failed to start playback: %v", err) + } + + res.Process = *s + res.Params = *req + + return nil +} + +// Stop a playback +func (c *Controller) Stop(r *http.Request, req *api.IDBody, res *Status) error { + p, _, err := c.pm.GetProcess(playback.ProcessName) + if err != nil { + return fmt.Errorf("failed to get playback status: %v", err) + } + + s, err := c.pm.Stop(playback.ProcessName) + if err != nil { + return fmt.Errorf("failed to stop playback: %v", err) + } + + res.Process = *s + res.Params = p.(*playback.Process).GetParams() + + return err +} + +// Status returns the current status of a playback +func (c *Controller) Status(r *http.Request, req *api.IDBody, res *Status) error { + p, s, err := c.pm.GetProcess(playback.ProcessName) + if err != nil { + return fmt.Errorf("failed to get playback status: %v", err) + } + + res.Process = *s + res.Params = p.(*playback.Process).GetParams() + + return nil +} diff --git a/pkg/api/playground/dmx_playground_controller.go b/pkg/api/playground/dmx_playground_controller.go new file mode 100644 index 0000000..4cbdb6e --- /dev/null +++ b/pkg/api/playground/dmx_playground_controller.go @@ -0,0 +1,135 @@ +package playground + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/artnet" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/cntl/dmx" + "github.com/StageAutoControl/controller/pkg/cntl/playback" + "github.com/StageAutoControl/controller/pkg/internal/logging" +) + +var ( + errControllerDisabled = errors.New("the ArtNet controller is not set, most likely it is disabled in your current instance") +) + +// DMXPlaygroundController to play around and test DMX settings +type DMXPlaygroundController struct { + logger logging.Logger + controller artnet.Controller + loader api.Loader +} + +// NewDMXPlaygroundController returns a new DMXPlaygroundController instance +func NewDMXPlaygroundController(logger logging.Logger, controller artnet.Controller, loader api.Loader) *DMXPlaygroundController { + return &DMXPlaygroundController{ + loader: loader, + logger: logger, + controller: controller, + } +} + +// SetChannelValue sets a single artnet/dmx value +func (c *DMXPlaygroundController) SetChannelValue(r *http.Request, value *artnet.ChannelValue, response *api.Empty) error { + if c.controller == nil { + return errControllerDisabled + } + + c.controller.SetDMXChannelValue(*value) + return nil +} + +// SetChannelValues sets multiple artnet/dmx values +func (c *DMXPlaygroundController) SetChannelValues(r *http.Request, values *[]artnet.ChannelValue, response *api.Empty) error { + if c.controller == nil { + return errControllerDisabled + } + + c.controller.SetDMXChannelValues(*values) + return nil +} + +// PlayOnceRequest is a request body to play a single entity (Scene, Preset) once +type PlayOnceRequest struct { + api.IDBody + cntl.BarParams +} + +func (c *DMXPlaygroundController) defaultBarParams(bp *cntl.BarParams) { + if bp.Speed == 0 { + bp.Speed = 140 + } + + if bp.NoteCount == 0 { + bp.NoteCount = 4 + } + + if bp.NoteValue == 0 { + bp.NoteValue = 4 + } +} + +// PlayScene plays the given Scene once +func (c *DMXPlaygroundController) PlayScene(r *http.Request, req *PlayOnceRequest, response *api.Empty) error { + if c.controller == nil { + return nil + } + + ds, err := c.loader.Load() + if err != nil { + return err + } + + scene, ok := ds.DMXScenes[req.ID] + if !ok { + return fmt.Errorf("failed to find scene with id %s", req.ID) + } + + dmxCommands, err := dmx.RenderScene(ds, scene) + if err != nil { + return fmt.Errorf("failed to render scene %s: %v", req.ID, err) + } + + c.defaultBarParams(&req.BarParams) + commands := playback.ToPlayable(req.BarParams, dmxCommands) + if err := playback.Play(context.Background(), c.logger, []playback.TransportWriter{c.controller}, commands); err != nil { + return fmt.Errorf("failed to start playback: %v", err) + } + + return nil +} + +// PlayPreset plays the given Preset once +func (c *DMXPlaygroundController) PlayPreset(r *http.Request, req *PlayOnceRequest, response *api.Empty) error { + if c.controller == nil { + return nil + } + + ds, err := c.loader.Load() + if err != nil { + return err + } + + preset, ok := ds.DMXPresets[req.ID] + if !ok { + return fmt.Errorf("failed to find preset with id %s", req.ID) + } + + dmxCommands, err := dmx.RenderPreset(ds, preset) + if err != nil { + return fmt.Errorf("failed to render preset %s: %v", req.ID, err) + } + + c.defaultBarParams(&req.BarParams) + commands := playback.ToPlayable(req.BarParams, dmxCommands) + if err := playback.Play(context.Background(), c.logger, []playback.TransportWriter{c.controller}, commands); err != nil { + return fmt.Errorf("failed to start playback: %v", err) + } + + return nil +} diff --git a/pkg/api/server.go b/pkg/api/server.go deleted file mode 100644 index dfc78bb..0000000 --- a/pkg/api/server.go +++ /dev/null @@ -1,80 +0,0 @@ -package api - -import ( - "context" - "fmt" - "github.com/StageAutoControl/controller/pkg/cntl" - "github.com/gorilla/rpc" - "github.com/gorilla/rpc/json" - "github.com/sirupsen/logrus" - "net/http" - "reflect" -) - -type Server struct { - *rpc.Server - logger *logrus.Entry - storage storage -} - -func NewServer(logger *logrus.Entry, storage storage) (*Server, error) { - server := &Server{ - Server: rpc.NewServer(), - logger: logger, - storage: storage, - } - - if err := server.registerControllers(); err != nil { - return nil, err - } - - return server, nil -} - -func (s *Server) registerControllers() error { - types := []interface{}{ - &cntl.DMXDevice{}, &cntl.DMXDeviceGroup{}, &cntl.DMXDeviceType{}, - } - - for _, t := range types { - name := reflect.TypeOf(t).Elem().Name() - err := s.RegisterService(newController(s.logger, s.storage, t), name) - if err != nil { - return fmt.Errorf("failed to register RPC controller for type %s: %v", name, err) - } - } - - return nil -} - -// Run runs the http server -func (s *Server) Run(ctx context.Context, endpoint string) error { - s.Server.RegisterCodec(json.NewCodec(), "application/json") - - r := http.NewServeMux() - r.Handle("/rpc", s.Server) - r.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { - if _, err := fmt.Fprint(rw, "OK"); err != nil { - s.logger.Errorf("failed to write content to response: %v", err) - } - }) - - httpServer := http.Server{ - Addr: endpoint, - Handler: r, - } - - go func() { - <-ctx.Done() - if err := httpServer.Shutdown(ctx); err != nil { - s.logger.Errorf("failed to shutdown http server: %v", err) - } - }() - - err := httpServer.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - return err - } - - return nil -} diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go new file mode 100644 index 0000000..ae6adbd --- /dev/null +++ b/pkg/api/server/server.go @@ -0,0 +1,128 @@ +package server + +import ( + "context" + "fmt" + "net/http" + _ "net/http/pprof" + + "github.com/gorilla/handlers" + "github.com/gorilla/rpc" + "github.com/gorilla/rpc/json" + "github.com/sirupsen/logrus" + + "github.com/StageAutoControl/controller/pkg/api" + "github.com/StageAutoControl/controller/pkg/api/datastore" + "github.com/StageAutoControl/controller/pkg/api/playback" + "github.com/StageAutoControl/controller/pkg/api/playground" + "github.com/StageAutoControl/controller/pkg/artnet" + "github.com/StageAutoControl/controller/pkg/process" + "github.com/StageAutoControl/controller/pkg/visualizer" +) + +// Server represents the controllers API server, aware of all the controllers +type Server struct { + *rpc.Server + logger *logrus.Entry + storage api.Storage + loader api.Loader + apiController map[string]interface{} + cntl artnet.Controller + pm process.Manager + visualizer *visualizer.Server +} + +// New returns a new Server instance +func New( + logger *logrus.Entry, + storage api.Storage, + loader api.Loader, + cntl artnet.Controller, + pm process.Manager, + visualizer *visualizer.Server, +) (*Server, error) { + + server := &Server{ + Server: rpc.NewServer(), + logger: logger, + storage: storage, + loader: loader, + cntl: cntl, + pm: pm, + visualizer: visualizer, + } + + if err := server.registerControllers(); err != nil { + return nil, err + } + + return server, nil +} + +func (s *Server) registerControllers() error { + s.apiController = map[string]interface{}{ + "DMXAnimation": datastore.NewDMXAnimationController(s.logger, s.storage), + "DMXDevice": datastore.NewDMXDeviceController(s.logger, s.storage), + "DMXDeviceGroup": datastore.NewDMXDeviceGroupController(s.logger, s.storage), + "DMXDeviceType": datastore.NewDMXDeviceTypeController(s.logger, s.storage), + "DMXPreset": datastore.NewDMXPresetController(s.logger, s.storage), + "DMXScene": datastore.NewDMXSceneController(s.logger, s.storage), + "DMXTransition": datastore.NewDMXTransitionController(s.logger, s.storage), + "DMXColorVariable": datastore.NewDMXColorVariableController(s.logger, s.storage), + "Song": datastore.NewSongController(s.logger, s.storage), + "SetList": datastore.NewSetListController(s.logger, s.storage), + "DMXPlayground": playground.NewDMXPlaygroundController(s.logger, s.cntl, s.loader), + "Playback": playback.NewController(s.pm), + } + + for name, cntl := range s.apiController { + if err := s.Server.RegisterService(cntl, name); err != nil { + return err + } + } + + return nil +} + +// Run runs the http server +func (s *Server) Run(ctx context.Context, endpoint string) error { + s.Server.RegisterCodec(json.NewCodec(), "application/json") + + r := http.DefaultServeMux + r.Handle(api.RPCPath, s.Server) + r.HandleFunc(api.VisualizerPath, s.visualizer.ServeRequest) + r.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + if _, err := fmt.Fprint(rw, "OK"); err != nil { + s.logger.Errorf("failed to write content to response: %v", err) + } + }) + + h := handlers.RecoveryHandler()(r) + h = handlers.CORS( + handlers.AllowCredentials(), + handlers.AllowedOrigins([]string{"*"}), + handlers.AllowedMethods([]string{"POST", "GET", "HEAD"}), + handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), + )(h) + + httpServer := http.Server{ + Addr: endpoint, + Handler: h, + } + + go func() { + <-ctx.Done() + if err := httpServer.Shutdown(ctx); err != nil { + s.logger.Errorf("failed to shutdown http server: %v", err) + } + }() + + s.logger.Infof("listening on %s", endpoint) + + err := httpServer.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + return err + } + + return nil +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 3ccbc94..42b91d1 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1,8 +1,47 @@ package api -type storage interface { +import ( + "errors" + + "github.com/StageAutoControl/controller/pkg/cntl" +) + +var ( + // RPCPath to where the RPC server should listen on + RPCPath = "/api" + VisualizerPath = "/visualizer-socket" + + // ErrNoIDGiven is returned when the request did not contain a valid ID + ErrNoIDGiven = errors.New("no ID was given with request") + // ErrExists is returned when the entity which is tried to create already exists + ErrExists = errors.New("entity with given ID already exists") + // ErrNotExists is returned when the entity tried to manage dies not exist + ErrNotExists = errors.New("entity with given ID does not exist") +) + +// Storage interface for abstraction in api usage +type Storage interface { + Has(key string, kind interface{}) bool Write(key string, value interface{}) error Read(key string, value interface{}) error List(kind interface{}) []string Delete(key string, kind interface{}) error } + +// Loader interface for abstraction in api usage +type Loader interface { + Load() (*cntl.DataStore, error) +} + +// IDBody is a request object only storing an ID +type IDBody struct { + ID string `json:"id"` +} + +// SuccessResponse returns a simple bool to state weather the operation was successful +type SuccessResponse struct { + Success bool `json:"success"` +} + +// Empty is ... yah, an empty request :shrug: +type Empty struct{} diff --git a/pkg/artnet/controller.go b/pkg/artnet/controller.go new file mode 100644 index 0000000..b3713b2 --- /dev/null +++ b/pkg/artnet/controller.go @@ -0,0 +1,137 @@ +package artnet + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/jsimonetti/go-artnet" + "github.com/sirupsen/logrus" + + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/logging" +) + +// Controller is a transport for the ArtNet protocol (DMX over UDP/IP) +type controller struct { + logger logging.Logger + sender *artnet.Controller + state *State + sendTrigger chan UniverseStateMap + context context.Context +} + +// NewController returns a artnet Controller as an anonymous interface +func NewController(logger logging.Logger) (Controller, error) { + ip, err := FindArtNetIP() + if err != nil { + return nil, fmt.Errorf("failed to find the art-net IP: %v", err) + } + + if len(ip) == 0 { + return nil, errors.New("failed to find the art-net IP: No interface found") + } + + host, err := os.Hostname() + if err != nil { + return nil, fmt.Errorf("failed to resolve hostname: %v", err) + } + + host = strings.ToLower(strings.Split(host, ".")[0]) + logger.Infof("Using ArtNet IP %s and hostname %s", ip.String(), host) + + senderLogger := artnet.NewLogger(logger.(*logrus.Entry).WithField("module", "artnet")) + control := &controller{ + logger: logger, + sender: artnet.NewController(host, ip, senderLogger), // , artnet.MaxFPS(26)), + state: NewState(), + sendTrigger: make(chan UniverseStateMap, 100), + } + + return control, nil +} + +// Start the controller +func (c *controller) Start(ctx context.Context) error { + if err := c.sender.Start(); err != nil { + return fmt.Errorf("failed to start Controller: %v", err) + } + + c.context = ctx + go c.sendBackground() + go c.debugDevices() + + return nil +} + +// Stop the controller +func (c *controller) Stop() { + close(c.sendTrigger) + c.sender.Stop() +} + +func (c *controller) SetDMXChannelValue(value ChannelValue) { + c.state.SetChannel(value.Universe, value.Channel, value.Value) + c.triggerSend() +} + +func (c *controller) SetDMXChannelValues(values []ChannelValue) { + c.state.SetChannelValues(values) + c.triggerSend() +} + +// Write implements the playback.TransportWriter interface to compatibility :) +func (c *controller) Write(cmd cntl.Command) error { + values := make([]ChannelValue, len(cmd.DMXCommands)) + for i, dmxCmd := range cmd.DMXCommands { + values[i].Universe = uint16(dmxCmd.Universe) + values[i].Channel = uint16(dmxCmd.Channel) + values[i].Value = dmxCmd.Value.Uint8() + } + + c.SetDMXChannelValues(values) + + return nil +} + +func (c *controller) triggerSend() { + c.sendTrigger <- c.state.Get() +} + +func (c *controller) sendBackground() { + for data := range c.sendTrigger { + for u, dmx := range data { + c.sender.SendDMXToAddress(dmx.toByteSlice(), c.universeToAddress(u)) + } + } +} + +// universeToAddress converts a dmx universe to a artnet address +// Code stolen from https://play.golang.org/p/pdQPC5u7JX +func (c *controller) universeToAddress(universe uint16) artnet.Address { + v := make([]uint8, 2) + binary.BigEndian.PutUint16(v, universe) + + return artnet.Address{ + Net: v[0], + SubUni: v[1], + } +} + +func (c *controller) debugDevices() { + t := time.NewTicker(30 * time.Second) + for range t.C { + c.logger.Debugf("Currently %d devices are registered: %+s", len(c.sender.Nodes), ips(c.sender.Nodes)) + } +} + +func ips(nodes []*artnet.ControlledNode) (ips []string) { + for _, n := range nodes { + ips = append(ips, NodeToString(n)) + } + return +} diff --git a/pkg/cntl/transport/artnet/ip.go b/pkg/artnet/ip.go similarity index 81% rename from pkg/cntl/transport/artnet/ip.go rename to pkg/artnet/ip.go index 162bb32..516afda 100644 --- a/pkg/cntl/transport/artnet/ip.go +++ b/pkg/artnet/ip.go @@ -13,26 +13,24 @@ const ( // FindArtNetIP finds the matching interface with an IP address inside of the addressRange func FindArtNetIP() (net.IP, error) { - var ip net.IP - _, cidrnet, _ := net.ParseCIDR(addressRange) addrs, err := net.InterfaceAddrs() if err != nil { - return ip, fmt.Errorf("error getting ips: %s", err) + return nil, fmt.Errorf("error getting ips: %s", err) } for _, addr := range addrs { - ip = addr.(*net.IPNet).IP + ip := addr.(*net.IPNet).IP if strings.Contains(ip.String(), ":") { continue } if cidrnet.Contains(ip) { - break + return ip, nil } } - return ip, nil + return nil, nil } diff --git a/pkg/cntl/transport/artnet/net.go b/pkg/artnet/net.go similarity index 100% rename from pkg/cntl/transport/artnet/net.go rename to pkg/artnet/net.go diff --git a/pkg/cntl/transport/artnet/net_test.go b/pkg/artnet/net_test.go similarity index 100% rename from pkg/cntl/transport/artnet/net_test.go rename to pkg/artnet/net_test.go diff --git a/pkg/cntl/transport/artnet/node.go b/pkg/artnet/node.go similarity index 100% rename from pkg/cntl/transport/artnet/node.go rename to pkg/artnet/node.go diff --git a/pkg/artnet/state.go b/pkg/artnet/state.go new file mode 100644 index 0000000..65baa91 --- /dev/null +++ b/pkg/artnet/state.go @@ -0,0 +1,99 @@ +package artnet + +import ( + "sort" + "sync" +) + +// State stores the state of universes +type State struct { + data UniverseStateMap + m sync.RWMutex +} + +// NewState returns a new state instance +func NewState() *State { + return &State{ + data: UniverseStateMap{}, + m: sync.RWMutex{}, + } +} + +// NewStateFromData takes the given data and stores it into a freshly created store +func NewStateFromData(data map[uint16]Universe) *State { + return NewStateFromUniverseStateMap(UniverseStateMap(data)) +} + +// NewStateFromUniverseStateMap takes the given data and stores it into a freshly created store +func NewStateFromUniverseStateMap(data map[uint16]Universe) *State { + s := NewState() + for k, value := range data { + s.SetUniverse(k, value) + } + return s +} + +// SetChannel sets a given channel on a given universe on a given value. +func (s *State) SetChannel(u, c uint16, v uint8) { + dmx := s.GetUniverse(u) + dmx[c] = byte(v) + s.SetUniverse(u, dmx) +} + +func (s *State) SetChannelValue(value ChannelValue) { + s.SetChannel(value.Universe, value.Channel, value.Value) +} + +// SetChannelValues sets a range of ChannelValues for convenience +func (s *State) SetChannelValues(values []ChannelValue) { + for _, value := range values { + s.SetChannelValue(value) + } +} + +// SetUniverse sets a complete DMX universe data +func (s *State) SetUniverse(u uint16, dmx Universe) { + s.m.Lock() + s.data[u] = dmx + s.m.Unlock() +} + +// GetUniverse gets a complete DMX universe data +func (s *State) GetUniverse(u uint16) Universe { + s.m.RLock() + + dmx, ok := s.data[u] + if !ok { + dmx = Universe{} + } + + s.m.RUnlock() + return dmx +} + +// Get returns all of the current state +func (s *State) Get() UniverseStateMap { + s.m.RLock() + c := make(UniverseStateMap) + for k, v := range s.data { + c[k] = v + } + s.m.RUnlock() + return c +} + +// GetUniverses returns a slice of all available universe indexes +func (s *State) GetUniverses() []uint16 { + universes := make([]uint16, 0) + + s.m.RLock() + + for u := range s.data { + universes = append(universes, u) + } + + sort.Slice(universes, func(i, j int) bool { return universes[i] < universes[j] }) + s.m.RUnlock() + + return universes +} diff --git a/pkg/artnet/state_test.go b/pkg/artnet/state_test.go new file mode 100644 index 0000000..e7c2ebc --- /dev/null +++ b/pkg/artnet/state_test.go @@ -0,0 +1,48 @@ +package artnet + +import ( + "reflect" + "testing" +) + +func TestState_Set(t *testing.T) { + cases := []struct { + before, after *State + u, c uint16 + v uint8 + }{ + { + before: NewStateFromData(map[uint16]Universe{}), + after: NewStateFromData(map[uint16]Universe{12: {14: 16}}), + u: 12, + c: 14, + v: 16, + }, + { + before: NewStateFromData(map[uint16]Universe{12: {14: 16}}), + after: NewStateFromData(map[uint16]Universe{12: {14: 16}, 2: {4: 6}}), + u: 2, + c: 4, + v: 6, + }, + } + + for i, c := range cases { + c.before.SetChannel(c.u, c.c, c.v) + + bu := c.before.GetUniverses() + au := c.after.GetUniverses() + + if !reflect.DeepEqual(bu, au) { + t.Errorf("Expected to get universes %+v at case %v, got %+v", au, i, bu) + continue + } + + for _, u := range au { + if !reflect.DeepEqual(c.before.GetUniverse(u), c.after.GetUniverse(u)) { + t.Errorf("Expected to have state %+v at case %v, got %+v", c.after.GetUniverse(u), i, c.before.GetUniverse(u)) + } + } + + } +} diff --git a/pkg/artnet/types.go b/pkg/artnet/types.go new file mode 100644 index 0000000..a82fa46 --- /dev/null +++ b/pkg/artnet/types.go @@ -0,0 +1,42 @@ +package artnet + +import ( + "context" + + "github.com/jsimonetti/go-artnet" + + "github.com/StageAutoControl/controller/pkg/cntl" +) + +// Sender is an artnet controller abstraction of the base implementation of jsimonetti +type Sender interface { + SendDMXToAddress(dmx [512]byte, address artnet.Address) + Start() error + Stop() +} + +// ChannelValue defines an ArtNet Universe and the value of the DMX channel +type ChannelValue struct { + Universe uint16 + Channel uint16 + Value uint8 +} + +// Controller is a convenience interface to use within this application +type Controller interface { + Write(cntl.Command) error + SetDMXChannelValue(value ChannelValue) + SetDMXChannelValues(values []ChannelValue) + Start(ctx context.Context) error + Stop() +} + +// Universe wraps the 512 byte array for convenience +type Universe [512]byte + +func (u Universe) toByteSlice() [512]byte { + return [512]byte(u) +} + +// UniverseStateMap holds the state of all used universes +type UniverseStateMap map[uint16]Universe diff --git a/pkg/cntl/data_store.go b/pkg/cntl/data_store.go new file mode 100644 index 0000000..2be9932 --- /dev/null +++ b/pkg/cntl/data_store.go @@ -0,0 +1,31 @@ +package cntl + +// A DataStore holds the controllers data state during playback, or more specifically during the rendering of a song into DMX frames +type DataStore struct { + SetLists map[string]*SetList + Songs map[string]*Song + DMXScenes map[string]*DMXScene + DMXPresets map[string]*DMXPreset + DMXAnimations map[string]*DMXAnimation + DMXTransitions map[string]*DMXTransition + DMXDevices map[string]*DMXDevice + DMXDeviceTypes map[string]*DMXDeviceType + DMXDeviceGroups map[string]*DMXDeviceGroup + DMXColorVariables map[string]*DMXColorVariable +} + +// NewStore creates a new DataStore instance +func NewStore() *DataStore { + return &DataStore{ + SetLists: make(map[string]*SetList), + Songs: make(map[string]*Song), + DMXScenes: make(map[string]*DMXScene), + DMXPresets: make(map[string]*DMXPreset), + DMXAnimations: make(map[string]*DMXAnimation), + DMXTransitions: make(map[string]*DMXTransition), + DMXDevices: make(map[string]*DMXDevice), + DMXDeviceTypes: make(map[string]*DMXDeviceType), + DMXDeviceGroups: make(map[string]*DMXDeviceGroup), + DMXColorVariables: make(map[string]*DMXColorVariable), + } +} diff --git a/pkg/cntl/dmx/animation.go b/pkg/cntl/dmx/animation.go index 3d7d22a..ddb50db 100644 --- a/pkg/cntl/dmx/animation.go +++ b/pkg/cntl/dmx/animation.go @@ -2,12 +2,13 @@ package dmx import ( "fmt" + "github.com/StageAutoControl/controller/pkg/cntl" ) // RenderAnimation renders the given DMXAnimation to an array of DMXCommands to be sent to a DMX device func RenderAnimation(ds *cntl.DataStore, dd []*cntl.DMXDevice, a *cntl.DMXAnimation) ([]cntl.DMXCommands, error) { - cmds := make([]cntl.DMXCommands, a.Length) + cmds := make([]cntl.DMXCommands, maxFrame(a)+1) for _, f := range a.Frames { ps, err := RenderParams(ds, dd, f.Params) if err != nil { @@ -19,3 +20,14 @@ func RenderAnimation(ds *cntl.DataStore, dd []*cntl.DMXDevice, a *cntl.DMXAnimat return cmds, nil } + +func maxFrame(a *cntl.DMXAnimation) uint8 { + var max uint8 + for _, f := range a.Frames { + if f.At > max { + max = f.At + } + } + + return max +} diff --git a/pkg/cntl/dmx/animation_test.go b/pkg/cntl/dmx/animation_test.go index b4bccc4..275e2ee 100644 --- a/pkg/cntl/dmx/animation_test.go +++ b/pkg/cntl/dmx/animation_test.go @@ -1,9 +1,10 @@ package dmx import ( - "github.com/StageAutoControl/controller/pkg/cntl" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) diff --git a/pkg/cntl/dmx/const.go b/pkg/cntl/dmx/const.go index 41220cc..9207535 100644 --- a/pkg/cntl/dmx/const.go +++ b/pkg/cntl/dmx/const.go @@ -12,5 +12,8 @@ const ( ChannelMode ChannelDimmer ChannelTilt + ChannelTiltFine ChannelPan + ChannelPanFine + ChannelPanTiltSpeed ) diff --git a/pkg/cntl/dmx/device.go b/pkg/cntl/dmx/device.go index 26c5b70..351a2f5 100644 --- a/pkg/cntl/dmx/device.go +++ b/pkg/cntl/dmx/device.go @@ -2,83 +2,92 @@ package dmx import ( "fmt" + "github.com/StageAutoControl/controller/pkg/cntl" ) func getDeviceChannel(ds *cntl.DataStore, d *cntl.DMXDevice, c cntl.DMXChannel, led uint16) (cntl.DMXChannel, error) { dt, ok := ds.DMXDeviceTypes[d.TypeID] if !ok { - return cntl.DMXChannel(0), fmt.Errorf("given DeviceType %q on device %q is unknown", d.TypeID, d.ID) + return 0, fmt.Errorf("given DeviceType %q on device %q is unknown", d.TypeID, d.ID) } // can a param affect multiple LEDs? // Should I switch the scheme of params to have an // slice of LEDs and apply all values to that? ledLen := len(dt.LEDs) - if ledLen > 0 && int(led) >= ledLen { - return cntl.DMXChannel(0), fmt.Errorf("given device has insufficient biggest index of LEDs %d to handle the given LED index %d", ledLen-1, led) + if int(led) >= ledLen { + return 0, fmt.Errorf("given device has insufficient biggest index of LEDs %d to handle the given LED index %d", ledLen-1, led) } var channel cntl.DMXChannel switch c { case ChannelRed: - channel = getLED(dt, led).Red + channel = dt.LEDs[led].Red case ChannelGreen: - channel = getLED(dt, led).Green + channel = dt.LEDs[led].Green case ChannelBlue: - channel = getLED(dt, led).Blue + channel = dt.LEDs[led].Blue case ChannelWhite: - channel = getLED(dt, led).White + channel = dt.LEDs[led].White case ChannelStrobe: if !dt.StrobeEnabled { - return cntl.DMXChannel(0), ErrDeviceHasDisabledStrobeChannel + return 0, ErrDeviceHasDisabledStrobeChannel } channel = dt.StrobeChannel case ChannelMode: if !dt.ModeEnabled { - return cntl.DMXChannel(0), ErrDeviceHasDisabledModeChannel + return 0, ErrDeviceHasDisabledModeChannel } channel = dt.ModeChannel case ChannelDimmer: if !dt.DimmerEnabled { - return cntl.DMXChannel(0), ErrDeviceHasDisabledDimmerChannel + return 0, ErrDeviceHasDisabledDimmerChannel } channel = dt.DimmerChannel case ChannelTilt: if !dt.Moving { - return cntl.DMXChannel(0), ErrDeviceIsNotMoving + return 0, ErrDeviceIsNotMoving } channel = dt.TiltChannel + case ChannelTiltFine: + if !dt.Moving { + return 0, ErrDeviceIsNotMoving + } + channel = dt.TiltFineChannel + case ChannelPan: if !dt.Moving { - return cntl.DMXChannel(0), ErrDeviceIsNotMoving + return 0, ErrDeviceIsNotMoving } channel = dt.PanChannel - default: - return cntl.DMXChannel(0), fmt.Errorf("channel %q is unknown", c) - } - - return d.StartChannel + channel, nil -} + case ChannelPanFine: + if !dt.Moving { + return 0, ErrDeviceIsNotMoving + } + channel = dt.PanFineChannel -func getLED(dt *cntl.DMXDeviceType, led uint16) *cntl.LED { - for _, l := range dt.LEDs { - if l.Position == led { - return &l + case ChannelPanTiltSpeed: + if !dt.Moving { + return 0, ErrDeviceIsNotMoving } + channel = dt.PanTiltSpeedChannel + + default: + return 0, fmt.Errorf("channel %q is unknown", c) } - return nil + return d.StartChannel + channel, nil } // ResolveDeviceSelector returns all DMXDevices that match the given selector diff --git a/pkg/cntl/dmx/device_test.go b/pkg/cntl/dmx/device_test.go index 4592da7..0249915 100644 --- a/pkg/cntl/dmx/device_test.go +++ b/pkg/cntl/dmx/device_test.go @@ -2,9 +2,10 @@ package dmx import ( "errors" - "github.com/StageAutoControl/controller/pkg/cntl" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) diff --git a/pkg/cntl/dmx/easing.go b/pkg/cntl/dmx/easing.go index 2826d48..219c6ab 100644 --- a/pkg/cntl/dmx/easing.go +++ b/pkg/cntl/dmx/easing.go @@ -2,6 +2,7 @@ package dmx import ( "fmt" + "github.com/StageAutoControl/controller/pkg/cntl" "github.com/creasty/go-easing" diff --git a/pkg/cntl/dmx/errors.go b/pkg/cntl/dmx/errors.go index 8a0f862..1831427 100644 --- a/pkg/cntl/dmx/errors.go +++ b/pkg/cntl/dmx/errors.go @@ -9,10 +9,11 @@ var ( ErrDeviceHasDisabledDimmerChannel = errors.New("device has disabled Dimmer channel") ErrDeviceIsNotMoving = errors.New("device is not moving, cannot use tilt and pan") - ErrDeviceParamsDevicesInvalid = errors.New("DMXDeviceParams must have either a group or a device") - ErrDeviceParamsValuesInvalid = errors.New("DMXDeviceParams must not have more the one of [Animation, Transition, Params]") - ErrDeviceParamsNoDevices = errors.New("DMXDeviceParams matches no device") - ErrTransitionDeviceParamsMustMatchLED = errors.New("DMXTransition contains a param set where the LED is not the same") - ErrDeviceSelectorMustHaveTagsOrID = errors.New("DMXDeviceSelector must have either tags or an ID") - ErrDeviceSelectorCannotHaveTagsAndID = errors.New("DMXDeviceSelector cannot have tags and an ID") + ErrDeviceParamsDevicesInvalid = errors.New("DMXDeviceParams must have either a group or a device") + ErrDeviceParamsValuesInvalid = errors.New("DMXDeviceParams must not have more the one of [Animation, Transition, Params]") + ErrDeviceParamsNoDevices = errors.New("DMXDeviceParams matches no device") + ErrDeviceParamsColorVarMustBeExclusive = errors.New("DMXDeviceParams cannot have a $color var and one of [red, green, blue, white]") + ErrTransitionDeviceParamsMustMatchLED = errors.New("DMXTransition contains a param set where the LED is not the same") + ErrDeviceSelectorMustHaveTagsOrID = errors.New("DMXDeviceSelector must have either tags or an ID") + ErrDeviceSelectorCannotHaveTagsAndID = errors.New("DMXDeviceSelector cannot have tags and an ID") ) diff --git a/pkg/cntl/dmx/merge_test.go b/pkg/cntl/dmx/merge_test.go index 8cd794c..71dd419 100644 --- a/pkg/cntl/dmx/merge_test.go +++ b/pkg/cntl/dmx/merge_test.go @@ -1,9 +1,10 @@ package dmx import ( - "github.com/StageAutoControl/controller/pkg/cntl" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) diff --git a/pkg/cntl/dmx/params.go b/pkg/cntl/dmx/params.go index 884fbaa..215e572 100644 --- a/pkg/cntl/dmx/params.go +++ b/pkg/cntl/dmx/params.go @@ -3,6 +3,7 @@ package dmx import ( "errors" "fmt" + "github.com/StageAutoControl/controller/pkg/cntl" ) @@ -20,7 +21,7 @@ func checkDeviceParams(dp *cntl.DMXDeviceParams) error { } valuesSet := 0 - if dp.Params != nil { + if dp.Params != nil && len(dp.Params) > 0 { valuesSet++ } if dp.Animation != nil { @@ -45,9 +46,10 @@ func RenderDeviceParams(ds *cntl.DataStore, dp *cntl.DMXDeviceParams) ([]cntl.DM var dd []*cntl.DMXDevice if dp.Group != nil { - g, ok := ds.DMXDeviceGroups[dp.Group.ID] + + g, ok := ds.DMXDeviceGroups[*dp.Group] if !ok { - return []cntl.DMXCommands{}, fmt.Errorf("failed to find DMXDeviceGroup %q", dp.Group.ID) + return []cntl.DMXCommands{}, fmt.Errorf("failed to find DMXDeviceGroup %q", *dp.Group) } for _, sel := range g.Devices { @@ -61,12 +63,12 @@ func RenderDeviceParams(ds *cntl.DataStore, dp *cntl.DMXDeviceParams) ([]cntl.DM } if dp.Device != nil { - d, err := ResolveDeviceSelector(ds, dp.Device) - if err != nil { - return []cntl.DMXCommands{}, err + d, ok := ds.DMXDevices[*dp.Device] + if !ok { + return []cntl.DMXCommands{}, fmt.Errorf("failed to find DMXDevice %q", *dp.Device) } - dd = append(dd, d...) + dd = append(dd, d) } if len(dd) == 0 { @@ -74,18 +76,18 @@ func RenderDeviceParams(ds *cntl.DataStore, dp *cntl.DMXDeviceParams) ([]cntl.DM } if dp.Animation != nil { - a, ok := ds.DMXAnimations[dp.Animation.ID] + a, ok := ds.DMXAnimations[*dp.Animation] if !ok { - return []cntl.DMXCommands{}, fmt.Errorf("unable to find DMXAnimation %q", dp.Animation.ID) + return []cntl.DMXCommands{}, fmt.Errorf("failed to find DMXAnimation %q", *dp.Animation) } return RenderAnimation(ds, dd, a) } if dp.Transition != nil { - t, ok := ds.DMXTransitions[dp.Transition.ID] + t, ok := ds.DMXTransitions[*dp.Transition] if !ok { - return []cntl.DMXCommands{}, fmt.Errorf("unable to find DMXTransition %q", dp.Animation.ID) + return []cntl.DMXCommands{}, fmt.Errorf("failed to find DMXTransition %q", *dp.Animation) } return RenderTransition(ds, dd, t) @@ -113,6 +115,10 @@ func RenderDeviceParams(ds *cntl.DataStore, dp *cntl.DMXDeviceParams) ([]cntl.DM func RenderParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, p cntl.DMXParams) (cmds cntl.DMXCommands, err error) { var channels cntl.DMXCommands + if err := resolveColorVar(ds, &p); err != nil { + return cntl.DMXCommands{}, err + } + if p.Red != nil { channels = append(channels, cntl.DMXCommand{ Channel: ChannelRed, @@ -168,19 +174,75 @@ func RenderParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, p cntl.DMXParams) (c }) } + // for each device in the resolved selectors and each channel set the correct LED value for _, d := range dd { for _, c := range channels { - ch, err := getDeviceChannel(ds, d, c.Channel, p.LED) - if err != nil { - return cntl.DMXCommands{}, err + for _, led := range resolveLEDs(ds, p, d) { + ch, err := getDeviceChannel(ds, d, c.Channel, led) + if err != nil { + return cntl.DMXCommands{}, err + } + cmds = append(cmds, cntl.DMXCommand{ + Universe: d.Universe, + Channel: ch, + Value: c.Value, + }) } - cmds = append(cmds, cntl.DMXCommand{ - Universe: d.Universe, - Channel: ch, - Value: c.Value, - }) } } return } + +func resolveLEDs(ds *cntl.DataStore, p cntl.DMXParams, d *cntl.DMXDevice) []uint16 { + if !p.LEDAll { + return []uint16{p.LED} + } + + // in this place we assume that we already resolved the device type before ... + dt, ok := ds.DMXDeviceTypes[d.TypeID] + if !ok { + panic("Alex is an idiot, the comment is wrong. Thanks.") + } + + // so in order to iterate all LEDs we just returns a slice with every LED index, which in fact is + // the index of the slice ... wow :D + + leds := make([]uint16, len(dt.LEDs)) + for index := range dt.LEDs { + leds[index] = uint16(index) + } + return leds +} + +func resolveColorVar(ds *cntl.DataStore, p *cntl.DMXParams) error { + if p.ColorVar == nil || *p.ColorVar == "" { + return nil + } + + if p.Red != nil || p.Green != nil || p.Blue != nil || p.White != nil { + return ErrDeviceParamsColorVarMustBeExclusive + } + + colorVar := getColorVar(ds, *p.ColorVar) + if colorVar == nil { + return fmt.Errorf("failed to find color variable with the name %q", *p.ColorVar) + } + + p.Red = colorVar.Red + p.Green = colorVar.Green + p.Blue = colorVar.Blue + p.White = colorVar.White + + return nil +} + +func getColorVar(ds *cntl.DataStore, name string) *cntl.DMXColorVariable { + for _, c := range ds.DMXColorVariables { + if c.Name == name { + return c + } + } + + return nil +} diff --git a/pkg/cntl/dmx/params_test.go b/pkg/cntl/dmx/params_test.go index c433529..737c805 100644 --- a/pkg/cntl/dmx/params_test.go +++ b/pkg/cntl/dmx/params_test.go @@ -2,9 +2,10 @@ package dmx import ( "fmt" - "github.com/StageAutoControl/controller/pkg/cntl" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) @@ -15,34 +16,34 @@ func TestCheckDeviceParams(t *testing.T) { }{ { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "asdf"}, - Group: &cntl.DMXDeviceGroupSelector{ID: "asdf2"}, - Transition: &cntl.TransitionSelector{ID: "anim1"}, - Animation: &cntl.AnimationSelector{ID: "anim2"}, + Device: fixtures.StrPtr("asdf"), + Group: fixtures.StrPtr("asdf2"), + Transition: fixtures.StrPtr("anim1"), + Animation: fixtures.StrPtr("anim2"), }, expectedErr: ErrDeviceParamsDevicesInvalid, }, { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "asdf"}, - Group: &cntl.DMXDeviceGroupSelector{ID: "asdf2"}, - Transition: &cntl.TransitionSelector{ID: "anim1"}, - Animation: &cntl.AnimationSelector{ID: "anim2"}, + Device: fixtures.StrPtr("asdf"), + Group: fixtures.StrPtr("asdf2"), + Transition: fixtures.StrPtr("anim1"), + Animation: fixtures.StrPtr("anim2"), }, expectedErr: ErrDeviceParamsDevicesInvalid, }, { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "asdf"}, - Transition: &cntl.TransitionSelector{ID: "anim1"}, - Animation: &cntl.AnimationSelector{ID: "anim2"}, + Device: fixtures.StrPtr("asdf"), + Transition: fixtures.StrPtr("anim1"), + Animation: fixtures.StrPtr("anim2"), }, expectedErr: ErrDeviceParamsValuesInvalid, }, { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "asdf"}, - Transition: &cntl.TransitionSelector{ID: "anim1"}, + Device: fixtures.StrPtr("asdf"), + Transition: fixtures.StrPtr("anim1"), Params: []cntl.DMXParams{ {Blue: fixtures.Value255}, }, @@ -51,16 +52,16 @@ func TestCheckDeviceParams(t *testing.T) { }, { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "asdf"}, - Animation: &cntl.AnimationSelector{ID: "anim1"}, + Device: fixtures.StrPtr("asdf"), + Animation: fixtures.StrPtr("anim1"), Params: []cntl.DMXParams{ - {Blue: fixtures.Value255},}, + {Blue: fixtures.Value255}}, }, expectedErr: ErrDeviceParamsValuesInvalid, }, { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "asdf"}, + Device: fixtures.StrPtr("asdf"), Params: []cntl.DMXParams{ {Blue: fixtures.Value255}, }, @@ -69,21 +70,21 @@ func TestCheckDeviceParams(t *testing.T) { }, { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "asdf"}, - Animation: &cntl.AnimationSelector{ID: "anim1"}, + Device: fixtures.StrPtr("asdf"), + Animation: fixtures.StrPtr("anim1"), }, expectedErr: nil, }, { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "asdf"}, - Transition: &cntl.TransitionSelector{ID: "anim1"}, + Device: fixtures.StrPtr("asdf"), + Transition: fixtures.StrPtr("anim1"), }, expectedErr: nil, }, { dp: &cntl.DMXDeviceParams{ - Group: &cntl.DMXDeviceGroupSelector{ID: "asdf"}, + Group: fixtures.StrPtr("asdf"), Params: []cntl.DMXParams{ {Blue: fixtures.Value255}, }, @@ -92,15 +93,15 @@ func TestCheckDeviceParams(t *testing.T) { }, { dp: &cntl.DMXDeviceParams{ - Group: &cntl.DMXDeviceGroupSelector{ID: "asdf"}, - Animation: &cntl.AnimationSelector{ID: "anim1"}, + Group: fixtures.StrPtr("asdf"), + Animation: fixtures.StrPtr("anim1"), }, expectedErr: nil, }, { dp: &cntl.DMXDeviceParams{ - Group: &cntl.DMXDeviceGroupSelector{ID: "asdf"}, - Transition: &cntl.TransitionSelector{ID: "anim1"}, + Group: fixtures.StrPtr("asdf"), + Transition: fixtures.StrPtr("anim1"), }, expectedErr: nil, }, @@ -123,7 +124,7 @@ func TestRenderDeviceParams(t *testing.T) { }{ { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "5e0335e0-0b17-11e7-ad6c-63a7138d926c"}, + Device: fixtures.StrPtr("5e0335e0-0b17-11e7-ad6c-63a7138d926c"), Params: []cntl.DMXParams{ { Red: fixtures.Value255, @@ -144,7 +145,56 @@ func TestRenderDeviceParams(t *testing.T) { }, { dp: &cntl.DMXDeviceParams{ - Group: &cntl.DMXDeviceGroupSelector{ID: "475b71a0-0b16-11e7-9406-e3f678e8b788"}, + Device: fixtures.StrPtr("35cae00a-0b17-11e7-8bca-bbf30c56f20e"), + Params: []cntl.DMXParams{ + { + Red: fixtures.Value255, + LEDAll: true, + }, + }, + }, + c: []cntl.DMXCommands{ + { + {Universe: 1, Channel: 222, Value: *fixtures.Value255}, + {Universe: 1, Channel: 226, Value: *fixtures.Value255}, + {Universe: 1, Channel: 230, Value: *fixtures.Value255}, + {Universe: 1, Channel: 234, Value: *fixtures.Value255}, + {Universe: 1, Channel: 238, Value: *fixtures.Value255}, + {Universe: 1, Channel: 242, Value: *fixtures.Value255}, + {Universe: 1, Channel: 246, Value: *fixtures.Value255}, + {Universe: 1, Channel: 250, Value: *fixtures.Value255}, + {Universe: 1, Channel: 254, Value: *fixtures.Value255}, + {Universe: 1, Channel: 258, Value: *fixtures.Value255}, + {Universe: 1, Channel: 262, Value: *fixtures.Value255}, + {Universe: 1, Channel: 266, Value: *fixtures.Value255}, + {Universe: 1, Channel: 270, Value: *fixtures.Value255}, + {Universe: 1, Channel: 274, Value: *fixtures.Value255}, + {Universe: 1, Channel: 278, Value: *fixtures.Value255}, + {Universe: 1, Channel: 282, Value: *fixtures.Value255}, + }, + }, + err: nil, + }, + { + dp: &cntl.DMXDeviceParams{ + Device: fixtures.StrPtr("5e0335e0-0b17-11e7-ad6c-63a7138d926c"), + Params: []cntl.DMXParams{ + { + Red: fixtures.Value255, + LEDAll: true, + }, + }, + }, + c: []cntl.DMXCommands{ + { + {Universe: 2, Channel: 26, Value: *fixtures.Value255}, + }, + }, + err: nil, + }, + { + dp: &cntl.DMXDeviceParams{ + Group: fixtures.StrPtr("475b71a0-0b16-11e7-9406-e3f678e8b788"), Params: []cntl.DMXParams{ { Red: fixtures.Value255, @@ -168,7 +218,7 @@ func TestRenderDeviceParams(t *testing.T) { }, { dp: &cntl.DMXDeviceParams{ - Group: &cntl.DMXDeviceGroupSelector{ID: "cb58bc10-0b16-11e7-b45a-7bee591b0adb"}, + Group: fixtures.StrPtr("cb58bc10-0b16-11e7-b45a-7bee591b0adb"), Params: []cntl.DMXParams{ {Mode: fixtures.Value200}, }, @@ -182,8 +232,8 @@ func TestRenderDeviceParams(t *testing.T) { }, { dp: &cntl.DMXDeviceParams{ - Device: &cntl.DMXDeviceSelector{ID: "35cae00a-0b17-11e7-8bca-bbf30c56f20e"}, - Animation: &cntl.AnimationSelector{ID: "a51f7b2a-0e7b-11e7-bfc8-57da167865d7"}, + Device: fixtures.StrPtr("35cae00a-0b17-11e7-8bca-bbf30c56f20e"), + Animation: fixtures.StrPtr("a51f7b2a-0e7b-11e7-bfc8-57da167865d7"), }, c: []cntl.DMXCommands{ { @@ -206,21 +256,21 @@ func TestRenderDeviceParams(t *testing.T) { for i, e := range exp { c, err := RenderDeviceParams(ds, e.dp) if e.err != nil && (err == nil || err.Error() != e.err.Error()) { - t.Fatalf("Expected to get error %v, got %v at index %d", e.err, err, i) + t.Fatalf("Expected to get error %v, got %v at case %d", e.err, err, i) } if len(c) != len(e.c) { - t.Fatalf("Expected to get %d commands, got %d at index %d", len(e.c), len(c), i) + t.Fatalf("Expected to get %d commands, got %d at case %d", len(e.c), len(c), i) } for j := range e.c { if len(e.c[j]) != len(c[j]) { - t.Fatalf("Expected to get length %d at command index %d, got %d at index %d", len(e.c[j]), j, len(c[j]), i) + t.Fatalf("Expected to get length %d at command index %d, got %d at case %d", len(e.c[j]), j, len(c[j]), i) } for _, cmd := range e.c[j] { if !c[j].Contains(cmd) { - t.Errorf("Expected %+v to have %+v, but hasn't index %d", c[j], cmd, i) + t.Errorf("Expected %+v to have %+v, but hasn't at case %d", c[j], cmd, i) } } } @@ -247,6 +297,18 @@ func TestRenderParams(t *testing.T) { {Universe: 2, Channel: 14, Value: cntl.DMXValue{Value: 255}}, }, }, + { + ds: []*cntl.DMXDevice{ + ds.DMXDevices["4a545466-0b17-11e7-9c61-d3c0693099ab"], + }, + p: cntl.DMXParams{ + ColorVar: fixtures.StrPtr("Red255"), + }, + count: 1, + cmds: cntl.DMXCommands{ + {Universe: 2, Channel: 14, Value: cntl.DMXValue{Value: 255}}, + }, + }, { ds: []*cntl.DMXDevice{ ds.DMXDevices["4a545466-0b17-11e7-9c61-d3c0693099ab"], diff --git a/pkg/cntl/dmx/preset.go b/pkg/cntl/dmx/preset.go index 25f28f3..64885f4 100644 --- a/pkg/cntl/dmx/preset.go +++ b/pkg/cntl/dmx/preset.go @@ -2,6 +2,7 @@ package dmx import ( "fmt" + "github.com/StageAutoControl/controller/pkg/cntl" ) diff --git a/pkg/cntl/dmx/preset_test.go b/pkg/cntl/dmx/preset_test.go index 651a10d..d83573a 100644 --- a/pkg/cntl/dmx/preset_test.go +++ b/pkg/cntl/dmx/preset_test.go @@ -1,9 +1,10 @@ package dmx import ( - "github.com/StageAutoControl/controller/pkg/cntl" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) diff --git a/pkg/cntl/dmx/scene.go b/pkg/cntl/dmx/scene.go index 4f0498a..daf6be5 100644 --- a/pkg/cntl/dmx/scene.go +++ b/pkg/cntl/dmx/scene.go @@ -51,9 +51,9 @@ func RenderScene(ds *cntl.DataStore, sc *cntl.DMXScene) ([]cntl.DMXCommands, err } if ss.Preset != nil { - p, ok := ds.DMXPresets[ss.Preset.ID] + p, ok := ds.DMXPresets[*ss.Preset] if !ok { - return []cntl.DMXCommands{}, fmt.Errorf("cannot find DMXPreset %q", ss.Preset.ID) + return []cntl.DMXCommands{}, fmt.Errorf("cannot find DMXPreset %q", *ss.Preset) } pcs, err := RenderPreset(ds, p) diff --git a/pkg/cntl/dmx/scene_test.go b/pkg/cntl/dmx/scene_test.go index 0dac6f1..bd376ae 100644 --- a/pkg/cntl/dmx/scene_test.go +++ b/pkg/cntl/dmx/scene_test.go @@ -1,10 +1,11 @@ package dmx import ( - "github.com/StageAutoControl/controller/pkg/cntl" "reflect" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) diff --git a/pkg/cntl/dmx/transition.go b/pkg/cntl/dmx/transition.go index 9a34751..2c2667c 100644 --- a/pkg/cntl/dmx/transition.go +++ b/pkg/cntl/dmx/transition.go @@ -2,8 +2,9 @@ package dmx import ( "fmt" - "github.com/StageAutoControl/controller/pkg/cntl" "math" + + "github.com/StageAutoControl/controller/pkg/cntl" ) // RenderTransition renders the given DMXTransition to an array of DMXCommands to be sent to a DMX device @@ -34,6 +35,14 @@ func RenderTransitionParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, t *cntl.DM return []cntl.DMXCommands{}, err } + if err := resolveColorVar(ds, &p.From); err != nil { + return []cntl.DMXCommands{}, err + } + + if err := resolveColorVar(ds, &p.To); err != nil { + return []cntl.DMXCommands{}, err + } + if p.From.Red != nil && p.To.Red != nil && p.From.Red.Value != p.To.Red.Value { steps, err := calcTransitionSteps(p.From.Red.Value, p.To.Red.Value, t.Length, ease) if err != nil { @@ -41,7 +50,7 @@ func RenderTransitionParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, t *cntl.DM } for i, step := range steps { - stepParam := cntl.DMXParams{LED: p.From.LED} + stepParam := cntl.DMXParams{LED: p.From.LED, LEDAll: p.From.LEDAll} stepParam.Red = &cntl.DMXValue{Value: step} cmd, err := RenderParams(ds, dd, stepParam) @@ -60,7 +69,7 @@ func RenderTransitionParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, t *cntl.DM } for i, step := range steps { - stepParam := cntl.DMXParams{LED: p.From.LED} + stepParam := cntl.DMXParams{LED: p.From.LED, LEDAll: p.From.LEDAll} stepParam.Green = &cntl.DMXValue{Value: step} cmd, err := RenderParams(ds, dd, stepParam) @@ -79,7 +88,7 @@ func RenderTransitionParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, t *cntl.DM } for i, step := range steps { - stepParam := cntl.DMXParams{LED: p.From.LED} + stepParam := cntl.DMXParams{LED: p.From.LED, LEDAll: p.From.LEDAll} stepParam.Blue = &cntl.DMXValue{Value: step} cmd, err := RenderParams(ds, dd, stepParam) @@ -98,7 +107,7 @@ func RenderTransitionParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, t *cntl.DM } for i, step := range steps { - stepParam := cntl.DMXParams{LED: p.From.LED} + stepParam := cntl.DMXParams{LED: p.From.LED, LEDAll: p.From.LEDAll} stepParam.White = &cntl.DMXValue{Value: step} cmd, err := RenderParams(ds, dd, stepParam) @@ -117,7 +126,7 @@ func RenderTransitionParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, t *cntl.DM } for i, step := range steps { - stepParam := cntl.DMXParams{LED: p.From.LED} + stepParam := cntl.DMXParams{LED: p.From.LED, LEDAll: p.From.LEDAll} stepParam.Pan = &cntl.DMXValue{Value: step} cmd, err := RenderParams(ds, dd, stepParam) @@ -136,7 +145,7 @@ func RenderTransitionParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, t *cntl.DM } for i, step := range steps { - stepParam := cntl.DMXParams{LED: p.From.LED} + stepParam := cntl.DMXParams{LED: p.From.LED, LEDAll: p.From.LEDAll} stepParam.Tilt = &cntl.DMXValue{Value: step} cmd, err := RenderParams(ds, dd, stepParam) @@ -151,7 +160,7 @@ func RenderTransitionParams(ds *cntl.DataStore, dd []*cntl.DMXDevice, t *cntl.DM return result, nil } -func calcTransitionSteps(from, to, steps uint8, easingFunc easingFunc) ([]uint8, error) { +func calcTransitionSteps(from, to uint8, steps uint16, easingFunc easingFunc) ([]uint8, error) { result := make([]uint8, steps) diff := float64(to) - float64(from) floatFrom := float64(from) diff --git a/pkg/cntl/dmx/transition_test.go b/pkg/cntl/dmx/transition_test.go index d79f37b..0298122 100644 --- a/pkg/cntl/dmx/transition_test.go +++ b/pkg/cntl/dmx/transition_test.go @@ -1,10 +1,11 @@ package dmx import ( - "github.com/StageAutoControl/controller/pkg/cntl" "log" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) diff --git a/pkg/cntl/dmx/utils_test.go b/pkg/cntl/dmx/utils_test.go index 2a78b87..3bf44b7 100644 --- a/pkg/cntl/dmx/utils_test.go +++ b/pkg/cntl/dmx/utils_test.go @@ -1,9 +1,10 @@ package dmx import ( - "github.com/StageAutoControl/controller/pkg/cntl" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) diff --git a/pkg/cntl/loader.go b/pkg/cntl/loader.go deleted file mode 100644 index 9c8a269..0000000 --- a/pkg/cntl/loader.go +++ /dev/null @@ -1,29 +0,0 @@ -package cntl - -// A DataStore holds the applications data state -type DataStore struct { - SetLists map[string]*SetList - Songs map[string]*Song - DMXScenes map[string]*DMXScene - DMXPresets map[string]*DMXPreset - DMXAnimations map[string]*DMXAnimation - DMXTransitions map[string]*DMXTransition - DMXDevices map[string]*DMXDevice - DMXDeviceTypes map[string]*DMXDeviceType - DMXDeviceGroups map[string]*DMXDeviceGroup -} - -// NewStore creates a new DataStore instance -func NewStore() *DataStore { - return &DataStore{ - SetLists: make(map[string]*SetList), - Songs: make(map[string]*Song), - DMXScenes: make(map[string]*DMXScene), - DMXPresets: make(map[string]*DMXPreset), - DMXAnimations: make(map[string]*DMXAnimation), - DMXTransitions: make(map[string]*DMXTransition), - DMXDevices: make(map[string]*DMXDevice), - DMXDeviceTypes: make(map[string]*DMXDeviceType), - DMXDeviceGroups: make(map[string]*DMXDeviceGroup), - } -} diff --git a/pkg/cntl/midi/midi_test.go b/pkg/cntl/midi/midi_test.go index 1c0664f..d7708b4 100644 --- a/pkg/cntl/midi/midi_test.go +++ b/pkg/cntl/midi/midi_test.go @@ -1,8 +1,9 @@ package midi import ( - "github.com/StageAutoControl/controller/pkg/cntl" "testing" + + "github.com/StageAutoControl/controller/pkg/cntl" ) func TestStreamlineMidiCommands(t *testing.T) { diff --git a/pkg/cntl/playback/const.go b/pkg/cntl/playback/const.go index 4c5a966..49c3932 100644 --- a/pkg/cntl/playback/const.go +++ b/pkg/cntl/playback/const.go @@ -4,5 +4,35 @@ import "errors" // Player errors var ( - ErrCancelled = errors.New("playback cancelled") + ErrCancelled = errors.New("playback cancelled") + ErrNoSongIDOrSetListIDGiven = errors.New("no songID or setListID given") ) + +const ( + // ProcessName defines the name of the playback process + ProcessName = "playback" + paramsStorageKey = "playback_process" +) + +var defaultConfig = ` +{ + "waiters": { + "audio": { + "enabled": true, + "threshold": 0.7 + } + }, + "transportWriters": { + "artNet": { + "enabled": true + }, + "visualizer": { + "enabled": true + }, + "midi": { + "enabled": false, + "outputDeviceId": 0 + } + } +} +` diff --git a/pkg/cntl/playback/default_config.go b/pkg/cntl/playback/default_config.go new file mode 100644 index 0000000..bcb3bc9 --- /dev/null +++ b/pkg/cntl/playback/default_config.go @@ -0,0 +1,28 @@ +package playback + +import ( + "encoding/json" + "fmt" + + "github.com/StageAutoControl/controller/pkg/disk" +) + +// EnsureDefaultConfig ensures that the default configuration exists in given storage +func EnsureDefaultConfig(storage storage) error { + config := &Config{} + if err := storage.Read(paramsStorageKey, config); err != nil { + if err != disk.ErrNotExists { + return fmt.Errorf("failed to find playback config: %v", err) + } + + if err := json.Unmarshal([]byte(defaultConfig), config); err != nil { + return fmt.Errorf("failed to decode the default config: %v", err) + } + + if err := storage.Write(paramsStorageKey, config); err != nil { + return fmt.Errorf("failed to write the default config to storage: %v", err) + } + } + + return nil +} diff --git a/pkg/cntl/playback/player.go b/pkg/cntl/playback/player.go index 5d65ee5..ed7d8b4 100644 --- a/pkg/cntl/playback/player.go +++ b/pkg/cntl/playback/player.go @@ -2,32 +2,36 @@ package playback import ( "context" - "github.com/StageAutoControl/controller/pkg/cntl" - "time" - "fmt" + "time" + "github.com/StageAutoControl/controller/pkg/cntl" "github.com/StageAutoControl/controller/pkg/cntl/song" - "github.com/sirupsen/logrus" + "github.com/StageAutoControl/controller/pkg/internal/logging" ) // Player plays various things from a given data store, for example songs or a whole SetList. type Player struct { - logger *logrus.Entry + logger logging.Logger dataStore *cntl.DataStore writers []TransportWriter waiters []Waiter } // NewPlayer returns a new Player instance -func NewPlayer(logger *logrus.Entry, ds *cntl.DataStore, writers []TransportWriter, waiters []Waiter) *Player { - return &Player{logger, ds, writers, waiters} +func NewPlayer(logger logging.Logger, ds *cntl.DataStore, writers []TransportWriter, waiters []Waiter) *Player { + return &Player{ + logger: logger, + dataStore: ds, + writers: writers, + waiters: waiters, + } } func (p *Player) checkSetList(setList *cntl.SetList) error { - for _, songSel := range setList.Songs { - if _, ok := p.dataStore.Songs[songSel.ID]; !ok { - return fmt.Errorf("cannot find Song %q", songSel.ID) + for _, songID := range setList.Songs { + if _, ok := p.dataStore.Songs[songID]; !ok { + return fmt.Errorf("cannot find Process %q", songID) } } @@ -45,7 +49,7 @@ func (p *Player) PlaySetList(ctx context.Context, setListID string) error { return err } - for _, songSel := range setList.Songs { + for _, songID := range setList.Songs { select { case <-ctx.Done(): p.logger.Warn("Aborting") @@ -53,9 +57,7 @@ func (p *Player) PlaySetList(ctx context.Context, setListID string) error { default: } - p.logger.Infof("Playing song %s", songSel.ID) - - if err := p.PlaySong(ctx, songSel.ID); err != nil { + if err := p.PlaySong(ctx, songID); err != nil { return err } } @@ -64,17 +66,24 @@ func (p *Player) PlaySetList(ctx context.Context, setListID string) error { } func (p *Player) wait(ctx context.Context) error { + if len(p.waiters) == 0 { + return nil + } + chanLen := len(p.waiters) + 1 done := make(chan struct{}, chanLen) cancel := make(chan struct{}, chanLen) - err := make(chan error, chanLen) defer func() { cancel <- struct{}{} }() for _, w := range p.waiters { - go w.Wait(done, cancel, err) + go func(w Waiter) { + if err := w.Wait(done, cancel); err != nil { + p.logger.Error(err) + } + }(w) } select { @@ -82,33 +91,48 @@ func (p *Player) wait(ctx context.Context) error { return ErrCancelled case <-done: return nil - case err := <-err: - return err } } // PlaySong plays a full song func (p *Player) PlaySong(ctx context.Context, songID string) error { - cmds, err := song.Render(p.dataStore, songID) + commands, err := song.Render(p.dataStore, songID) if err != nil { return err } - p.logger.Infof("Waiting for waiters before playing song %s", songID) + s, ok := p.dataStore.Songs[songID] + if !ok { + return fmt.Errorf("failed to find song %v", songID) + } + + p.logger.Infof("Playing song %v", s.Name) + + p.logger.Infof("Waiting for waiters before playing song %v", s.Name) if err := p.wait(ctx); err != nil { return err } - l := len(cmds) - t := time.NewTicker(1 * time.Nanosecond) + p.logger.Infof("Playing song %v", s.Name) + return Play(ctx, p.logger, p.writers, commands) +} - p.logger.Infof("Playing song %s", songID) +// CalcRenderSpeed calculates the render speed of a BarChange to a time.Duration +func CalcRenderSpeed(bc *cntl.BarChange) time.Duration { + return time.Minute / time.Duration(bc.Speed*uint16(bc.NoteValue)/4) / time.Duration(cntl.RenderFrames/bc.NoteValue) +} + +// Play plays a given slice of commands and send it to the given writers +func Play(ctx context.Context, logger logging.Logger, writers []TransportWriter, commands []cntl.Command) error { + l := len(commands) + t := time.NewTicker(1 * time.Nanosecond) + done := ctx.Done() var i int var cmd cntl.Command for { select { - case <-ctx.Done(): + case <-done: return ErrCancelled case <-t.C: @@ -117,14 +141,18 @@ func (p *Player) PlaySong(ctx context.Context, songID string) error { return nil } - cmd = cmds[i] + cmd = commands[i] if cmd.BarChange != nil { t.Stop() t = time.NewTicker(CalcRenderSpeed(cmd.BarChange)) } - for _, w := range p.writers { - go w.Write(cmd) + for _, w := range writers { + go func(w TransportWriter) { + if err := w.Write(cmd); err != nil { + logger.Error(err) + } + }(w) } i++ @@ -132,7 +160,21 @@ func (p *Player) PlaySong(ctx context.Context, songID string) error { } } -// CalcRenderSpeed calculates the render speed of a BarChange to a time.Duration -func CalcRenderSpeed(bc *cntl.BarChange) time.Duration { - return time.Minute / time.Duration(bc.Speed*uint16(bc.NoteValue)/4) / time.Duration(cntl.RenderFrames/bc.NoteValue) +// ToPlayable takes a slice of DMXCommands and combines it with the given BarParams to a playable slice of Commands +func ToPlayable(bp cntl.BarParams, dmxCommands []cntl.DMXCommands) []cntl.Command { + commands := make([]cntl.Command, len(dmxCommands)) + for i, cmd := range dmxCommands { + commands[i] = cntl.Command{ + + DMXCommands: cmd, + MIDICommands: []cntl.MIDICommand{}, + } + } + + commands[0].BarChange = &cntl.BarChange{ + At: 0, + BarParams: bp, + } + + return commands } diff --git a/pkg/cntl/playback/player_test.go b/pkg/cntl/playback/player_test.go index 1ac96c0..6deaa46 100644 --- a/pkg/cntl/playback/player_test.go +++ b/pkg/cntl/playback/player_test.go @@ -1,9 +1,10 @@ package playback import ( - "github.com/StageAutoControl/controller/pkg/cntl" "testing" "time" + + "github.com/StageAutoControl/controller/pkg/cntl" ) func TestCalcRenderSpeed(t *testing.T) { @@ -11,13 +12,13 @@ func TestCalcRenderSpeed(t *testing.T) { bc cntl.BarChange speed time.Duration }{ - {cntl.BarChange{Speed: 120, NoteValue: 4}, time.Minute / 1920}, - {cntl.BarChange{Speed: 120, NoteValue: 8}, time.Minute / 1920}, - {cntl.BarChange{Speed: 120, NoteValue: 16}, time.Minute / 1920}, - {cntl.BarChange{Speed: 120, NoteValue: 32}, time.Minute / 1920}, + {cntl.BarChange{BarParams: cntl.BarParams{Speed: 120, NoteValue: 4}}, time.Minute / 1920}, + {cntl.BarChange{BarParams: cntl.BarParams{Speed: 120, NoteValue: 8}}, time.Minute / 1920}, + {cntl.BarChange{BarParams: cntl.BarParams{Speed: 120, NoteValue: 16}}, time.Minute / 1920}, + {cntl.BarChange{BarParams: cntl.BarParams{Speed: 120, NoteValue: 32}}, time.Minute / 1920}, - {cntl.BarChange{Speed: 60, NoteValue: 4}, time.Minute / 960}, - {cntl.BarChange{Speed: 60, NoteValue: 8}, time.Minute / 960}, + {cntl.BarChange{BarParams: cntl.BarParams{Speed: 60, NoteValue: 4}}, time.Minute / 960}, + {cntl.BarChange{BarParams: cntl.BarParams{Speed: 60, NoteValue: 8}}, time.Minute / 960}, } for i, e := range exp { diff --git a/pkg/cntl/playback/process.go b/pkg/cntl/playback/process.go new file mode 100644 index 0000000..4873e4e --- /dev/null +++ b/pkg/cntl/playback/process.go @@ -0,0 +1,135 @@ +package playback + +import ( + "context" + "fmt" + + "github.com/StageAutoControl/controller/pkg/artnet" + "github.com/StageAutoControl/controller/pkg/cntl/transport" + "github.com/StageAutoControl/controller/pkg/cntl/waiter" + "github.com/StageAutoControl/controller/pkg/internal/logging" + "github.com/StageAutoControl/controller/pkg/visualizer" +) + +// Process handles the playback of a single song +type Process struct { + logger logging.Logger + loader loader + storage storage + params Params + controller artnet.Controller + player *Player + cancel context.CancelFunc + visualizer *visualizer.Server +} + +// NewProcess returns a new playback process instance +func NewProcess(loader loader, storage storage, controller artnet.Controller, visualizer *visualizer.Server) *Process { + return &Process{ + loader: loader, + storage: storage, + controller: controller, + visualizer: visualizer, + } +} + +// SetParams tells the playback process whether to playback a song or set list and the corresponding ID +func (p *Process) SetParams(params Params) { + p.params = params +} + +// GetParams returns the params the process is currently running with +func (p *Process) GetParams() Params { + return p.params +} + +// SetLogger sets the logger for the process +func (p *Process) SetLogger(logger logging.Logger) { + p.logger = logger +} + +// Start the process, i.e. start the player with all the collected information +func (p *Process) Start(ctx context.Context) error { + ds, err := p.loader.Load() + if err != nil { + return fmt.Errorf("failed to load data from disk: %v", err) + } + + config := &Config{} + if err := p.storage.Read(paramsStorageKey, config); err != nil { + return fmt.Errorf("failed to find playback config: %v", err) + } + + cfg, err := p.parseConfig(config) + if err != nil { + return err + } + p.player = NewPlayer(p.logger, ds, cfg.writers, cfg.waiters) + ctx, p.cancel = context.WithCancel(ctx) + + if p.params.SetList.ID != "" { + if err := p.player.PlaySetList(ctx, p.params.SetList.ID); err != nil && err != ErrCancelled { + return fmt.Errorf("failed to start setlist playbaack: %v", err) + } + } else if p.params.Song.ID != "" { + if err := p.player.PlaySong(ctx, p.params.Song.ID); err != nil && err != ErrCancelled { + return fmt.Errorf("failed to start song playback: %v", err) + } + } else { + return ErrNoSongIDOrSetListIDGiven + } + + // return p.Stop() + // we don't need to explicitly stop the process when it's done as it's marked as blocking + return nil +} + +func (p *Process) parseConfig(config *Config) (*parsedConfig, error) { + cfg := &parsedConfig{ + waiters: []Waiter{}, + writers: []TransportWriter{}, + } + + if config.TransportWriters.ArtNet.Enabled { + aw, err := transport.NewArtNet(p.controller) + if err != nil { + return nil, fmt.Errorf("failed to create artnet transport writer: %v", err) + } + + cfg.writers = append(cfg.writers, aw) + } + + if config.TransportWriters.MIDI.Enabled { + mw, err := transport.NewMIDI(p.logger, config.TransportWriters.MIDI.OutputDeviceID) + if err != nil { + return nil, fmt.Errorf("failed to create midi transport writer: %v", err) + } + + cfg.writers = append(cfg.writers, mw) + } + + if config.TransportWriters.Visualizer.Enabled { + cfg.writers = append(cfg.writers, p.visualizer) + } + + if config.Waiters.Audio.Enabled { + cfg.waiters = append(cfg.waiters, waiter.NewAudio(p.logger, config.Waiters.Audio.Threshold)) + } + + return cfg, nil +} + +// Stop the process, i.e. cancel the playback context +func (p *Process) Stop() error { + if p.cancel != nil { + p.cancel() + } + p.player = nil + + return nil +} + +// Blocking returns true if calling Start() is a blocking operation and the process is stopped after start returned +func (p *Process) Blocking() bool { + return true +} diff --git a/pkg/cntl/playback/types.go b/pkg/cntl/playback/types.go index 6ead4d4..8668660 100644 --- a/pkg/cntl/playback/types.go +++ b/pkg/cntl/playback/types.go @@ -9,5 +9,54 @@ type TransportWriter interface { // Waiter waits for a trigger to happen type Waiter interface { - Wait(done chan struct{}, cancel chan struct{}, err chan error) error + Wait(done chan struct{}, cancel chan struct{}) error +} + +type storage interface { + Has(key string, kind interface{}) bool + Write(key string, value interface{}) error + Read(key string, value interface{}) error + List(kind interface{}) []string + Delete(key string, kind interface{}) error +} + +type loader interface { + Load() (*cntl.DataStore, error) +} + +// Params specifies how to run a playback +type Params struct { + Song struct { + ID string `json:"id"` + } `json:"song"` + SetList struct { + ID string `json:"id"` + } `json:"setList"` +} + +type parsedConfig struct { + waiters []Waiter + writers []TransportWriter +} + +// Config stores the information on which waiters and/or transport writers are enabled and what their config is +type Config struct { + Waiters struct { + Audio struct { + Enabled bool `json:"enabled"` + Threshold float32 `json:"threshold"` + } `json:"audio"` + } `json:"waiters"` + TransportWriters struct { + ArtNet struct { + Enabled bool `json:"enabled"` + } `json:"artNet"` + Visualizer struct { + Enabled bool `json:"enabled"` + } `json:"visualizer"` + MIDI struct { + Enabled bool `json:"enabled"` + OutputDeviceID int8 `json:"outputDeviceId"` + } `json:"midi"` + } `json:"transportWriters"` } diff --git a/pkg/cntl/song/const.go b/pkg/cntl/song/const.go new file mode 100644 index 0000000..10126ab --- /dev/null +++ b/pkg/cntl/song/const.go @@ -0,0 +1,8 @@ +package song + +import "errors" + +// Errors which can be thrown during validation +var ( + ErrSongMustHaveABarChangeAtFrame0 = errors.New("song needs to have a bar change at frame 0") +) diff --git a/pkg/cntl/song/helper.go b/pkg/cntl/song/helper.go index a54ce62..f68a95a 100644 --- a/pkg/cntl/song/helper.go +++ b/pkg/cntl/song/helper.go @@ -2,9 +2,9 @@ package song import ( "errors" - "github.com/StageAutoControl/controller/pkg/cntl" - "log" "reflect" + + "github.com/StageAutoControl/controller/pkg/cntl" ) // max returns the bigger of two given uint64 values @@ -48,15 +48,7 @@ func maxKey(search interface{}) uint64 { return biggest } -func makeCommand() cntl.Command { - return cntl.Command{ - MIDICommands: make([]cntl.MIDICommand, 0), - DMXCommands: make([]cntl.DMXCommand, 0), - } -} - func makeCommandArray(length uint64) []cntl.Command { - log.Print(length) cmds := make([]cntl.Command, length) for i := range cmds { @@ -66,7 +58,8 @@ func makeCommandArray(length uint64) []cntl.Command { return cmds } -func streamlineBarChanges(s *cntl.Song) map[uint64]cntl.BarChange { +// StreamlineBarChanges fills the bar changes of the given song into a map indexed by the frame the BC is at +func StreamlineBarChanges(s *cntl.Song) map[uint64]cntl.BarChange { bcs := make(map[uint64]cntl.BarChange) for _, bc := range s.BarChanges { bcs[bc.At] = bc @@ -75,6 +68,17 @@ func streamlineBarChanges(s *cntl.Song) map[uint64]cntl.BarChange { return bcs } +// ValidateBarChanges the given streamlined map of BarChanges +func ValidateBarChanges(bc map[uint64]cntl.BarChange) error { + if _, ok := bc[0]; !ok { + return ErrSongMustHaveABarChangeAtFrame0 + } + + // @TODO Add validation of bar change distance, so that one can't add a BC if the previous bar isn't finished yet + + return nil +} + // CalcBarLength calculates the length of a bar by given BarChange func CalcBarLength(bc *cntl.BarChange) uint64 { return uint64(bc.NoteCount) * CalcNoteLength(bc) diff --git a/pkg/cntl/song/helper_test.go b/pkg/cntl/song/helper_test.go index 947d0d4..7fa4ad7 100644 --- a/pkg/cntl/song/helper_test.go +++ b/pkg/cntl/song/helper_test.go @@ -1,8 +1,9 @@ package song import ( - "github.com/StageAutoControl/controller/pkg/cntl" "testing" + + "github.com/StageAutoControl/controller/pkg/cntl" ) func TestCalcBarLength(t *testing.T) { @@ -10,11 +11,11 @@ func TestCalcBarLength(t *testing.T) { bc cntl.BarChange length uint64 }{ - {cntl.BarChange{At: 0, NoteCount: 3, NoteValue: 4}, 48}, - {cntl.BarChange{At: 63, NoteCount: 12, NoteValue: 8}, 96}, - {cntl.BarChange{At: 10, NoteCount: 11, NoteValue: 4}, 176}, - {cntl.BarChange{At: 104, NoteCount: 4, NoteValue: 4}, 64}, - {cntl.BarChange{At: 5, NoteCount: 9, NoteValue: 8}, 72}, + {cntl.BarChange{At: 0, BarParams: cntl.BarParams{NoteCount: 3, NoteValue: 4}}, 48}, + {cntl.BarChange{At: 63, BarParams: cntl.BarParams{NoteCount: 12, NoteValue: 8}}, 96}, + {cntl.BarChange{At: 10, BarParams: cntl.BarParams{NoteCount: 11, NoteValue: 4}}, 176}, + {cntl.BarChange{At: 104, BarParams: cntl.BarParams{NoteCount: 4, NoteValue: 4}}, 64}, + {cntl.BarChange{At: 5, BarParams: cntl.BarParams{NoteCount: 9, NoteValue: 8}}, 72}, } for i, e := range exp { diff --git a/pkg/cntl/song/song.go b/pkg/cntl/song/song.go index 2b61839..926c5a0 100644 --- a/pkg/cntl/song/song.go +++ b/pkg/cntl/song/song.go @@ -2,6 +2,7 @@ package song import ( "fmt" + "github.com/StageAutoControl/controller/pkg/cntl" "github.com/StageAutoControl/controller/pkg/cntl/dmx" @@ -12,15 +13,19 @@ import ( func Render(ds *cntl.DataStore, songID string) ([]cntl.Command, error) { s, ok := ds.Songs[songID] if !ok { - return []cntl.Command{}, fmt.Errorf("cannot find Song %q", songID) + return nil, fmt.Errorf("cannot find Song %q", songID) } scs, err := dmx.StreamlineScenes(ds, s) if err != nil { - return []cntl.Command{}, err + return nil, err + } + + bcs := StreamlineBarChanges(s) + if err := ValidateBarChanges(bcs); err != nil { + return nil, fmt.Errorf("failed to validate bar changes: %v", err) } - bcs := streamlineBarChanges(s) mcs := midi.StreamlineMidiCommands(s) fb := &frameBrain{} @@ -43,7 +48,7 @@ func Render(ds *cntl.DataStore, songID string) ([]cntl.Command, error) { for _, sc := range scs { dcs, err := dmx.RenderScene(ds, sc) if err != nil { - return []cntl.Command{}, err + return nil, err } for j, dc := range dcs { diff --git a/pkg/cntl/song/song_test.go b/pkg/cntl/song/song_test.go index 06cce27..0051849 100644 --- a/pkg/cntl/song/song_test.go +++ b/pkg/cntl/song/song_test.go @@ -2,10 +2,11 @@ package song import ( "fmt" - "github.com/StageAutoControl/controller/pkg/cntl" "reflect" "testing" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" ) @@ -27,16 +28,16 @@ func TestStreamlineBarChanges(t *testing.T) { { s: ds.Songs["3c1065c8-0b14-11e7-96eb-5b134621c411"], m: map[uint64]cntl.BarChange{ - 0: {At: 0, NoteCount: 4, NoteValue: 4, Speed: 160}, - 512: {At: 512, NoteCount: 3, NoteValue: 4}, - 1184: {At: 1184, NoteCount: 7, NoteValue: 8}, - 1632: {At: 1632, NoteCount: 4, NoteValue: 4}, + 0: {At: 0, BarParams: cntl.BarParams{NoteCount: 4, NoteValue: 4, Speed: 160}}, + 512: {At: 512, BarParams: cntl.BarParams{NoteCount: 3, NoteValue: 4}}, + 1184: {At: 1184, BarParams: cntl.BarParams{NoteCount: 7, NoteValue: 8}}, + 1632: {At: 1632, BarParams: cntl.BarParams{NoteCount: 4, NoteValue: 4}}, }, }, } for i, e := range exp { - res := streamlineBarChanges(e.s) + res := StreamlineBarChanges(e.s) for k, v := range e.m { resv, ok := res[k] diff --git a/pkg/cntl/transport/artnet.go b/pkg/cntl/transport/artnet.go index b175819..2ba70a4 100644 --- a/pkg/cntl/transport/artnet.go +++ b/pkg/cntl/transport/artnet.go @@ -1,71 +1,34 @@ package transport import ( - "encoding/binary" - "errors" - "fmt" + art "github.com/StageAutoControl/controller/pkg/artnet" "github.com/StageAutoControl/controller/pkg/cntl" - "time" - - artnetTransport "github.com/StageAutoControl/controller/pkg/cntl/transport/artnet" - "github.com/jsimonetti/go-artnet" - "github.com/sirupsen/logrus" ) // ArtNet is a transport for the ArtNet protocol (DMX over UDP/IP) type ArtNet struct { - name string - c *artnet.Controller - state artnetTransport.State + controller art.Controller } // NewArtNet returns a new ArtNet transport instance -func NewArtNet(logger *logrus.Entry, name string) (*ArtNet, error) { - ip, err := artnetTransport.FindArtNetIP() - if err != nil { - return nil, fmt.Errorf("failed to find the art-net IP: %v", err) - } - - if len(ip) == 0 { - return nil, errors.New("failed to find the art-net IP: No interface found") - } - - c := artnet.NewController(name, ip, artnet.NewLogger(logger)) - if err := c.Start(); err != nil { - return nil, fmt.Errorf("failed to start controller: %v", err) - } - - logger.Info("Waiting 5 seconds for nodes to register") - time.Sleep(5 * time.Second) - +func NewArtNet(controller art.Controller) (*ArtNet, error) { return &ArtNet{ - name: name, - c: c, - state: artnetTransport.NewState(), + controller: controller, }, nil } func (a *ArtNet) Write(cmd cntl.Command) error { + values := make([]art.ChannelValue, 0) + for _, c := range cmd.DMXCommands { - a.state.Set(uint16(c.Universe), uint8(c.Channel), c.Value.Uint8()) + values = append(values, art.ChannelValue{ + Universe: uint16(c.Universe), + Channel: uint16(c.Channel), + Value: c.Value.Uint8(), + }) } - for u, dmx := range a.state { - a.c.SendDMXToAddress(dmx, UniverseToAddress(cntl.DMXUniverse(u))) - } + a.controller.SetDMXChannelValues(values) return nil } - -// UniverseToAddress converts a dmx universe to a artnet address -func UniverseToAddress(u cntl.DMXUniverse) artnet.Address { - // https://play.golang.org/p/pdQPC5u7JX - - v := make([]uint8, 2) - binary.BigEndian.PutUint16(v, uint16(u)) - - return artnet.Address{ - Net: v[0], - SubUni: v[1], - } -} diff --git a/pkg/cntl/transport/artnet/state.go b/pkg/cntl/transport/artnet/state.go deleted file mode 100644 index 1f0b791..0000000 --- a/pkg/cntl/transport/artnet/state.go +++ /dev/null @@ -1,45 +0,0 @@ -package artnet - -// State stores the state of universes -type State map[uint16][512]byte - -// NewState returns a new state instance -func NewState() State { - return make(State) -} - -// Set sets a given channel on a given universe on a given value. -func (state State) Set(u uint16, c, v uint8) { - state.addUniverse(u) - - dmx := state[u] - dmx[c] = byte(v) - state[u] = dmx -} - -func (state State) addUniverse(u uint16) { - if _, ok := state[u]; ok { - return - } - - state[u] = [512]byte{} -} - -// Equals compares two states for equality -func (state State) Equals(state2 State) bool { - if len(state) != len(state2) { - return false - } - - for u := range state { - if _, ok := state2[u]; !ok { - return false - } - - if state2[u] != state[u] { - return false - } - } - - return true -} diff --git a/pkg/cntl/transport/artnet/state_test.go b/pkg/cntl/transport/artnet/state_test.go deleted file mode 100644 index db3f6a9..0000000 --- a/pkg/cntl/transport/artnet/state_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package artnet - -import ( - "testing" -) - -func TestState_Set(t *testing.T) { - cases := []struct { - before, after State - u uint16 - c, v uint8 - }{ - { - before: State(map[uint16][512]byte{}), - after: State(map[uint16][512]byte{12: {14: 16}}), - u: 12, - c: 14, - v: 16, - }, - { - before: State(map[uint16][512]byte{12: {14: 16}}), - after: State(map[uint16][512]byte{12: {14: 16}, 2: {4: 6}}), - u: 2, - c: 4, - v: 6, - }, - } - - for i, c := range cases { - c.before.Set(c.u, c.c, c.v) - if !c.before.Equals(c.after) { - t.Errorf("Expected to have state %+v at case %v, got %+v", c.after, i, c.before) - } - } -} diff --git a/pkg/cntl/transport/bar_logger.go b/pkg/cntl/transport/bar_logger.go index c0e7fa0..3e08799 100644 --- a/pkg/cntl/transport/bar_logger.go +++ b/pkg/cntl/transport/bar_logger.go @@ -2,6 +2,7 @@ package transport import ( "fmt" + "github.com/StageAutoControl/controller/pkg/cntl" ) diff --git a/pkg/cntl/transport/midi.go b/pkg/cntl/transport/midi.go index 29bfc53..26c5c1c 100644 --- a/pkg/cntl/transport/midi.go +++ b/pkg/cntl/transport/midi.go @@ -1,42 +1,38 @@ package transport import ( - "fmt" + "errors" + "github.com/StageAutoControl/controller/pkg/cntl" - "strconv" + "github.com/StageAutoControl/controller/pkg/internal/logging" "github.com/rakyll/portmidi" - "github.com/sirupsen/logrus" ) // MIDI is a transport that sends MIDI signals using portmidi. type MIDI struct { - logger *logrus.Entry + logger logging.Logger deviceInfo *portmidi.DeviceInfo deviceID portmidi.DeviceID out *portmidi.Stream } // NewMIDI creates a new MIDI transport -func NewMIDI(logger *logrus.Entry, deviceID string) (*MIDI, error) { +func NewMIDI(logger logging.Logger, deviceID int8) (*MIDI, error) { if err := portmidi.Initialize(); err != nil { return nil, err } var d portmidi.DeviceID - if deviceID == "" { + if deviceID < 0 { d = portmidi.DefaultOutputDeviceID() } else { - i, err := strconv.Atoi(deviceID) - if err != nil { - return nil, fmt.Errorf("failed to transform deviceID %q to int: %v", deviceID, err) - } - d = portmidi.DeviceID(i) + d = portmidi.DeviceID(deviceID) } info := portmidi.Info(d) if info == nil { - logger.Fatal("Unable to read default output device") + return nil, errors.New("unable to read default output device") } out, err := portmidi.NewOutputStream(d, 10, 0) diff --git a/pkg/cntl/transport/stream.go b/pkg/cntl/transport/stream.go index 1ec0f84..4473d21 100644 --- a/pkg/cntl/transport/stream.go +++ b/pkg/cntl/transport/stream.go @@ -2,23 +2,23 @@ package transport import ( "fmt" - "github.com/StageAutoControl/controller/pkg/cntl" "io" - "strings" + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/logging" - "github.com/sirupsen/logrus" + "strings" ) // Stream is an output channel that can render to a buffer type Stream struct { - logger *logrus.Entry + logger logging.Logger w io.Writer i uint64 } // NewStream returns a new Stream instance -func NewStream(logger *logrus.Entry, w io.Writer) *Stream { +func NewStream(logger logging.Logger, w io.Writer) *Stream { fmt.Fprint(w, " Position [ BarChange ] [ Midi ] [ DMX ] \n") return &Stream{logger, w, 0} diff --git a/pkg/cntl/transport/visualizer.go b/pkg/cntl/transport/visualizer.go deleted file mode 100644 index 7870e50..0000000 --- a/pkg/cntl/transport/visualizer.go +++ /dev/null @@ -1,74 +0,0 @@ -package transport - -import ( - "encoding/json" - "fmt" - "github.com/StageAutoControl/controller/pkg/cntl" - "net" - "strings" - - "github.com/sirupsen/logrus" -) - -// Visualizer is a writer to the visualizer tool -type Visualizer struct { - logger *logrus.Entry - endpoint string - socket net.Conn -} - -// NewVisualizer creates a new Visualizer -func NewVisualizer(logger *logrus.Entry, endpoint string) (*Visualizer, error) { - socket, err := net.Dial("tcp", endpoint) - if err != nil { - return nil, err - } - - return &Visualizer{ - logger: logger, - socket: socket, - endpoint: endpoint, - }, nil -} - -// Write writes to the visualizer stream -func (t *Visualizer) Write(cmd cntl.Command) error { - b, err := json.Marshal(&cmd) - if err != nil { - return err - } - - // append delimiter byte - b = append(b, 0x0) - if n, err := t.socket.Write(b); err != nil { - return err - } else if n == 0 { - return fmt.Errorf("did not sent anything, sent %d bytes", n) - } - - go t.debug(cmd, b) - - return nil -} - -func (t *Visualizer) debug(cs cntl.Command, b []byte) { - t.logger.Debugf("Sent %d commands to visualizer: %v", len(cs.DMXCommands), renderDMXCommands(cs)) -} - -func renderDMXCommands(cmds cntl.Command) string { - s := make([]string, len(cmds.DMXCommands)) - - for i, c := range cmds.DMXCommands { - s[i] = fmt.Sprintf("%d:%d -> %d", c.Universe, c.Channel, c.Value) - } - - return fmt.Sprintf("%s --> %s", renderBarChange(cmds.BarChange), strings.Join(s, " | ")) -} - -func renderBarChange(bc *cntl.BarChange) string { - if bc == nil { - return strings.Repeat(" ", 20) - } - - return fmt.Sprintf("%19s", fmt.Sprintf("#%d %d/%d @%d bpm", bc.At, bc.NoteCount, bc.NoteValue, bc.Speed)) -} diff --git a/pkg/cntl/types.go b/pkg/cntl/types.go index a98a4a3..1e0ed0d 100644 --- a/pkg/cntl/types.go +++ b/pkg/cntl/types.go @@ -1,52 +1,40 @@ package cntl -// A Loader is responsible for loading the applications data. This could either be a remote or a local store. -type Loader interface { - Load() (*DataStore, error) -} - -// An Enhancer enhances the given datastore -type Enhancer interface { - Enhance(*DataStore) []error -} - -// SongSelector is a selector for a song -type SongSelector struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` -} - // SetList is a set of songs in a specific order type SetList struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` - Songs []SongSelector `json:"songs" yaml:"songs"` + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Songs []string `json:"songs" yaml:"songs"` } -// BarChange describes the changes of tempo and notes during a song -type BarChange struct { - At uint64 `json:"at" yaml:"at"` +// BarParams are a reusable informational struct on how fast and in what scheme something should be played +type BarParams struct { NoteValue uint8 `json:"noteValue" yaml:"noteValue"` NoteCount uint8 `json:"noteCount" yaml:"noteCount"` Speed uint16 `json:"speed" yaml:"speed"` } -// ScenePosition describes the position of a DMX scene within a song -type ScenePosition struct { +// BarChange describes the changes of tempo and notes during a song +type BarChange struct { + BarParams + At uint64 `json:"at" yaml:"at"` +} + +// DMXScenePosition describes the position of a DMX scene within a song +type DMXScenePosition struct { ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` At uint64 `json:"at" yaml:"at"` Repeat uint8 `json:"repeat" yaml:"repeat"` + Marker string `json:"marker" yaml:"marker"` } // Song is the whole container for everything that needs to be controlled during a song. type Song struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` - BarChanges []BarChange `json:"barChanges" yaml:"barChanges"` - DMXScenes []ScenePosition `json:"dmxScenes" yaml:"dmxScenes"` - DMXDeviceParams []DMXDeviceParams `json:"dmxDeviceParams" yaml:"dmxDeviceParams"` - MIDICommands []MIDICommand `json:"midiCommands" yaml:"midiCommands"` + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + BarChanges []BarChange `json:"barChanges" yaml:"barChanges"` + DMXScenes []DMXScenePosition `json:"dmxScenes" yaml:"dmxScenes"` + MIDICommands []MIDICommand `json:"midiCommands" yaml:"midiCommands"` } // Tag is a string literal tagging a DMX device @@ -64,45 +52,39 @@ type DMXDevice struct { // DMXDeviceType is the type of a DMXDevice type DMXDeviceType struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` - Key string `json:"key" yaml:"key"` - ChannelCount uint16 `json:"addressCount" yaml:"addressCount"` - ChannelsPerLED uint16 `json:"channelsPerLED" yaml:"channelsPerLED"` - StrobeEnabled bool `json:"strobeEnabled" yaml:"strobeEnabled"` - StrobeChannel DMXChannel `json:"strobeChannel" yaml:"strobeChannel"` - DimmerEnabled bool `json:"dimmerEnabled" yaml:"dimmerEnabled"` - DimmerChannel DMXChannel `json:"dimmerChannel" yaml:"dimmerChannel"` - ModeEnabled bool `json:"modeEnabled" yaml:"modeEnabled"` - ModeChannel DMXChannel `json:"modeChannel" yaml:"modeChannel"` - Moving bool `json:"moving" yaml:"moving"` - TiltChannel DMXChannel `json:"tiltChannel" yaml:"tiltChannel"` - PanChannel DMXChannel `json:"panChannel" yaml:"panChannel"` - LEDs []LED `json:"leds"` + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + ChannelCount uint16 `json:"channelCount" yaml:"channelCount"` + ChannelsPerLED uint16 `json:"channelsPerLED" yaml:"channelsPerLED"` + StrobeEnabled bool `json:"strobeEnabled" yaml:"strobeEnabled"` + StrobeChannel DMXChannel `json:"strobeChannel" yaml:"strobeChannel"` + DimmerEnabled bool `json:"dimmerEnabled" yaml:"dimmerEnabled"` + DimmerChannel DMXChannel `json:"dimmerChannel" yaml:"dimmerChannel"` + ModeEnabled bool `json:"modeEnabled" yaml:"modeEnabled"` + ModeChannel DMXChannel `json:"modeChannel" yaml:"modeChannel"` + Moving bool `json:"moving" yaml:"moving"` + PanChannel DMXChannel `json:"panChannel" yaml:"panChannel"` + PanFineChannel DMXChannel `json:"panFineChannel" yaml:"panFineChannel"` + TiltChannel DMXChannel `json:"tiltChannel" yaml:"tiltChannel"` + TiltFineChannel DMXChannel `json:"tiltFineChannel" yaml:"tiltFineChannel"` + PanTiltSpeedChannel DMXChannel `json:"panTiltSpeedChannel" yaml:"panTiltSpeedChannel"` + LEDs []LED `json:"leds"` } // LED maps a single LEDs DMX channels type LED struct { - Position uint16 `json:"position" yaml:"position"` - Red DMXChannel `json:"red" yaml:"red"` - Green DMXChannel `json:"green" yaml:"green"` - Blue DMXChannel `json:"blue" yaml:"blue"` - White DMXChannel `json:"white" yaml:"white"` + Red DMXChannel `json:"red" yaml:"red"` + Green DMXChannel `json:"green" yaml:"green"` + Blue DMXChannel `json:"blue" yaml:"blue"` + White DMXChannel `json:"white" yaml:"white"` } // DMXDeviceSelector is a selector for DMX devices type DMXDeviceSelector struct { ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` Tags []Tag `json:"tags" yaml:"tags"` } -// DMXDeviceGroupSelector is a selector for DMX device groups -type DMXDeviceGroupSelector struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` -} - // DMXDeviceGroup is a DMX device group type DMXDeviceGroup struct { ID string `json:"id" yaml:"id"` @@ -110,25 +92,13 @@ type DMXDeviceGroup struct { Devices []DMXDeviceSelector `json:"devices" yaml:"devices"` } -// AnimationSelector selects an animation -type AnimationSelector struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` -} - -// TransitionSelector selects a transition -type TransitionSelector struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` -} - // DMXDeviceParams is an object storing DMX parameters including the selection of either groups or devices type DMXDeviceParams struct { - Group *DMXDeviceGroupSelector `json:"group" yaml:"group"` - Device *DMXDeviceSelector `json:"device" yaml:"device"` - Params []DMXParams `json:"params" yaml:"params"` - Animation *AnimationSelector `json:"animation" yaml:"animation"` - Transition *TransitionSelector `json:"transition" yaml:"transition"` + Group *string `json:"group" yaml:"group"` + Device *string `json:"device" yaml:"device"` + Params []DMXParams `json:"params" yaml:"params"` + Animation *string `json:"animation" yaml:"animation"` + Transition *string `json:"transition" yaml:"transition"` } // DMXScene is a whole light scene @@ -140,38 +110,46 @@ type DMXScene struct { SubScenes []DMXSubScene `json:"subScenes" yaml:"subScenes"` } -// PresetSelector is a selector for a preset -type PresetSelector struct { - ID string `json:"id" yaml:"id"` - Name string `json:"name" yaml:"name"` -} - // DMXSubScene is a sub scene of a light scene type DMXSubScene struct { At []uint64 `json:"at" yaml:"at"` DeviceParams []DMXDeviceParams `json:"deviceParams" yaml:"deviceParams"` - Preset *PresetSelector `json:"preset" yaml:"preset"` + Preset *string `json:"preset" yaml:"preset"` +} + +// DMXColorVariable is a global variable for a DMX color +type DMXColorVariable struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Red *DMXValue `json:"red" yaml:"red"` + Green *DMXValue `json:"green" yaml:"green"` + Blue *DMXValue `json:"blue" yaml:"blue"` + White *DMXValue `json:"white" yaml:"white"` } // DMXParams is a DMX parameter object type DMXParams struct { - LED uint16 `json:"led" yaml:"led"` - Red *DMXValue `json:"red" yaml:"red"` - Green *DMXValue `json:"green" yaml:"green"` - Blue *DMXValue `json:"blue" yaml:"blue"` - White *DMXValue `json:"white" yaml:"white"` - Pan *DMXValue `json:"pan" yaml:"pan"` - Tilt *DMXValue `json:"tilt" yaml:"tilt"` - Strobe *DMXValue `json:"strobe" yaml:"strobe"` - Mode *DMXValue `json:"preset" yaml:"preset"` - Dimmer *DMXValue `json:"dimmer" yaml:"dimmer"` + LEDAll bool `json:"ledAll"` + LED uint16 `json:"led" yaml:"led"` + ColorVar *string `json:"$color" yaml:"$color"` + Red *DMXValue `json:"red" yaml:"red"` + Green *DMXValue `json:"green" yaml:"green"` + Blue *DMXValue `json:"blue" yaml:"blue"` + White *DMXValue `json:"white" yaml:"white"` + Pan *DMXValue `json:"pan" yaml:"pan"` + PanFine *DMXValue `json:"panFine" yaml:"panFine"` + Tilt *DMXValue `json:"tilt" yaml:"tilt"` + TiltFine *DMXValue `json:"tiltFine" yaml:"tiltFine"` + PanTiltSpeed *DMXValue `json:"panTiltSpeed" yaml:"panTiltSpeed"` + Strobe *DMXValue `json:"strobe" yaml:"strobe"` + Mode *DMXValue `json:"mode" yaml:"mode"` + Dimmer *DMXValue `json:"dimmer" yaml:"dimmer"` } // DMXAnimation is an animation of dmx params in relation to time type DMXAnimation struct { ID string `json:"id" yaml:"id"` Name string `json:"name" yaml:"name"` - Length uint8 `json:"length" yaml:"length"` Frames []DMXAnimationFrame `json:"frames" yaml:"frames"` } @@ -180,7 +158,7 @@ type DMXTransition struct { ID string `json:"id" yaml:"id"` Name string `json:"name" yaml:"name"` Ease EaseFunc `json:"ease" yaml:"ease"` - Length uint8 `json:"length" yaml:"length"` + Length uint16 `json:"length" yaml:"length"` Params []DMXTransitionParams `json:"params" yaml:"params"` } diff --git a/pkg/cntl/types_equals.go b/pkg/cntl/types_equals.go index 2f86e0b..4ff65b2 100644 --- a/pkg/cntl/types_equals.go +++ b/pkg/cntl/types_equals.go @@ -1,10 +1,5 @@ package cntl -// Equals returns whether the two given objects are equal -func (v1 SongSelector) Equals(v2 SongSelector) bool { - return v1.ID == v2.ID -} - // Equals returns whether the two given objects are equal func (v1 *SetList) Equals(v2 *SetList) bool { return v1.ID == v2.ID && @@ -21,7 +16,7 @@ func (v1 BarChange) Equals(v2 BarChange) bool { } // Equals returns whether the two given objects are equal -func (v1 ScenePosition) Equals(v2 ScenePosition) bool { +func (v1 DMXScenePosition) Equals(v2 DMXScenePosition) bool { return v1.At == v2.At && v1.ID == v2.ID && v1.Repeat == v2.Repeat @@ -32,8 +27,7 @@ func (v1 *Song) Equals(v2 *Song) bool { return v1.ID == v2.ID && v1.Name == v2.Name && barChangeList(v1.BarChanges).Equals(barChangeList(v2.BarChanges)) && - scenePositionList(v1.DMXScenes).Equals(scenePositionList(v2.DMXScenes)) && - dmxDeviceParamsList(v1.DMXDeviceParams).Equals(dmxDeviceParamsList(v2.DMXDeviceParams)) + scenePositionList(v1.DMXScenes).Equals(scenePositionList(v2.DMXScenes)) } // Equals returns whether the two given objects are equal @@ -55,7 +49,6 @@ func (v1 *DMXDevice) Equals(v2 *DMXDevice) bool { func (v1 *DMXDeviceType) Equals(v2 *DMXDeviceType) bool { return v1.ID == v2.ID && v1.Name == v2.Name && - v1.Key == v2.Key && v1.ChannelCount == v2.ChannelCount && v1.ChannelsPerLED == v2.ChannelsPerLED && v1.StrobeEnabled == v2.StrobeEnabled && @@ -70,8 +63,7 @@ func (v1 *DMXDeviceType) Equals(v2 *DMXDeviceType) bool { // Equals returns whether the two given objects are equal func (v1 LED) Equals(v2 LED) bool { - return v1.Position == v2.Position && - v1.Red == v2.Red && + return v1.Red == v2.Red && v1.Green == v2.Green && v1.Blue == v2.Blue && v1.White == v2.White @@ -83,11 +75,6 @@ func (v1 DMXDeviceSelector) Equals(v2 DMXDeviceSelector) bool { tagList(v1.Tags).Equals(tagList(v2.Tags)) } -// Equals returns whether the two given objects are equal -func (v1 DMXDeviceGroupSelector) Equals(v2 DMXDeviceGroupSelector) bool { - return v1.ID == v2.ID -} - // Equals returns whether the two given objects are equal func (v1 *DMXDeviceGroup) Equals(v2 *DMXDeviceGroup) bool { return v1.ID == v2.ID && @@ -141,8 +128,7 @@ func (v1 DMXParams) Equals(v2 DMXParams) bool { // Equals returns whether the two given objects are equal func (v1 DMXAnimation) Equals(v2 DMXAnimation) bool { - return v2.ID == v2.ID && - v1.Length == v2.Length && + return v1.ID == v2.ID && dmxAnimationFrameList(v1.Frames).Equals(dmxAnimationFrameList(v2.Frames)) } @@ -153,10 +139,10 @@ func (v1 DMXAnimationFrame) Equals(v2 DMXAnimationFrame) bool { } // Equals returns whether the two given objects are equal -func (d DMXPreset) Equals(v2 DMXPreset) bool { - return v2.ID == v2.ID && - d.Name == v2.Name && - dmxDeviceParamsList(d.DeviceParams).Equals(dmxDeviceParamsList(v2.DeviceParams)) +func (v1 DMXPreset) Equals(v2 DMXPreset) bool { + return v1.ID == v2.ID && + v1.Name == v2.Name && + dmxDeviceParamsList(v1.DeviceParams).Equals(dmxDeviceParamsList(v2.DeviceParams)) } // Contains returns whether given DMXCommand is in the called collection diff --git a/pkg/cntl/types_equals_lists.go b/pkg/cntl/types_equals_lists.go index ed8f0a4..94c3b6a 100644 --- a/pkg/cntl/types_equals_lists.go +++ b/pkg/cntl/types_equals_lists.go @@ -1,6 +1,6 @@ package cntl -type songSelectorList []SongSelector +type songSelectorList []string func (v1 songSelectorList) Equals(v2 songSelectorList) bool { if len(v1) != len(v2) { @@ -8,7 +8,7 @@ func (v1 songSelectorList) Equals(v2 songSelectorList) bool { } for i := range v1 { - if !v1[i].Equals(v2[i]) { + if v1[i] != v2[i] { return false } } @@ -32,7 +32,7 @@ func (v1 barChangeList) Equals(v2 barChangeList) bool { return true } -type scenePositionList []ScenePosition +type scenePositionList []DMXScenePosition func (v1 scenePositionList) Equals(v2 scenePositionList) bool { if len(v1) != len(v2) { diff --git a/pkg/cntl/waiter/audio.go b/pkg/cntl/waiter/audio.go index 6d233e8..186f95d 100644 --- a/pkg/cntl/waiter/audio.go +++ b/pkg/cntl/waiter/audio.go @@ -1,122 +1,126 @@ package waiter import ( + "fmt" + "github.com/gordonklaus/portaudio" - "github.com/sirupsen/logrus" + + "github.com/StageAutoControl/controller/pkg/internal/logging" ) // Audio is a waiter that does nothing type Audio struct { - logger *logrus.Entry + logger logging.Logger threshold float32 - fanOut []chan struct{} + notify chan struct{} buf []float32 stream *portaudio.Stream - stop chan struct{} + cancel chan struct{} err chan error } // NewAudio creates a new Audio waiter -func NewAudio(logger *logrus.Entry, threshold float32) (*Audio, error) { - portaudio.Initialize() +func NewAudio(logger logging.Logger, threshold float32) *Audio { + return &Audio{ + logger: logger, + threshold: threshold, + } +} + +func (a *Audio) start() (err error) { + a.notify = make(chan struct{}, 1) + a.buf = make([]float32, 64) + a.cancel = make(chan struct{}, 1) + a.err = make(chan error, 5) - buf := make([]float32, 64) - stream, err := portaudio.OpenDefaultStream(1, 0, sampleRate, len(buf), buf) + a.stream, err = portaudio.OpenDefaultStream(1, 0, sampleRate, len(a.buf), a.buf) if err != nil { - return nil, err + return fmt.Errorf("failed to open default portaudio stream: %v", err) } - a := &Audio{ - logger: logger, - threshold: threshold, - fanOut: make([]chan struct{}, 0), - buf: buf, - stream: stream, - stop: make(chan struct{}, 1), - err: make(chan error, 1), + if err := a.stream.Start(); err != nil { + return fmt.Errorf("failed to start portaudio stream: %v", err) } go a.readStream() - return a, nil + return nil } func (a *Audio) readStream() { - if err := a.stream.Start(); err != nil { - a.err <- err - return - } - - defer func() { - if err := a.stream.Stop(); err != nil { - a.err <- err - return - } - }() - for { err := a.stream.Read() if err != nil { - a.logger.Infof("Error reading audio stream: %s", err) + a.err <- err + a.logger.Infof("Error reading portaudio stream: %s", err) return } - a.checkForPeak() + if a.checkForPeak() { + return + } select { - case <-a.stop: + case <-a.cancel: return default: } } } -func (a *Audio) checkForPeak() { +func (a *Audio) checkForPeak() bool { for _, i := range a.buf { if i >= a.threshold || i <= (a.threshold*-1) { - a.notifyWait() - return + a.notify <- struct{}{} + return true } } -} -func (a *Audio) notifyWait() { - for _, c := range a.fanOut { - c <- struct{}{} - } + return false } -// Wait waits for a specific event to happen. In this case, nothing. -func (a *Audio) Wait(done chan struct{}, cancel chan struct{}, err chan error) error { - waitForPeak := make(chan struct{}, 1) - a.fanOut = append(a.fanOut, waitForPeak) - - // remove channel from fanout, we don't want to have further updates - defer func() { - a.fanOut = a.fanOut[:len(a.fanOut)-1] - }() +// Wait for a peak in the incoming audio stream +func (a *Audio) Wait(done chan struct{}, cancel chan struct{}) error { + if err := a.start(); err != nil { + return err + } +loop: for { select { - case <-waitForPeak: - a.logger.Info("Found peak. Starting playback!") + case <-a.notify: done <- struct{}{} - return nil + break loop case <-cancel: - return nil - case err := <-a.err: - return err + break loop + case e := <-a.err: + return e } - - return nil } + + return a.stop() } // Stop stops the audio stream -func (a *Audio) Stop() (err error) { - a.stop <- struct{}{} +func (a *Audio) stop() (err error) { + a.cancel <- struct{}{} + + if err := a.stream.Stop(); err != nil { + a.err <- err + a.logger.Errorf("failed to stop portaudio stream: %v", err) + // don't return the error, stream.Close has to be called + // return err + } + + if err := a.stream.Close(); err != nil { + a.err <- err + a.logger.Errorf("failed to close portaudio stream: %v", err) + return err + } - defer portaudio.Terminate() + close(a.notify) + close(a.cancel) + close(a.err) - return a.stream.Close() + return nil } diff --git a/pkg/cntl/waiter/const.go b/pkg/cntl/waiter/const.go index a9422a0..52c7c4f 100644 --- a/pkg/cntl/waiter/const.go +++ b/pkg/cntl/waiter/const.go @@ -6,5 +6,4 @@ const ( TypeAudio = "audio" sampleRate = 44100 - precision = 0.9 ) diff --git a/pkg/cntl/waiter/none.go b/pkg/cntl/waiter/none.go index f098fbd..df9d540 100644 --- a/pkg/cntl/waiter/none.go +++ b/pkg/cntl/waiter/none.go @@ -1,19 +1,19 @@ package waiter -import "github.com/sirupsen/logrus" +import "github.com/StageAutoControl/controller/pkg/internal/logging" // None is a waiter that does nothing type None struct { - logger *logrus.Entry + logger logging.Logger } // NewNone creates a new None waiter -func NewNone(logger *logrus.Entry) *None { +func NewNone(logger logging.Logger) *None { return &None{logger} } // Wait waits for a specific event to happen. In this case, nothing. -func (t *None) Wait(done chan struct{}, cancel chan struct{}, err chan error) error { +func (t *None) Wait(done chan struct{}, cancel chan struct{}) error { t.logger.Info("Not waiting") done <- struct{}{} return nil diff --git a/pkg/disk/errors.go b/pkg/disk/errors.go new file mode 100644 index 0000000..e90f855 --- /dev/null +++ b/pkg/disk/errors.go @@ -0,0 +1,8 @@ +package disk + +import "errors" + +// storage errors +var ( + ErrNotExists = errors.New("object does not exist in storage") +) diff --git a/pkg/disk/loader.go b/pkg/disk/loader.go new file mode 100644 index 0000000..837b89e --- /dev/null +++ b/pkg/disk/loader.go @@ -0,0 +1,122 @@ +package disk + +import "github.com/StageAutoControl/controller/pkg/cntl" + +// Loader loads the DataStore from the storage +type Loader struct { + storage *Storage +} + +// NewLoader returns a new Loader instance +func NewLoader(storage *Storage) *Loader { + return &Loader{ + storage: storage, + } +} + +// Load the data from the storage and return a populated data store +func (l *Loader) Load() (*cntl.DataStore, error) { + data := cntl.NewStore() + + for _, id := range l.storage.List(&cntl.SetList{}) { + setList := &cntl.SetList{} + err := l.storage.Read(id, setList) + if err != nil { + return nil, err + } + + data.SetLists[id] = setList + } + + for _, id := range l.storage.List(&cntl.Song{}) { + song := &cntl.Song{} + err := l.storage.Read(id, song) + if err != nil { + return nil, err + } + + data.Songs[id] = song + } + + for _, id := range l.storage.List(&cntl.DMXDevice{}) { + dmxDevice := &cntl.DMXDevice{} + err := l.storage.Read(id, dmxDevice) + if err != nil { + return nil, err + } + + data.DMXDevices[id] = dmxDevice + } + + for _, id := range l.storage.List(&cntl.DMXDeviceGroup{}) { + dmxDeviceGroup := &cntl.DMXDeviceGroup{} + err := l.storage.Read(id, dmxDeviceGroup) + if err != nil { + return nil, err + } + + data.DMXDeviceGroups[id] = dmxDeviceGroup + } + + for _, id := range l.storage.List(&cntl.DMXDeviceType{}) { + dmxDeviceType := &cntl.DMXDeviceType{} + err := l.storage.Read(id, dmxDeviceType) + if err != nil { + return nil, err + } + + data.DMXDeviceTypes[id] = dmxDeviceType + } + + for _, id := range l.storage.List(&cntl.DMXPreset{}) { + dmxPreset := &cntl.DMXPreset{} + err := l.storage.Read(id, dmxPreset) + if err != nil { + return nil, err + } + + data.DMXPresets[id] = dmxPreset + } + + for _, id := range l.storage.List(&cntl.DMXScene{}) { + dmxScene := &cntl.DMXScene{} + err := l.storage.Read(id, dmxScene) + if err != nil { + return nil, err + } + + data.DMXScenes[id] = dmxScene + } + + for _, id := range l.storage.List(&cntl.DMXAnimation{}) { + dmxAnimation := &cntl.DMXAnimation{} + err := l.storage.Read(id, dmxAnimation) + if err != nil { + return nil, err + } + + data.DMXAnimations[id] = dmxAnimation + } + + for _, id := range l.storage.List(&cntl.DMXTransition{}) { + dmxTransition := &cntl.DMXTransition{} + err := l.storage.Read(id, dmxTransition) + if err != nil { + return nil, err + } + + data.DMXTransitions[id] = dmxTransition + } + + for _, id := range l.storage.List(&cntl.DMXColorVariable{}) { + dmxColorVariable := &cntl.DMXColorVariable{} + err := l.storage.Read(id, dmxColorVariable) + if err != nil { + return nil, err + } + + data.DMXColorVariables[id] = dmxColorVariable + } + + return data, nil +} diff --git a/pkg/storage/storage.go b/pkg/disk/storage.go similarity index 78% rename from pkg/storage/storage.go rename to pkg/disk/storage.go index a5cdb3f..66b12b3 100644 --- a/pkg/storage/storage.go +++ b/pkg/disk/storage.go @@ -1,4 +1,4 @@ -package storage +package disk import ( "encoding/json" @@ -7,6 +7,8 @@ import ( "reflect" "strings" + "github.com/StageAutoControl/controller/pkg/internal/stringslice" + "github.com/peterbourgon/diskv" ) @@ -17,7 +19,7 @@ type Storage struct { func transform(s string) []string { parts := strings.Split(s, "_") - if len(parts) == 0 { + if len(parts) == 1 { return parts } @@ -39,6 +41,11 @@ func (s *Storage) buildFileName(key string, value interface{}) string { return fmt.Sprintf("%s_%s.json", s.getType(value), key) } +// Has returns weather the storage has the given entity or not +func (s *Storage) Has(key string, kind interface{}) bool { + return stringslice.Contains(key, s.List(kind)) +} + // Write a given value with the given fileName to disk func (s *Storage) Write(key string, value interface{}) error { b, err := json.Marshal(value) @@ -60,11 +67,15 @@ func (s *Storage) Read(key string, value interface{}) error { b, err := s.disk.Read(fileName) if err != nil { - return fmt.Errorf("failed to read value of type %s from disk: %v", err) + if os.IsNotExist(err) { + return ErrNotExists + } + + return fmt.Errorf("failed to read value of type %s from disk: %v", s.getType(value), err) } if err := json.Unmarshal(b, value); err != nil { - return fmt.Errorf("failed to unmarshal value of type %s: %v", err) + return fmt.Errorf("failed to unmarshal value of type %s: %v", s.getType(value), err) } return nil @@ -72,13 +83,13 @@ func (s *Storage) Read(key string, value interface{}) error { // List the keys of a given kind func (s *Storage) List(kind interface{}) []string { - fileName := s.buildFileName("", kind) - var keys []string - for key := range s.disk.Keys(nil) { + kindType := s.getType(kind) + + for key := range s.disk.KeysPrefix(kindType, nil) { // Remove the custom file schema from the name, which should only return the pure key key = strings.TrimSuffix(key, ".json") - key = strings.TrimPrefix(key, fileName) + key = strings.TrimPrefix(key, fmt.Sprintf("%s_", kindType)) keys = append(keys, key) } @@ -96,9 +107,10 @@ func (s *Storage) Delete(key string, kind interface{}) error { } func (s *Storage) getType(kind interface{}) string { - if t := reflect.TypeOf(kind); t.Kind() == reflect.Ptr { + t := reflect.TypeOf(kind) + if t.Kind() == reflect.Ptr { return t.Elem().Name() - } else { - return t.Name() } + + return t.Name() } diff --git a/pkg/storage/storage_test.go b/pkg/disk/storage_test.go similarity index 69% rename from pkg/storage/storage_test.go rename to pkg/disk/storage_test.go index 907da9e..d953211 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/disk/storage_test.go @@ -1,12 +1,15 @@ -package storage +package disk import ( - "github.com/StageAutoControl/controller/pkg/cntl" - "github.com/StageAutoControl/controller/pkg/internal/fixtures" "io/ioutil" "os" "path/filepath" "testing" + + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/fixtures" + "github.com/StageAutoControl/controller/pkg/internal/stringslice" + internalTesting "github.com/StageAutoControl/controller/pkg/internal/testing" ) var ( @@ -18,12 +21,6 @@ var ( expectedContent = "{\"id\":\"35cae00a-0b17-11e7-8bca-bbf30c56f20e\",\"name\":\"LED-Bar below drums front\",\"typeId\":\"1555d67e-1187-11e7-8135-9b41038b5b75\",\"universe\":1,\"startChannel\":222,\"tags\":[\"bar\",\"drums\"]}" ) -func cleanup(t *testing.T, path string) { - if err := os.RemoveAll(path); err != nil { - t.Errorf("failed to remove test storage dir: %v", err) - } -} - func TestStorage_buildFileName(t *testing.T) { storage := New(path) generated := storage.buildFileName(key, &cntl.DMXDevice{}) @@ -44,7 +41,7 @@ func TestStorage_getType(t *testing.T) { } func TestStorage_Write(t *testing.T) { - defer cleanup(t, path) + defer internalTesting.Cleanup(t, path) storage := New(path) err := storage.Write(key, device) @@ -70,7 +67,7 @@ func TestStorage_Write(t *testing.T) { } func TestStorage_Read(t *testing.T) { - defer cleanup(t, path) + defer internalTesting.Cleanup(t, path) storage := New(path) if err := os.MkdirAll(filepath.Dir(expectedFileName), 0755); err != nil { @@ -94,8 +91,44 @@ func TestStorage_Read(t *testing.T) { } } +func TestStorage_Has_Existing(t *testing.T) { + defer internalTesting.Cleanup(t, path) + storage := New(path) + + if err := os.MkdirAll(filepath.Dir(expectedFileName), 0755); err != nil { + t.Fatalf("failed to prepare disk directory path: %v", err) + } + + if err := ioutil.WriteFile(expectedFileName, []byte(expectedContent), 0755); err != nil { + t.Fatalf("failed to prepare disk file: %v", err) + } + + expDevice := &cntl.DMXDevice{} + has := storage.Has(key, expDevice) + if !has { + t.Errorf("expected storage to have id %q, but doesn't.", key) + return + } +} + +func TestStorage_Has_NotExisting(t *testing.T) { + defer internalTesting.Cleanup(t, path) + storage := New(path) + + if err := os.MkdirAll(filepath.Dir(expectedFileName), 0755); err != nil { + t.Fatalf("failed to prepare disk directory path: %v", err) + } + + expDevice := &cntl.DMXDevice{} + has := storage.Has(key, expDevice) + if has { + t.Errorf("expected storage to NOT have id %q, but does.", key) + return + } +} + func TestStorage_List(t *testing.T) { - defer cleanup(t, path) + defer internalTesting.Cleanup(t, path) storage := New(path) for k, dev := range ds.DMXDevices { @@ -109,10 +142,17 @@ func TestStorage_List(t *testing.T) { if len(keys) != len(ds.DMXDevices) { t.Errorf("expected to get %d keys, got %d keys", len(ds.DMXDevices), len(keys)) } + + for k := range ds.DMXDevices { + if !stringslice.Contains(k, keys) { + t.Errorf("Expected result list %s to have key %s", keys, k) + } + } + } func TestStorage_Delete(t *testing.T) { - defer cleanup(t, path) + defer internalTesting.Cleanup(t, path) storage := New(path) if err := os.MkdirAll(filepath.Dir(expectedFileName), 0755); err != nil { diff --git a/pkg/enhance/enhancer.go b/pkg/enhance/enhancer.go deleted file mode 100644 index 1dcda94..0000000 --- a/pkg/enhance/enhancer.go +++ /dev/null @@ -1,6 +0,0 @@ -package enhance - -import "github.com/StageAutoControl/controller/pkg/cntl" - -// Enhancers stores the globally registered enhancers -var Enhancers = make([]cntl.Enhancer, 0) diff --git a/pkg/enhance/name_to_id.go b/pkg/enhance/name_to_id.go deleted file mode 100644 index e91447c..0000000 --- a/pkg/enhance/name_to_id.go +++ /dev/null @@ -1,215 +0,0 @@ -package enhance - -import ( - "fmt" - - "github.com/StageAutoControl/controller/pkg/cntl" -) - -func init() { - Enhancers = append(Enhancers, &NameToIDEnhancer{}) -} - -// NameToIDEnhancer enhances the given data by resolving names to IDs -type NameToIDEnhancer struct{} - -// Enhance implements the cntl.Enhancer interface, executes the single entity enhancement methods -func (e *NameToIDEnhancer) Enhance(store *cntl.DataStore) []error { - errs := make([]error, 0) - - errs = append(errs, e.setLists(store)...) - errs = append(errs, e.songs(store)...) - errs = append(errs, e.scenes(store)...) - errs = append(errs, e.presets(store)...) - errs = append(errs, e.deviceGroups(store)...) - - return errs -} - -func (e *NameToIDEnhancer) setLists(store *cntl.DataStore) []error { - errs := make([]error, 0) - - for _, s := range store.SetLists { - for i := range s.Songs { - if s.Songs[i].Name != "" { - s.Songs[i].ID = e.findSong(s.Songs[i].Name, store.Songs) - if s.Songs[i].ID == "" { - errs = append(errs, fmt.Errorf("cannot find Song %q", s.Songs[i].Name)) - } - } - } - } - - return errs -} - -func (e *NameToIDEnhancer) songs(store *cntl.DataStore) []error { - errs := make([]error, 0) - - for _, s := range store.Songs { - for i := range s.DMXScenes { - if s.DMXScenes[i].Name != "" { - s.DMXScenes[i].ID = e.findScene(s.DMXScenes[i].Name, store.DMXScenes) - if s.DMXScenes[i].ID == "" { - errs = append(errs, fmt.Errorf("cannot find scene %q", s.DMXScenes[i].Name)) - } - } - } - } - - return errs -} - -func (e *NameToIDEnhancer) scenes(store *cntl.DataStore) []error { - errs := make([]error, 0) - - for _, s := range store.DMXScenes { - for i := range s.SubScenes { - for p := range s.SubScenes[i].DeviceParams { - errs = append(errs, e.nestedDeviceParams(&s.SubScenes[i].DeviceParams[p], store)...) - } - - if s.SubScenes[i].Preset != nil && s.SubScenes[i].Preset.Name != "" { - s.SubScenes[i].Preset.ID = e.findPreset(s.SubScenes[i].Preset.Name, store.DMXPresets) - if s.SubScenes[i].Preset.ID == "" { - errs = append(errs, fmt.Errorf("cannot find preset %q", s.SubScenes[i].Preset.Name)) - } - } - } - } - - return errs -} - -func (e *NameToIDEnhancer) presets(store *cntl.DataStore) []error { - errs := make([]error, 0) - - for _, s := range store.DMXPresets { - for i := range s.DeviceParams { - errs = append(errs, e.nestedDeviceParams(&s.DeviceParams[i], store)...) - } - } - - return errs -} - -func (e *NameToIDEnhancer) nestedDeviceParams(params *cntl.DMXDeviceParams, store *cntl.DataStore) []error { - errs := make([]error, 0) - - if params.Device != nil && params.Device.Name != "" { - params.Device.ID = e.findDevice(params.Device.Name, store.DMXDevices) - if params.Device.ID == "" { - errs = append(errs, fmt.Errorf("cannot find device %q", params.Device.Name)) - } - } - - if params.Group != nil && params.Group.ID == "" { - params.Group.ID = e.findDeviceGroup(params.Group.Name, store.DMXDeviceGroups) - if params.Group.ID == "" { - errs = append(errs, fmt.Errorf("cannot find device group %q", params.Group.Name)) - } - } - - if params.Animation != nil && params.Animation.ID == "" { - params.Animation.ID = e.findAnimation(params.Animation.Name, store.DMXAnimations) - if params.Animation.ID == "" { - errs = append(errs, fmt.Errorf("cannot find animation %q", params.Animation.Name)) - } - } - - if params.Transition != nil && params.Transition.ID == "" { - params.Transition.ID = e.findTransition(params.Transition.Name, store.DMXTransitions) - if params.Transition.ID == "" { - errs = append(errs, fmt.Errorf("cannot find transition %q", params.Transition.Name)) - } - } - - return errs -} - -func (e *NameToIDEnhancer) deviceGroups(store *cntl.DataStore) []error { - errs := make([]error, 0) - - for _, s := range store.DMXDeviceGroups { - for i := range s.Devices { - if s.Devices[i].Name != "" { - s.Devices[i].ID = e.findDevice(s.Devices[i].Name, store.DMXDevices) - if s.Devices[i].ID == "" { - errs = append(errs, fmt.Errorf("cannot find device %q", s.Devices[i].Name)) - } - } - } - } - - return errs -} - -func (e *NameToIDEnhancer) findSong(name string, values map[string]*cntl.Song) string { - for _, v := range values { - if v.Name == name { - return v.ID - } - } - - return "" -} - -func (e *NameToIDEnhancer) findScene(name string, values map[string]*cntl.DMXScene) string { - for _, v := range values { - if v.Name == name { - return v.ID - } - } - - return "" -} - -func (e *NameToIDEnhancer) findPreset(name string, values map[string]*cntl.DMXPreset) string { - for _, v := range values { - if v.Name == name { - return v.ID - } - } - - return "" -} - -func (e *NameToIDEnhancer) findDevice(name string, values map[string]*cntl.DMXDevice) string { - for _, v := range values { - if v.Name == name { - return v.ID - } - } - - return "" -} - -func (e *NameToIDEnhancer) findDeviceGroup(name string, values map[string]*cntl.DMXDeviceGroup) string { - for _, v := range values { - if v.Name == name { - return v.ID - } - } - - return "" -} - -func (e *NameToIDEnhancer) findAnimation(name string, values map[string]*cntl.DMXAnimation) string { - for _, v := range values { - if v.Name == name { - return v.ID - } - } - - return "" -} - -func (e *NameToIDEnhancer) findTransition(name string, values map[string]*cntl.DMXTransition) string { - for _, v := range values { - if v.Name == name { - return v.ID - } - } - - return "" -} diff --git a/pkg/internal/fixtures/fixtures.go b/pkg/internal/fixtures/fixtures.go index 9ae60a6..f0c2b76 100644 --- a/pkg/internal/fixtures/fixtures.go +++ b/pkg/internal/fixtures/fixtures.go @@ -6,21 +6,26 @@ import ( // fixture values var ( - Value0 = &cntl.DMXValue{0} - Value31 = &cntl.DMXValue{31} - Value63 = &cntl.DMXValue{63} - Value127 = &cntl.DMXValue{127} - Value200 = &cntl.DMXValue{200} - Value255 = &cntl.DMXValue{255} + Value0 = &cntl.DMXValue{Value: 0} + Value31 = &cntl.DMXValue{Value: 31} + Value63 = &cntl.DMXValue{Value: 63} + Value127 = &cntl.DMXValue{Value: 127} + Value200 = &cntl.DMXValue{Value: 200} + Value255 = &cntl.DMXValue{Value: 255} ) +// StrPtr returns a pointer to the given string +func StrPtr(str string) *string { + return &str +} + var data = &cntl.DataStore{ SetLists: map[string]*cntl.SetList{ "f5b4be8a-0b18-11e7-b837-4bac99d86956": { ID: "f5b4be8a-0b18-11e7-b837-4bac99d86956", Name: "Regular gig", - Songs: []cntl.SongSelector{ - {ID: "3c1065c8-0b14-11e7-96eb-5b134621c411"}, + Songs: []string{ + "3c1065c8-0b14-11e7-96eb-5b134621c411", }, }, }, @@ -29,12 +34,12 @@ var data = &cntl.DataStore{ ID: "3c1065c8-0b14-11e7-96eb-5b134621c411", Name: "Test song", BarChanges: []cntl.BarChange{ - {At: 0, NoteCount: 4, NoteValue: 4, Speed: 160}, - {At: 512, NoteCount: 3, NoteValue: 4}, - {At: 1184, NoteCount: 7, NoteValue: 8}, - {At: 1632, NoteCount: 4, NoteValue: 4}, + {At: 0, BarParams: cntl.BarParams{NoteCount: 4, NoteValue: 4, Speed: 160}}, + {At: 512, BarParams: cntl.BarParams{NoteCount: 3, NoteValue: 4}}, + {At: 1184, BarParams: cntl.BarParams{NoteCount: 7, NoteValue: 8}}, + {At: 1632, BarParams: cntl.BarParams{NoteCount: 4, NoteValue: 4}}, }, - DMXScenes: []cntl.ScenePosition{ + DMXScenes: []cntl.DMXScenePosition{ {At: 0, ID: "492cef2e-0b14-11e7-be89-c3fa25f9cabb", Repeat: 3}, {At: 512, ID: "a44f8dee-0b14-11e7-b5b9-bf1015384192", Repeat: 3}, {At: 1408, ID: "99b86a5e-0e7a-11e7-a01a-5b5fbdeba3d6", Repeat: 2}, @@ -48,11 +53,9 @@ var data = &cntl.DataStore{ Name: "Test-Preset 1", DeviceParams: []cntl.DMXDeviceParams{ { - Device: &cntl.DMXDeviceSelector{ - ID: "35cae00a-0b17-11e7-8bca-bbf30c56f20e", - }, + Device: StrPtr("35cae00a-0b17-11e7-8bca-bbf30c56f20e"), Params: []cntl.DMXParams{ - {Red: Value255}, + {ColorVar: StrPtr("Red255")}, }, }, }}, @@ -61,11 +64,9 @@ var data = &cntl.DataStore{ Name: "Test-Preset 2", DeviceParams: []cntl.DMXDeviceParams{ { - Device: &cntl.DMXDeviceSelector{ - ID: "35cae00a-0b17-11e7-8bca-bbf30c56f20e", - }, + Device: StrPtr("35cae00a-0b17-11e7-8bca-bbf30c56f20e"), Params: []cntl.DMXParams{ - {Blue: Value255}, + {ColorVar: StrPtr("Blue255")}, }, }, }}, @@ -74,11 +75,9 @@ var data = &cntl.DataStore{ Name: "Test-Preset 3", DeviceParams: []cntl.DMXDeviceParams{ { - Device: &cntl.DMXDeviceSelector{ - ID: "35cae00a-0b17-11e7-8bca-bbf30c56f20e", - }, + Device: StrPtr("35cae00a-0b17-11e7-8bca-bbf30c56f20e"), Params: []cntl.DMXParams{ - {Green: Value255}, + {ColorVar: StrPtr("Green255")}, }, }, }}, @@ -87,9 +86,8 @@ var data = &cntl.DataStore{ ID: "5d3a415a-0b15-11e7-90b9-03c2b960e034", Name: "Test-Preset 4", DeviceParams: []cntl.DMXDeviceParams{ - {Group: &cntl.DMXDeviceGroupSelector{ - ID: "cb58bc10-0b16-11e7-b45a-7bee591b0adb", - }, + { + Group: StrPtr("cb58bc10-0b16-11e7-b45a-7bee591b0adb"), Params: []cntl.DMXParams{ {Strobe: Value255}, }, @@ -101,7 +99,7 @@ var data = &cntl.DataStore{ Name: "Test-Preset 5", DeviceParams: []cntl.DMXDeviceParams{ { - Group: &cntl.DMXDeviceGroupSelector{ID: "475b71a0-0b16-11e7-9406-e3f678e8b788"}, + Group: StrPtr("475b71a0-0b16-11e7-9406-e3f678e8b788"), Params: []cntl.DMXParams{ {Red: Value200}, }, @@ -117,10 +115,8 @@ var data = &cntl.DataStore{ NoteValue: 4, SubScenes: []cntl.DMXSubScene{ { - At: []uint64{0, 1, 2, 3}, - Preset: &cntl.PresetSelector{ - ID: "0de258e0-0e7b-11e7-afd4-ebf6036983dc", - }, + At: []uint64{0, 1, 2, 3}, + Preset: StrPtr("0de258e0-0e7b-11e7-afd4-ebf6036983dc"), }, }, }, @@ -131,10 +127,8 @@ var data = &cntl.DataStore{ NoteValue: 4, SubScenes: []cntl.DMXSubScene{ { - At: []uint64{0, 1}, - Preset: &cntl.PresetSelector{ - ID: "11adf93e-0e7b-11e7-998c-5bd2bd0df396", - }, + At: []uint64{0, 1}, + Preset: StrPtr("11adf93e-0e7b-11e7-998c-5bd2bd0df396"), }, }, }, @@ -148,12 +142,8 @@ var data = &cntl.DataStore{ At: []uint64{0, 4, 8, 12}, DeviceParams: []cntl.DMXDeviceParams{ { - Device: &cntl.DMXDeviceSelector{ - ID: "35cae00a-0b17-11e7-8bca-bbf30c56f20e", - }, - Animation: &cntl.AnimationSelector{ - ID: "a51f7b2a-0e7b-11e7-bfc8-57da167865d7", - }, + Device: StrPtr("35cae00a-0b17-11e7-8bca-bbf30c56f20e"), + Animation: StrPtr("a51f7b2a-0e7b-11e7-bfc8-57da167865d7"), }, }, }, @@ -166,10 +156,8 @@ var data = &cntl.DataStore{ NoteValue: 4, SubScenes: []cntl.DMXSubScene{ { - At: []uint64{0, 1, 2, 3}, - Preset: &cntl.PresetSelector{ - ID: "0de258e0-0e7b-11e7-afd4-ebf6036983dc", - }, + At: []uint64{0, 1, 2, 3}, + Preset: StrPtr("0de258e0-0e7b-11e7-afd4-ebf6036983dc"), }, }, }, @@ -183,12 +171,8 @@ var data = &cntl.DataStore{ At: []uint64{0}, DeviceParams: []cntl.DMXDeviceParams{ { - Device: &cntl.DMXDeviceSelector{ - ID: "35cae00a-0b17-11e7-8bca-bbf30c56f20e", - }, - Transition: &cntl.TransitionSelector{ - ID: "a1a02b6c-12dd-4d7b-bc3e-24cc823adf21", - }, + Device: StrPtr("35cae00a-0b17-11e7-8bca-bbf30c56f20e"), + Transition: StrPtr("a1a02b6c-12dd-4d7b-bc3e-24cc823adf21"), }, }, }, @@ -228,7 +212,6 @@ var data = &cntl.DataStore{ "1555d67e-1187-11e7-8135-9b41038b5b75": { ID: "1555d67e-1187-11e7-8135-9b41038b5b75", Name: "LED-Bar 67 Channel", - Key: "LEDBar67", ChannelCount: 67, ChannelsPerLED: 4, ModeEnabled: true, @@ -238,28 +221,27 @@ var data = &cntl.DataStore{ StrobeEnabled: true, StrobeChannel: 2, LEDs: []cntl.LED{ - {Position: 0, Red: 0, Green: 1, Blue: 2, White: 3}, - {Position: 1, Red: 4, Green: 5, Blue: 6, White: 7}, - {Position: 2, Red: 8, Green: 9, Blue: 10, White: 11}, - {Position: 3, Red: 12, Green: 13, Blue: 14, White: 15}, - {Position: 4, Red: 16, Green: 17, Blue: 18, White: 19}, - {Position: 5, Red: 20, Green: 21, Blue: 22, White: 23}, - {Position: 6, Red: 24, Green: 25, Blue: 26, White: 27}, - {Position: 7, Red: 28, Green: 29, Blue: 30, White: 31}, - {Position: 8, Red: 32, Green: 33, Blue: 34, White: 35}, - {Position: 9, Red: 36, Green: 37, Blue: 38, White: 39}, - {Position: 10, Red: 40, Green: 41, Blue: 42, White: 43}, - {Position: 11, Red: 44, Green: 45, Blue: 46, White: 47}, - {Position: 12, Red: 48, Green: 49, Blue: 50, White: 51}, - {Position: 13, Red: 52, Green: 53, Blue: 54, White: 55}, - {Position: 14, Red: 56, Green: 57, Blue: 58, White: 59}, - {Position: 15, Red: 60, Green: 61, Blue: 62, White: 63}, + {Red: 0, Green: 1, Blue: 2, White: 3}, + {Red: 4, Green: 5, Blue: 6, White: 7}, + {Red: 8, Green: 9, Blue: 10, White: 11}, + {Red: 12, Green: 13, Blue: 14, White: 15}, + {Red: 16, Green: 17, Blue: 18, White: 19}, + {Red: 20, Green: 21, Blue: 22, White: 23}, + {Red: 24, Green: 25, Blue: 26, White: 27}, + {Red: 28, Green: 29, Blue: 30, White: 31}, + {Red: 32, Green: 33, Blue: 34, White: 35}, + {Red: 36, Green: 37, Blue: 38, White: 39}, + {Red: 40, Green: 41, Blue: 42, White: 43}, + {Red: 44, Green: 45, Blue: 46, White: 47}, + {Red: 48, Green: 49, Blue: 50, White: 51}, + {Red: 52, Green: 53, Blue: 54, White: 55}, + {Red: 56, Green: 57, Blue: 58, White: 59}, + {Red: 60, Green: 61, Blue: 62, White: 63}, }, }, "628fc3ea-1188-11e7-8824-5f72d80c17b6": { ID: "628fc3ea-1188-11e7-8824-5f72d80c17b6", Name: "PAR 5 channel", - Key: "PAR5", ChannelCount: 5, ChannelsPerLED: 3, ModeEnabled: false, @@ -269,13 +251,12 @@ var data = &cntl.DataStore{ StrobeEnabled: true, StrobeChannel: 4, LEDs: []cntl.LED{ - {Position: 0, Red: 0, Green: 1, Blue: 2}, + {Red: 0, Green: 1, Blue: 2}, }, }, "5ccc43ee-118c-11e7-8d53-974b41748b71": { ID: "5ccc43ee-118c-11e7-8d53-974b41748b71", Name: "Strobe", - Key: "Strobe", StrobeEnabled: true, StrobeChannel: 0, }, @@ -332,8 +313,7 @@ var data = &cntl.DataStore{ }, DMXAnimations: map[string]*cntl.DMXAnimation{ "a51f7b2a-0e7b-11e7-bfc8-57da167865d7": { - ID: "a51f7b2a-0e7b-11e7-bfc8-57da167865d7", - Length: 4, + ID: "a51f7b2a-0e7b-11e7-bfc8-57da167865d7", Frames: []cntl.DMXAnimationFrame{ {At: 0, Params: cntl.DMXParams{LED: 1, Blue: Value31}}, {At: 1, Params: cntl.DMXParams{LED: 1, Blue: Value63}}, @@ -422,6 +402,23 @@ var data = &cntl.DataStore{ }, }, }, + DMXColorVariables: map[string]*cntl.DMXColorVariable{ + "4b848ea8-5094-4509-a067-09a0e568220d": { + ID: "4b848ea8-5094-4509-a067-09a0e568220d", + Name: "Red255", + Red: Value255, + }, + "dbd2dbca-e680-4a25-a758-0dbf5c847932": { + ID: "dbd2dbca-e680-4a25-a758-0dbf5c847932", + Name: "Blue255", + Blue: Value255, + }, + "10484f74-fc19-47c1-a21c-202d0ffbe66b": { + ID: "10484f74-fc19-47c1-a21c-202d0ffbe66b", + Name: "Green255", + Green: Value255, + }, + }, } // DataStore returns the go object representation of a working set of fixtures diff --git a/pkg/internal/logging/logger.go b/pkg/internal/logging/logger.go new file mode 100644 index 0000000..077be83 --- /dev/null +++ b/pkg/internal/logging/logger.go @@ -0,0 +1,16 @@ +package logging + +// Logger is a logging interface used for convenience across the application +type Logger interface { + Debugf(format string, args ...interface{}) + Infof(format string, args ...interface{}) + Warnf(format string, args ...interface{}) + Warningf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + + Debug(args ...interface{}) + Info(args ...interface{}) + Warn(args ...interface{}) + Warning(args ...interface{}) + Error(args ...interface{}) +} diff --git a/pkg/internal/stringslice/contains.go b/pkg/internal/stringslice/contains.go new file mode 100644 index 0000000..559940a --- /dev/null +++ b/pkg/internal/stringslice/contains.go @@ -0,0 +1,12 @@ +package stringslice + +// Contains returns weather the given slice contains the given element +func Contains(elem string, slice []string) bool { + for _, s := range slice { + if s == elem { + return true + } + } + + return false +} diff --git a/pkg/internal/stringslice/contains_test.go b/pkg/internal/stringslice/contains_test.go new file mode 100644 index 0000000..eecbd2e --- /dev/null +++ b/pkg/internal/stringslice/contains_test.go @@ -0,0 +1,15 @@ +package stringslice + +import "testing" + +func TestContains(t *testing.T) { + slice := []string{"a", "b", "c"} + + if !Contains("b", slice) { + t.Errorf("Expected contains to returns true as the slice contains the element, but it returned false") + } + + if Contains("x", slice) { + t.Errorf("Expected contains to returns false as the slice does not contain the element, but it returned true") + } +} diff --git a/pkg/internal/testing/cleanup.go b/pkg/internal/testing/cleanup.go new file mode 100644 index 0000000..fdfc7b7 --- /dev/null +++ b/pkg/internal/testing/cleanup.go @@ -0,0 +1,13 @@ +package testing + +import ( + "os" + "testing" +) + +// Cleanup a storage path after a test run. Method is meant to be run deferred +func Cleanup(t *testing.T, path string) { + if err := os.RemoveAll(path); err != nil { + t.Errorf("failed to remove test storage dir: %v", err) + } +} diff --git a/pkg/loader/files/.gitignore b/pkg/loader/files/.gitignore old mode 100644 new mode 100755 diff --git a/pkg/loader/files/db.go b/pkg/loader/files/db.go old mode 100644 new mode 100755 index ba0cc7c..8e6c775 --- a/pkg/loader/files/db.go +++ b/pkg/loader/files/db.go @@ -5,15 +5,16 @@ import ( ) type fileData struct { - SetLists []*cntl.SetList `json:"set_lists"` - Songs []*cntl.Song `json:"songs"` - DMXScenes []*cntl.DMXScene `json:"dmx_scenes"` - DMXPresets []*cntl.DMXPreset `json:"dmx_presets"` - DMXAnimations []*cntl.DMXAnimation `json:"dmx_animations"` - DMXTransitions []*cntl.DMXTransition `json:"dmx_transitions"` - DMXDevices []*cntl.DMXDevice `json:"dmx_devices"` - DMXDeviceTypes []*cntl.DMXDeviceType `json:"dmx_device_types"` - DMXDeviceGroups []*cntl.DMXDeviceGroup `json:"dmx_device_groups"` + SetLists []*cntl.SetList `json:"set_lists"` + Songs []*cntl.Song `json:"songs"` + DMXScenes []*cntl.DMXScene `json:"dmx_scenes"` + DMXPresets []*cntl.DMXPreset `json:"dmx_presets"` + DMXAnimations []*cntl.DMXAnimation `json:"dmx_animations"` + DMXTransitions []*cntl.DMXTransition `json:"dmx_transitions"` + DMXDevices []*cntl.DMXDevice `json:"dmx_devices"` + DMXDeviceTypes []*cntl.DMXDeviceType `json:"dmx_device_types"` + DMXDeviceGroups []*cntl.DMXDeviceGroup `json:"dmx_device_groups"` + DMXColorVariables []*cntl.DMXColorVariable `json:"dmx_color_variables"` } // Database is a file repository diff --git a/pkg/loader/files/db_test.go b/pkg/loader/files/db_test.go old mode 100644 new mode 100755 diff --git a/pkg/loader/files/fixtures/dmx_presets.json b/pkg/loader/files/fixtures/dmx_presets.json index aca4878..18ff90e 100755 --- a/pkg/loader/files/fixtures/dmx_presets.json +++ b/pkg/loader/files/fixtures/dmx_presets.json @@ -3,9 +3,7 @@ { "deviceParams": [ { - "device": { - "id": "35cae00a-0b17-11e7-8bca-bbf30c56f20e" - }, + "device": "35cae00a-0b17-11e7-8bca-bbf30c56f20e", "params": [ { "red": 255 @@ -19,9 +17,7 @@ { "deviceParams": [ { - "device": { - "id": "35cae00a-0b17-11e7-8bca-bbf30c56f20e" - }, + "device": "35cae00a-0b17-11e7-8bca-bbf30c56f20e", "params": [ { "blue": 255 @@ -35,9 +31,7 @@ { "deviceParams": [ { - "device": { - "id": "35cae00a-0b17-11e7-8bca-bbf30c56f20e" - }, + "device": "35cae00a-0b17-11e7-8bca-bbf30c56f20e", "params": [ { "green": 255 @@ -51,9 +45,7 @@ { "deviceParams": [ { - "group": { - "id": "cb58bc10-0b16-11e7-b45a-7bee591b0adb" - }, + "group": "cb58bc10-0b16-11e7-b45a-7bee591b0adb", "params": [ { "strobe": 255 @@ -67,9 +59,7 @@ { "deviceParams": [ { - "group": { - "id": "475b71a0-0b16-11e7-9406-e3f678e8b788" - }, + "group": "475b71a0-0b16-11e7-9406-e3f678e8b788", "params": [ { "red": 200 diff --git a/pkg/loader/files/fixtures/dmx_scenes.json b/pkg/loader/files/fixtures/dmx_scenes.json index 89d42d3..10ab619 100755 --- a/pkg/loader/files/fixtures/dmx_scenes.json +++ b/pkg/loader/files/fixtures/dmx_scenes.json @@ -13,9 +13,7 @@ 2, 3 ], - "preset": { - "id": "0de258e0-0e7b-11e7-afd4-ebf6036983dc" - } + "preset": "0de258e0-0e7b-11e7-afd4-ebf6036983dc" } ] }, @@ -30,9 +28,7 @@ 0, 1 ], - "preset": { - "id": "11adf93e-0e7b-11e7-998c-5bd2bd0df396" - } + "preset": "11adf93e-0e7b-11e7-998c-5bd2bd0df396" } ] }, @@ -55,12 +51,8 @@ ], "deviceParams": [ { - "animation": { - "id": "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" - }, - "device": { - "id": "35cae00a-0b17-11e7-8bca-bbf30c56f20e" - } + "animation": "a51f7b2a-0e7b-11e7-bfc8-57da167865d7", + "device": "35cae00a-0b17-11e7-8bca-bbf30c56f20e" } ] } @@ -79,9 +71,7 @@ 2, 3 ], - "preset": { - "id": "0de258e0-0e7b-11e7-afd4-ebf6036983dc" - } + "preset": "0de258e0-0e7b-11e7-afd4-ebf6036983dc" } ] }, @@ -97,16 +87,12 @@ ], "deviceParams": [ { - "transition": { - "id": "a51f7b2a-0e7b-11e7-bfc8-57da167865d7" - }, - "device": { - "id": "35cae00a-0b17-11e7-8bca-bbf30c56f20e" - } + "transition": "a51f7b2a-0e7b-11e7-bfc8-57da167865d7", + "device": "35cae00a-0b17-11e7-8bca-bbf30c56f20e" } ] } ] } ] -} \ No newline at end of file +} diff --git a/pkg/loader/files/fixtures/set_lists.json b/pkg/loader/files/fixtures/set_lists.json index 71b4409..9fa18b6 100755 --- a/pkg/loader/files/fixtures/set_lists.json +++ b/pkg/loader/files/fixtures/set_lists.json @@ -4,10 +4,8 @@ "id": "f5b4be8a-0b18-11e7-b837-4bac99d86956", "name": "Regular gig", "songs": [ - { - "id": "3c1065c8-0b14-11e7-96eb-5b134621c411" - } + "3c1065c8-0b14-11e7-96eb-5b134621c411" ] } ] -} \ No newline at end of file +} diff --git a/pkg/loader/files/read.go b/pkg/loader/files/read.go old mode 100644 new mode 100755 index 3304c90..022ecf5 --- a/pkg/loader/files/read.go +++ b/pkg/loader/files/read.go @@ -55,6 +55,7 @@ func mergeFileData(data, fd *fileData) *fileData { newData.DMXDevices = append(data.DMXDevices, fd.DMXDevices...) newData.DMXDeviceTypes = append(data.DMXDeviceTypes, fd.DMXDeviceTypes...) newData.DMXDeviceGroups = append(data.DMXDeviceGroups, fd.DMXDeviceGroups...) + newData.DMXColorVariables = append(data.DMXColorVariables, fd.DMXColorVariables...) return newData } @@ -95,4 +96,8 @@ func expandData(data *cntl.DataStore, fileData *fileData) { for _, t := range fileData.DMXTransitions { data.DMXTransitions[t.ID] = t } + + for _, t := range fileData.DMXColorVariables { + data.DMXColorVariables[t.ID] = t + } } diff --git a/pkg/process/buffered_logger.go b/pkg/process/buffered_logger.go new file mode 100644 index 0000000..50e9622 --- /dev/null +++ b/pkg/process/buffered_logger.go @@ -0,0 +1,82 @@ +package process + +import ( + "fmt" + "time" +) + +// BufferedLogger appends the logs to a given slice of Log entries which is passed by reference +type BufferedLogger struct { + logs *[]Log + verbose bool +} + +// NewBufferedLogger returns a new BufferedLogger instance +func NewBufferedLogger(logs *[]Log, verbose bool) *BufferedLogger { + return &BufferedLogger{ + logs: logs, + verbose: verbose, + } +} + +func (l *BufferedLogger) log(level, msg string) { + if !l.verbose && level == "debug" { + return + } + + *l.logs = append(*l.logs, Log{ + Time: JSONTime{Time: time.Now()}, + Level: level, + Message: msg, + }) +} + +// Debugf log method +func (l *BufferedLogger) Debugf(format string, args ...interface{}) { + l.log("debug", fmt.Sprintf(format, args...)) +} + +// Infof log method +func (l *BufferedLogger) Infof(format string, args ...interface{}) { + l.log("info", fmt.Sprintf(format, args...)) +} + +// Warnf log method +func (l *BufferedLogger) Warnf(format string, args ...interface{}) { + l.log("warn", fmt.Sprintf(format, args...)) +} + +// Warningf log method +func (l *BufferedLogger) Warningf(format string, args ...interface{}) { + l.log("warning", fmt.Sprintf(format, args...)) +} + +// Errorf log method +func (l *BufferedLogger) Errorf(format string, args ...interface{}) { + l.log("error", fmt.Sprintf(format, args...)) +} + +// Debug log method +func (l *BufferedLogger) Debug(args ...interface{}) { + l.log("debug", fmt.Sprint(args...)) +} + +// Info log method +func (l *BufferedLogger) Info(args ...interface{}) { + l.log("info", fmt.Sprint(args...)) +} + +// Warn log method +func (l *BufferedLogger) Warn(args ...interface{}) { + l.log("warn", fmt.Sprint(args...)) +} + +// Warning log method +func (l *BufferedLogger) Warning(args ...interface{}) { + l.log("warning", fmt.Sprint(args...)) +} + +// Error log method +func (l *BufferedLogger) Error(args ...interface{}) { + l.log("error", fmt.Sprint(args...)) +} diff --git a/pkg/process/errors.go b/pkg/process/errors.go new file mode 100644 index 0000000..e3afec0 --- /dev/null +++ b/pkg/process/errors.go @@ -0,0 +1,10 @@ +package process + +import "errors" + +var ( + errProcessNotFound = errors.New("the process with given name was not found or isn't running") + errProcessAlreadyExists = errors.New("the process with the given name already exists") + errProcessAlreadyRunning = errors.New("the process with the given name is already running") + errProcessNotRunning = errors.New("the process with the given name is not running") +) diff --git a/pkg/process/manager.go b/pkg/process/manager.go new file mode 100644 index 0000000..e97e11e --- /dev/null +++ b/pkg/process/manager.go @@ -0,0 +1,134 @@ +package process + +import ( + "context" + "fmt" + "time" + + "github.com/StageAutoControl/controller/pkg/internal/logging" +) + +type processInfo struct { + process Process + status Status + logger logging.Logger +} + +type manager struct { + ctx context.Context + logger logging.Logger + processes map[string]*processInfo +} + +// NewManager returns a new process manager instance +func NewManager(ctx context.Context, logger logging.Logger) Manager { + m := &manager{ + ctx: ctx, + logger: logger, + processes: make(map[string]*processInfo), + } + + go m.listenExit() + + return m +} + +func (m *manager) listenExit() { + <-m.ctx.Done() + for name := range m.processes { + if p, _, err := m.GetProcess(name); err != nil { + m.logger.Errorf("failed to find process %q while shutting down: %v", name, err) + + } else if err := p.Stop(); err != nil && err != errProcessNotRunning { + m.logger.Errorf("failed to stop process %q: %v", name, err) + + } + } +} + +func (m *manager) AddProcess(name string, process Process, verbose bool) error { + if _, ok := m.processes[name]; ok { + return errProcessAlreadyExists + } + + m.processes[name] = &processInfo{ + process: process, + status: Status{ + Name: name, + Running: false, + Logs: make([]Log, 0), + Verbose: verbose, + }, + } + return nil +} + +func (m *manager) GetProcess(name string) (Process, *Status, error) { + info, ok := m.processes[name] + if !ok { + return nil, nil, errProcessNotFound + } + + return info.process, &info.status, nil +} + +func (m *manager) Start(name string) (*Status, error) { + info, ok := m.processes[name] + if !ok { + return nil, errProcessNotFound + } + + if info.status.Running { + return nil, errProcessAlreadyRunning + } + + info.status.Running = true + info.status.Error = "" + info.status.StartedAt = &JSONTime{Time: time.Now()} + info.status.StoppedAt = nil + info.status.Logs = make([]Log, 0) + info.logger = NewBufferedLogger(&info.status.Logs, info.status.Verbose) + info.process.SetLogger(info.logger) + + go func() { + if err := info.process.Start(m.ctx); err != nil { + info.status.Error = err.Error() + info.status.Running = false + m.logger.Errorf("failed to start process %s: %v", name, err) + return + } + + if info.process.Blocking() { + if _, err := m.Stop(name); err != nil && err != errProcessNotRunning { + m.logger.Error(err) + info.logger.Error(err) + } + } + }() + + return &info.status, nil +} + +// Stop a given process ID +func (m *manager) Stop(name string) (*Status, error) { + p, ok := m.processes[name] + if !ok { + return nil, errProcessNotFound + } + + if !p.status.Running { + return nil, errProcessNotRunning + } + + if err := p.process.Stop(); err != nil { + err = fmt.Errorf("failed to stop process %q: %v", name, err) + p.logger.Error(err) + return &p.status, err + } + + p.status.Running = false + p.status.StoppedAt = &JSONTime{Time: time.Now()} + p.logger.Infof("Process finished") + + return &p.status, nil +} diff --git a/pkg/process/manager_test.go b/pkg/process/manager_test.go new file mode 100644 index 0000000..54f75d5 --- /dev/null +++ b/pkg/process/manager_test.go @@ -0,0 +1,10 @@ +package process + +import ( + "testing" +) + +func TestNewManager(t *testing.T) { + //ctx := context.Background() + +} diff --git a/pkg/process/time.go b/pkg/process/time.go new file mode 100644 index 0000000..fcd14f3 --- /dev/null +++ b/pkg/process/time.go @@ -0,0 +1,22 @@ +package process + +import ( + "fmt" + "time" +) + +// JSONTime handles parsing and formatting timestamps according the ISO8601 standard +type JSONTime struct { + time.Time +} + +// String returns a string representation of the time. +func (t JSONTime) String() string { + return t.Format(time.RFC3339) +} + +// MarshalJSON formats the timestamp as JSON +func (t JSONTime) MarshalJSON() ([]byte, error) { + date := fmt.Sprintf("%q", t.String()) + return []byte(date), nil +} diff --git a/pkg/process/time_test.go b/pkg/process/time_test.go new file mode 100644 index 0000000..2cde8e1 --- /dev/null +++ b/pkg/process/time_test.go @@ -0,0 +1,54 @@ +package process + +import ( + "bytes" + "encoding/json" + "testing" + "time" +) + +type testJSON struct { + Test JSONTime `json:"test"` +} + +var ( + jsonDate = []byte("{\"test\":\"2018-08-18T10:31:17+02:00\"}") + rawDate = "2018-08-18T10:31:17+02:00" +) + +func getTestTime(t *testing.T) JSONTime { + date, err := time.Parse(time.RFC3339, rawDate) + if err != nil { + t.Fatal(err) + } + + return JSONTime{Time: date} +} + +func TestJSONTime_MarshalJSON(t *testing.T) { + test := &testJSON{ + Test: JSONTime(getTestTime(t)), + } + + b, err := json.Marshal(test) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(b, jsonDate) { + t.Fatalf("Expected to get %q, got %q", string(jsonDate), string(b)) + } +} + +func TestJSONTime_UnmarshalJSON(t *testing.T) { + test := &testJSON{} + testTime := getTestTime(t) + + if err := json.Unmarshal(jsonDate, test); err != nil { + t.Fatal(err) + } + + if !test.Test.Equal(testTime.Time) { + t.Fatalf("Dates are not equal: %v, %v", test.Test, testTime) + } +} diff --git a/pkg/process/types.go b/pkg/process/types.go new file mode 100644 index 0000000..091ff9e --- /dev/null +++ b/pkg/process/types.go @@ -0,0 +1,49 @@ +package process + +import ( + "context" + + "github.com/StageAutoControl/controller/pkg/internal/logging" +) + +// Status of a process as handled by the manager +type Status struct { + Name string `json:"name"` + Running bool `json:"running"` + StartedAt *JSONTime `json:"startedAt"` + StoppedAt *JSONTime `json:"stoppedAt"` + Error string `json:"error"` + Logs []Log `json:"logs"` + Verbose bool `json:"verbose"` +} + +// Log represents a log line printed by the process +type Log struct { + Time JSONTime `json:"time"` + Level string `json:"level"` + Message string `json:"message"` +} + +// Process carries the information how and what to manage as a process, it implements the custom logic +type Process interface { + // SetLogger sets the logger of the process, which in fact is a buffering logger + SetLogger(logger logging.Logger) + + // Start should care about starting the process, including handling errors if the process does not come up. + // When Start executed without an error the process manager assumes that the process is up and running. + Start(ctx context.Context) error + + // Stop should fully stop the process and also clean up any leftovers (state, files, go routines, ...) + Stop() error + + // Blocking returns true if calling Start() is a blocking operation and the process is stopped after start returned + Blocking() bool +} + +// Manager to handle a set of processes as pets +type Manager interface { + AddProcess(name string, process Process, verbose bool) error + GetProcess(name string) (Process, *Status, error) + Start(name string) (*Status, error) + Stop(name string) (*Status, error) +} diff --git a/pkg/visualizer/client.go b/pkg/visualizer/client.go new file mode 100644 index 0000000..d05ac0c --- /dev/null +++ b/pkg/visualizer/client.go @@ -0,0 +1,65 @@ +package visualizer + +import ( + "github.com/gorilla/websocket" + + "github.com/StageAutoControl/controller/pkg/internal/logging" +) + +var ( + newline = []byte{'\n'} +) + +type client struct { + logger logging.Logger + server *Server + conn *websocket.Conn + send chan []byte +} + +func (c *client) read() { + defer func() { + c.server.unregister <- c + if err := c.conn.Close(); err != nil { + c.logger.Error(err) + } + }() + for { + if _, _, err := c.conn.NextReader(); err != nil { + if err := c.conn.Close(); err != nil { + c.logger.Error(err) + } + break + } + } +} + +func (c *client) stop() { + close(c.send) +} + +func (c *client) write() { + for message := range c.send { + w, err := c.conn.NextWriter(websocket.TextMessage) + if err != nil { + return + } + if _, err := w.Write(message); err != nil { + c.logger.Error(err) + } + + n := len(c.send) + for i := 0; i < n; i++ { + if _, err := w.Write(newline); err != nil { + c.logger.Error(err) + } + if _, err := w.Write(<-c.send); err != nil { + c.logger.Error(err) + } + } + + if err := w.Close(); err != nil { + return + } + } +} diff --git a/pkg/visualizer/server.go b/pkg/visualizer/server.go new file mode 100644 index 0000000..008678e --- /dev/null +++ b/pkg/visualizer/server.go @@ -0,0 +1,117 @@ +package visualizer + +import ( + "context" + "encoding/json" + "log" + "net/http" + + "github.com/gorilla/websocket" + + "github.com/StageAutoControl/controller/pkg/cntl" + "github.com/StageAutoControl/controller/pkg/internal/logging" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(_ *http.Request) bool { + return true + }, +} + +type Server struct { + logger logging.Logger + clients map[*client]bool + broadcast chan []byte + register chan *client + unregister chan *client +} + +func NewServer(logger logging.Logger) *Server { + return &Server{ + logger: logger, + broadcast: make(chan []byte), + register: make(chan *client), + unregister: make(chan *client), + clients: make(map[*client]bool), + } +} + +// Write the given command to the sockets +func (s *Server) Write(cmd cntl.Command) error { + b, err := json.Marshal(&cmd) + if err != nil { + return err + } + + s.send(b) + return nil +} + +// send the given byte slide to all sockets +func (s *Server) send(msg []byte) { + s.broadcast <- msg +} + +// Run the server +func (s *Server) Run(ctx context.Context) { + go func() { + for message := range s.broadcast { + for client := range s.clients { + select { + case client.send <- message: + default: + close(client.send) + delete(s.clients, client) + } + } + } + }() + + for { + select { + case <-ctx.Done(): + s.stop() + return + case client := <-s.register: + s.clients[client] = true + case client := <-s.unregister: + if _, ok := s.clients[client]; ok { + delete(s.clients, client) + close(client.send) + } + + } + } +} + +func (s *Server) stop() { + close(s.broadcast) + close(s.register) + close(s.unregister) + + for c := range s.clients { + c.stop() + } +} + +// ServeRequest handles incoming http connections and upgrades them +func (s *Server) ServeRequest(rw http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(rw, r, nil) + if err != nil { + log.Println(err) + return + } + client := &client{ + logger: s.logger, + server: s, + conn: conn, + send: make(chan []byte, 1024), + } + s.register <- client + + // we don't care for what the client tells us + go client.read() + client.write() +}