diff --git a/db/migrations/20140524160241_CreateEventSettings.sql b/db/migrations/20140524160241_CreateEventSettings.sql index 6501c68..61ba51d 100644 --- a/db/migrations/20140524160241_CreateEventSettings.sql +++ b/db/migrations/20140524160241_CreateEventSettings.sql @@ -26,7 +26,10 @@ CREATE TABLE event_settings ( adminpassword VARCHAR(255), readerpassword VARCHAR(255), stemtvpublishingenabled bool, - stemtveventcode VARCHAR(16) + stemtveventcode VARCHAR(16), + scaleledaddress VARCHAR(255), + redswitchledaddress VARCHAR(255), + blueswitchledaddress VARCHAR(255) ); -- +goose Down diff --git a/field/arena.go b/field/arena.go index 28f5341..c8d6961 100644 --- a/field/arena.go +++ b/field/arena.go @@ -73,7 +73,9 @@ type Arena struct { AllianceSelectionNotifier *Notifier LowerThirdNotifier *Notifier ReloadDisplaysNotifier *Notifier - RedSwitchLedStrip *led.LedStrip + ScaleLeds led.Controller + RedSwitchLeds led.Controller + BlueSwitchLeds led.Controller scale *game.Seesaw redSwitch *game.Seesaw blueSwitch *game.Seesaw @@ -146,12 +148,6 @@ 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 } @@ -171,9 +167,21 @@ func (arena *Arena) LoadSettings() error { arena.TbaClient = partner.NewTbaClient(settings.TbaEventCode, settings.TbaSecretId, settings.TbaSecret) arena.StemTvClient = partner.NewStemTvClient(settings.StemTvEventCode) - err = arena.accessPoint.ConfigureAdminWifi() - if err != nil { - return nil + if arena.EventSettings.NetworkSecurityEnabled { + if err = arena.accessPoint.ConfigureAdminWifi(); err != nil { + return err + } + } + + // Initialize LEDs. + if err = arena.ScaleLeds.SetAddress(settings.ScaleLedAddress); err != nil { + return err + } + if err = arena.RedSwitchLeds.SetAddress(settings.RedSwitchLedAddress); err != nil { + return err + } + if err = arena.BlueSwitchLeds.SetAddress(settings.BlueSwitchLedAddress); err != nil { + return err } return nil @@ -224,6 +232,14 @@ func (arena *Arena) LoadMatch(match *model.Match) error { arena.blueVault = new(game.Vault) game.ResetPowerUps() + // Set a consistent initial value for field element sidedness. + arena.scale.SetSidedness(true) + arena.redSwitch.SetSidedness(true) + arena.blueSwitch.SetSidedness(true) + arena.ScaleLeds.SetSidedness(true) + arena.RedSwitchLeds.SetSidedness(true) + arena.BlueSwitchLeds.SetSidedness(true) + // Notify any listeners about the new match. arena.MatchLoadTeamsNotifier.Notify(nil) arena.RealtimeScoreNotifier.Notify(nil) @@ -301,6 +317,16 @@ func (arena *Arena) StartMatch() error { arena.CurrentMatch.GameSpecificData = game.GenerateGameSpecificData() } + // Configure the field elements with the game-specific data. + switchNearIsRed := arena.CurrentMatch.GameSpecificData[0] == 'L' + scaleNearIsRed := arena.CurrentMatch.GameSpecificData[1] == 'L' + arena.scale.SetSidedness(scaleNearIsRed) + arena.redSwitch.SetSidedness(switchNearIsRed) + arena.blueSwitch.SetSidedness(switchNearIsRed) + arena.ScaleLeds.SetSidedness(scaleNearIsRed) + arena.RedSwitchLeds.SetSidedness(switchNearIsRed) + arena.BlueSwitchLeds.SetSidedness(switchNearIsRed) + // Save the match start time and game-specifc data to the database for posterity. arena.CurrentMatch.StartedAt = time.Now() if arena.CurrentMatch.Type != "test" { @@ -384,7 +410,9 @@ func (arena *Arena) Update() { arena.AudienceDisplayScreen = "match" arena.AudienceDisplayNotifier.Notify(nil) arena.sendGameSpecificDataPacket() - arena.RedSwitchLedStrip.SetMode(led.WarmupMode) + arena.ScaleLeds.SetMode(led.WarmupMode, led.WarmupMode) + arena.RedSwitchLeds.SetMode(led.WarmupMode, led.WarmupMode) + arena.BlueSwitchLeds.SetMode(led.WarmupMode, led.WarmupMode) if !arena.MuteMatchSounds { arena.PlaySoundNotifier.Notify("match-warmup") } @@ -481,7 +509,9 @@ func (arena *Arena) Update() { arena.handlePlcInput() arena.handlePlcOutput() - arena.RedSwitchLedStrip.Update() + arena.ScaleLeds.Update() + arena.RedSwitchLeds.Update() + arena.BlueSwitchLeds.Update() } // Loops indefinitely to track and update the arena components. diff --git a/game/seesaw.go b/game/seesaw.go index cb6bb46..fd3dbf4 100644 --- a/game/seesaw.go +++ b/game/seesaw.go @@ -32,7 +32,7 @@ type Ownership struct { // Sets which side of the scale or switch belongs to which alliance. A value of true indicates that the side nearest the // scoring table is red. -func (seesaw *Seesaw) SetRandomization(nearIsRed bool) { +func (seesaw *Seesaw) SetSidedness(nearIsRed bool) { seesaw.nearIsRed = nearIsRed } diff --git a/game/seesaw_test.go b/game/seesaw_test.go index 4d9e94f..8326ab6 100644 --- a/game/seesaw_test.go +++ b/game/seesaw_test.go @@ -32,7 +32,7 @@ func TestSecondCounting(t *testing.T) { ResetPowerUps() redSwitch := &Seesaw{kind: redAlliance} - redSwitch.SetRandomization(true) + redSwitch.SetSidedness(true) // Test that there is no accumulation before the start of the match. redSwitch.UpdateState([2]bool{true, false}, timeAfterStart(-20)) @@ -70,9 +70,9 @@ func TestForce(t *testing.T) { ResetPowerUps() blueSwitch := &Seesaw{kind: blueAlliance} - blueSwitch.SetRandomization(true) + blueSwitch.SetSidedness(true) scale := &Seesaw{kind: neitherAlliance} - scale.SetRandomization(true) + scale.SetSidedness(true) // Force switch only. blueSwitch.UpdateState([2]bool{true, false}, timeAfterStart(0)) @@ -115,9 +115,9 @@ func TestBoost(t *testing.T) { ResetPowerUps() blueSwitch := &Seesaw{kind: blueAlliance} - blueSwitch.SetRandomization(true) + blueSwitch.SetSidedness(true) scale := &Seesaw{kind: neitherAlliance} - scale.SetRandomization(false) + scale.SetSidedness(false) // Test within continuous ownership period. blueSwitch.UpdateState([2]bool{false, true}, timeAfterStart(20)) diff --git a/led/color.go b/led/color.go new file mode 100644 index 0000000..db3e72e --- /dev/null +++ b/led/color.go @@ -0,0 +1,40 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Contains pixel RGB mappings for common colors. + +package led + +type color int + +const ( + red color = iota + orange + yellow + green + teal + blue + purple + white + black + purpleRed + purpleBlue + dimRed + dimBlue +) + +var colors = map[color][3]byte{ + red: {255, 0, 0}, + orange: {255, 50, 0}, + yellow: {255, 255, 0}, + green: {0, 255, 0}, + teal: {0, 100, 100}, + blue: {0, 0, 255}, + purple: {100, 0, 100}, + white: {255, 255, 255}, + black: {0, 0, 0}, + purpleRed: {200, 0, 50}, + purpleBlue: {50, 0, 200}, + dimRed: {100, 0, 0}, + dimBlue: {0, 0, 100}, +} diff --git a/led/controller.go b/led/controller.go new file mode 100644 index 0000000..fd78645 --- /dev/null +++ b/led/controller.go @@ -0,0 +1,225 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Represents an E1.31 sACN (DMX over Ethernet) LED controller with four outputs. + +package led + +import ( + "fmt" + "log" + "net" +) + +const ( + port = 5568 + sourceName = "Cheesy Arena" + packetTimeoutSec = 1 + numPixels = 150 + pixelDataOffset = 126 + nearStrip1Universe = 1 + nearStrip2Universe = 2 + farStrip1Universe = 3 + farStrip2Universe = 4 +) + +type Controller struct { + nearStrip strip + farStrip strip + conn net.Conn + packet []byte +} + +func (controller *Controller) SetAddress(address string) error { + if controller.conn != nil { + controller.conn.Close() + controller.conn = nil + } + + if address != "" { + var err error + if controller.conn, err = net.Dial("udp4", fmt.Sprintf("%s:%d", address, port)); err != nil { + return err + } + } + + return nil +} + +// Sets the current LED sequence mode and resets the intra-sequence counter to the beginning. +func (controller *Controller) SetMode(nearMode, farMode Mode) { + controller.nearStrip.currentMode = nearMode + controller.nearStrip.counter = 0 + controller.farStrip.currentMode = farMode + controller.farStrip.counter = 0 +} + +// Returns the current mode if both sides are in the same mode, or off otherwise. +func (controller *Controller) GetCurrentMode() Mode { + if controller.nearStrip.currentMode == controller.farStrip.currentMode { + return controller.nearStrip.currentMode + } else { + return OffMode + } +} + +// Sets which side of the scale or switch belongs to which alliance. A value of true indicates that the side nearest the +// scoring table is red. +func (controller *Controller) SetSidedness(nearIsRed bool) { + controller.nearStrip.isRed = nearIsRed + controller.farStrip.isRed = !nearIsRed +} + +// Advances the pixel values through the current sequence and sends a packet if necessary. Should be called from a timed +// loop. +func (controller *Controller) Update() error { + if controller.conn == nil { + // This controller is not configured; do nothing. + return nil + } + + controller.nearStrip.updatePixels() + controller.farStrip.updatePixels() + + // Create the template packet if it doesn't already exist. + if len(controller.packet) == 0 { + controller.packet = createBlankPacket(numPixels) + } + + // Send packets if the pixel values have changed. + if controller.nearStrip.shouldSendPacket() { + controller.nearStrip.populatePacketPixels(controller.packet[pixelDataOffset:]) + controller.sendPacket(nearStrip1Universe) + controller.sendPacket(nearStrip2Universe) + } + if controller.farStrip.shouldSendPacket() { + controller.farStrip.populatePacketPixels(controller.packet[pixelDataOffset:]) + controller.sendPacket(farStrip1Universe) + controller.sendPacket(farStrip2Universe) + } + + 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(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; will be updated whenever packet is sent) + packet[111] = 0x00 + + // Options flags + packet[112] = 0x00 + + // DMX universe (will be populated whenever packet is sent) + packet[113] = 0x00 + packet[114] = 0x00 + + // 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 + + // Remainder of packet is pixel data which will be populated whenever packet is sent. + return packet +} + +func (controller *Controller) sendPacket(dmxUniverse int) error { + log.Printf("Sending packet for universe %d\n", dmxUniverse) + // Update non-static packet fields. + controller.packet[111]++ + controller.packet[113] = byte(dmxUniverse >> 8) + controller.packet[114] = byte(dmxUniverse & 0xff) + + _, err := controller.conn.Write(controller.packet) + if err != nil { + return err + } + + return nil +} diff --git a/led/led_strip.go b/led/led_strip.go deleted file mode 100644 index 89ceab2..0000000 --- a/led/led_strip.go +++ /dev/null @@ -1,535 +0,0 @@ -// 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 -type Mode int - -const ( - OffMode Mode = iota - RedMode - GreenMode - BlueMode - WhiteMode - ChaseMode - WarmupMode - Warmup2Mode - Warmup3Mode - Warmup4Mode - OwnedMode - ForceMode - BoostMode - RandomMode - FadeMode - GradientMode - BlinkMode -) - -var ModeNames = map[Mode]string{ - OffMode: "Off", - RedMode: "Red", - GreenMode: "Green", - BlueMode: "Blue", - WhiteMode: "White", - ChaseMode: "Chase", - WarmupMode: "Warmup", - Warmup2Mode: "Warmup Purple", - Warmup3Mode: "Warmup Sneaky", - Warmup4Mode: "Warmup Gradient", - OwnedMode: "Owned", - ForceMode: "Force", - BoostMode: "Boost", - RandomMode: "Random", - FadeMode: "Fade", - GradientMode: "Gradient", - BlinkMode: "Blink", -} - -// Color RGB mappings -type color int - -const ( - red color = iota - orange - yellow - green - teal - blue - purple - white - black - purpleRed - purpleBlue - dimRed - dimBlue -) - -var colors = map[color][3]byte{ - red: {255, 0, 0}, - orange: {255, 50, 0}, - yellow: {255, 255, 0}, - green: {0, 255, 0}, - teal: {0, 100, 100}, - blue: {0, 0, 255}, - purple: {100, 0, 100}, - white: {255, 255, 255}, - black: {0, 0, 0}, - purpleRed: {200, 0, 50}, - purpleBlue: {50, 0, 200}, - dimRed: {100, 0, 0}, - dimBlue: {0, 0, 100}, -} - -type LedStrip struct { - CurrentMode Mode - 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 Mode) { - strip.CurrentMode = 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.CurrentMode { - 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 Warmup2Mode: - strip.updateWarmup2Mode() - case Warmup3Mode: - strip.updateWarmup3Mode() - case Warmup4Mode: - strip.updateWarmup4Mode() - case OwnedMode: - strip.updateOwnedMode() - case ForceMode: - strip.updateForceMode() - case BoostMode: - strip.updateBoostMode() - case RandomMode: - strip.updateRandomMode() - case FadeMode: - strip.updateFadeMode() - case GradientMode: - strip.updateGradientMode() - case BlinkMode: - strip.updateBlinkMode() - 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 color) { - 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 := 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) updateWarmup2Mode() { - startCounter := 100 - endCounter := 250 - if strip.counter < startCounter { - // Show solid purple to start. - for i := 0; i < len(strip.pixels); i++ { - strip.pixels[i] = colors[purple] - } - } else if strip.counter <= endCounter { - for i := 0; i < len(strip.pixels); i++ { - strip.pixels[i] = getFadeColor(purple, red, strip.counter-startCounter, endCounter-startCounter) - } - } else { - // MaintainPrevent the counter from rolling over. - strip.counter = endCounter - } -} - -func (strip *LedStrip) updateWarmup3Mode() { - startCounter := 50 - middleCounter := 225 - endCounter := 250 - if strip.counter < startCounter { - // Show solid purple to start. - for i := 0; i < len(strip.pixels); i++ { - strip.pixels[i] = colors[purple] - } - } else if strip.counter < middleCounter { - for i := 0; i < len(strip.pixels); i++ { - strip.pixels[i] = getFadeColor(purple, purpleBlue, strip.counter-startCounter, middleCounter-startCounter) - } - } else if strip.counter <= endCounter { - for i := 0; i < len(strip.pixels); i++ { - strip.pixels[i] = getFadeColor(purpleBlue, red, strip.counter-middleCounter, endCounter-middleCounter) - } - } else { - // Maintain the current value and prevent the counter from rolling over. - strip.counter = endCounter - } -} - -func (strip *LedStrip) updateWarmup4Mode() { - startOffset := 50 - middleCounter := 100 - for i := 0; i < len(strip.pixels); i++ { - strip.pixels[len(strip.pixels)-i-1] = getGradientColor(i+strip.counter+startOffset, 75) - } - if strip.counter >= middleCounter { - for i := 0; i < len(strip.pixels); i++ { - if i < strip.counter-middleCounter { - strip.pixels[i] = colors[red] - } - } - } -} - -func (strip *LedStrip) updateOwnedMode() { - speedDivisor := 30 - pixelSpacing := 4 - if strip.counter%speedDivisor != 0 { - return - } - for i := 0; i < len(strip.pixels); i++ { - if i%pixelSpacing == strip.counter/speedDivisor%pixelSpacing { - strip.pixels[i] = colors[red] - } else { - strip.pixels[i] = colors[black] - } - } -} - -func (strip *LedStrip) updateForceMode() { - speedDivisor := 30 - pixelSpacing := 7 - if strip.counter%speedDivisor != 0 { - return - } - for i := 0; i < len(strip.pixels); i++ { - switch (i + strip.counter/speedDivisor) % pixelSpacing { - case 2: - fallthrough - case 4: - strip.pixels[i] = colors[red] - case 3: - strip.pixels[i] = colors[dimBlue] - default: - strip.pixels[i] = colors[black] - } - } -} - -func (strip *LedStrip) updateBoostMode() { - speedDivisor := 4 - pixelSpacing := 4 - if strip.counter%speedDivisor != 0 { - return - } - for i := 0; i < len(strip.pixels); i++ { - if i%pixelSpacing == strip.counter/speedDivisor%pixelSpacing { - strip.pixels[i] = colors[blue] - } else { - strip.pixels[i] = colors[black] - } - } -} - -func (strip *LedStrip) updateRandomMode() { - if strip.counter%10 != 0 { - return - } - for i := 0; i < len(strip.pixels); i++ { - strip.pixels[i] = colors[color(rand.Intn(int(black)))] // Ignore colors listed after white. - } -} - -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) - } - } -} - -func (strip *LedStrip) updateGradientMode() { - for i := 0; i < len(strip.pixels); i++ { - strip.pixels[len(strip.pixels)-i-1] = getGradientColor(i+strip.counter, 75) - } -} - -func (strip *LedStrip) updateBlinkMode() { - divisor := 10 - for i := 0; i < len(strip.pixels); i++ { - if strip.counter%divisor < divisor/2 { - strip.pixels[i] = colors[white] - } else { - strip.pixels[i] = colors[black] - } - } -} - -// Interpolates between the two colors based on the given fraction. -func getFadeColor(fromColor, toColor color, 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 -} - -// Calculates the value of a single pixel in a gradient. -func getGradientColor(offset, numPixels int) [3]byte { - offset %= numPixels - if 3*offset < numPixels { - return getFadeColor(red, green, 3*offset, numPixels) - } else if 3*offset < 2*numPixels { - return getFadeColor(green, blue, 3*offset-numPixels, numPixels) - } else { - return getFadeColor(blue, red, 3*offset-2*numPixels, numPixels) - } -} diff --git a/led/mode.go b/led/mode.go new file mode 100644 index 0000000..39cbc25 --- /dev/null +++ b/led/mode.go @@ -0,0 +1,50 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Contains the set of display modes for an LED strip. + +package led + +type Mode int + +const ( + OffMode Mode = iota + RedMode + GreenMode + BlueMode + WhiteMode + ChaseMode + WarmupMode + Warmup2Mode + Warmup3Mode + Warmup4Mode + OwnedMode + NotOwnedMode + ForceMode + BoostMode + RandomMode + FadeMode + GradientMode + BlinkMode +) + +var ModeNames = map[Mode]string{ + OffMode: "Off", + RedMode: "Red", + GreenMode: "Green", + BlueMode: "Blue", + WhiteMode: "White", + ChaseMode: "Chase", + WarmupMode: "Warmup", + Warmup2Mode: "Warmup Purple", + Warmup3Mode: "Warmup Sneaky", + Warmup4Mode: "Warmup Gradient", + OwnedMode: "Owned", + NotOwnedMode: "Not Owned", + ForceMode: "Force", + BoostMode: "Boost", + RandomMode: "Random", + FadeMode: "Fade", + GradientMode: "Gradient", + BlinkMode: "Blink", +} diff --git a/led/strip.go b/led/strip.go new file mode 100644 index 0000000..b97431e --- /dev/null +++ b/led/strip.go @@ -0,0 +1,358 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Represents an independently controlled LED strip making up one half of a scale or switch LED array. + +package led + +import ( + "math/rand" + "time" +) + +type strip struct { + currentMode Mode + isRed bool + pixels [numPixels][3]byte + oldPixels [numPixels][3]byte + counter int + lastPacketTime time.Time +} + +// Calculates the current pixel values depending on the mode and elapsed counter cycles. +func (strip *strip) updatePixels() { + switch strip.currentMode { + 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 Warmup2Mode: + strip.updateWarmup2Mode() + case Warmup3Mode: + strip.updateWarmup3Mode() + case Warmup4Mode: + strip.updateWarmup4Mode() + case OwnedMode: + strip.updateOwnedMode() + case NotOwnedMode: + strip.updateNotOwnedMode() + case ForceMode: + strip.updateForceMode() + case BoostMode: + strip.updateBoostMode() + case RandomMode: + strip.updateRandomMode() + case FadeMode: + strip.updateFadeMode() + case GradientMode: + strip.updateGradientMode() + case BlinkMode: + strip.updateBlinkMode() + default: + strip.updateOffMode() + } + strip.counter++ +} + +// Returns true if the pixel data has changed or it has been too long since the last packet was sent. +func (strip *strip) shouldSendPacket() bool { + for i := 0; i < numPixels; i++ { + if strip.pixels[i] != strip.oldPixels[i] { + return true + } + } + return time.Since(strip.lastPacketTime).Seconds() > packetTimeoutSec +} + +// Writes the pixel RGB values into the given packet in preparation for sending. +func (strip *strip) populatePacketPixels(pixelData []byte) { + for i, pixel := range strip.pixels { + pixelData[3*i] = pixel[0] + pixelData[3*i+1] = pixel[1] + pixelData[3*i+2] = pixel[2] + } + + // Keep a record of the pixel values in order to detect future changes. + strip.oldPixels = strip.pixels + strip.lastPacketTime = time.Now() +} + +// Returns the primary color (red or blue) of this strip. +func (strip *strip) getColor() color { + if strip.isRed { + return red + } + return blue +} + +// Returns the opposite primary color (red or blue) to this strip. +func (strip *strip) getOppositeColor() color { + if strip.isRed { + return blue + } + return red +} + +// Returns a color partway between purple and the primary color (red or blue) of this strip. +func (strip *strip) getMidColor() color { + if strip.isRed { + return purpleBlue + } + return purpleRed +} + +// Returns a dim version of the primary color (red or blue) of this strip. +func (strip *strip) getDimColor() color { + if strip.isRed { + return dimRed + } + return dimBlue +} + +// Returns the starting offset for the gradient mode for this strip. +func (strip *strip) getGradientStartOffset() int { + if strip.isRed { + return numPixels / 3 + } + return 2 * numPixels / 3 +} + +func (strip *strip) updateOffMode() { + for i := 0; i < numPixels; i++ { + strip.pixels[i] = colors[black] + } +} + +func (strip *strip) updateSingleColorMode(color color) { + for i := 0; i < numPixels; i++ { + strip.pixels[i] = colors[color] + } +} + +func (strip *strip) updateChaseMode() { + if strip.counter == int(black)*numPixels { // Ignore colors listed after white. + strip.counter = 0 + } + color := color(strip.counter / numPixels) + pixelIndex := strip.counter % numPixels + strip.pixels[pixelIndex] = colors[color] +} + +func (strip *strip) updateWarmupMode() { + endCounter := 250 + if strip.counter == 0 { + // Show solid white to start. + for i := 0; i < numPixels; i++ { + strip.pixels[i] = colors[white] + } + } else if strip.counter <= endCounter { + // Build to the alliance color from each side. + numLitPixels := numPixels / 2 * strip.counter / endCounter + for i := 0; i < numLitPixels; i++ { + strip.pixels[i] = colors[strip.getColor()] + strip.pixels[numPixels-i-1] = colors[strip.getColor()] + } + } else { + // Prevent the counter from rolling over. + strip.counter = endCounter + } +} + +func (strip *strip) updateWarmup2Mode() { + startCounter := 100 + endCounter := 250 + if strip.counter < startCounter { + // Show solid purple to start. + for i := 0; i < numPixels; i++ { + strip.pixels[i] = colors[purple] + } + } else if strip.counter <= endCounter { + for i := 0; i < numPixels; i++ { + strip.pixels[i] = getFadeColor(purple, strip.getColor(), strip.counter-startCounter, + endCounter-startCounter) + } + } else { + // Prevent the counter from rolling over. + strip.counter = endCounter + } +} + +func (strip *strip) updateWarmup3Mode() { + startCounter := 50 + middleCounter := 225 + endCounter := 250 + if strip.counter < startCounter { + // Show solid purple to start. + for i := 0; i < numPixels; i++ { + strip.pixels[i] = colors[purple] + } + } else if strip.counter < middleCounter { + for i := 0; i < numPixels; i++ { + strip.pixels[i] = getFadeColor(purple, strip.getMidColor(), strip.counter-startCounter, + middleCounter-startCounter) + } + } else if strip.counter <= endCounter { + for i := 0; i < numPixels; i++ { + strip.pixels[i] = getFadeColor(strip.getMidColor(), strip.getColor(), strip.counter-middleCounter, + endCounter-middleCounter) + } + } else { + // Maintain the current value and prevent the counter from rolling over. + strip.counter = endCounter + } +} + +func (strip *strip) updateWarmup4Mode() { + middleCounter := 100 + for i := 0; i < numPixels; i++ { + strip.pixels[numPixels-i-1] = getGradientColor(i+strip.counter+strip.getGradientStartOffset(), numPixels/2) + } + if strip.counter >= middleCounter { + for i := 0; i < numPixels; i++ { + if i < strip.counter-middleCounter { + strip.pixels[i] = colors[strip.getColor()] + } + } + } +} + +func (strip *strip) updateOwnedMode() { + speedDivisor := 30 + pixelSpacing := 4 + if strip.counter%speedDivisor != 0 { + return + } + for i := 0; i < numPixels; i++ { + if i%pixelSpacing == strip.counter/speedDivisor%pixelSpacing { + strip.pixels[i] = colors[strip.getColor()] + } else { + strip.pixels[i] = colors[black] + } + } +} + +func (strip *strip) updateNotOwnedMode() { + for i := 0; i < numPixels; i++ { + strip.pixels[i] = colors[strip.getDimColor()] + } +} + +func (strip *strip) updateForceMode() { + speedDivisor := 30 + pixelSpacing := 7 + if strip.counter%speedDivisor != 0 { + return + } + for i := 0; i < numPixels; i++ { + switch (i + strip.counter/speedDivisor) % pixelSpacing { + case 2: + fallthrough + case 4: + strip.pixels[i] = colors[strip.getOppositeColor()] + case 3: + strip.pixels[i] = colors[strip.getDimColor()] + default: + strip.pixels[i] = colors[black] + } + } +} + +func (strip *strip) updateBoostMode() { + speedDivisor := 4 + pixelSpacing := 4 + if strip.counter%speedDivisor != 0 { + return + } + for i := 0; i < numPixels; i++ { + if i%pixelSpacing == strip.counter/speedDivisor%pixelSpacing { + strip.pixels[i] = colors[strip.getColor()] + } else { + strip.pixels[i] = colors[black] + } + } +} + +func (strip *strip) updateRandomMode() { + if strip.counter%10 != 0 { + return + } + for i := 0; i < numPixels; i++ { + strip.pixels[i] = colors[color(rand.Intn(int(black)))] // Ignore colors listed after white. + } +} + +func (strip *strip) updateFadeMode() { + fadeCycles := 40 + holdCycles := 10 + if strip.counter == 4*holdCycles+4*fadeCycles { + strip.counter = 0 + } + + for i := 0; i < numPixels; 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) + } + } +} + +func (strip *strip) updateGradientMode() { + for i := 0; i < numPixels; i++ { + strip.pixels[numPixels-i-1] = getGradientColor(i+strip.counter, 75) + } +} + +func (strip *strip) updateBlinkMode() { + divisor := 10 + for i := 0; i < numPixels; i++ { + if strip.counter%divisor < divisor/2 { + strip.pixels[i] = colors[white] + } else { + strip.pixels[i] = colors[black] + } + } +} + +// Interpolates between the two colors based on the given fraction. +func getFadeColor(fromColor, toColor color, 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 +} + +// Calculates the value of a single pixel in a gradient. +func getGradientColor(offset, numPixels int) [3]byte { + offset %= numPixels + if 3*offset < numPixels { + return getFadeColor(red, green, 3*offset, numPixels) + } else if 3*offset < 2*numPixels { + return getFadeColor(green, blue, 3*offset-numPixels, numPixels) + } else { + return getFadeColor(blue, red, 3*offset-2*numPixels, numPixels) + } +} diff --git a/model/event_settings.go b/model/event_settings.go index 62f5b3e..f017176 100644 --- a/model/event_settings.go +++ b/model/event_settings.go @@ -32,6 +32,9 @@ type EventSettings struct { ReaderPassword string StemTvPublishingEnabled bool StemTvEventCode string + ScaleLedAddress string + RedSwitchLedAddress string + BlueSwitchLedAddress string } const eventSettingsId = 0 diff --git a/templates/setup_settings.html b/templates/setup_settings.html index 026f066..a421232 100644 --- a/templates/setup_settings.html +++ b/templates/setup_settings.html @@ -239,6 +239,27 @@ +