From 2609f121f60d4be689be020009bde0bba09b392d Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 3 Aug 2019 13:21:16 -0700 Subject: [PATCH] Refactor scoring panel to support independant score commits for each active instance. --- field/arena.go | 4 ++ field/arena_notifiers.go | 11 ++-- field/realtime_score.go | 7 +- field/scoring_panel_registry.go | 78 +++++++++++++++++++++++ field/scoring_panel_registry_test.go | 51 +++++++++++++++ static/js/match_play.js | 10 ++- templates/match_play.html | 4 +- web/scoring_panel.go | 12 ++-- web/scoring_panel_test.go | 95 ++++++++++++++++++---------- 9 files changed, 218 insertions(+), 54 deletions(-) create mode 100644 field/scoring_panel_registry.go create mode 100644 field/scoring_panel_registry_test.go diff --git a/field/arena.go b/field/arena.go index f03d5bc..828b53e 100644 --- a/field/arena.go +++ b/field/arena.go @@ -47,6 +47,7 @@ type Arena struct { TbaClient *partner.TbaClient AllianceStations map[string]*AllianceStation Displays map[string]*Display + ScoringPanelRegistry ArenaNotifiers MatchState lastMatchState MatchState @@ -105,6 +106,8 @@ func NewArena(dbPath string) (*Arena, error) { arena.configureNotifiers() + arena.ScoringPanelRegistry.initialize() + // Load empty match as current. arena.MatchState = PreMatch arena.LoadTestMatch() @@ -186,6 +189,7 @@ func (arena *Arena) LoadMatch(match *model.Match) error { arena.BlueRealtimeScore = NewRealtimeScore() arena.FieldVolunteers = false arena.FieldReset = false + arena.ScoringPanelRegistry.resetScoreCommitted() // Notify any listeners about the new match. arena.MatchLoadNotifier.Notify() diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index 7cfbe44..951f2f3 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -214,11 +214,14 @@ func (arena *Arena) generateScorePostedMessage() interface{} { func (arena *Arena) generateScoringStatusMessage() interface{} { return &struct { - RefereeScoreReady bool - RedScoreReady bool - BlueScoreReady bool + RefereeScoreReady bool + NumRedScoringPanels int + NumRedScoringPanelsReady int + NumBlueScoringPanels int + NumBlueScoringPanelsReady int }{arena.RedRealtimeScore.FoulsCommitted && arena.BlueRealtimeScore.FoulsCommitted, - arena.RedRealtimeScore.TeleopCommitted, arena.BlueRealtimeScore.TeleopCommitted} + arena.ScoringPanelRegistry.GetNumPanels("red"), arena.ScoringPanelRegistry.GetNumScoreCommitted("red"), + arena.ScoringPanelRegistry.GetNumPanels("blue"), arena.ScoringPanelRegistry.GetNumScoreCommitted("blue")} } // Constructs the data object for one alliance sent to the audience display for the realtime scoring overlay. diff --git a/field/realtime_score.go b/field/realtime_score.go index 1ac753e..5e23321 100644 --- a/field/realtime_score.go +++ b/field/realtime_score.go @@ -8,10 +8,9 @@ package field import "github.com/Team254/cheesy-arena/game" type RealtimeScore struct { - CurrentScore game.Score - Cards map[string]string - TeleopCommitted bool - FoulsCommitted bool + CurrentScore game.Score + Cards map[string]string + FoulsCommitted bool } func NewRealtimeScore() *RealtimeScore { diff --git a/field/scoring_panel_registry.go b/field/scoring_panel_registry.go new file mode 100644 index 0000000..7ae563e --- /dev/null +++ b/field/scoring_panel_registry.go @@ -0,0 +1,78 @@ +// Copyright 2019 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Model representing and methods for tracking the state of a realtime scoring panel. + +package field + +import ( + "github.com/Team254/cheesy-arena/websocket" + "sync" +) + +type ScoringPanelRegistry struct { + scoringPanels map[string]map[*websocket.Websocket]bool // The score committed state for each panel. + mutex sync.Mutex +} + +func (registry *ScoringPanelRegistry) initialize() { + registry.scoringPanels = map[string]map[*websocket.Websocket]bool{"red": {}, "blue": {}} +} + +// Resets the score committed state for each registered panel to false. +func (registry *ScoringPanelRegistry) resetScoreCommitted() { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + for _, alliancePanels := range registry.scoringPanels { + for key := range alliancePanels { + alliancePanels[key] = false + } + } +} + +// Returns the number of registered panels for the given alliance. +func (registry *ScoringPanelRegistry) GetNumPanels(alliance string) int { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + return len(registry.scoringPanels[alliance]) +} + +// Returns the number of registered panels whose score is committed for the given alliance. +func (registry *ScoringPanelRegistry) GetNumScoreCommitted(alliance string) int { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + numCommitted := 0 + for _, panel := range registry.scoringPanels[alliance] { + if panel { + numCommitted++ + } + } + return numCommitted +} + +// Adds a panel to the registry, referenced by its websocket pointer. +func (registry *ScoringPanelRegistry) RegisterPanel(alliance string, ws *websocket.Websocket) { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + registry.scoringPanels[alliance][ws] = false +} + +// Sets the score committed state to true for the given panel, referenced by its websocket pointer. +func (registry *ScoringPanelRegistry) SetScoreCommitted(alliance string, ws *websocket.Websocket) { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + registry.scoringPanels[alliance][ws] = true +} + +// Removes a panel from the registry, referenced by its websocket pointer. +func (registry *ScoringPanelRegistry) UnregisterPanel(alliance string, ws *websocket.Websocket) { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + delete(registry.scoringPanels[alliance], ws) +} diff --git a/field/scoring_panel_registry_test.go b/field/scoring_panel_registry_test.go new file mode 100644 index 0000000..bbc37cb --- /dev/null +++ b/field/scoring_panel_registry_test.go @@ -0,0 +1,51 @@ +// Copyright 2019 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) + +package field + +import ( + "github.com/Team254/cheesy-arena/websocket" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestScoringPanelRegistry(t *testing.T) { + var registry ScoringPanelRegistry + registry.initialize() + assert.Equal(t, 0, registry.GetNumPanels("red")) + assert.Equal(t, 0, registry.GetNumScoreCommitted("red")) + assert.Equal(t, 0, registry.GetNumPanels("blue")) + assert.Equal(t, 0, registry.GetNumScoreCommitted("blue")) + + ws1 := new(websocket.Websocket) + ws2 := new(websocket.Websocket) + ws3 := new(websocket.Websocket) + registry.RegisterPanel("red", ws1) + registry.RegisterPanel("blue", ws2) + registry.RegisterPanel("red", ws3) + assert.Equal(t, 2, registry.GetNumPanels("red")) + assert.Equal(t, 0, registry.GetNumScoreCommitted("red")) + assert.Equal(t, 1, registry.GetNumPanels("blue")) + assert.Equal(t, 0, registry.GetNumScoreCommitted("blue")) + + registry.SetScoreCommitted("red", ws3) + registry.SetScoreCommitted("blue", ws2) + registry.SetScoreCommitted("blue", ws2) + assert.Equal(t, 2, registry.GetNumPanels("red")) + assert.Equal(t, 1, registry.GetNumScoreCommitted("red")) + assert.Equal(t, 1, registry.GetNumPanels("blue")) + assert.Equal(t, 1, registry.GetNumScoreCommitted("blue")) + + registry.UnregisterPanel("red", ws1) + registry.UnregisterPanel("blue", ws2) + assert.Equal(t, 1, registry.GetNumPanels("red")) + assert.Equal(t, 1, registry.GetNumScoreCommitted("red")) + assert.Equal(t, 0, registry.GetNumPanels("blue")) + assert.Equal(t, 0, registry.GetNumScoreCommitted("blue")) + + registry.resetScoreCommitted() + assert.Equal(t, 1, registry.GetNumPanels("red")) + assert.Equal(t, 0, registry.GetNumScoreCommitted("red")) + assert.Equal(t, 0, registry.GetNumPanels("blue")) + assert.Equal(t, 0, registry.GetNumScoreCommitted("blue")) +} diff --git a/static/js/match_play.js b/static/js/match_play.js index 43baaf8..110e3bf 100644 --- a/static/js/match_play.js +++ b/static/js/match_play.js @@ -211,10 +211,14 @@ var handleAudienceDisplayMode = function(data) { // Handles a websocket message to signal whether the referee and scorers have committed after the match. var handleScoringStatus = function(data) { - scoreIsReady = data.RefereeScoreReady && data.RedScoreReady && data.BlueScoreReady; + var redScoreReady = data.NumRedScoringPanels > 0 && data.NumRedScoringPanelsReady >= data.NumRedScoringPanels; + var blueScoreReady = data.NumBlueScoringPanels > 0 && data.NumBlueScoringPanelsReady >= data.NumBlueScoringPanels; + scoreIsReady = data.RefereeScoreReady && redScoreReady && blueScoreReady; $("#refereeScoreStatus").attr("data-ready", data.RefereeScoreReady); - $("#redScoreStatus").attr("data-ready", data.RedScoreReady); - $("#blueScoreStatus").attr("data-ready", data.BlueScoreReady); + $("#redScoreStatus").text("Red Scoring " + data.NumRedScoringPanelsReady + "/" + data.NumRedScoringPanels); + $("#redScoreStatus").attr("data-ready", redScoreReady); + $("#blueScoreStatus").text("Blue Scoring " + data.NumBlueScoringPanelsReady + "/" + data.NumBlueScoringPanels); + $("#blueScoreStatus").attr("data-ready", blueScoreReady); }; // Handles a websocket message to update the alliance station display screen selector. diff --git a/templates/match_play.html b/templates/match_play.html index 2c97233..c512adb 100644 --- a/templates/match_play.html +++ b/templates/match_play.html @@ -114,8 +114,8 @@

Scoring Status

Referee
- Red Scoring
- Blue Scoring

+
+


{{if .EventSettings.PlcAddress}}

PLC Status

diff --git a/web/scoring_panel.go b/web/scoring_panel.go index 6998bec..7d5b728 100644 --- a/web/scoring_panel.go +++ b/web/scoring_panel.go @@ -107,6 +107,10 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ return } defer ws.Close() + web.arena.ScoringPanelRegistry.RegisterPanel(alliance, ws) + web.arena.ScoringStatusNotifier.Notify() + defer web.arena.ScoringStatusNotifier.Notify() + defer web.arena.ScoringPanelRegistry.UnregisterPanel(alliance, ws) // 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, @@ -132,12 +136,8 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ ws.WriteError("Cannot commit score: Match is not over.") continue } - - if !(*realtimeScore).TeleopCommitted { - (*realtimeScore).TeleopCommitted = true - web.arena.ScoringStatusNotifier.Notify() - scoreChanged = true - } + web.arena.ScoringPanelRegistry.SetScoreCommitted(alliance, ws) + web.arena.ScoringStatusNotifier.Notify() } else if number, err := strconv.Atoi(command); err == nil && number >= 1 && number <= 9 { // Handle per-robot scoring fields. if number <= 3 { diff --git a/web/scoring_panel_test.go b/web/scoring_panel_test.go index 715bc70..63eb9d6 100644 --- a/web/scoring_panel_test.go +++ b/web/scoring_panel_test.go @@ -5,6 +5,7 @@ package web import ( "github.com/Team254/cheesy-arena/field" + "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/websocket" gorillawebsocket "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" @@ -36,12 +37,16 @@ func TestScoringPanelWebsocket(t *testing.T) { assert.Nil(t, err) defer redConn.Close() redWs := websocket.NewTestWebsocket(redConn) + assert.Equal(t, 1, web.arena.ScoringPanelRegistry.GetNumPanels("red")) + assert.Equal(t, 0, web.arena.ScoringPanelRegistry.GetNumPanels("blue")) blueConn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/panels/scoring/blue/websocket", nil) assert.Nil(t, err) defer blueConn.Close() blueWs := websocket.NewTestWebsocket(blueConn) + assert.Equal(t, 1, web.arena.ScoringPanelRegistry.GetNumPanels("red")) + assert.Equal(t, 1, web.arena.ScoringPanelRegistry.GetNumPanels("blue")) - // Should receive a score update right after connection. + // Should get a few status updates right after connection. readWebsocketType(t, redWs, "matchLoad") readWebsocketType(t, redWs, "matchTime") readWebsocketType(t, redWs, "realtimeScore") @@ -49,57 +54,77 @@ func TestScoringPanelWebsocket(t *testing.T) { readWebsocketType(t, blueWs, "matchTime") readWebsocketType(t, blueWs, "realtimeScore") - // TODO(pat): Update for 2019. - /* - // Send a match worth of scoring commands in. - redWs.Write("r", nil) - blueWs.Write("r", nil) - blueWs.Write("r", nil) - blueWs.Write("r", nil) - blueWs.Write("r", nil) - blueWs.Write("R", nil) - for i := 0; i < 5; i++ { - readWebsocketType(t, redWs, "realtimeScore") - readWebsocketType(t, blueWs, "realtimeScore") - } - redWs.Write("\r", nil) - blueWs.Write("\r", nil) - redWs.Write("a", nil) - redWs.Write("\r", nil) - for i := 0; i < 4; i++ { - readWebsocketType(t, redWs, "realtimeScore") - readWebsocketType(t, blueWs, "realtimeScore") - } + // Send a some pre-match scoring commands. + redWs.Write("1", nil) + blueWs.Write("2", nil) + blueWs.Write("2", nil) + blueWs.Write("2", nil) + blueWs.Write("2", nil) + for i := 0; i < 5; i++ { + readWebsocketType(t, redWs, "realtimeScore") + readWebsocketType(t, blueWs, "realtimeScore") + } + assert.Equal(t, 1, web.arena.RedRealtimeScore.CurrentScore.RobotStartLevels[0]) + assert.Equal(t, 0, web.arena.BlueRealtimeScore.CurrentScore.RobotStartLevels[1]) + redWs.Write("e", nil) + redWs.Write("i", nil) + redWs.Write("i", nil) + redWs.Write("v", nil) + redWs.Write("q", nil) + redWs.Write(",", nil) + for i := 0; i < 3; i++ { + readWebsocketType(t, redWs, "realtimeScore") + readWebsocketType(t, blueWs, "realtimeScore") + } + assert.Equal(t, [8]game.BayStatus{1, 0, 0, 0, 0, 0, 0, 3}, + web.arena.RedRealtimeScore.CurrentScore.CargoBaysPreMatch) + assert.Equal(t, [8]game.BayStatus{1, 0, 0, 0, 0, 0, 0, 3}, web.arena.RedRealtimeScore.CurrentScore.CargoBays) + assert.Equal(t, [3]game.BayStatus{0, 0, 0}, web.arena.RedRealtimeScore.CurrentScore.RocketNearLeftBays) + assert.Equal(t, [3]game.BayStatus{0, 0, 0}, web.arena.RedRealtimeScore.CurrentScore.RocketNearRightBays) + assert.Equal(t, [3]game.BayStatus{0, 0, 0}, web.arena.RedRealtimeScore.CurrentScore.RocketFarLeftBays) + assert.Equal(t, [3]game.BayStatus{0, 0, 0}, web.arena.RedRealtimeScore.CurrentScore.RocketFarRightBays) - assert.Equal(t, 1, web.arena.RedRealtimeScore.CurrentScore.AutoRuns) - assert.Equal(t, 2, web.arena.BlueRealtimeScore.CurrentScore.AutoRuns) - - redWs.Write("r", nil) - - // Make sure auto scores haven't changed in teleop. - assert.Equal(t, 1, web.arena.RedRealtimeScore.CurrentScore.AutoRuns) - assert.Equal(t, 2, web.arena.BlueRealtimeScore.CurrentScore.AutoRuns) - */ + // Send some in-match scoring commands. + web.arena.MatchState = field.AutoPeriod + redWs.Write("e", nil) + redWs.Write("i", nil) + redWs.Write("k", nil) + redWs.Write("4", nil) + blueWs.Write("9", nil) + for i := 0; i < 5; i++ { + readWebsocketType(t, redWs, "realtimeScore") + readWebsocketType(t, blueWs, "realtimeScore") + } + assert.Equal(t, [8]game.BayStatus{1, 0, 0, 0, 0, 0, 0, 3}, + web.arena.RedRealtimeScore.CurrentScore.CargoBaysPreMatch) + assert.Equal(t, [8]game.BayStatus{2, 0, 0, 0, 0, 0, 0, 2}, web.arena.RedRealtimeScore.CurrentScore.CargoBays) + assert.Equal(t, [3]game.BayStatus{0, 1, 0}, web.arena.RedRealtimeScore.CurrentScore.RocketFarRightBays) + assert.True(t, web.arena.RedRealtimeScore.CurrentScore.SandstormBonuses[0]) + assert.Equal(t, 1, web.arena.BlueRealtimeScore.CurrentScore.RobotEndLevels[2]) // Test committing logic. redWs.Write("commitMatch", nil) readWebsocketType(t, redWs, "error") blueWs.Write("commitMatch", nil) readWebsocketType(t, blueWs, "error") - assert.False(t, web.arena.RedRealtimeScore.TeleopCommitted) - assert.False(t, web.arena.BlueRealtimeScore.TeleopCommitted) + assert.Equal(t, 0, web.arena.ScoringPanelRegistry.GetNumScoreCommitted("red")) + assert.Equal(t, 0, web.arena.ScoringPanelRegistry.GetNumScoreCommitted("blue")) web.arena.MatchState = field.PostMatch redWs.Write("commitMatch", nil) blueWs.Write("commitMatch", nil) time.Sleep(time.Millisecond * 10) // Allow some time for the commands to be processed. - assert.True(t, web.arena.RedRealtimeScore.TeleopCommitted) - assert.True(t, web.arena.BlueRealtimeScore.TeleopCommitted) + assert.Equal(t, 1, web.arena.ScoringPanelRegistry.GetNumScoreCommitted("red")) + assert.Equal(t, 1, web.arena.ScoringPanelRegistry.GetNumScoreCommitted("blue")) // Load another match to reset the results. web.arena.ResetMatch() web.arena.LoadTestMatch() + readWebsocketType(t, redWs, "matchLoad") readWebsocketType(t, redWs, "realtimeScore") + readWebsocketType(t, blueWs, "matchLoad") readWebsocketType(t, blueWs, "realtimeScore") 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")) + assert.Equal(t, 0, web.arena.ScoringPanelRegistry.GetNumScoreCommitted("blue")) }