diff --git a/internal/patterns/common.go b/internal/patterns/common.go new file mode 100755 index 0000000..7a5df91 --- /dev/null +++ b/internal/patterns/common.go @@ -0,0 +1,27 @@ +package patterns + +func Even(number int) bool { + return number %2 == 0 +} + +func FillPanel(w int, h int, r byte, g byte, b byte) [][]RGBcolor { + grid := make([][]RGBcolor, w) + for i := 0; i < w; i++ { + grid[i] = make([]RGBcolor, h) + } + for x := 0; x < w; x++ { + for y := 0; y < h; y++ { + var rgb [3]byte + rgb[0] = r + rgb[1] = g + rgb[2] = b + grid[x][y] = rgb + } + } + return grid +} + + +func FillPanelRGB(w int, h int, colour RGBcolor) [][]RGBcolor { + return FillPanel(w,h,colour[0],colour[1],colour[2]) +} diff --git a/internal/patterns/experiment.go b/internal/patterns/experiment.go new file mode 100755 index 0000000..0f2c578 --- /dev/null +++ b/internal/patterns/experiment.go @@ -0,0 +1,49 @@ +package patterns + +import "math" + +var sineoffset int = 0 +var lastSineSpeed uint16 = 0 + +func Sinewave(w int, h int, bgColor RGBcolor, fgColor RGBcolor, speed uint16) [][]RGBcolor { + if speed != lastSineSpeed { + lastSineSpeed = speed + sineoffset = 0 + } + sineoffset += int(speed/40) + background := FillPanelRGB(w,h,bgColor) + for x := 0; x < w; x++ { + y := int(math.Round((math.Sin(float64(x+sineoffset))+1) / 2 * float64(h-1))) + background[x][y] = fgColor + } + return background +} + +var lastSinePos int = 0 + +func SineChase(w int, h int, bgColor RGBcolor, fgColor RGBcolor, speed uint16) [][]RGBcolor { + if speed != lastSineSpeed { + lastSineSpeed = speed + lastSinePos = 0 + } + background := FillPanelRGB(w,h,bgColor) + y := int(math.Round((math.Sin(float64(lastSinePos))+1) / 2 * float64(h-1))) + background[lastSinePos][y] = fgColor + lastSinePos += 1 + if lastSinePos >= w { + lastSinePos = 0 + } + return background +} + +func ZigZag(w int, h int) [][]RGBcolor { + var bgColor = [3]byte{0,0,0} + var fgColor = [3]byte{255,0,0} + //var a = [5]int{2, 4, 6, 8, 10} + background := FillPanelRGB(w,h,bgColor) + for x := 0; x < w; x++ { + y := x % (h-1) + background[x][y] = fgColor + } + return background +} diff --git a/internal/patterns/gradient.go b/internal/patterns/gradient.go new file mode 100755 index 0000000..b1f0ed1 --- /dev/null +++ b/internal/patterns/gradient.go @@ -0,0 +1,16 @@ +package patterns + +// gradient returns from fromX to toX fading from (fromR,fromG,fromB) to (toR,toG,toB) +// at least that's the idea. +func Gradient(fromR byte, fromG byte, fromB byte, toR byte, toG byte, toB byte, fromX int, toX int) []byte { + ret := make([]byte, toX*3) + var stepR float32 = (float32(toR) - float32(fromR)) / (float32(toX) - float32(fromX)) + var stepG float32 = (float32(toG) - float32(fromG)) / (float32(toX) - float32(fromX)) + var stepB float32 = (float32(toB) - float32(fromB)) / (float32(toX) - float32(fromX)) + for i := fromX; i < toX*3; i += 3 { + ret[i] = fromR + byte(float32(i/3)*stepR) + ret[i+1] = fromG + byte(float32(i/3)*stepG) + ret[i+2] = fromB + byte(float32(i/3)*stepB) + } + return ret +} \ No newline at end of file diff --git a/internal/patterns/plaincolour.go b/internal/patterns/plaincolour.go new file mode 100755 index 0000000..d4b82da --- /dev/null +++ b/internal/patterns/plaincolour.go @@ -0,0 +1,27 @@ +package patterns + +import "math/rand" +import "time" + +func RedPanel(w int, h int) [][]RGBcolor { + return FillPanel(w, h, 255, 0, 0) +} + +var lastColour RGBcolor +var lastIteration uint16 = 0 + +func RandomColourPanel(w int, h int, speed uint16) [][]RGBcolor { + if lastIteration > 0 { + lastIteration -= 40 + } + if lastIteration <= 0 { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + var newColour RGBcolor + newColour[0] = byte(r.Intn(255)) + newColour[1] = byte(r.Intn(255)) + newColour[2] = byte(r.Intn(255)) + lastColour = newColour + lastIteration = speed + } + return FillPanel(w, h, lastColour[0], lastColour[1], lastColour[2]) +} \ No newline at end of file diff --git a/internal/patterns/plasma.go b/internal/patterns/plasma.go new file mode 100755 index 0000000..442b5ba --- /dev/null +++ b/internal/patterns/plasma.go @@ -0,0 +1,68 @@ +package patterns + +import "math" + +type RGBcolor = [3]byte + +func plasmaRGBFromVal(val byte) (byte, byte, byte) { + var r byte + var g byte + var b byte + if val < 85 { + r = val * 3 + g = 255 - r + b = 0 + } else if val < 170 { + b = (val - 85) * 3 + r = 255 - b + g = 0 + } else { + g = (val - 170) * 3 + b = 255 - g + r = 0 + } + return r, g, b +} + + +func LinearPlasma(l int) []byte { + line := make([]byte, l*3) + for i := 0; i < l*3; i += 3 { + val := byte(i % 256) + r, g, b := plasmaRGBFromVal(val) + line[i] = r + line[i+1] = g + line[i+2] = b + } + return line +} + +var offset float64 = 0.5 + +func PlasmaPanel(w int, h int, speed uint16) [][]RGBcolor { + grid := make([][]RGBcolor, w) + for i := 0; i < w; i++ { + grid[i] = make([]RGBcolor, h) + } + scale := math.Pi * 2.0 / float64(w) + step := float64(5 * speed / 40) + offset -= step + if offset < 0 { + offset += 1.0 + } + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + u := math.Cos((float64(x) + offset) * scale) + v := math.Cos((float64(y) + offset) * scale) + w := math.Cos((float64(w) + offset) * scale) + e := (u + v + w + 3.0) / 6.0 + r, g, b := plasmaRGBFromVal(byte((offset + e) * 255)) + var rgb [3]byte + rgb[0] = r + rgb[1] = g + rgb[2] = b + grid[x][y] = rgb + } + } + return grid +} \ No newline at end of file diff --git a/internal/patterns/staticpatterns.go b/internal/patterns/staticpatterns.go new file mode 100755 index 0000000..78810d3 --- /dev/null +++ b/internal/patterns/staticpatterns.go @@ -0,0 +1,17 @@ +package patterns + +func AlternateRGBLinear(R0 byte, G0 byte, B0 byte, R1 byte, G1 byte, B1 byte, fromX int, toX int) []byte { + ret := make([]byte, toX*3) + for i := fromX; i < toX*3; i += 3 { + if Even(i) { + ret[i] = R0 + ret[i+1] = G0 + ret[i+2] = B0 + } else { + ret[i] = R1 + ret[i+1] = G1 + ret[i+2] = B1 + } + } + return ret +} \ No newline at end of file diff --git a/internal/queue/queue.go b/internal/queue/queue.go new file mode 100755 index 0000000..3a25e4a --- /dev/null +++ b/internal/queue/queue.go @@ -0,0 +1,7 @@ +package queue + +type QueueItem struct { + Effect string + Duration uint16 + Speed uint16 //only used by some patterns +} \ No newline at end of file diff --git a/internal/remapping/remapping.go b/internal/remapping/remapping.go new file mode 100755 index 0000000..31194df --- /dev/null +++ b/internal/remapping/remapping.go @@ -0,0 +1,88 @@ +package remapping + +type RGBcolor = [3]byte + +func Even(number int) bool { + return number %2 == 0 +} + +func Slice512(s []byte) [512]byte { + var ret [512]byte + for i := range s { + if i < 512 { + ret[i] = s[i] + } + } + return ret +} + +func SliceUnlenthed(s [512]byte) []byte { + ret := make([]byte, 512) + for i := range s { + ret[i] = s[i] + } + return ret +} + +func SliceRearrange(rowwidth int, rows int, alternaterows bool, inslice []byte) [][]byte { + var flippedslice []byte + if alternaterows { + flippedslice = make([]byte, len(inslice)+3) + for r := 0; r < rows; r++ { + rowzero := (r * rowwidth * 3) + if Even(r) { + for c := 0; c < (rowwidth)*3; c += 3 { + flippedslice[rowzero+c] = inslice[rowzero+c] + flippedslice[rowzero+c+1] = inslice[rowzero+c+1] + flippedslice[rowzero+c+2] = inslice[rowzero+c+2] + } + } else { + x := rowwidth * 3 + for c := 0; c < (rowwidth)*3; c += 3 { + x = x - 3 + flippedslice[rowzero+x] = inslice[rowzero+c] + flippedslice[rowzero+x+1] = inslice[rowzero+c+1] + flippedslice[rowzero+x+2] = inslice[rowzero+c+2] + } + } + } + } else { + flippedslice = inslice + } + currentUniverse := 0 + currentUniversePosition := 0 + universes := make([][]byte, (len(flippedslice)/510)+1) + currentUniverseSlice := make([]byte, 511) + currentRowReverse := false + for i := range flippedslice { + if currentUniversePosition >= 510 { + universes[currentUniverse] = currentUniverseSlice + if !currentRowReverse { + currentRowReverse = true + } else { + currentRowReverse = false + } + currentUniverse += 1 + currentUniversePosition = 0 + currentUniverseSlice = make([]byte, 511) + } + currentUniverseSlice[currentUniversePosition] = flippedslice[i] + currentUniversePosition += 1 + } + universes[currentUniverse] = currentUniverseSlice + return universes +} + +func XYGridToLinear(w int, h int, grid [][]RGBcolor) []byte { + ret := make([]byte, w*h*3) + i := 0 + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + ret[i] = grid[x][y][0] + ret[i+1] = grid[x][y][1] + ret[i+2] = grid[x][y][2] + i += 3 + } + } + return ret +} \ No newline at end of file diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go new file mode 100755 index 0000000..6ee788a --- /dev/null +++ b/internal/webserver/webserver.go @@ -0,0 +1,70 @@ +package webserver + +import ( + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + queue "git.martyn.berlin/martyn/LEDController/internal/queue" + + "fmt" + "net/http" + "os" + "time" + "strconv" +) + +func HealthHandler(response http.ResponseWriter, request *http.Request) { + response.Header().Add("Content-type", "text/plain") + fmt.Fprint(response, "I'm okay jack!") +} + +func NotFoundHandler(response http.ResponseWriter, request *http.Request) { + response.WriteHeader(404) + fmt.Fprint(response, "I'm lost!") +} + +func RootHandler(response http.ResponseWriter, request *http.Request) { + response.Header().Add("Content-type", "text/plain") + fmt.Fprint(response, "Not implemented") +} + +var globalQueue chan queue.QueueItem; + +func PatternHandler(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + response.Header().Add("Content-type", "text/plain") + var e queue.QueueItem + e.Effect = vars["pattern"] + i, _ := strconv.Atoi(vars["duration"]) + e.Duration = uint16(i) + _, found := vars["speed"] + if found { + i, _ := strconv.Atoi(vars["speed"]) + e.Speed = uint16(i) + fmt.Printf("Speed passed %d\n",e.Speed) + } else { + e.Speed = 40 + } + globalQueue <- e + fmt.Fprint(response, "OKAY") +} + +func HandleHTTP(queueChannel chan queue.QueueItem) { + globalQueue = queueChannel + r := mux.NewRouter() + loggedRouter := handlers.LoggingHandler(os.Stdout, r) + r.NotFoundHandler = http.HandlerFunc(NotFoundHandler) + r.HandleFunc("/", RootHandler) + r.HandleFunc("/healthz", HealthHandler) + r.HandleFunc("/pattern/{pattern}/{duration}", PatternHandler) + r.HandleFunc("/pattern/{pattern}/{duration}/{speed}", PatternHandler) + http.Handle("/", r) + srv := &http.Server{ + Handler: loggedRouter, + Addr: "0.0.0.0:5353", + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + fmt.Println("Listening on 0.0.0.0:5353") + srv.ListenAndServe() +} diff --git a/main.go b/main.go index 83cfe08..1213e67 100755 --- a/main.go +++ b/main.go @@ -1,154 +1,114 @@ package main import ( + "fmt" "log" "time" - "fmt" + + webserver "git.martyn.berlin/martyn/LEDController/internal/webserver" + queue "git.martyn.berlin/martyn/LEDController/internal/queue" + remapping "git.martyn.berlin/martyn/LEDController/internal/remapping" + patterns "git.martyn.berlin/martyn/LEDController/internal/patterns" "github.com/Hundemeier/go-sacn/sacn" ) -func even(number int) bool { - return number%2 == 0 -} +type RGBcolor = [3]byte -func slice_rearrange(rowwidth int, rows int, alternaterows bool, inslice []byte) [][]byte { - var flippedslice []byte - if alternaterows { - flippedslice = make([]byte, len(inslice)+3) - for r := 0; r < rows; r++ { - rowzero := (r*rowwidth*3) - if even(r) { - for c := 0; c < (rowwidth)*3; c+=3 { - flippedslice[rowzero+c] = inslice[rowzero+c] - flippedslice[rowzero+c+1] = inslice[rowzero+c+1] - flippedslice[rowzero+c+2] = inslice[rowzero+c+2] - } - } else { - x := rowwidth*3 - for c := 0; c < (rowwidth)*3; c+=3 { - x = x-3 - fmt.Println(x) - flippedslice[rowzero+x] = inslice[rowzero+c] - flippedslice[rowzero+x+1] = inslice[rowzero+c+1] - flippedslice[rowzero+x+2] = inslice[rowzero+c+2]/* - flippedslice[rowzero+c] = inslice[rowzero+(((rowwidth-1)*3)-c)] - flippedslice[rowzero+c+1] = inslice[rowzero+(((rowwidth-1)*3)-c)-1] - flippedslice[rowzero+c+2] = inslice[rowzero+(((rowwidth-1)*3)-c)-2]*/ - } - } - } - } else { - flippedslice = inslice - } - currentUniverse := 0 - currentUniversePosition := 0 - universes := make([][]byte, (len(flippedslice)/510)+1) - currentUniverseSlice := make([]byte, 511) - currentRowReverse := false; - for i := range(flippedslice) { - if currentUniversePosition >= 510 { - fmt.Println("Reached end of universe!") - universes[currentUniverse] = currentUniverseSlice - if (!currentRowReverse) { - currentRowReverse = true - } else { - currentRowReverse = false - } - currentUniverse += 1 - currentUniversePosition = 0 - currentUniverseSlice = make([]byte, 511) - } - currentUniverseSlice[currentUniversePosition] = flippedslice[i] - currentUniversePosition += 1 - } - universes[currentUniverse] = currentUniverseSlice - return universes -} +var currentFrame [2][512]byte +var channels [2]chan<-[512]byte +var sema = make(chan struct{}, 1) // a binary semaphore guarding currentFrame -func alternate(R0 byte, G0 byte, B0 byte, R1 byte, G1 byte, B1 byte, fromX int, toX int) []byte { - ret := make([]byte, toX*3); - for i := fromX; i < toX*3; i+=3 { - if even(i) { - ret[i] = R0 - ret[i+1] = G0 - ret[i+2] = B0 - } else { - ret[i] = R1 - ret[i+1] = G1 - ret[i+2] = B1 +var currentEffect queue.QueueItem; +var globalEffectChannel = make(chan queue.QueueItem, 1024) + +func foreverLoop() { + for /*ever*/ { + time.Sleep(40 * time.Millisecond) //25fps + for u := 0; u < 2; u++ { + sema <- struct{}{} // acquire token + channels[u] <- currentFrame[u] + <- sema } } - return ret -} - -// gradient returns from fromX to toX fading from (fromR,fromG,fromB) to (toR,toG,toB) -// at least that's the idea. -func gradient(fromR byte, fromG byte, fromB byte, toR byte, toG byte, toB byte, fromX int, toX int) []byte { - ret := make([]byte, toX*3); - var stepR float32 = (float32(toR) - float32(fromR)) / (float32(toX) - float32(fromX)) - var stepG float32 = (float32(toG) - float32(fromG)) / (float32(toX) - float32(fromX)) - var stepB float32 = (float32(toB) - float32(fromB)) / (float32(toX) - float32(fromX)) - for i := fromX; i < toX*3; i+=3 { - ret[i] = fromR+byte(float32(i/3)*stepR) - ret[i+1] = fromG+byte(float32(i/3)*stepG) - ret[i+2] = fromB+byte(float32(i/3)*stepB) - } - return ret -} - -func slice512(s []byte) [512]byte { - var ret [512]byte - for i := range(s) { - if i < 512 { - ret[i] = s[i] - } - } - return ret -} - -func sliceUnlenthed(s [512]byte) []byte { - ret := make([]byte, 512) - for i := range(s) { - ret[i] = s[i] - } - return ret } func main() { + go func() { + fmt.Printf("Starting webserver on port %s\n", "5353") + webserver.HandleHTTP(globalEffectChannel) + }() //instead of "" you could provide an ip-address that the socket should bind to - trans, err := sacn.NewTransmitter("", [16]byte{1, 2, 3}, "test") + trans, err := sacn.NewTransmitter("", [16]byte{1, 2}, "test") if err != nil { log.Fatal(err) } - - //activates the first universe - ch1, err := trans.Activate(1) - if err != nil { - log.Fatal(err) - } - ch2, err := trans.Activate(2) - if err != nil { - log.Fatal(err) - } - //deactivate the channel on exit - defer close(ch1) - defer close(ch2) - //set a unicast destination, and/or use multicast - trans.SetMulticast(1, false)//this specific setup will not multicast on windows, - //because no bind address was provided - - //set some example ip-addresses - trans.SetDestinations(1, []string{"192.168.1.139"}) - trans.SetMulticast(2, false)//this specific setup will not multicast on windows, - trans.SetDestinations(2, []string{"192.168.1.139"}) - - //send some random data for 10 seconds - for i := 0; i < 20; i++ { - channels := slice_rearrange(68,4,true,gradient(255,255,255,0,0,0,0,272)) - ch1 <- slice512(channels[0]) - ch2 <- slice512(channels[1]) - time.Sleep(500 * time.Millisecond) + //activates the universes + for i := 0; i < 2; i++ { + channels[i], err = trans.Activate(uint16(i+1)) + //deactivate the channel on exit + defer close(channels[i]) + trans.SetMulticast(uint16(i+1), false) //this specific setup will not multicast on windows, + trans.SetDestinations(uint16(i+1), []string{"192.168.1.139"}) } -} \ No newline at end of file + + // Plasma for start frame + rearranged := remapping.SliceRearrange(68, 4, true, remapping.XYGridToLinear(68, 4, patterns.PlasmaPanel(68, 4, 40))) + currentFrame[0] = remapping.Slice512(rearranged[0]) + currentFrame[1] = remapping.Slice512(rearranged[1]) + var e queue.QueueItem + e.Effect = "red" + e.Duration = 40 * 50 + globalEffectChannel <- e + e.Effect = "plasma" + e.Duration = 0 + e.Speed = 40 + globalEffectChannel <- e + + go func() { + for /*ever*/ { + if currentEffect.Duration > 0 { + currentEffect.Duration -= 40 + } else { + if len(globalEffectChannel) > 0 { + currentEffect = <- globalEffectChannel + } + } + var rearranged [][]byte + switch currentEffect.Effect { + case "line": + rearranged = remapping.SliceRearrange(68, 4, true, remapping.XYGridToLinear(68, 4, patterns.ZigZag(68, 4))) + case "plasma": + rearranged = remapping.SliceRearrange(68, 4, true, remapping.XYGridToLinear(68, 4, patterns.PlasmaPanel(68, 4, currentEffect.Speed))) + case "red": + rearranged = remapping.SliceRearrange(68, 4, false, remapping.XYGridToLinear(68, 4, patterns.RedPanel(68,4))) + case "random": + rearranged = remapping.SliceRearrange(68, 4, false, remapping.XYGridToLinear(68, 4, patterns.RandomColourPanel(68,4,currentEffect.Speed))) + case "linearplasma": + rearranged = remapping.SliceRearrange(68, 4, false, patterns.LinearPlasma(68*4)) + case "gradientred": + rearranged = remapping.SliceRearrange(68, 4, false, patterns.Gradient(255, 0, 0, 0, 0, 255, 0, 68*4)) + case "sine": + var black patterns.RGBcolor + var red patterns.RGBcolor + red[0] = 255 + rearranged = remapping.SliceRearrange(68, 4, true, remapping.XYGridToLinear(68, 4, patterns.Sinewave(68, 4, black, red, currentEffect.Speed))) + case "sinechase": + var black patterns.RGBcolor + var red patterns.RGBcolor + red[0] = 255 + rearranged = remapping.SliceRearrange(68, 4, true, remapping.XYGridToLinear(68, 4, patterns.SineChase(68, 4, black, red, currentEffect.Speed))) + default: + rearranged = remapping.SliceRearrange(68, 4, false, remapping.XYGridToLinear(68, 4, patterns.FillPanel(68,4, 128,0,128))) + } + //rearranged := remapping.SliceRearrange(68,4,true,linearPlasma(68*4)) + sema <- struct{}{} // acquire token + currentFrame[0] = remapping.Slice512(rearranged[0]) + currentFrame[1] = remapping.Slice512(rearranged[1]) + <- sema + time.Sleep(40 * time.Millisecond) + } + }() + foreverLoop(); +}