diff --git a/arena.go b/arena.go index 968715b..a5bead2 100644 --- a/arena.go +++ b/arena.go @@ -8,7 +8,6 @@ package main import ( "fmt" "log" - "math/rand" "time" ) @@ -83,9 +82,15 @@ type Arena struct { lastMatchTimeSec float64 savedMatch *Match savedMatchResult *MatchResult - leftGoalHotFirst bool lights Lights muteMatchSounds bool + fieldReset bool +} + +type RealtimeScoreFields struct { + Score int + TowerStrength int + DefensesStrength [5]int } var mainArena Arena // Named thusly to avoid polluting the global namespace with something more generic. @@ -126,6 +131,8 @@ func (arena *Arena) Setup() { arena.reloadDisplaysNotifier = NewNotifier() arena.defenseSelectionNotifier = NewNotifier() + arena.lights.Setup() + // Load empty match as current. arena.MatchState = PRE_MATCH arena.LoadTestMatch() @@ -138,8 +145,6 @@ func (arena *Arena) Setup() { arena.savedMatchResult = &MatchResult{} arena.allianceStationDisplays = make(map[string]string) arena.allianceStationDisplayScreen = "match" - - arena.lights.Setup() } // Loads a team into an alliance station, cleaning up the previous team there if there is one. @@ -222,6 +227,8 @@ func (arena *Arena) LoadMatch(match *Match) error { // Reset the realtime scores. arena.redRealtimeScore = NewRealtimeScore() arena.blueRealtimeScore = NewRealtimeScore() + arena.fieldReset = false + arena.lights.ClearAll() // Notify any listeners about the new match. arena.matchLoadTeamsNotifier.Notify(nil) @@ -410,7 +417,6 @@ func (arena *Arena) Update() { arena.MatchState = AUTO_PERIOD arena.matchStartTime = time.Now() arena.lastMatchTimeSec = -1 - arena.leftGoalHotFirst = rand.Intn(2) == 1 auto = true enabled = true sendDsPacket = true @@ -494,6 +500,8 @@ func (arena *Arena) Update() { arena.sendDsPacket(auto, enabled) arena.robotStatusNotifier.Notify(nil) } + + arena.handleLighting() } // Loops indefinitely to track and update the arena components. @@ -522,3 +530,37 @@ func (arena *Arena) sendDsPacket(auto bool, enabled bool) { func (realtimeScore *RealtimeScore) Score(opponentFouls []Foul) int { return scoreSummary(&realtimeScore.CurrentScore, opponentFouls, mainArena.currentMatch.Type).Score } + +// Calculates the integer score, tower strength, and defenses strength for the given realtime snapshot. +func (realtimeScore *RealtimeScore) ScoreFields(opponentFouls []Foul) *RealtimeScoreFields { + scoreSummary := scoreSummary(&realtimeScore.CurrentScore, opponentFouls, mainArena.currentMatch.Type) + var defensesStrength [5]int + for i := 0; i < 5; i++ { + defensesStrength[i] = 2 - realtimeScore.CurrentScore.AutoDefensesCrossed[i] - + realtimeScore.CurrentScore.DefensesCrossed[i] + } + return &RealtimeScoreFields{scoreSummary.Score, scoreSummary.TowerStrength, defensesStrength} +} + +// Manipulates the arena LED lighting based on the current state of the match. +func (arena *Arena) handleLighting() { + switch arena.MatchState { + case AUTO_PERIOD: + fallthrough + case PAUSE_PERIOD: + fallthrough + case TELEOP_PERIOD: + fallthrough + case ENDGAME_PERIOD: + redScoreFields := arena.redRealtimeScore.ScoreFields(arena.blueRealtimeScore.CurrentScore.Fouls) + blueScoreFields := arena.blueRealtimeScore.ScoreFields(arena.redRealtimeScore.CurrentScore.Fouls) + arena.lights.SetGoals(redScoreFields.TowerStrength, blueScoreFields.TowerStrength) + arena.lights.SetDefenses(redScoreFields.DefensesStrength, blueScoreFields.DefensesStrength) + case POST_MATCH: + if mainArena.fieldReset { + arena.lights.SetFieldReset() + } else { + arena.lights.ClearAll() + } + } +} diff --git a/audience_display.go b/audience_display.go index 483d391..bf765ff 100644 --- a/audience_display.go +++ b/audience_display.go @@ -12,12 +12,6 @@ import ( "text/template" ) -type RealtimeScoreFields struct { - Score int - TowerStrength int - DefensesStrength [5]int -} - // Renders the audience display to be chroma keyed over the video feed. func AudienceDisplayHandler(w http.ResponseWriter, r *http.Request) { if !UserIsReader(w, r) { @@ -222,14 +216,3 @@ func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } } } - -// Calculates the integer score, tower strength, and defenses strength for the given realtime snapshot. -func (realtimeScore *RealtimeScore) ScoreFields(opponentFouls []Foul) *RealtimeScoreFields { - scoreSummary := scoreSummary(&realtimeScore.CurrentScore, opponentFouls, mainArena.currentMatch.Type) - var defensesStrength [5]int - for i := 0; i < 5; i++ { - defensesStrength[i] = 2 - realtimeScore.CurrentScore.AutoDefensesCrossed[i] - - realtimeScore.CurrentScore.DefensesCrossed[i] - } - return &RealtimeScoreFields{scoreSummary.Score, scoreSummary.TowerStrength, defensesStrength} -} diff --git a/lights.go b/lights.go index 8fc4e80..8a1e07f 100644 --- a/lights.go +++ b/lights.go @@ -11,6 +11,13 @@ import ( "time" ) +const ( + RED_GOAL = "redGoal" + RED_DEFENSE = "redDefense" + BLUE_GOAL = "blueGoal" + BLUE_DEFENSE = "blueDefense" +) + type LightPacket [32]byte type Lights struct { @@ -78,11 +85,15 @@ func (lights *Lights) Setup() error { } lights.packets = make(map[string]*LightPacket) - lights.packets["red"] = &LightPacket{} - lights.packets["blue"] = &LightPacket{} + lights.packets[RED_GOAL] = &LightPacket{} + lights.packets[RED_DEFENSE] = &LightPacket{} + lights.packets[BLUE_GOAL] = &LightPacket{} + lights.packets[BLUE_DEFENSE] = &LightPacket{} lights.oldPackets = make(map[string]*LightPacket) - lights.oldPackets["red"] = &LightPacket{} - lights.oldPackets["blue"] = &LightPacket{} + lights.oldPackets[RED_GOAL] = &LightPacket{} + lights.oldPackets[RED_DEFENSE] = &LightPacket{} + lights.oldPackets[BLUE_GOAL] = &LightPacket{} + lights.oldPackets[BLUE_DEFENSE] = &LightPacket{} lights.sendLights() @@ -98,34 +109,50 @@ func (lights *Lights) Setup() error { func (lights *Lights) SetupConnections() error { lights.connections = make(map[string]*net.Conn) - - // Don't enable lights for a side if the controller address is not configured. - if len(eventSettings.RedGoalLightsAddress) != 0 { - conn, err := net.Dial("udp4", eventSettings.RedGoalLightsAddress) - lights.connections["red"] = &conn - if err != nil { - return err - } - } else { - lights.connections["red"] = nil + if err := lights.connect(RED_GOAL, eventSettings.RedGoalLightsAddress); err != nil { + return err } - if len(eventSettings.BlueGoalLightsAddress) != 0 { - conn, err := net.Dial("udp4", eventSettings.BlueGoalLightsAddress) - lights.connections["blue"] = &conn - if err != nil { - return err - } - } else { - lights.connections["blue"] = nil + if err := lights.connect(RED_DEFENSE, eventSettings.RedDefenseLightsAddress); err != nil { + return err + } + if err := lights.connect(BLUE_GOAL, eventSettings.BlueGoalLightsAddress); err != nil { + return err + } + if err := lights.connect(BLUE_DEFENSE, eventSettings.BlueDefenseLightsAddress); err != nil { + return err } lights.newConnections = true return nil } +func (lights *Lights) connect(controller, address string) error { + // Don't enable lights for a side if the controller address is not configured. + if len(address) != 0 { + conn, err := net.Dial("udp4", address) + lights.connections[controller] = &conn + if err != nil { + return err + } + } else { + lights.connections[controller] = nil + } + return nil +} + +func (lights *Lights) ClearAll() { + lights.packets[RED_GOAL].setAllColorFade("off", 10) + lights.packets[RED_DEFENSE].setAllColorFade("off", 10) + lights.packets[BLUE_GOAL].setAllColorFade("off", 10) + lights.packets[BLUE_DEFENSE].setAllColorFade("off", 10) + lights.sendLights() +} + // Turns all lights green to signal that the field is safe to enter. func (lights *Lights) SetFieldReset() { - lights.packets["red"].setAllColor("green") - lights.packets["blue"].setAllColor("green") + lights.packets[RED_GOAL].setAllColor("green") + lights.packets[RED_DEFENSE].setAllColor("green") + lights.packets[BLUE_GOAL].setAllColor("green") + lights.packets[BLUE_DEFENSE].setAllColor("green") lights.sendLights() } @@ -136,36 +163,46 @@ func (lights *Lights) SetMode(mode string) { switch mode { case "off": - lights.packets["red"].setAllColor("off") - lights.packets["blue"].setAllColor("off") + lights.packets[RED_GOAL].setAllColor("off") + lights.packets[RED_DEFENSE].setAllColor("off") + lights.packets[BLUE_GOAL].setAllColor("off") + lights.packets[BLUE_DEFENSE].setAllColor("off") case "all_white": - lights.packets["red"].setAllColor("white") - lights.packets["blue"].setAllColor("white") + lights.packets[RED_GOAL].setAllColor("white") + lights.packets[RED_DEFENSE].setAllColor("white") + lights.packets[BLUE_GOAL].setAllColor("white") + lights.packets[BLUE_DEFENSE].setAllColor("white") case "all_red": - lights.packets["red"].setAllColor("red") - lights.packets["blue"].setAllColor("red") + lights.packets[RED_GOAL].setAllColor("red") + lights.packets[RED_DEFENSE].setAllColor("red") + lights.packets[BLUE_GOAL].setAllColor("red") + lights.packets[BLUE_DEFENSE].setAllColor("red") case "all_green": - lights.packets["red"].setAllColor("green") - lights.packets["blue"].setAllColor("green") + lights.packets[RED_GOAL].setAllColor("green") + lights.packets[RED_DEFENSE].setAllColor("green") + lights.packets[BLUE_GOAL].setAllColor("green") + lights.packets[BLUE_DEFENSE].setAllColor("green") case "all_blue": - lights.packets["red"].setAllColor("blue") - lights.packets["blue"].setAllColor("blue") + lights.packets[RED_GOAL].setAllColor("blue") + lights.packets[RED_DEFENSE].setAllColor("blue") + lights.packets[BLUE_GOAL].setAllColor("blue") + lights.packets[BLUE_DEFENSE].setAllColor("blue") } lights.sendLights() } // Sends a control packet to the LED controllers only if their state needs to be updated. func (lights *Lights) sendLights() { - for alliance, connection := range lights.connections { - if lights.newConnections || *lights.packets[alliance] != *lights.oldPackets[alliance] { + for controller, connection := range lights.connections { + if lights.newConnections || *lights.packets[controller] != *lights.oldPackets[controller] { if connection != nil { - _, err := (*connection).Write(lights.packets[alliance][:]) + _, err := (*connection).Write(lights.packets[controller][:]) if err != nil { - log.Printf("Failed to send %s light packet.", alliance) + log.Printf("Failed to send %s light packet.", controller) } } } - *lights.oldPackets[alliance] = *lights.packets[alliance] + *lights.oldPackets[controller] = *lights.packets[controller] } lights.newConnections = false } @@ -178,11 +215,15 @@ func (lights *Lights) animate() { case "strobe": switch lights.animationCount { case 1: - lights.packets["red"].setAllColor("white") - lights.packets["blue"].setAllColor("off") + lights.packets[RED_GOAL].setAllColor("white") + lights.packets[RED_DEFENSE].setAllColor("white") + lights.packets[BLUE_GOAL].setAllColor("off") + lights.packets[BLUE_DEFENSE].setAllColor("off") case 2: - lights.packets["red"].setAllColor("off") - lights.packets["blue"].setAllColor("white") + lights.packets[RED_GOAL].setAllColor("off") + lights.packets[RED_DEFENSE].setAllColor("off") + lights.packets[BLUE_GOAL].setAllColor("white") + lights.packets[BLUE_DEFENSE].setAllColor("white") fallthrough default: lights.animationCount = 0 @@ -190,36 +231,87 @@ func (lights *Lights) animate() { lights.sendLights() case "fade_red": if lights.animationCount == 1 { - lights.packets["red"].setAllColorFade("red", 18) - lights.packets["blue"].setAllColorFade("red", 18) + lights.packets[RED_GOAL].setAllColorFade("red", 18) + lights.packets[RED_DEFENSE].setAllColorFade("red", 18) + lights.packets[BLUE_GOAL].setAllColorFade("red", 18) + lights.packets[BLUE_DEFENSE].setAllColorFade("red", 18) } else if lights.animationCount == 61 { - lights.packets["red"].setAllColorFade("darkred", 18) - lights.packets["blue"].setAllColorFade("darkred", 18) + lights.packets[RED_GOAL].setAllColorFade("darkred", 18) + lights.packets[RED_DEFENSE].setAllColorFade("darkred", 18) + lights.packets[BLUE_GOAL].setAllColorFade("darkred", 18) + lights.packets[BLUE_DEFENSE].setAllColorFade("darkred", 18) } else if lights.animationCount > 120 { lights.animationCount = 0 } lights.sendLights() case "fade_blue": if lights.animationCount == 1 { - lights.packets["red"].setAllColorFade("blue", 18) - lights.packets["blue"].setAllColorFade("blue", 18) + lights.packets[RED_GOAL].setAllColorFade("blue", 18) + lights.packets[RED_DEFENSE].setAllColorFade("blue", 18) + lights.packets[BLUE_GOAL].setAllColorFade("blue", 18) + lights.packets[BLUE_DEFENSE].setAllColorFade("blue", 18) } else if lights.animationCount == 61 { - lights.packets["red"].setAllColorFade("darkblue", 18) - lights.packets["blue"].setAllColorFade("darkblue", 18) + lights.packets[RED_GOAL].setAllColorFade("darkblue", 18) + lights.packets[RED_DEFENSE].setAllColorFade("darkblue", 18) + lights.packets[BLUE_GOAL].setAllColorFade("darkblue", 18) + lights.packets[BLUE_DEFENSE].setAllColorFade("darkblue", 18) } else if lights.animationCount > 120 { lights.animationCount = 0 } lights.sendLights() case "fade_red_blue": if lights.animationCount == 1 { - lights.packets["red"].setAllColorFade("blue", 18) - lights.packets["blue"].setAllColorFade("darkred", 18) + lights.packets[RED_GOAL].setAllColorFade("blue", 18) + lights.packets[RED_DEFENSE].setAllColorFade("blue", 18) + lights.packets[BLUE_GOAL].setAllColorFade("darkred", 18) + lights.packets[BLUE_DEFENSE].setAllColorFade("darkred", 18) } else if lights.animationCount == 61 { - lights.packets["red"].setAllColorFade("darkblue", 18) - lights.packets["blue"].setAllColorFade("red", 18) + lights.packets[RED_GOAL].setAllColorFade("darkblue", 18) + lights.packets[RED_DEFENSE].setAllColorFade("darkblue", 18) + lights.packets[BLUE_GOAL].setAllColorFade("red", 18) + lights.packets[BLUE_DEFENSE].setAllColorFade("red", 18) } else if lights.animationCount > 120 { lights.animationCount = 0 } lights.sendLights() } } + +// Turns on a number of channels corresponding to the tower strength (all on for 8 or higher). +func (lights *Lights) SetGoals(redTowerStrength, blueTowerStrength int) { + for i := 0; i < 8; i++ { + if redTowerStrength > i { + lights.packets[RED_GOAL].setColorFade(i, "red", 10) + } else { + lights.packets[RED_GOAL].setColorFade(i, "off", 10) + } + if blueTowerStrength > i { + lights.packets[BLUE_GOAL].setColorFade(i, "blue", 10) + } else { + lights.packets[BLUE_GOAL].setColorFade(i, "off", 10) + } + } + lights.sendLights() +} + +// Turns on the lights below the defenses, with one channel per defense. +func (lights *Lights) SetDefenses(redDefensesStrength, blueDefensesStrength [5]int) { + for i := 0; i < 5; i++ { + if redDefensesStrength[i] == 0 { + lights.packets[RED_DEFENSE].setColorFade(i, "off", 10) + } else if redDefensesStrength[i] == 1 { + lights.packets[RED_DEFENSE].setColorFade(i, "yellow", 10) + } else { + lights.packets[RED_DEFENSE].setColorFade(i, "red", 10) + } + + if blueDefensesStrength[i] == 0 { + lights.packets[BLUE_DEFENSE].setColorFade(i, "off", 10) + } else if blueDefensesStrength[i] == 1 { + lights.packets[BLUE_DEFENSE].setColorFade(i, "yellow", 10) + } else { + lights.packets[BLUE_DEFENSE].setColorFade(i, "blue", 10) + } + } + lights.sendLights() +} diff --git a/referee_display.go b/referee_display.go index 8cc7f60..34e8922 100644 --- a/referee_display.go +++ b/referee_display.go @@ -229,6 +229,7 @@ func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { // Don't allow clearing the field until the match is over. continue } + mainArena.fieldReset = true mainArena.allianceStationDisplayScreen = "fieldReset" mainArena.allianceStationDisplayNotifier.Notify(nil) continue // Don't reload. @@ -239,6 +240,7 @@ func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } mainArena.redRealtimeScore.FoulsCommitted = true mainArena.blueRealtimeScore.FoulsCommitted = true + mainArena.fieldReset = true mainArena.allianceStationDisplayScreen = "fieldReset" mainArena.allianceStationDisplayNotifier.Notify(nil) mainArena.scoringStatusNotifier.Notify(nil)