mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Send out game data when Stage 3 capacity is reached.
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
58
game/control_panel.go
Normal 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 ""
|
||||
}
|
||||
44
game/control_panel_test.go
Normal file
44
game/control_panel_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user