From 27dc4a877324254aa372320b83b0b493e1a36303 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sun, 22 Mar 2020 17:21:32 -0700 Subject: [PATCH] Send out game data when Stage 3 capacity is reached. --- field/arena.go | 42 ++++++++++++++++++++ field/arena_notifiers.go | 9 +++++ field/driver_station_connection.go | 23 +++++++++++ game/control_panel.go | 58 +++++++++++++++++++++++++++ game/control_panel_test.go | 44 +++++++++++++++++++++ game/score.go | 17 +++----- partner/tba.go | 5 ++- static/css/scoring_panel.css | 18 +++++++++ static/js/scoring_panel.js | 63 ++++++++++++++++++++++-------- templates/scoring_panel.html | 6 +++ web/scoring_panel.go | 16 +++++++- web/scoring_panel_test.go | 11 +++++- 12 files changed, 279 insertions(+), 33 deletions(-) create mode 100644 game/control_panel.go create mode 100644 game/control_panel_test.go diff --git a/field/arena.go b/field/arena.go index 62d53a0..588035b 100644 --- a/field/arena.go +++ b/field/arena.go @@ -70,6 +70,8 @@ type Arena struct { MuteMatchSounds bool matchAborted bool soundsPlayed map[*game.MatchSound]struct{} + RedControlPanel *game.ControlPanel + BlueControlPanel *game.ControlPanel } type AllianceStation struct { @@ -206,10 +208,13 @@ func (arena *Arena) LoadMatch(match *model.Match) error { arena.FieldVolunteers = false arena.FieldReset = false arena.ScoringPanelRegistry.resetScoreCommitted() + arena.RedControlPanel = new(game.ControlPanel) + arena.BlueControlPanel = new(game.ControlPanel) // Notify any listeners about the new match. arena.MatchLoadNotifier.Notify() arena.RealtimeScoreNotifier.Notify() + arena.ControlPanelColorNotifier.Notify() arena.AllianceStationDisplayMode = "match" arena.AllianceStationDisplayModeNotifier.Notify() @@ -686,6 +691,23 @@ func (arena *Arena) sendDsPacket(auto bool, enabled bool) { arena.lastDsPacketTime = time.Now() } +// Sends a game data packet encoded with the given Control Panel target color to the given stations. +func (arena *Arena) sendGameDataPacket(color game.ControlPanelColor, stations ...string) { + gameData := game.GetGameDataForColor(color) + log.Printf("Sending game data packet '%s' to stations %v", gameData, stations) + for _, station := range stations { + if allianceStation, ok := arena.AllianceStations[station]; ok { + dsConn := allianceStation.DsConn + if dsConn != nil { + err := dsConn.sendGameDataPacket(gameData) + if err != nil { + log.Printf("Error sending game data packet to Team %d: %v", dsConn.TeamId, err) + } + } + } + } +} + // Returns the alliance station identifier for the given team, or the empty string if the team is not present // in the current match. func (arena *Arena) getAssignedAllianceStation(teamId int) string { @@ -717,6 +739,26 @@ func (arena *Arena) handlePlcInput() { // Don't do anything if we're outside the match, otherwise we may overwrite manual edits. return } + + redScore := &arena.RedRealtimeScore.CurrentScore + oldRedScore := *redScore + blueScore := &arena.BlueRealtimeScore.CurrentScore + oldBlueScore := *blueScore + + if redScore.StageAtCapacity(game.Stage3, arena.MatchState >= TeleopPeriod) && + redScore.Stage3TargetColor == game.ColorUnknown || + blueScore.StageAtCapacity(game.Stage3, arena.MatchState >= TeleopPeriod) && + blueScore.Stage3TargetColor == game.ColorUnknown { + // Determine the position control target colors and send packets to inform the driver stations. + redScore.Stage3TargetColor = arena.RedControlPanel.GetStage3TargetColor() + blueScore.Stage3TargetColor = arena.BlueControlPanel.GetStage3TargetColor() + arena.sendGameDataPacket(redScore.Stage3TargetColor, "R1", "R2", "R3") + arena.sendGameDataPacket(blueScore.Stage3TargetColor, "B1", "B2", "B3") + } + + if !oldRedScore.Equals(redScore) || !oldBlueScore.Equals(blueScore) { + arena.RealtimeScoreNotifier.Notify() + } } func (arena *Arena) handlePlcOutput() { diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index a19a1b3..3e59037 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -29,6 +29,7 @@ type ArenaNotifiers struct { ReloadDisplaysNotifier *websocket.Notifier ScorePostedNotifier *websocket.Notifier ScoringStatusNotifier *websocket.Notifier + ControlPanelColorNotifier *websocket.Notifier } type DisplayConfigurationMessage struct { @@ -65,6 +66,7 @@ func (arena *Arena) configureNotifiers() { arena.ReloadDisplaysNotifier = websocket.NewNotifier("reload", nil) arena.ScorePostedNotifier = websocket.NewNotifier("scorePosted", arena.generateScorePostedMessage) arena.ScoringStatusNotifier = websocket.NewNotifier("scoringStatus", arena.generateScoringStatusMessage) + arena.ControlPanelColorNotifier = websocket.NewNotifier("controlPanelColor", arena.generateControlPanelColorMessage) } func (arena *Arena) generateAllianceSelectionMessage() interface{} { @@ -226,6 +228,13 @@ func (arena *Arena) generateScoringStatusMessage() interface{} { arena.ScoringPanelRegistry.GetNumPanels("blue"), arena.ScoringPanelRegistry.GetNumScoreCommitted("blue")} } +func (arena *Arena) generateControlPanelColorMessage() interface{} { + return &struct { + RedControlPanelColor game.ControlPanelColor + BlueControlPanelColor game.ControlPanelColor + }{arena.RedControlPanel.CurrentColor, arena.BlueControlPanel.CurrentColor} +} + // Constructs the data object for one alliance sent to the audience display for the realtime scoring overlay. func getAudienceAllianceScoreFields(allianceScore *RealtimeScore, allianceScoreSummary *game.ScoreSummary) *audienceAllianceScoreFields { diff --git a/field/driver_station_connection.go b/field/driver_station_connection.go index 39fbfd9..7fa10ef 100644 --- a/field/driver_station_connection.go +++ b/field/driver_station_connection.go @@ -385,3 +385,26 @@ func (dsConn *DriverStationConnection) handleTcpConnection(arena *Arena) { } } } + +// Sends a TCP packet containing the given game data to the driver station. +func (dsConn *DriverStationConnection) sendGameDataPacket(gameData string) error { + byteData := []byte(gameData) + size := len(byteData) + packet := make([]byte, size+4) + + packet[0] = 0 // Packet size + packet[1] = byte(size + 2) // Packet size + packet[2] = 28 // Packet type + packet[3] = byte(size) // Data size + + // Fill the rest of the packet with the data. + for i, character := range byteData { + packet[i+4] = character + } + + if dsConn.tcpConn != nil { + _, err := dsConn.tcpConn.Write(packet) + return err + } + return nil +} diff --git a/game/control_panel.go b/game/control_panel.go new file mode 100644 index 0000000..d1b7cdd --- /dev/null +++ b/game/control_panel.go @@ -0,0 +1,58 @@ +// Copyright 2020 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Represents the state of an alliance Control Panel in the 2020 game. + +package game + +import "math/rand" + +type ControlPanel struct { + CurrentColor ControlPanelColor +} + +type ControlPanelColor int + +const ( + ColorUnknown ControlPanelColor = iota + ColorRed + ColorGreen + ColorBlue + ColorYellow +) + +type ControlPanelStatus int + +const ( + ControlPanelNone ControlPanelStatus = iota + ControlPanelRotation + ControlPanelPosition +) + +// Returns a random color that does not match the current color. +func (controlPanel *ControlPanel) GetStage3TargetColor() ControlPanelColor { + if controlPanel.CurrentColor == ColorUnknown { + // If the sensor or manual scorekeeping did not detect/set the current color, pick one of the four at random. + return ControlPanelColor(rand.Intn(4) + 1) + } + newColor := int(controlPanel.CurrentColor) + rand.Intn(3) + 1 + if newColor > 4 { + newColor -= 4 + } + return ControlPanelColor(newColor) +} + +// Returns the string that is to be sent to the driver station for the given color. +func GetGameDataForColor(color ControlPanelColor) string { + switch color { + case ColorRed: + return "R" + case ColorGreen: + return "G" + case ColorBlue: + return "B" + case ColorYellow: + return "Y" + } + return "" +} diff --git a/game/control_panel_test.go b/game/control_panel_test.go new file mode 100644 index 0000000..785d69f --- /dev/null +++ b/game/control_panel_test.go @@ -0,0 +1,44 @@ +// Copyright 2020 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package game + +import ( + "github.com/stretchr/testify/assert" + "math/rand" + "testing" +) + +func TestControlPanelGetStage3TargetColor(t *testing.T) { + rand.Seed(0) + var controlPanel ControlPanel + + controlPanel.CurrentColor = ColorUnknown + results := getStage3TargetColorNTimes(&controlPanel, 10000) + assert.Equal(t, [5]int{0, 2543, 2527, 2510, 2420}, results) + + controlPanel.CurrentColor = ColorRed + results = getStage3TargetColorNTimes(&controlPanel, 10000) + assert.Equal(t, [5]int{0, 0, 3351, 3311, 3338}, results) + + controlPanel.CurrentColor = ColorGreen + results = getStage3TargetColorNTimes(&controlPanel, 10000) + assert.Equal(t, [5]int{0, 3335, 0, 3320, 3345}, results) + + controlPanel.CurrentColor = ColorBlue + results = getStage3TargetColorNTimes(&controlPanel, 10000) + assert.Equal(t, [5]int{0, 3328, 3296, 0, 3376}, results) + + controlPanel.CurrentColor = ColorYellow + results = getStage3TargetColorNTimes(&controlPanel, 10000) + assert.Equal(t, [5]int{0, 3303, 3388, 3309, 0}, results) +} + +// Invokes the method N times and returns a map of the counts for each result, for statistical testing. +func getStage3TargetColorNTimes(controlPanel *ControlPanel, n int) [5]int { + var results [5]int + for i := 0; i < n; i++ { + results[controlPanel.GetStage3TargetColor()]++ + } + return results +} diff --git a/game/score.go b/game/score.go index ddfb9d7..cb2b97c 100644 --- a/game/score.go +++ b/game/score.go @@ -16,10 +16,11 @@ type Score struct { TeleopCellsOuter [4]int TeleopCellsInner [4]int ControlPanelStatus - EndgameStatuses [3]EndgameStatus - RungIsLevel bool - Fouls []Foul - ElimDq bool + EndgameStatuses [3]EndgameStatus + RungIsLevel bool + Fouls []Foul + ElimDq bool + Stage3TargetColor ControlPanelColor } type ScoreSummary struct { @@ -55,14 +56,6 @@ const ( StageExtra ) -type ControlPanelStatus int - -const ( - ControlPanelNone ControlPanelStatus = iota - ControlPanelRotation - ControlPanelPosition -) - // Represents the state of a robot at the end of the match. type EndgameStatus int diff --git a/partner/tba.go b/partner/tba.go index 63e3781..03969a1 100644 --- a/partner/tba.go +++ b/partner/tba.go @@ -138,6 +138,8 @@ type TbaPublishedAward struct { } var exitedInitLineMapping = map[bool]string{false: "None", true: "Exited"} +var controlPanelColorMapping = map[game.ControlPanelColor]string{game.ColorUnknown: "Unknown", game.ColorRed: "Red", + game.ColorGreen: "Green", game.ColorBlue: "Blue", game.ColorYellow: "Yellow"} var endgameMapping = []string{"None", "Park", "Hang"} var rungIsLevelMapping = map[bool]string{false: "NotLevel", true: "IsLevel"} @@ -543,8 +545,7 @@ func createTbaScoringBreakdown(match *model.Match, matchResult *model.MatchResul breakdown.Stage1Activated = scoreSummary.StagesActivated[0] breakdown.Stage2Activated = scoreSummary.StagesActivated[1] breakdown.Stage3Activated = scoreSummary.StagesActivated[2] - // TODO(pat): Add once the Arena logic is in place. - // breakdown.Stage3TargetColor = + breakdown.Stage3TargetColor = controlPanelColorMapping[score.Stage3TargetColor] breakdown.EndgameRobot1 = endgameMapping[score.EndgameStatuses[0]] breakdown.EndgameRobot2 = endgameMapping[score.EndgameStatuses[1]] breakdown.EndgameRobot3 = endgameMapping[score.EndgameStatuses[2]] diff --git a/static/css/scoring_panel.css b/static/css/scoring_panel.css index dd968a8..b25eeb3 100644 --- a/static/css/scoring_panel.css +++ b/static/css/scoring_panel.css @@ -86,6 +86,24 @@ body { .control-panel[data-value="true"] { background-color: #263; } +.control-panel-color[data-value="0"] { + background-color: #333; +} +.control-panel-color[data-value="0"][data-control-panel-status="1"] { + border: 3px solid red; +} +.control-panel-color[data-value="1"] { + background-color: #633; +} +.control-panel-color[data-value="2"] { + background-color: #263; +} +.control-panel-color[data-value="3"] { + background-color: #236; +} +.control-panel-color[data-value="4"] { + background-color: #882; +} .shortcut { margin: 0 0.2vw; font-size: 1vw; diff --git a/static/js/scoring_panel.js b/static/js/scoring_panel.js index 7029956..17e8c8e 100644 --- a/static/js/scoring_panel.js +++ b/static/js/scoring_panel.js @@ -20,6 +20,24 @@ var handleMatchLoad = function(data) { } }; +// Handles a websocket message to update the match status. +var handleMatchTime = function(data) { + switch (matchStates[data.MatchState]) { + case "PRE_MATCH": + // Pre-match message state is set in handleRealtimeScore(). + $("#postMatchMessage").hide(); + $("#commitMatchScore").hide(); + break; + case "POST_MATCH": + $("#postMatchMessage").hide(); + $("#commitMatchScore").css("display", "flex"); + break; + default: + $("#postMatchMessage").hide(); + $("#commitMatchScore").hide(); + } +}; + // Handles a websocket message to update the realtime scoring fields. var handleRealtimeScore = function(data) { var realtimeScore; @@ -67,24 +85,20 @@ var handleRealtimeScore = function(data) { } $("#rungIsLevel>.value").text(score.RungIsLevel ? "Yes" : "No"); $("#rungIsLevel").attr("data-value", score.RungIsLevel); + $("#controlPanelColor").attr("data-control-panel-status", score.ControlPanelStatus) }; -// Handles a websocket message to update the match status. -var handleMatchTime = function(data) { - switch (matchStates[data.MatchState]) { - case "PRE_MATCH": - // Pre-match message state is set in handleRealtimeScore(). - $("#postMatchMessage").hide(); - $("#commitMatchScore").hide(); - break; - case "POST_MATCH": - $("#postMatchMessage").hide(); - $("#commitMatchScore").css("display", "flex"); - break; - default: - $("#postMatchMessage").hide(); - $("#commitMatchScore").hide(); +// Handles a websocket message to update the Control Panel color. +var handleControlPanelColor = function(data) { + var color; + if (alliance === "red") { + color = data.RedControlPanelColor; + } else { + color = data.BlueControlPanelColor; } + + $("#controlPanelColor>.value").text(getControlPanelColorText(color)); + $("#controlPanelColor").attr("data-value", color); }; // Handles a keyboard event and sends the appropriate websocket message. @@ -116,6 +130,22 @@ var getEndgameStatusText = function(level) { } }; +// Returns the display text corresponding to the given integer Control Panel color value. +var getControlPanelColorText = function(level) { + switch (level) { + case 1: + return "Red"; + case 2: + return "Green"; + case 3: + return "Blue"; + case 4: + return "Yellow"; + default: + return "Unknown"; + } +}; + // Updates the power cell count for a goal, given the element and score values. var setGoalValue = function(element, powerCells) { var total = 0; @@ -133,7 +163,8 @@ $(function() { websocket = new CheesyWebsocket("/panels/scoring/" + alliance + "/websocket", { matchLoad: function(event) { handleMatchLoad(event.data); }, matchTime: function(event) { handleMatchTime(event.data); }, - realtimeScore: function(event) { handleRealtimeScore(event.data); } + realtimeScore: function(event) { handleRealtimeScore(event.data); }, + controlPanelColor: function(event) { handleControlPanelColor(event.data); } }); $(document).keypress(handleKeyPress); diff --git a/templates/scoring_panel.html b/templates/scoring_panel.html index f5728ae..c90d0fe 100644 --- a/templates/scoring_panel.html +++ b/templates/scoring_panel.html @@ -55,6 +55,7 @@
Rotation Control
+
Color After Rotation
Position Control
Rung Is Level
@@ -64,6 +65,11 @@
+
+
K
+
+
+
P
diff --git a/web/scoring_panel.go b/web/scoring_panel.go index 37315d5..0199351 100644 --- a/web/scoring_panel.go +++ b/web/scoring_panel.go @@ -81,7 +81,7 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ // Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine. go ws.HandleNotifiers(web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier, - web.arena.ReloadDisplaysNotifier) + web.arena.ControlPanelColorNotifier, web.arena.ReloadDisplaysNotifier) // Loop, waiting for commands and responding to them, until the client closes the connection. for { @@ -189,6 +189,20 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ score.ControlPanelStatus = game.ControlPanelRotation } scoreChanged = true + case "K": + if score.ControlPanelStatus == game.ControlPanelRotation { + var controlPanel *game.ControlPanel + if alliance == "red" { + controlPanel = web.arena.RedControlPanel + } else { + controlPanel = web.arena.BlueControlPanel + } + controlPanel.CurrentColor++ + if controlPanel.CurrentColor == 5 { + controlPanel.CurrentColor = 1 + } + web.arena.ControlPanelColorNotifier.Notify() + } case "P": if score.ControlPanelStatus == game.ControlPanelPosition { score.ControlPanelStatus = game.ControlPanelRotation diff --git a/web/scoring_panel_test.go b/web/scoring_panel_test.go index 5ec694e..490773b 100644 --- a/web/scoring_panel_test.go +++ b/web/scoring_panel_test.go @@ -50,9 +50,11 @@ func TestScoringPanelWebsocket(t *testing.T) { readWebsocketType(t, redWs, "matchLoad") readWebsocketType(t, redWs, "matchTime") readWebsocketType(t, redWs, "realtimeScore") + readWebsocketType(t, redWs, "controlPanelColor") readWebsocketType(t, blueWs, "matchLoad") readWebsocketType(t, blueWs, "matchTime") readWebsocketType(t, blueWs, "realtimeScore") + readWebsocketType(t, blueWs, "controlPanelColor") // Send some autonomous period scoring commands. web.arena.MatchState = field.AutoPeriod @@ -79,6 +81,7 @@ func TestScoringPanelWebsocket(t *testing.T) { blueWs.Write("5", nil) blueWs.Write("5", nil) blueWs.Write("L", nil) + blueWs.Write("k", nil) for i := 0; i < 6; i++ { readWebsocketType(t, redWs, "realtimeScore") readWebsocketType(t, blueWs, "realtimeScore") @@ -106,9 +109,13 @@ func TestScoringPanelWebsocket(t *testing.T) { web.arena.ResetMatch() web.arena.LoadTestMatch() readWebsocketType(t, redWs, "matchLoad") - readWebsocketType(t, redWs, "realtimeScore") + messages := readWebsocketMultiple(t, redWs, 2) + assert.Contains(t, messages, "realtimeScore") + assert.Contains(t, messages, "controlPanelColor") readWebsocketType(t, blueWs, "matchLoad") - readWebsocketType(t, blueWs, "realtimeScore") + messages = readWebsocketMultiple(t, blueWs, 2) + assert.Contains(t, messages, "realtimeScore") + assert.Contains(t, messages, "controlPanelColor") assert.Equal(t, field.NewRealtimeScore(), web.arena.RedRealtimeScore) assert.Equal(t, field.NewRealtimeScore(), web.arena.BlueRealtimeScore) assert.Equal(t, 0, web.arena.ScoringPanelRegistry.GetNumScoreCommitted("red"))