mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Add goal and defense LED control.
This commit is contained in:
52
arena.go
52
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
202
lights.go
202
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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user