Send out game data when Stage 3 capacity is reached.

This commit is contained in:
Patrick Fairbank
2020-03-22 17:21:32 -07:00
parent de976ab59f
commit 27dc4a8773
12 changed files with 279 additions and 33 deletions

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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
}

58
game/control_panel.go Normal file
View File

@@ -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 ""
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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]]

View File

@@ -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;

View File

@@ -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);

View File

@@ -55,6 +55,7 @@
<div class="scoring-section">
<div class="scoring-header">
<div>Rotation Control</div>
<div>Color After Rotation</div>
<div>Position Control</div>
<div>Rung Is Level</div>
</div>
@@ -64,6 +65,11 @@
<div class="value"></div>
<div class="shortcut"></div>
</div>
<div id="controlPanelColor" class="control-panel-color robot-field" onclick="handleClick('K');">
<div class="shortcut">K</div>
<div class="value"></div>
<div class="shortcut"></div>
</div>
<div id="positionControl" class="control-panel robot-field" onclick="handleClick('P');">
<div class="shortcut">P</div>
<div class="value"></div>

View File

@@ -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

View File

@@ -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"))