From 4480dcc97ac69b339d01d342522f04a62dca2032 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sun, 15 Apr 2018 14:59:16 -0700 Subject: [PATCH] Add initial LED implementation for an E1.31 DMX over Ethernet controller. --- field/arena.go | 38 ++-- led/led_strip.go | 343 +++++++++++++++++++++++++++++++++++++ templates/setup_field.html | 141 +++++++-------- web/setup_field.go | 56 +----- web/setup_field_test.go | 4 +- 5 files changed, 427 insertions(+), 155 deletions(-) create mode 100644 led/led_strip.go diff --git a/field/arena.go b/field/arena.go index ab2a533..318b13e 100644 --- a/field/arena.go +++ b/field/arena.go @@ -8,6 +8,7 @@ package field import ( "fmt" "github.com/Team254/cheesy-arena/game" + "github.com/Team254/cheesy-arena/led" "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/partner" "log" @@ -56,7 +57,6 @@ type Arena struct { AllianceStationDisplays map[string]string AllianceStationDisplayScreen string MuteMatchSounds bool - FieldTestMode string matchAborted bool matchStateNotifier *Notifier MatchTimeNotifier *Notifier @@ -71,6 +71,7 @@ type Arena struct { AllianceSelectionNotifier *Notifier LowerThirdNotifier *Notifier ReloadDisplaysNotifier *Notifier + RedSwitchLedStrip *led.LedStrip scale *game.Seesaw redSwitch *game.Seesaw blueSwitch *game.Seesaw @@ -143,6 +144,12 @@ func NewArena(dbPath string) (*Arena, error) { arena.AllianceStationDisplays = make(map[string]string) arena.AllianceStationDisplayScreen = "match" + // Initialize LEDs. + arena.RedSwitchLedStrip, err = led.NewLedStrip("10.0.0.30", 1, 150) + if err != nil { + return nil, err + } + return arena, nil } @@ -374,8 +381,8 @@ func (arena *Arena) Update() { enabled = false arena.AudienceDisplayScreen = "match" arena.AudienceDisplayNotifier.Notify(nil) - arena.FieldTestMode = "" arena.sendGameSpecificDataPacket() + arena.RedSwitchLedStrip.SetMode(led.WarmupMode) if !arena.MuteMatchSounds { arena.PlaySoundNotifier.Notify("match-warmup") } @@ -471,6 +478,8 @@ func (arena *Arena) Update() { // Handle field sensors/lights/motors. arena.handlePlcInput() arena.handlePlcOutput() + + arena.RedSwitchLedStrip.Update() } // Loops indefinitely to track and update the arena components. @@ -694,30 +703,7 @@ func (arena *Arena) handlePlcInput() { // Writes light/motor commands to the field PLC. func (arena *Arena) handlePlcOutput() { - if arena.FieldTestMode != "" { - // PLC output is being manually overridden. - // TODO(patrick): Update for 2018. - /* - if arena.FieldTestMode == "flash" { - blinkState := arena.Plc.GetCycleState(2, 0, 1) - arena.Plc.SetTouchpadLights([3]bool{blinkState, blinkState, blinkState}, - [3]bool{blinkState, blinkState, blinkState}) - } else if arena.FieldTestMode == "cycle" { - arena.Plc.SetTouchpadLights( - [3]bool{arena.Plc.GetCycleState(3, 2, 1), arena.Plc.GetCycleState(3, 1, 1), arena.Plc.GetCycleState(3, 0, 1)}, - [3]bool{arena.Plc.GetCycleState(3, 0, 1), arena.Plc.GetCycleState(3, 1, 1), arena.Plc.GetCycleState(3, 2, 1)}) - } else if arena.FieldTestMode == "chase" { - arena.Plc.SetTouchpadLights( - [3]bool{arena.Plc.GetCycleState(12, 2, 2), arena.Plc.GetCycleState(12, 1, 2), arena.Plc.GetCycleState(12, 0, 2)}, - [3]bool{arena.Plc.GetCycleState(12, 3, 2), arena.Plc.GetCycleState(12, 4, 2), arena.Plc.GetCycleState(12, 5, 2)}) - } else if arena.FieldTestMode == "slowChase" { - arena.Plc.SetTouchpadLights( - [3]bool{arena.Plc.GetCycleState(6, 2, 8), arena.Plc.GetCycleState(6, 1, 8), arena.Plc.GetCycleState(6, 0, 8)}, - [3]bool{arena.Plc.GetCycleState(6, 3, 8), arena.Plc.GetCycleState(6, 4, 8), arena.Plc.GetCycleState(6, 5, 8)}) - } - return - */ - } + // TODO(patrick): Update for 2018. } func (arena *Arena) handleEstop(station string, state bool) { diff --git a/led/led_strip.go b/led/led_strip.go new file mode 100644 index 0000000..e22a6d1 --- /dev/null +++ b/led/led_strip.go @@ -0,0 +1,343 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Represents a LED strip attached to an E1.31 sACN (DMX over Ethernet) controller. + +package led + +import ( + "fmt" + "math/rand" + "net" + "time" +) + +const ( + controllerPort = 5568 + sourceName = "Cheesy Arena" + packetTimeoutSec = 1 +) + +// LED sequence modes +const ( + OffMode = iota + RedMode + GreenMode + BlueMode + WhiteMode + ChaseMode + WarmupMode + RandomMode + FadeMode +) + +// Color RGB mappings +const ( + red = iota + orange + yellow + green + teal + blue + purple + white + black +) +var colors = [][3]byte{ + {255, 0, 0}, // Red + {255, 50, 0}, // Orange + {255, 255, 0}, // Yellow + {0, 255, 0}, // Green + {0, 100, 100}, // Teal + {0, 0, 255}, // Blue + {100, 0, 100}, // Purple + {255, 255, 255}, // White + {0, 0, 0}, // Black +} + +type LedStrip struct { + Mode int + conn net.Conn + pixels [][3]byte + oldPixels [][3]byte + packet []byte + counter int + lastPacketTime time.Time +} + +func NewLedStrip(controllerAddress string, dmxUniverse int, numPixels int) (*LedStrip, error) { + ledStrip := new(LedStrip) + + var err error + ledStrip.conn, err = net.Dial("udp4", fmt.Sprintf("%s:%d", controllerAddress, controllerPort)) + if err != nil { + return nil, err + } + + ledStrip.pixels = make([][3]byte, numPixels) + ledStrip.oldPixels = make([][3]byte, numPixels) + ledStrip.packet = createBlankPacket(dmxUniverse, numPixels) + + return ledStrip, nil +} + +// Sets the current LED sequence mode and resets the intra-sequence counter to the beginning. +func (strip *LedStrip) SetMode(mode int) { + strip.Mode = mode + strip.counter = 0 +} + +// Advances the pixel values through the current sequence and sends a packet if necessary. Should be called from a timed +// loop. +func (strip *LedStrip) Update() error { + // Determine the pixel values. + switch strip.Mode { + case RedMode: + strip.updateSingleColorMode(red) + case GreenMode: + strip.updateSingleColorMode(green) + case BlueMode: + strip.updateSingleColorMode(blue) + case WhiteMode: + strip.updateSingleColorMode(white) + case ChaseMode: + strip.updateChaseMode() + case WarmupMode: + strip.updateWarmupMode() + case RandomMode: + strip.updateRandomMode() + case FadeMode: + strip.updateFadeMode() + default: + strip.updateOffMode() + } + strip.counter++ + + // Update non-static packet fields. + strip.packet[111]++ + for i, pixel := range strip.pixels { + strip.packet[126+3*i] = pixel[0] + strip.packet[127+3*i] = pixel[1] + strip.packet[128+3*i] = pixel[2] + } + + // Send the packet if it hasn't changed. + if strip.shouldSendPacket() { + _, err := strip.conn.Write(strip.packet) + if err != nil { + return err + } + strip.lastPacketTime = time.Now() + + // Keep a record of the pixel values in order to detect future changes. + copy(strip.oldPixels, strip.pixels) + } + + return nil +} + +// Constructs the structure of an E1.31 data packet that can be re-used indefinitely by updating the pixel data and +// re-sending it. +func createBlankPacket(dmxUniverse, numPixels int) []byte { + size := 126 + 3*numPixels + packet := make([]byte, size) + + // Preamble size + packet[0] = 0x00 + packet[1] = 0x10 + + // Postamble size + packet[2] = 0x00 + packet[3] = 0x00 + + // ACN packet identifier + packet[4] = 0x41 + packet[5] = 0x53 + packet[6] = 0x43 + packet[7] = 0x2d + packet[8] = 0x45 + packet[9] = 0x31 + packet[10] = 0x2e + packet[11] = 0x31 + packet[12] = 0x37 + packet[13] = 0x00 + packet[14] = 0x00 + packet[15] = 0x00 + + // Root PDU length and flags + rootPduLength := size - 16 + packet[16] = 0x70 | byte(rootPduLength>>8) + packet[17] = byte(rootPduLength & 0xff) + + // E1.31 vector indicating that this is a data packet + packet[18] = 0x00 + packet[19] = 0x00 + packet[20] = 0x00 + packet[21] = 0x04 + + // Component ID + for i, b := range []byte(sourceName) { + packet[22+i] = b + } + + // Framing PDU length and flags + framingPduLength := size - 38 + packet[38] = 0x70 | byte(framingPduLength>>8) + packet[39] = byte(framingPduLength & 0xff) + + // E1.31 vector indicating that this is a data packet + packet[40] = 0x00 + packet[41] = 0x00 + packet[42] = 0x00 + packet[43] = 0x02 + + // Source name + for i, b := range []byte(sourceName) { + packet[44+i] = b + } + + // Priority + packet[108] = 100 + + // Universe for synchronization packets + packet[109] = 0x00 + packet[110] = 0x00 + + // Sequence number (initial value) + packet[111] = 0x00 + + // Options flags + packet[112] = 0x00 + + // Universe + packet[113] = byte(dmxUniverse >> 8) + packet[114] = byte(dmxUniverse & 0xff) + + // DMP layer PDU length + dmpPduLength := size - 115 + packet[115] = 0x70 | byte(dmpPduLength>>8) + packet[116] = byte(dmpPduLength & 0xff) + + // E1.31 vector indicating set property + packet[117] = 0x02 + + // Address and data type + packet[118] = 0xa1 + + // First property address + packet[119] = 0x00 + packet[120] = 0x00 + + // Address increment + packet[121] = 0x00 + packet[122] = 0x01 + + // Property value count + count := 1 + 3*numPixels + packet[123] = byte(count >> 8) + packet[124] = byte(count & 0xff) + + // DMX start code + packet[125] = 0 + + return packet +} + +// Returns true if the pixel data has changed or it has been too long since the last packet was sent. +func (strip *LedStrip) shouldSendPacket() bool { + for i := 0; i < len(strip.pixels); i++ { + if strip.pixels[i] != strip.oldPixels[i] { + return true + } + } + return time.Since(strip.lastPacketTime).Seconds() > packetTimeoutSec +} + +func (strip *LedStrip) updateOffMode() { + for i := 0; i < len(strip.pixels); i++ { + strip.pixels[i] = colors[black] + } +} + +func (strip *LedStrip) updateSingleColorMode(color int) { + for i := 0; i < len(strip.pixels); i++ { + strip.pixels[i] = colors[color] + } +} + +func (strip *LedStrip) updateChaseMode() { + if strip.counter == len(colors)*len(strip.pixels) { + strip.counter = 0 + } + color := strip.counter / len(strip.pixels) + pixelIndex := strip.counter % len(strip.pixels) + strip.pixels[pixelIndex] = colors[color] +} + +func (strip *LedStrip) updateWarmupMode() { + endCounter := 250 + if strip.counter == 0 { + // Show solid white to start. + for i := 0; i < len(strip.pixels); i++ { + strip.pixels[i] = colors[white] + } + } else if strip.counter <= endCounter { + // Build to the alliance color from each side. + numLitPixels := len(strip.pixels) / 2 * strip.counter / endCounter + for i := 0; i < numLitPixels; i++ { + strip.pixels[i] = colors[red] + strip.pixels[len(strip.pixels)-i-1] = colors[red] + } + } else { + // MaintainPrevent the counter from rolling over. + strip.counter = endCounter + } +} + +func (strip *LedStrip) updateRandomMode() { + if strip.counter%10 != 0 { + return + } + for i := 0; i < len(strip.pixels); i++ { + strip.pixels[i] = colors[rand.Intn(len(colors))] + } +} + +func (strip *LedStrip) updateFadeMode() { + fadeCycles := 40 + holdCycles := 10 + if strip.counter == 4*holdCycles+4*fadeCycles { + strip.counter = 0 + } + + for i := 0; i < len(strip.pixels); i++ { + if strip.counter < holdCycles { + strip.pixels[i] = colors[black] + } else if strip.counter < holdCycles+fadeCycles { + strip.pixels[i] = getFadeColor(black, red, strip.counter-holdCycles, fadeCycles) + } else if strip.counter < 2*holdCycles+fadeCycles { + strip.pixels[i] = colors[red] + } else if strip.counter < 2*holdCycles+2*fadeCycles { + strip.pixels[i] = getFadeColor(red, black, strip.counter-2*holdCycles-fadeCycles, fadeCycles) + } else if strip.counter < 3*holdCycles+2*fadeCycles { + strip.pixels[i] = colors[black] + } else if strip.counter < 3*holdCycles+3*fadeCycles { + strip.pixels[i] = getFadeColor(black, blue, strip.counter-3*holdCycles-2*fadeCycles, fadeCycles) + } else if strip.counter < 4*holdCycles+3*fadeCycles { + strip.pixels[i] = colors[blue] + } else if strip.counter < 4*holdCycles+4*fadeCycles { + strip.pixels[i] = getFadeColor(blue, black, strip.counter-4*holdCycles-3*fadeCycles, fadeCycles) + } + } +} + +// Interpolates between the two colors based on the given fraction. +func getFadeColor(fromColor, toColor, numerator, denominator int) [3]byte { + from := colors[fromColor] + to := colors[toColor] + var fadeColor [3]byte + for i := 0; i < 3; i++ { + fadeColor[i] = byte(int(from[i]) + numerator*(int(to[i])-int(from[i]))/denominator) + } + return fadeColor +} diff --git a/templates/setup_field.html b/templates/setup_field.html index e2f79ad..0b851a6 100644 --- a/templates/setup_field.html +++ b/templates/setup_field.html @@ -7,7 +7,7 @@ {{define "title"}}Field Configuration{{end}} {{define "body"}}
-
+
Alliance Station Displays {{range $displayId, $station := .AllianceStationDisplays}} @@ -38,82 +38,6 @@
PLC -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
@@ -157,6 +81,69 @@ +
+
+ LEDs +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
{{end}} {{define "script"}} diff --git a/web/setup_field.go b/web/setup_field.go index 3efd091..ce046a2 100644 --- a/web/setup_field.go +++ b/web/setup_field.go @@ -9,6 +9,7 @@ import ( "github.com/Team254/cheesy-arena/field" "github.com/Team254/cheesy-arena/model" "net/http" + "strconv" ) // Shows the field configuration page. @@ -25,12 +26,12 @@ func (web *Web) fieldGetHandler(w http.ResponseWriter, r *http.Request) { data := struct { *model.EventSettings AllianceStationDisplays map[string]string - FieldTestMode string Inputs []bool Counters []uint16 Coils []bool - }{web.arena.EventSettings, web.arena.AllianceStationDisplays, web.arena.FieldTestMode, web.arena.Plc.Inputs[:], - web.arena.Plc.Registers[:], web.arena.Plc.Coils[:]} + LedMode int + }{web.arena.EventSettings, web.arena.AllianceStationDisplays, web.arena.Plc.Inputs[:], + web.arena.Plc.Registers[:], web.arena.Plc.Coils[:], web.arena.RedSwitchLedStrip.Mode} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -72,53 +73,8 @@ func (web *Web) fieldTestPostHandler(w http.ResponseWriter, r *http.Request) { return } - // TODO(patrick): Update for 2018. - mode := r.PostFormValue("mode") - /* - switch mode { - case "boiler": - web.arena.Plc.SetBoilerMotors(true) - web.arena.Plc.SetRotorMotors(0, 0) - web.arena.Plc.SetRotorLights(0, 0) - web.arena.Plc.SetTouchpadLights([3]bool{false, false, false}, [3]bool{false, false, false}) - case "rotor1": - web.arena.Plc.SetBoilerMotors(false) - web.arena.Plc.SetRotorMotors(1, 1) - web.arena.Plc.SetRotorLights(1, 1) - web.arena.Plc.SetTouchpadLights([3]bool{true, false, false}, [3]bool{true, false, false}) - case "rotor2": - web.arena.Plc.SetBoilerMotors(false) - web.arena.Plc.SetRotorMotors(2, 2) - web.arena.Plc.SetRotorLights(2, 2) - web.arena.Plc.SetTouchpadLights([3]bool{false, true, false}, [3]bool{false, true, false}) - case "rotor3": - web.arena.Plc.SetBoilerMotors(false) - web.arena.Plc.SetRotorMotors(3, 3) - web.arena.Plc.SetRotorLights(2, 2) - web.arena.Plc.SetTouchpadLights([3]bool{false, false, true}, [3]bool{false, false, true}) - case "rotor4": - web.arena.Plc.SetBoilerMotors(false) - web.arena.Plc.SetRotorMotors(4, 4) - web.arena.Plc.SetRotorLights(2, 2) - web.arena.Plc.SetTouchpadLights([3]bool{false, false, false}, [3]bool{false, false, false}) - case "red": - web.arena.Plc.SetBoilerMotors(false) - web.arena.Plc.SetRotorMotors(4, 0) - web.arena.Plc.SetRotorLights(2, 0) - web.arena.Plc.SetTouchpadLights([3]bool{true, true, true}, [3]bool{false, false, false}) - case "blue": - web.arena.Plc.SetBoilerMotors(false) - web.arena.Plc.SetRotorMotors(0, 4) - web.arena.Plc.SetRotorLights(0, 2) - web.arena.Plc.SetTouchpadLights([3]bool{false, false, false}, [3]bool{true, true, true}) - default: - web.arena.Plc.SetBoilerMotors(false) - web.arena.Plc.SetRotorMotors(0, 0) - web.arena.Plc.SetRotorLights(0, 0) - web.arena.Plc.SetTouchpadLights([3]bool{false, false, false}, [3]bool{false, false, false}) - } - */ + mode, _ := strconv.Atoi(r.PostFormValue("mode")) + web.arena.RedSwitchLedStrip.SetMode(mode) - web.arena.FieldTestMode = mode http.Redirect(w, r, "/setup/field", 303) } diff --git a/web/setup_field_test.go b/web/setup_field_test.go index 807409d..7a92bf6 100644 --- a/web/setup_field_test.go +++ b/web/setup_field_test.go @@ -23,7 +23,7 @@ func TestSetupField(t *testing.T) { assert.Contains(t, recorder.Body.String(), "12345") assert.Contains(t, recorder.Body.String(), "selected") - recorder = web.postHttpResponse("/setup/field/test", "mode=rotor2") + recorder = web.postHttpResponse("/setup/field/test", "mode=1") assert.Equal(t, 303, recorder.Code) - assert.Equal(t, "rotor2", web.arena.FieldTestMode) + assert.Equal(t, 1, web.arena.RedSwitchLedStrip.Mode) }