From 27c38f739339284d79835a110fb13a75244b8b94 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Fri, 31 Aug 2018 22:40:08 -0700 Subject: [PATCH] Refactor websocket model to reduce duplicated code. --- coverage | 2 +- field/arena.go | 177 +++++++------------ field/arena_notifiers.go | 184 +++++++++++++++++++ field/notifier.go | 57 ------ field/notifier_test.go | 84 --------- field/plc.go | 104 ++++++----- field/realtime_score.go | 1 + static/css/alliance_station_display.css | 10 +- static/js/alliance_station_display.js | 41 ++--- static/js/announcer_display.js | 39 ++--- static/js/audience_display.js | 78 +++++---- static/js/cheesy-websocket.js | 6 +- static/js/fta_display.js | 4 +- static/js/match_play.js | 20 +-- static/js/referee_display.js | 11 ++ static/js/scoring_display.js | 22 ++- templates/announcer_display.html | 24 +-- web/alliance_station_display.go | 181 +++---------------- web/alliance_station_display_test.go | 41 ++--- web/announcer_display.go | 181 ++----------------- web/announcer_display_test.go | 30 ++-- web/audience_display.go | 224 ++---------------------- web/audience_display_test.go | 31 ++-- web/fta_display.go | 60 +------ web/match_play.go | 191 +++++--------------- web/match_play_test.go | 65 +++---- web/pit_display.go | 45 +---- web/pit_display_test.go | 8 +- web/referee_display.go | 68 +++---- web/referee_display_test.go | 21 ++- web/scoring_display.go | 130 +++++--------- web/scoring_display_test.go | 48 ++--- web/setup_alliance_selection.go | 6 +- web/setup_field.go | 48 +---- web/setup_lower_thirds.go | 39 +++-- web/setup_lower_thirds_test.go | 14 +- web/web_test.go | 12 +- web/websocket.go | 68 ------- websocket/notifier.go | 80 +++++++++ websocket/notifier_test.go | 90 ++++++++++ websocket/websocket.go | 148 ++++++++++++++++ websocket/websocket_test.go | 116 ++++++++++++ 42 files changed, 1190 insertions(+), 1619 deletions(-) create mode 100644 field/arena_notifiers.go delete mode 100644 field/notifier.go delete mode 100644 field/notifier_test.go delete mode 100644 web/websocket.go create mode 100644 websocket/notifier.go create mode 100644 websocket/notifier_test.go create mode 100644 websocket/websocket.go create mode 100644 websocket/websocket_test.go diff --git a/coverage b/coverage index d414076..b745af2 100755 --- a/coverage +++ b/coverage @@ -1 +1 @@ -go test -coverprofile=coverage.out && sleep 1 && go tool cover -html=coverage.out +go test -coverprofile=coverage.out ./... && sleep 1 && go tool cover -html=coverage.out diff --git a/field/arena.go b/field/arena.go index 42d4f84..e1e8ab4 100644 --- a/field/arena.go +++ b/field/arena.go @@ -46,58 +46,37 @@ type Arena struct { TbaClient *partner.TbaClient StemTvClient *partner.StemTvClient AllianceStations map[string]*AllianceStation - CurrentMatch *model.Match + ArenaNotifiers MatchState - lastMatchState MatchState - MatchStartTime time.Time - LastMatchTimeSec float64 - RedRealtimeScore *RealtimeScore - BlueRealtimeScore *RealtimeScore - lastDsPacketTime time.Time - FieldVolunteers bool - FieldReset bool - AudienceDisplayScreen string - SavedMatch *model.Match - SavedMatchResult *model.MatchResult - AllianceStationDisplays map[string]string - AllianceStationDisplayScreen string - MuteMatchSounds bool - matchAborted bool - matchStateNotifier *Notifier - MatchTimeNotifier *Notifier - RobotStatusNotifier *Notifier - MatchLoadTeamsNotifier *Notifier - ScoringStatusNotifier *Notifier - RealtimeScoreNotifier *Notifier - ScorePostedNotifier *Notifier - AudienceDisplayNotifier *Notifier - PlaySoundNotifier *Notifier - AllianceStationDisplayNotifier *Notifier - AllianceSelectionNotifier *Notifier - LowerThirdNotifier *Notifier - ReloadDisplaysNotifier *Notifier - Scale *game.Seesaw - RedSwitch *game.Seesaw - BlueSwitch *game.Seesaw - RedVault *game.Vault - BlueVault *game.Vault - ScaleLeds led.Controller - RedSwitchLeds led.Controller - BlueSwitchLeds led.Controller - RedVaultLeds vaultled.Controller - BlueVaultLeds vaultled.Controller - warmupLedMode led.Mode - lastRedAllianceReady bool - lastBlueAllianceReady bool -} - -type ArenaStatus struct { - AllianceStations map[string]*AllianceStation - MatchState - CanStartMatch bool - PlcIsHealthy bool - FieldEstop bool - GameSpecificData string + lastMatchState MatchState + CurrentMatch *model.Match + MatchStartTime time.Time + LastMatchTimeSec float64 + RedRealtimeScore *RealtimeScore + BlueRealtimeScore *RealtimeScore + lastDsPacketTime time.Time + FieldVolunteers bool + FieldReset bool + AudienceDisplayMode string + SavedMatch *model.Match + SavedMatchResult *model.MatchResult + AllianceStationDisplays map[string]string + AllianceStationDisplayMode string + MuteMatchSounds bool + matchAborted bool + Scale *game.Seesaw + RedSwitch *game.Seesaw + BlueSwitch *game.Seesaw + RedVault *game.Vault + BlueVault *game.Vault + ScaleLeds led.Controller + RedSwitchLeds led.Controller + BlueSwitchLeds led.Controller + RedVaultLeds vaultled.Controller + BlueVaultLeds vaultled.Controller + warmupLedMode led.Mode + lastRedAllianceReady bool + lastBlueAllianceReady bool } type AllianceStation struct { @@ -130,32 +109,20 @@ func NewArena(dbPath string) (*Arena, error) { arena.AllianceStations["B2"] = new(AllianceStation) arena.AllianceStations["B3"] = new(AllianceStation) - arena.matchStateNotifier = NewNotifier() - arena.MatchTimeNotifier = NewNotifier() - arena.RobotStatusNotifier = NewNotifier() - arena.MatchLoadTeamsNotifier = NewNotifier() - arena.ScoringStatusNotifier = NewNotifier() - arena.RealtimeScoreNotifier = NewNotifier() - arena.ScorePostedNotifier = NewNotifier() - arena.AudienceDisplayNotifier = NewNotifier() - arena.PlaySoundNotifier = NewNotifier() - arena.AllianceStationDisplayNotifier = NewNotifier() - arena.AllianceSelectionNotifier = NewNotifier() - arena.LowerThirdNotifier = NewNotifier() - arena.ReloadDisplaysNotifier = NewNotifier() + arena.configureNotifiers() // Load empty match as current. arena.MatchState = PreMatch arena.LoadTestMatch() - arena.lastMatchState = -1 arena.LastMatchTimeSec = 0 + arena.lastMatchState = -1 // Initialize display parameters. - arena.AudienceDisplayScreen = "blank" + arena.AudienceDisplayMode = "blank" arena.SavedMatch = &model.Match{} arena.SavedMatchResult = model.NewMatchResult() arena.AllianceStationDisplays = make(map[string]string) - arena.AllianceStationDisplayScreen = "match" + arena.AllianceStationDisplayMode = "match" return arena, nil } @@ -257,10 +224,10 @@ func (arena *Arena) LoadMatch(match *model.Match) error { arena.BlueSwitchLeds.SetSidedness(true) // Notify any listeners about the new match. - arena.MatchLoadTeamsNotifier.Notify(nil) - arena.RealtimeScoreNotifier.Notify(nil) - arena.AllianceStationDisplayScreen = "match" - arena.AllianceStationDisplayNotifier.Notify(nil) + arena.MatchLoadNotifier.Notify() + arena.RealtimeScoreNotifier.Notify() + arena.AllianceStationDisplayMode = "match" + arena.AllianceStationDisplayModeNotifier.Notify() // Set the initial state of the lights. arena.ScaleLeds.SetMode(led.OffMode, led.OffMode) @@ -325,7 +292,7 @@ func (arena *Arena) SubstituteTeam(teamId int, station string) error { arena.CurrentMatch.Blue3 = teamId } arena.setupNetwork() - arena.MatchLoadTeamsNotifier.Notify(nil) + arena.MatchLoadNotifier.Notify() if arena.CurrentMatch.Type != "test" { arena.Database.SaveMatch(arena.CurrentMatch) @@ -379,12 +346,14 @@ func (arena *Arena) AbortMatch() error { return fmt.Errorf("Cannot abort match when it is not in progress.") } if !arena.MuteMatchSounds && arena.MatchState != WarmupPeriod { - arena.PlaySoundNotifier.Notify("match-abort") + arena.PlaySoundNotifier.NotifyWithMessage("match-abort") } arena.MatchState = PostMatch arena.matchAborted = true - arena.AudienceDisplayScreen = "blank" - arena.AudienceDisplayNotifier.Notify(nil) + arena.AudienceDisplayMode = "blank" + arena.AudienceDisplayModeNotifier.Notify() + arena.AllianceStationDisplayMode = "logo" + arena.AllianceStationDisplayModeNotifier.Notify() return nil } @@ -432,11 +401,13 @@ func (arena *Arena) Update() { arena.LastMatchTimeSec = -1 auto = true enabled = false - arena.AudienceDisplayScreen = "match" - arena.AudienceDisplayNotifier.Notify(nil) + arena.AudienceDisplayMode = "match" + arena.AudienceDisplayModeNotifier.Notify() + arena.AllianceStationDisplayMode = "match" + arena.AllianceStationDisplayModeNotifier.Notify() arena.sendGameSpecificDataPacket() if !arena.MuteMatchSounds { - arena.PlaySoundNotifier.Notify("match-warmup") + arena.PlaySoundNotifier.NotifyWithMessage("match-warmup") } // Pick an LED warmup mode at random to keep things interesting. allWarmupModes := []led.Mode{led.WarmupMode, led.Warmup2Mode, led.Warmup3Mode, led.Warmup4Mode} @@ -450,7 +421,7 @@ func (arena *Arena) Update() { enabled = true sendDsPacket = true if !arena.MuteMatchSounds { - arena.PlaySoundNotifier.Notify("match-start") + arena.PlaySoundNotifier.NotifyWithMessage("match-start") } } case AutoPeriod: @@ -462,7 +433,7 @@ func (arena *Arena) Update() { enabled = false sendDsPacket = true if !arena.MuteMatchSounds { - arena.PlaySoundNotifier.Notify("match-end") + arena.PlaySoundNotifier.NotifyWithMessage("match-end") } } case PausePeriod: @@ -475,7 +446,7 @@ func (arena *Arena) Update() { enabled = true sendDsPacket = true if !arena.MuteMatchSounds { - arena.PlaySoundNotifier.Notify("match-resume") + arena.PlaySoundNotifier.NotifyWithMessage("match-resume") } } case TeleopPeriod: @@ -486,7 +457,7 @@ func (arena *Arena) Update() { arena.MatchState = EndgamePeriod sendDsPacket = false if !arena.MuteMatchSounds { - arena.PlaySoundNotifier.Notify("match-endgame") + arena.PlaySoundNotifier.NotifyWithMessage("match-endgame") } } case EndgamePeriod: @@ -501,38 +472,32 @@ func (arena *Arena) Update() { go func() { // Leave the scores on the screen briefly at the end of the match. time.Sleep(time.Second * matchEndScoreDwellSec) - arena.AudienceDisplayScreen = "blank" - arena.AudienceDisplayNotifier.Notify(nil) - arena.AllianceStationDisplayScreen = "logo" - arena.AllianceStationDisplayNotifier.Notify(nil) + arena.AudienceDisplayMode = "blank" + arena.AudienceDisplayModeNotifier.Notify() + arena.AllianceStationDisplayMode = "logo" + arena.AllianceStationDisplayModeNotifier.Notify() }() if !arena.MuteMatchSounds { - arena.PlaySoundNotifier.Notify("match-end") + arena.PlaySoundNotifier.NotifyWithMessage("match-end") } } } - // Send a notification if the match state has changed. - if arena.MatchState != arena.lastMatchState { - arena.matchStateNotifier.Notify(arena.MatchState) - } - arena.lastMatchState = arena.MatchState - - // Send a match tick notification if passing an integer second threshold. - if int(matchTimeSec) != int(arena.LastMatchTimeSec) { - arena.MatchTimeNotifier.Notify(int(matchTimeSec)) + // Send a match tick notification if passing an integer second threshold or if the match state changed. + if int(matchTimeSec) != int(arena.LastMatchTimeSec) || arena.MatchState != arena.lastMatchState { + arena.MatchTimeNotifier.Notify() } arena.LastMatchTimeSec = matchTimeSec + arena.lastMatchState = arena.MatchState // Send a packet if at a period transition point or if it's been long enough since the last one. if sendDsPacket || time.Since(arena.lastDsPacketTime).Seconds()*1000 >= dsPacketPeriodMs { arena.sendDsPacket(auto, enabled) - arena.RobotStatusNotifier.Notify(nil) + arena.ArenaStatusNotifier.Notify() } // Handle field sensors/lights/motors. arena.handlePlcInput() - arena.handlePlcOutput() arena.handleLeds() } @@ -560,11 +525,6 @@ func (arena *Arena) BlueScoreSummary() *game.ScoreSummary { return arena.BlueRealtimeScore.CurrentScore.Summarize(arena.RedRealtimeScore.CurrentScore.Fouls) } -func (arena *Arena) GetStatus() *ArenaStatus { - return &ArenaStatus{arena.AllianceStations, arena.MatchState, arena.checkCanStartMatch() == nil, - arena.Plc.IsHealthy, arena.Plc.GetFieldEstop(), arena.CurrentMatch.GameSpecificData} -} - // Loads a team into an alliance station, cleaning up the previous team there if there is one. func (arena *Arena) assignTeam(teamId int, station string) error { // Reject invalid station values. @@ -766,23 +726,18 @@ func (arena *Arena) handlePlcInput() { // Check if a power up has been newly played and trigger the accompanying sound effect if so. newRedPowerUp := arena.RedVault.CheckForNewlyPlayedPowerUp() if newRedPowerUp != "" && !arena.MuteMatchSounds { - arena.PlaySoundNotifier.Notify("match-" + newRedPowerUp) + arena.PlaySoundNotifier.NotifyWithMessage("match-" + newRedPowerUp) } newBluePowerUp := arena.BlueVault.CheckForNewlyPlayedPowerUp() if newBluePowerUp != "" && !arena.MuteMatchSounds { - arena.PlaySoundNotifier.Notify("match-" + newBluePowerUp) + arena.PlaySoundNotifier.NotifyWithMessage("match-" + newBluePowerUp) } if !oldRedScore.Equals(redScore) || !oldBlueScore.Equals(blueScore) || ownershipChanged { - arena.RealtimeScoreNotifier.Notify(nil) + arena.RealtimeScoreNotifier.Notify() } } -// Writes light/motor commands to the field PLC. -func (arena *Arena) handlePlcOutput() { - // TODO(patrick): Update for 2018. -} - func (arena *Arena) handleLeds() { switch arena.MatchState { case PreMatch: diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go new file mode 100644 index 0000000..1ac61ed --- /dev/null +++ b/field/arena_notifiers.go @@ -0,0 +1,184 @@ +// Copyright 2018 Team 254. All Rights Reserved. +// Author: pat@patfairbank.com (Patrick Fairbank) +// +// Contains configuration of the publish-subscribe notifiers that allow the arena to push updates to websocket clients. + +package field + +import ( + "github.com/Team254/cheesy-arena/game" + "github.com/Team254/cheesy-arena/model" + "github.com/Team254/cheesy-arena/websocket" + "strconv" + "time" +) + +type ArenaNotifiers struct { + AllianceSelectionNotifier *websocket.Notifier + AllianceStationDisplayModeNotifier *websocket.Notifier + ArenaStatusNotifier *websocket.Notifier + AudienceDisplayModeNotifier *websocket.Notifier + LowerThirdNotifier *websocket.Notifier + MatchLoadNotifier *websocket.Notifier + MatchTimeNotifier *websocket.Notifier + PlaySoundNotifier *websocket.Notifier + RealtimeScoreNotifier *websocket.Notifier + ReloadDisplaysNotifier *websocket.Notifier + ScorePostedNotifier *websocket.Notifier + ScoringStatusNotifier *websocket.Notifier +} + +type MatchTimeMessage struct { + MatchState int + MatchTimeSec int +} + +type audienceAllianceScoreFields struct { + Score int + RealtimeScore *RealtimeScore + ForceState game.PowerUpState + LevitateState game.PowerUpState + BoostState game.PowerUpState + SwitchOwnedBy game.Alliance +} + +// Instantiates notifiers and configures their message producing methods. +func (arena *Arena) configureNotifiers() { + arena.AllianceSelectionNotifier = websocket.NewNotifier("allianceSelection", nil) + arena.AllianceStationDisplayModeNotifier = websocket.NewNotifier("allianceStationDisplayMode", + arena.generateAllianceStationDisplayModeMessage) + arena.ArenaStatusNotifier = websocket.NewNotifier("arenaStatus", arena.generateArenaStatusMessage) + arena.AudienceDisplayModeNotifier = websocket.NewNotifier("audienceDisplayMode", + arena.generateAudienceDisplayModeMessage) + arena.LowerThirdNotifier = websocket.NewNotifier("lowerThird", nil) + arena.MatchLoadNotifier = websocket.NewNotifier("matchLoad", arena.generateMatchLoadMessage) + arena.MatchTimeNotifier = websocket.NewNotifier("matchTime", arena.generateMatchTimeMessage) + arena.PlaySoundNotifier = websocket.NewNotifier("playSound", nil) + arena.RealtimeScoreNotifier = websocket.NewNotifier("realtimeScore", arena.generateRealtimeScoreMessage) + arena.ReloadDisplaysNotifier = websocket.NewNotifier("reload", nil) + arena.ScorePostedNotifier = websocket.NewNotifier("scorePosted", arena.generateScorePostedMessage) + arena.ScoringStatusNotifier = websocket.NewNotifier("scoringStatus", arena.generateScoringStatusMessage) +} + +func (arena *Arena) generateAllianceStationDisplayModeMessage() interface{} { + return arena.AllianceStationDisplayMode +} + +func (arena *Arena) generateArenaStatusMessage() interface{} { + return &struct { + AllianceStations map[string]*AllianceStation + MatchState + CanStartMatch bool + PlcIsHealthy bool + FieldEstop bool + GameSpecificData string + }{arena.AllianceStations, arena.MatchState, arena.checkCanStartMatch() == nil, arena.Plc.IsHealthy, + arena.Plc.GetFieldEstop(), arena.CurrentMatch.GameSpecificData} +} + +func (arena *Arena) generateAudienceDisplayModeMessage() interface{} { + return arena.AudienceDisplayMode +} + +func (arena *Arena) generateMatchLoadMessage() interface{} { + teams := make(map[string]*model.Team) + for station, allianceStation := range arena.AllianceStations { + teams[station] = allianceStation.Team + } + + rankings := make(map[string]*game.Ranking) + for _, allianceStation := range arena.AllianceStations { + if allianceStation.Team != nil { + rankings[strconv.Itoa(allianceStation.Team.Id)], _ = + arena.Database.GetRankingForTeam(allianceStation.Team.Id) + } + } + + return &struct { + MatchType string + Match *model.Match + Teams map[string]*model.Team + Rankings map[string]*game.Ranking + }{arena.CurrentMatch.CapitalizedType(), arena.CurrentMatch, teams, rankings} +} + +func (arena *Arena) generateMatchTimeMessage() interface{} { + return MatchTimeMessage{int(arena.MatchState), int(arena.MatchTimeSec())} +} + +func (arena *Arena) generateRealtimeScoreMessage() interface{} { + fields := struct { + Red *audienceAllianceScoreFields + Blue *audienceAllianceScoreFields + ScaleOwnedBy game.Alliance + }{} + fields.Red = getAudienceAllianceScoreFields(arena.RedRealtimeScore, arena.RedScoreSummary(), + arena.RedVault, arena.RedSwitch) + fields.Blue = getAudienceAllianceScoreFields(arena.BlueRealtimeScore, arena.BlueScoreSummary(), + arena.BlueVault, arena.BlueSwitch) + fields.ScaleOwnedBy = arena.Scale.GetOwnedBy() + return &fields +} + +func (arena *Arena) generateScorePostedMessage() interface{} { + return &struct { + MatchType string + Match *model.Match + RedScoreSummary *game.ScoreSummary + BlueScoreSummary *game.ScoreSummary + RedFouls []game.Foul + BlueFouls []game.Foul + RedCards map[string]string + BlueCards map[string]string + }{arena.SavedMatch.CapitalizedType(), arena.SavedMatch, arena.SavedMatchResult.RedScoreSummary(), + arena.SavedMatchResult.BlueScoreSummary(), populateFoulDescriptions(arena.SavedMatchResult.RedScore.Fouls), + populateFoulDescriptions(arena.SavedMatchResult.BlueScore.Fouls), arena.SavedMatchResult.RedCards, + arena.SavedMatchResult.BlueCards} +} + +func (arena *Arena) generateScoringStatusMessage() interface{} { + return &struct { + RefereeScoreReady bool + RedScoreReady bool + BlueScoreReady bool + }{arena.RedRealtimeScore.FoulsCommitted && arena.BlueRealtimeScore.FoulsCommitted, + arena.RedRealtimeScore.TeleopCommitted, arena.BlueRealtimeScore.TeleopCommitted} +} + +// Constructs the data object for one alliance sent to the audience display for the realtime scoring overlay. +func getAudienceAllianceScoreFields(allianceScore *RealtimeScore, allianceScoreSummary *game.ScoreSummary, + allianceVault *game.Vault, allianceSwitch *game.Seesaw) *audienceAllianceScoreFields { + fields := new(audienceAllianceScoreFields) + fields.RealtimeScore = allianceScore + fields.Score = allianceScoreSummary.Score + if allianceVault.ForcePowerUp != nil { + fields.ForceState = allianceVault.ForcePowerUp.GetState(time.Now()) + } else { + fields.ForceState = game.Unplayed + } + if allianceVault.LevitatePlayed { + fields.LevitateState = game.Expired + } else { + fields.LevitateState = game.Unplayed + } + if allianceVault.BoostPowerUp != nil { + fields.BoostState = allianceVault.BoostPowerUp.GetState(time.Now()) + } else { + fields.BoostState = game.Unplayed + } + fields.SwitchOwnedBy = allianceSwitch.GetOwnedBy() + return fields +} + +// Copy the description from the rules to the fouls so that they are available to the announcer. +func populateFoulDescriptions(fouls []game.Foul) []game.Foul { + for i := range fouls { + for _, rule := range game.Rules { + if fouls[i].RuleNumber == rule.RuleNumber { + fouls[i].Description = rule.Description + break + } + } + } + return fouls +} diff --git a/field/notifier.go b/field/notifier.go deleted file mode 100644 index 920ab0b..0000000 --- a/field/notifier.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2014 Team 254. All Rights Reserved. -// Author: pat@patfairbank.com (Patrick Fairbank) -// -// Publish-subscribe model for nonblocking notification of server events to websocket clients. - -package field - -import ( - "log" -) - -// Allow the listeners to buffer a small number of notifications to streamline delivery. -const notifyBufferSize = 3 - -type Notifier struct { - // The map is essentially a set; the value is ignored. - listeners map[chan interface{}]struct{} -} - -func NewNotifier() *Notifier { - notifier := new(Notifier) - notifier.listeners = make(map[chan interface{}]struct{}) - return notifier -} - -// Registers and returns a channel that can be read from to receive notification messages. The caller is -// responsible for closing the channel, which will cause it to be reaped from the list of listeners. -func (notifier *Notifier) Listen() chan interface{} { - listener := make(chan interface{}, notifyBufferSize) - notifier.listeners[listener] = struct{}{} - return listener -} - -// Sends the given message to all registered listeners, and cleans up any listeners that have closed. -func (notifier *Notifier) Notify(message interface{}) { - for listener, _ := range notifier.listeners { - notifier.notifyListener(listener, message) - } -} - -func (notifier *Notifier) notifyListener(listener chan interface{}, message interface{}) { - defer func() { - // If channel is closed sending to it will cause a panic; recover and remove it from the list. - if r := recover(); r != nil { - delete(notifier.listeners, listener) - } - }() - - // Do a non-blocking send. This guarantees that sending notifications won't interrupt the main event loop, - // at the risk of clients missing some messages if they don't read them all promptly. - select { - case listener <- message: - // The notification was sent and received successfully. - default: - log.Println("Failed to send a notification due to blocked listener.") - } -} diff --git a/field/notifier_test.go b/field/notifier_test.go deleted file mode 100644 index 14c340a..0000000 --- a/field/notifier_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2014 Team 254. All Rights Reserved. -// Author: pat@patfairbank.com (Patrick Fairbank) - -package field - -import ( - "github.com/stretchr/testify/assert" - "io/ioutil" - "log" - "testing" -) - -func TestNotifier(t *testing.T) { - notifier := NewNotifier() - - // Should do nothing when there are no listeners. - notifier.Notify("test message") - notifier.Notify(12345) - notifier.Notify(struct{}{}) - - listener := notifier.Listen() - notifier.Notify("test message") - assert.Equal(t, "test message", <-listener) - notifier.Notify(12345) - assert.Equal(t, 12345, <-listener) - - // Should allow multiple messages without blocking. - notifier.Notify("message1") - notifier.Notify("message2") - notifier.Notify("message3") - assert.Equal(t, "message1", <-listener) - assert.Equal(t, "message2", <-listener) - assert.Equal(t, "message3", <-listener) - - // Should stop sending messages and not block once the buffer is full. - log.SetOutput(ioutil.Discard) // Silence noisy log output. - for i := 0; i < 20; i++ { - notifier.Notify(i) - } - var value interface{} - var lastValue interface{} - for lastValue == nil { - select { - case value = <-listener: - default: - lastValue = value - return - } - } - notifier.Notify("next message") - assert.True(t, lastValue.(int) < 10) - assert.Equal(t, "next message", <-listener) -} - -func TestNotifyMultipleListeners(t *testing.T) { - notifier := NewNotifier() - listeners := [50]chan interface{}{} - for i := 0; i < len(listeners); i++ { - listeners[i] = notifier.Listen() - } - - notifier.Notify("test message") - notifier.Notify(12345) - for listener, _ := range notifier.listeners { - assert.Equal(t, "test message", <-listener) - assert.Equal(t, 12345, <-listener) - } - - // Should reap closed channels automatically. - close(listeners[4]) - notifier.Notify("message1") - assert.Equal(t, 49, len(notifier.listeners)) - for listener, _ := range notifier.listeners { - assert.Equal(t, "message1", <-listener) - } - close(listeners[16]) - close(listeners[21]) - close(listeners[49]) - notifier.Notify("message2") - assert.Equal(t, 46, len(notifier.listeners)) - for listener, _ := range notifier.listeners { - assert.Equal(t, "message2", <-listener) - } -} diff --git a/field/plc.go b/field/plc.go index 8afc753..b759dae 100644 --- a/field/plc.go +++ b/field/plc.go @@ -7,6 +7,7 @@ package field import ( "fmt" + "github.com/Team254/cheesy-arena/websocket" "github.com/goburrow/modbus" "log" "time" @@ -14,16 +15,16 @@ import ( type Plc struct { IsHealthy bool + IoChangeNotifier *websocket.Notifier address string handler *modbus.TCPClientHandler client modbus.Client - Inputs [inputCount]bool - Registers [registerCount]uint16 - Coils [coilCount]bool + inputs [inputCount]bool + registers [registerCount]uint16 + coils [coilCount]bool oldInputs [inputCount]bool oldRegisters [registerCount]uint16 oldCoils [coilCount]bool - IoChangeNotifier *Notifier cycleCounter int } @@ -112,7 +113,8 @@ func (plc *Plc) SetAddress(address string) { // Loops indefinitely to read inputs from and write outputs to PLC. func (plc *Plc) Run() { - plc.IoChangeNotifier = NewNotifier() + // Register a notifier that listeners can subscribe to to get websocket updates about I/O value changes. + plc.IoChangeNotifier = websocket.NewNotifier("plcIoChange", plc.generateIoChangeMessage) for { if plc.handler == nil { @@ -146,11 +148,11 @@ func (plc *Plc) Run() { } // Detect any changes in input or output and notify listeners if so. - if plc.Inputs != plc.oldInputs || plc.Registers != plc.oldRegisters || plc.Coils != plc.oldCoils { - plc.IoChangeNotifier.Notify(nil) - plc.oldInputs = plc.Inputs - plc.oldRegisters = plc.Registers - plc.oldCoils = plc.Coils + if plc.inputs != plc.oldInputs || plc.registers != plc.oldRegisters || plc.coils != plc.oldCoils { + plc.IoChangeNotifier.Notify() + plc.oldInputs = plc.inputs + plc.oldRegisters = plc.registers + plc.oldCoils = plc.coils } time.Sleep(time.Until(startTime.Add(time.Millisecond * plcLoopPeriodMs))) @@ -159,19 +161,19 @@ func (plc *Plc) Run() { // Returns the state of the field emergency stop button (true if e-stop is active). func (plc *Plc) GetFieldEstop() bool { - return plc.address != "" && !plc.Inputs[fieldEstop] + return plc.address != "" && !plc.inputs[fieldEstop] } // Returns the state of the red and blue driver station emergency stop buttons (true if e-stop is active). func (plc *Plc) GetTeamEstops() ([3]bool, [3]bool) { var redEstops, blueEstops [3]bool if plc.address != "" { - redEstops[0] = !plc.Inputs[redEstop1] - redEstops[1] = !plc.Inputs[redEstop2] - redEstops[2] = !plc.Inputs[redEstop3] - blueEstops[0] = !plc.Inputs[blueEstop1] - blueEstops[1] = !plc.Inputs[blueEstop2] - blueEstops[2] = !plc.Inputs[blueEstop3] + redEstops[0] = !plc.inputs[redEstop1] + redEstops[1] = !plc.inputs[redEstop2] + redEstops[2] = !plc.inputs[redEstop3] + blueEstops[0] = !plc.inputs[blueEstop1] + blueEstops[1] = !plc.inputs[blueEstop2] + blueEstops[2] = !plc.inputs[blueEstop3] } return redEstops, blueEstops } @@ -180,38 +182,38 @@ func (plc *Plc) GetTeamEstops() ([3]bool, [3]bool) { func (plc *Plc) GetScaleAndSwitches() ([2]bool, [2]bool, [2]bool) { var scale, redSwitch, blueSwitch [2]bool - scale[0] = plc.Inputs[scaleNear] - scale[1] = plc.Inputs[scaleFar] - redSwitch[0] = plc.Inputs[redSwitchNear] - redSwitch[1] = plc.Inputs[redSwitchFar] - blueSwitch[0] = plc.Inputs[blueSwitchNear] - blueSwitch[1] = plc.Inputs[blueSwitchFar] + scale[0] = plc.inputs[scaleNear] + scale[1] = plc.inputs[scaleFar] + redSwitch[0] = plc.inputs[redSwitchNear] + redSwitch[1] = plc.inputs[redSwitchFar] + blueSwitch[0] = plc.inputs[blueSwitchNear] + blueSwitch[1] = plc.inputs[blueSwitchFar] return scale, redSwitch, blueSwitch } // Returns the state of the red and blue vault power cube sensors. func (plc *Plc) GetVaults() (uint16, uint16, uint16, uint16, uint16, uint16) { - return plc.Registers[redForceDistance], plc.Registers[redLevitateDistance], plc.Registers[redBoostDistance], - plc.Registers[blueForceDistance], plc.Registers[blueLevitateDistance], plc.Registers[blueBoostDistance] + return plc.registers[redForceDistance], plc.registers[redLevitateDistance], plc.registers[redBoostDistance], + plc.registers[blueForceDistance], plc.registers[blueLevitateDistance], plc.registers[blueBoostDistance] } // Returns the state of the red and blue power up buttons on the vaults. func (plc *Plc) GetPowerUpButtons() (bool, bool, bool, bool, bool, bool) { - return plc.Inputs[redForceActivate], plc.Inputs[redLevitateActivate], plc.Inputs[redBoostActivate], - plc.Inputs[blueForceActivate], plc.Inputs[blueLevitateActivate], plc.Inputs[blueBoostActivate] + return plc.inputs[redForceActivate], plc.inputs[redLevitateActivate], plc.inputs[redBoostActivate], + plc.inputs[blueForceActivate], plc.inputs[blueLevitateActivate], plc.inputs[blueBoostActivate] } // Set the on/off state of the stack lights on the scoring table. func (plc *Plc) SetStackLights(red, blue, green bool) { - plc.Coils[stackLightRed] = red - plc.Coils[stackLightBlue] = blue - plc.Coils[stackLightGreen] = green + plc.coils[stackLightRed] = red + plc.coils[stackLightBlue] = blue + plc.coils[stackLightGreen] = green } // Set the on/off state of the stack lights on the scoring table. func (plc *Plc) SetStackBuzzer(state bool) { - plc.Coils[stackLightBuzzer] = state + plc.coils[stackLightBuzzer] = state } func (plc *Plc) GetCycleState(max, index, duration int) bool { @@ -220,7 +222,7 @@ func (plc *Plc) GetCycleState(max, index, duration int) bool { func (plc *Plc) GetInputNames() []string { inputNames := make([]string, inputCount) - for i := range plc.Inputs { + for i := range plc.inputs { inputNames[i] = input(i).String() } return inputNames @@ -228,7 +230,7 @@ func (plc *Plc) GetInputNames() []string { func (plc *Plc) GetRegisterNames() []string { registerNames := make([]string, registerCount) - for i := range plc.Registers { + for i := range plc.registers { registerNames[i] = register(i).String() } return registerNames @@ -236,7 +238,7 @@ func (plc *Plc) GetRegisterNames() []string { func (plc *Plc) GetCoilNames() []string { coilNames := make([]string, coilCount) - for i := range plc.Coils { + for i := range plc.coils { coilNames[i] = coil(i).String() } return coilNames @@ -267,50 +269,50 @@ func (plc *Plc) resetConnection() { } func (plc *Plc) readInputs() bool { - if len(plc.Inputs) == 0 { + if len(plc.inputs) == 0 { return true } - inputs, err := plc.client.ReadDiscreteInputs(0, uint16(len(plc.Inputs))) + inputs, err := plc.client.ReadDiscreteInputs(0, uint16(len(plc.inputs))) if err != nil { log.Printf("PLC error reading inputs: %v", err) return false } - if len(inputs)*8 < len(plc.Inputs) { - log.Printf("Insufficient length of PLC inputs: got %d bytes, expected %d bits.", len(inputs), len(plc.Inputs)) + if len(inputs)*8 < len(plc.inputs) { + log.Printf("Insufficient length of PLC inputs: got %d bytes, expected %d bits.", len(inputs), len(plc.inputs)) return false } - copy(plc.Inputs[:], byteToBool(inputs, len(plc.Inputs))) + copy(plc.inputs[:], byteToBool(inputs, len(plc.inputs))) return true } func (plc *Plc) readCounters() bool { - if len(plc.Registers) == 0 { + if len(plc.registers) == 0 { return true } - registers, err := plc.client.ReadHoldingRegisters(0, uint16(len(plc.Registers))) + registers, err := plc.client.ReadHoldingRegisters(0, uint16(len(plc.registers))) if err != nil { log.Printf("PLC error reading registers: %v", err) return false } - if len(registers)/2 < len(plc.Registers) { + if len(registers)/2 < len(plc.registers) { log.Printf("Insufficient length of PLC counters: got %d bytes, expected %d words.", len(registers), - len(plc.Registers)) + len(plc.registers)) return false } - copy(plc.Registers[:], byteToUint(registers, len(plc.Registers))) + copy(plc.registers[:], byteToUint(registers, len(plc.registers))) return true } func (plc *Plc) writeCoils() bool { // Send a heartbeat to the PLC so that it can disable outputs if the connection is lost. - plc.Coils[heartbeat] = true + plc.coils[heartbeat] = true - coils := boolToByte(plc.Coils[:]) - _, err := plc.client.WriteMultipleCoils(0, uint16(len(plc.Coils)), coils) + coils := boolToByte(plc.coils[:]) + _, err := plc.client.WriteMultipleCoils(0, uint16(len(plc.coils)), coils) if err != nil { log.Printf("PLC error writing coils: %v", err) return false @@ -319,6 +321,14 @@ func (plc *Plc) writeCoils() bool { return true } +func (plc *Plc) generateIoChangeMessage() interface{} { + return &struct { + Inputs []bool + Registers []uint16 + Coils []bool + }{plc.inputs[:], plc.registers[:], plc.coils[:]} +} + func byteToBool(bytes []byte, size int) []bool { bools := make([]bool, size) for i := 0; i < size; i++ { diff --git a/field/realtime_score.go b/field/realtime_score.go index 1ac753e..a16e3aa 100644 --- a/field/realtime_score.go +++ b/field/realtime_score.go @@ -10,6 +10,7 @@ import "github.com/Team254/cheesy-arena/game" type RealtimeScore struct { CurrentScore game.Score Cards map[string]string + AutoCommitted bool TeleopCommitted bool FoulsCommitted bool } diff --git a/static/css/alliance_station_display.css b/static/css/alliance_station_display.css index b2a2ef0..bf98154 100644 --- a/static/css/alliance_station_display.css +++ b/static/css/alliance_station_display.css @@ -83,8 +83,8 @@ body[data-mode=fieldReset] .mode#fieldReset { width: 100%; height: 100%; } -#match[data-state=AUTO_PERIOD], #match[data-state=PAUSE_PERIOD], #match[data-state=TELEOP_PERIOD], - #match[data-state=ENDGAME_PERIOD], #match[data-state=POST_MATCH] { +#match[data-state=WARMUP_PERIOD], #match[data-state=AUTO_PERIOD], #match[data-state=PAUSE_PERIOD], + #match[data-state=TELEOP_PERIOD], #match[data-state=ENDGAME_PERIOD], #match[data-state=POST_MATCH] { background-color: #fff; color: #000; } @@ -94,9 +94,9 @@ body[data-mode=fieldReset] .mode#fieldReset { #match[data-state=PRE_MATCH] #preMatch { display: block; } -#match[data-state=AUTO_PERIOD] #inMatch, #match[data-state=PAUSE_PERIOD] #inMatch, - #match[data-state=TELEOP_PERIOD] #inMatch, #match[data-state=ENDGAME_PERIOD] #inMatch, - #match[data-state=POST_MATCH] #inMatch { +#match[data-state=WARMUP_PERIOD] #inMatch, #match[data-state=AUTO_PERIOD] #inMatch, + #match[data-state=PAUSE_PERIOD] #inMatch, #match[data-state=TELEOP_PERIOD] #inMatch, + #match[data-state=ENDGAME_PERIOD] #inMatch, #match[data-state=POST_MATCH] #inMatch { display: block; } diff --git a/static/js/alliance_station_display.js b/static/js/alliance_station_display.js index 995eaf6..4209db9 100644 --- a/static/js/alliance_station_display.js +++ b/static/js/alliance_station_display.js @@ -8,12 +8,17 @@ var blinkInterval; var currentScreen = "blank"; var websocket; +// Handles a websocket message to set which alliance station this display represents. +var handleAllianceStation = function(station) { + allianceStation = station; +} + // Handles a websocket message to change which screen is displayed. -var handleSetAllianceStationDisplay = function(targetScreen) { +var handleAllianceStationDisplayMode = function(targetScreen) { currentScreen = targetScreen; - if (allianceStation == "") { - // Don't show anything if this screen hasn't been assigned a position yet. - targetScreen = "blank"; + if (allianceStation === "") { + // Don't do anything if this screen hasn't been assigned a position yet. + return; } $("body").attr("data-mode", targetScreen); switch (allianceStation[1]) { @@ -30,16 +35,7 @@ var handleSetAllianceStationDisplay = function(targetScreen) { }; // Handles a websocket message to update the team to display. -var handleSetMatch = function(data) { - if (allianceStation != "" && data.AllianceStation == "") { - // The client knows better what display this should be; let the server know. - websocket.send("setAllianceStation", allianceStation); - } else if (allianceStation != data.AllianceStation) { - // The server knows better what display this should be; sync up. - allianceStation = data.AllianceStation; - handleSetAllianceStationDisplay(currentScreen); - } - +var handleMatchLoad = function(data) { if (allianceStation != "") { var team = data.Teams[allianceStation]; if (team) { @@ -47,7 +43,7 @@ var handleSetMatch = function(data) { $("#teamNameText").attr("data-alliance-bg", allianceStation[0]).text(team.Nickname); var ranking = data.Rankings[team.Id]; - if (ranking && data.MatchType == "qualification") { + if (ranking && data.MatchType == "Qualification") { var rankingText = ranking.Rank; $("#teamRank").attr("data-alliance-bg", allianceStation[0]).text(rankingText); } else { @@ -64,7 +60,7 @@ var handleSetMatch = function(data) { }; // Handles a websocket message to update the team connection status. -var handleStatus = function(data) { +var handleArenaStatus = function(data) { stationStatus = data.AllianceStations[allianceStation]; var blink = false; if (stationStatus && stationStatus.Bypass) { @@ -106,8 +102,8 @@ var handleMatchTime = function(data) { // Handles a websocket message to update the match score. var handleRealtimeScore = function(data) { - $("#redScore").text(data.RedScore); - $("#blueScore").text(data.BlueScore); + $("#redScore").text(data.Red.Score); + $("#blueScore").text(data.Blue.Score); }; $(function() { @@ -119,11 +115,12 @@ $(function() { // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/alliance_station/websocket?displayId=" + displayId, { - setAllianceStationDisplay: function(event) { handleSetAllianceStationDisplay(event.data); }, - setMatch: function(event) { handleSetMatch(event.data); }, - status: function(event) { handleStatus(event.data); }, - matchTiming: function(event) { handleMatchTiming(event.data); }, + allianceStation: function(event) { handleAllianceStation(event.data); }, + allianceStationDisplayMode: function(event) { handleAllianceStationDisplayMode(event.data); }, + arenaStatus: function(event) { handleArenaStatus(event.data); }, + matchLoad: function(event) { handleMatchLoad(event.data); }, matchTime: function(event) { handleMatchTime(event.data); }, + matchTiming: function(event) { handleMatchTiming(event.data); }, realtimeScore: function(event) { handleRealtimeScore(event.data); } }); }); diff --git a/static/js/announcer_display.js b/static/js/announcer_display.js index 95c5001..d361010 100644 --- a/static/js/announcer_display.js +++ b/static/js/announcer_display.js @@ -16,7 +16,7 @@ Handlebars.registerHelper("eachMapEntry", function(context, options) { }); // Handles a websocket message to hide the score dialog once the next match is being introduced. -var handleSetAudienceDisplay = function(targetScreen) { +var handleAudienceDisplayMode = function(targetScreen) { // Hide the final results so that they aren't blocking the current teams when the announcer needs them most. if (targetScreen == "intro" || targetScreen == "match") { $("#matchResult").modal("hide"); @@ -24,14 +24,14 @@ var handleSetAudienceDisplay = function(targetScreen) { }; // Handles a websocket message to update the teams for the current match. -var handleSetMatch = function(data) { - $("#matchName").text(data.MatchType + " Match " + data.MatchDisplayName); - $("#red1").html(teamTemplate(formatTeam(data.Red1))); - $("#red2").html(teamTemplate(formatTeam(data.Red2))); - $("#red3").html(teamTemplate(formatTeam(data.Red3))); - $("#blue1").html(teamTemplate(formatTeam(data.Blue1))); - $("#blue2").html(teamTemplate(formatTeam(data.Blue2))); - $("#blue3").html(teamTemplate(formatTeam(data.Blue3))); +var handleMatchLoad = function(data) { + $("#matchName").text(data.MatchType + " Match " + data.Match.DisplayName); + $("#red1").html(teamTemplate(formatTeam(data.Teams["R1"]))); + $("#red2").html(teamTemplate(formatTeam(data.Teams["R2"]))); + $("#red3").html(teamTemplate(formatTeam(data.Teams["R3"]))); + $("#blue1").html(teamTemplate(formatTeam(data.Teams["B1"]))); + $("#blue2").html(teamTemplate(formatTeam(data.Teams["B2"]))); + $("#blue3").html(teamTemplate(formatTeam(data.Teams["B3"]))); }; // Handles a websocket message to update the match time countdown. @@ -44,13 +44,13 @@ var handleMatchTime = function(data) { // Handles a websocket message to update the match score. var handleRealtimeScore = function(data) { - $("#redScore").text(data.RedScore); - $("#blueScore").text(data.BlueScore); + $("#redScore").text(data.Red.Score); + $("#blueScore").text(data.Blue.Score); }; // Handles a websocket message to populate the final score data. -var handleSetFinalScore = function(data) { - $("#scoreMatchName").text(data.MatchType + " Match " + data.MatchDisplayName); +var handleScorePosted = function(data) { + $("#scoreMatchName").text(data.MatchType + " Match " + data.Match.DisplayName); $("#redScoreDetails").html(matchResultTemplate({score: data.RedScoreSummary, fouls: data.RedFouls, cards: data.RedCards})); $("#blueScoreDetails").html(matchResultTemplate({score: data.BlueScoreSummary, fouls: data.BlueFouls, @@ -61,11 +61,6 @@ var handleSetFinalScore = function(data) { $("[data-toggle=tooltip]").tooltip({"placement": "top"}); }; -var postMatchResult = function(data) { - $("#savedMatchResult").attr("data-blink", false); - websocket.send("setAudienceDisplay", "score"); -} - // Replaces newlines in team fields with HTML line breaks. var formatTeam = function(team) { if (team) { @@ -77,12 +72,12 @@ var formatTeam = function(team) { $(function() { // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/announcer/websocket", { - setMatch: function(event) { handleSetMatch(event.data); }, - matchTiming: function(event) { handleMatchTiming(event.data); }, + audienceDisplayMode: function(event) { handleAudienceDisplayMode(event.data); }, + matchLoad: function(event) { handleMatchLoad(event.data); }, matchTime: function(event) { handleMatchTime(event.data); }, + matchTiming: function(event) { handleMatchTiming(event.data); }, realtimeScore: function(event) { handleRealtimeScore(event.data); }, - setFinalScore: function(event) { handleSetFinalScore(event.data); }, - setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); } + scorePosted: function(event) { handleScorePosted(event.data); } }); // Make the score blink. diff --git a/static/js/audience_display.js b/static/js/audience_display.js index 7507760..3b6c366 100644 --- a/static/js/audience_display.js +++ b/static/js/audience_display.js @@ -10,7 +10,7 @@ var currentScreen = "blank"; var allianceSelectionTemplate = Handlebars.compile($("#allianceSelectionTemplate").html()); // Handles a websocket message to change which screen is displayed. -var handleSetAudienceDisplay = function(targetScreen) { +var handleAudienceDisplayMode = function(targetScreen) { if (targetScreen == currentScreen) { return; } @@ -28,7 +28,7 @@ var handleSetAudienceDisplay = function(targetScreen) { }; // Handles a websocket message to update the teams for the current match. -var handleSetMatch = function(data) { +var handleMatchLoad = function(data) { $("#redTeam1").text(data.Match.Red1) $("#redTeam2").text(data.Match.Red2) $("#redTeam3").text(data.Match.Red3) @@ -52,21 +52,23 @@ var handleMatchTime = function(data) { // Handles a websocket message to update the match score. var handleRealtimeScore = function(data) { + var redScoreBreakdown = data.Red.RealtimeScore.CurrentScore; $("#redScoreNumber").text(data.Red.Score); $("#redForceCubesIcon").attr("data-state", data.Red.ForceState); - $("#redForceCubes").text(data.Red.ForceCubes).attr("data-state", data.Red.ForceState); + $("#redForceCubes").text(redScoreBreakdown.ForceCubes).attr("data-state", data.Red.ForceState); $("#redLevitateCubesIcon").attr("data-state", data.Red.LevitateState); - $("#redLevitateCubes").text(data.Red.LevitateCubes).attr("data-state", data.Red.LevitateState); + $("#redLevitateCubes").text(redScoreBreakdown.LevitateCubes).attr("data-state", data.Red.LevitateState); $("#redBoostCubesIcon").attr("data-state", data.Red.BoostState); - $("#redBoostCubes").text(data.Red.BoostCubes).attr("data-state", data.Red.BoostState); + $("#redBoostCubes").text(redScoreBreakdown.BoostCubes).attr("data-state", data.Red.BoostState); + var blueScoreBreakdown = data.Blue.RealtimeScore.CurrentScore; $("#blueScoreNumber").text(data.Blue.Score); $("#blueForceCubesIcon").attr("data-state", data.Blue.ForceState); - $("#blueForceCubes").text(data.Blue.ForceCubes).attr("data-state", data.Blue.ForceState); + $("#blueForceCubes").text(blueScoreBreakdown.ForceCubes).attr("data-state", data.Blue.ForceState); $("#blueLevitateCubesIcon").attr("data-state", data.Blue.LevitateState); - $("#blueLevitateCubes").text(data.Blue.LevitateCubes).attr("data-state", data.Blue.LevitateState); + $("#blueLevitateCubes").text(blueScoreBreakdown.LevitateCubes).attr("data-state", data.Blue.LevitateState); $("#blueBoostCubesIcon").attr("data-state", data.Blue.BoostState); - $("#blueBoostCubes").text(data.Blue.BoostCubes).attr("data-state", data.Blue.BoostState); + $("#blueBoostCubes").text(blueScoreBreakdown.BoostCubes).attr("data-state", data.Blue.BoostState); // Switch/scale indicators. $("#scaleIndicator").attr("data-owned-by", data.ScaleOwnedBy); @@ -85,33 +87,33 @@ var handleRealtimeScore = function(data) { }; // Handles a websocket message to populate the final score data. -var handleSetFinalScore = function(data) { - $("#redFinalScore").text(data.RedScore.Score); +var handleScorePosted = function(data) { + $("#redFinalScore").text(data.RedScoreSummary.Score); $("#redFinalTeam1").text(data.Match.Red1); $("#redFinalTeam2").text(data.Match.Red2); $("#redFinalTeam3").text(data.Match.Red3); - $("#redFinalAutoRunPoints").text(data.RedScore.AutoRunPoints); - $("#redFinalOwnershipPoints").text(data.RedScore.OwnershipPoints); - $("#redFinalVaultPoints").text(data.RedScore.VaultPoints); - $("#redFinalParkClimbPoints").text(data.RedScore.ParkClimbPoints); - $("#redFinalFoulPoints").text(data.RedScore.FoulPoints); - $("#redFinalAutoQuest").html(data.RedScore.AutoQuest ? "✔" : "✘"); - $("#redFinalAutoQuest").attr("data-checked", data.RedScore.AutoQuest); - $("#redFinalFaceTheBoss").html(data.RedScore.FaceTheBoss ? "✔" : "✘"); - $("#redFinalFaceTheBoss").attr("data-checked", data.RedScore.FaceTheBoss); - $("#blueFinalScore").text(data.BlueScore.Score); + $("#redFinalAutoRunPoints").text(data.RedScoreSummary.AutoRunPoints); + $("#redFinalOwnershipPoints").text(data.RedScoreSummary.OwnershipPoints); + $("#redFinalVaultPoints").text(data.RedScoreSummary.VaultPoints); + $("#redFinalParkClimbPoints").text(data.RedScoreSummary.ParkClimbPoints); + $("#redFinalFoulPoints").text(data.RedScoreSummary.FoulPoints); + $("#redFinalAutoQuest").html(data.RedScoreSummary.AutoQuest ? "✔" : "✘"); + $("#redFinalAutoQuest").attr("data-checked", data.RedScoreSummary.AutoQuest); + $("#redFinalFaceTheBoss").html(data.RedScoreSummary.FaceTheBoss ? "✔" : "✘"); + $("#redFinalFaceTheBoss").attr("data-checked", data.RedScoreSummary.FaceTheBoss); + $("#blueFinalScore").text(data.BlueScoreSummary.Score); $("#blueFinalTeam1").text(data.Match.Blue1); $("#blueFinalTeam2").text(data.Match.Blue2); $("#blueFinalTeam3").text(data.Match.Blue3); - $("#blueFinalAutoRunPoints").text(data.BlueScore.AutoRunPoints); - $("#blueFinalOwnershipPoints").text(data.BlueScore.OwnershipPoints); - $("#blueFinalVaultPoints").text(data.BlueScore.VaultPoints); - $("#blueFinalParkClimbPoints").text(data.BlueScore.ParkClimbPoints); - $("#blueFinalFoulPoints").text(data.BlueScore.FoulPoints); - $("#blueFinalAutoQuest").html(data.BlueScore.AutoQuest ? "✔" : "✘"); - $("#blueFinalAutoQuest").attr("data-checked", data.BlueScore.AutoQuest); - $("#blueFinalFaceTheBoss").html(data.BlueScore.FaceTheBoss ? "✔" : "✘"); - $("#blueFinalFaceTheBoss").attr("data-checked", data.BlueScore.FaceTheBoss); + $("#blueFinalAutoRunPoints").text(data.BlueScoreSummary.AutoRunPoints); + $("#blueFinalOwnershipPoints").text(data.BlueScoreSummary.OwnershipPoints); + $("#blueFinalVaultPoints").text(data.BlueScoreSummary.VaultPoints); + $("#blueFinalParkClimbPoints").text(data.BlueScoreSummary.ParkClimbPoints); + $("#blueFinalFoulPoints").text(data.BlueScoreSummary.FoulPoints); + $("#blueFinalAutoQuest").html(data.BlueScoreSummary.AutoQuest ? "✔" : "✘"); + $("#blueFinalAutoQuest").attr("data-checked", data.BlueScoreSummary.AutoQuest); + $("#blueFinalFaceTheBoss").html(data.BlueScoreSummary.FaceTheBoss ? "✔" : "✘"); + $("#blueFinalFaceTheBoss").attr("data-checked", data.BlueScoreSummary.FaceTheBoss); $("#finalMatchName").text(data.MatchName + " " + data.Match.DisplayName); }; @@ -127,7 +129,7 @@ var handlePlaySound = function(sound) { // Handles a websocket message to update the alliance selection screen. var handleAllianceSelection = function(alliances) { - if (alliances) { + if (alliances && alliances.length > 0) { var numColumns = alliances[0].length + 1; $.each(alliances, function(k, v) { v.Index = k + 1; @@ -399,15 +401,15 @@ var initializeSponsorDisplay = function() { $(function() { // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/audience/websocket", { - setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); }, - setMatch: function(event) { handleSetMatch(event.data); }, - matchTiming: function(event) { handleMatchTiming(event.data); }, - matchTime: function(event) { handleMatchTime(event.data); }, - realtimeScore: function(event) { handleRealtimeScore(event.data); }, - setFinalScore: function(event) { handleSetFinalScore(event.data); }, - playSound: function(event) { handlePlaySound(event.data); }, allianceSelection: function(event) { handleAllianceSelection(event.data); }, - lowerThird: function(event) { handleLowerThird(event.data); } + audienceDisplayMode: function(event) { handleAudienceDisplayMode(event.data); }, + lowerThird: function(event) { handleLowerThird(event.data); }, + matchLoad: function(event) { handleMatchLoad(event.data); }, + matchTime: function(event) { handleMatchTime(event.data); }, + matchTiming: function(event) { handleMatchTiming(event.data); }, + playSound: function(event) { handlePlaySound(event.data); }, + realtimeScore: function(event) { handleRealtimeScore(event.data); }, + scorePosted: function(event) { handleScorePosted(event.data); } }); initializeSponsorDisplay(); diff --git a/static/js/cheesy-websocket.js b/static/js/cheesy-websocket.js index 7859ac4..4e65d48 100644 --- a/static/js/cheesy-websocket.js +++ b/static/js/cheesy-websocket.js @@ -20,14 +20,10 @@ var CheesyWebsocket = function(path, events) { events.error = function(event) { // Data is just an error string. console.log(event.data); + alert(event.data); } } - // Insert an event to show a dialog when the server wishes it. - events.dialog = function(event) { - alert(event.data); - } - // Insert an event to allow the server to force-reload the client for any display. events.reload = function(event) { location.reload(); diff --git a/static/js/fta_display.js b/static/js/fta_display.js index 7c2a42d..634a24d 100644 --- a/static/js/fta_display.js +++ b/static/js/fta_display.js @@ -7,7 +7,7 @@ var websocket; // Handles a websocket message to update the team connection status. -var handleStatus = function(data) { +var handleArenaStatus = function(data) { // Update the team status view. $.each(data.AllianceStations, function(station, stationStatus) { if (stationStatus.Team) { @@ -72,6 +72,6 @@ $(function() { // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/fta/websocket", { - status: function(event) { handleStatus(event.data); } + arenaStatus: function(event) { handleArenaStatus(event.data); } }); }); diff --git a/static/js/match_play.js b/static/js/match_play.js index 6e207d8..12f7772 100644 --- a/static/js/match_play.js +++ b/static/js/match_play.js @@ -59,7 +59,7 @@ var confirmCommit = function(isReplay) { }; // Handles a websocket message to update the team connection status. -var handleStatus = function(data) { +var handleArenaStatus = function(data) { // Update the team status view. $.each(data.AllianceStations, function(station, stationStatus) { if (stationStatus.DsConn) { @@ -153,12 +153,12 @@ var handleMatchTime = function(data) { // Handles a websocket message to update the match score. var handleRealtimeScore = function(data) { - $("#redScore").text(data.RedScore); - $("#blueScore").text(data.BlueScore); + $("#redScore").text(data.Red.Score); + $("#blueScore").text(data.Blue.Score); }; // Handles a websocket message to update the audience display screen selector. -var handleSetAudienceDisplay = function(data) { +var handleAudienceDisplayMode = function(data) { $("input[name=audienceDisplay]:checked").prop("checked", false); $("input[name=audienceDisplay][value=" + data + "]").prop("checked", true); }; @@ -172,7 +172,7 @@ var handleScoringStatus = function(data) { }; // Handles a websocket message to update the alliance station display screen selector. -var handleSetAllianceStationDisplay = function(data) { +var handleAllianceStationDisplayMode = function(data) { $("input[name=allianceStationDisplay]:checked").prop("checked", false); $("input[name=allianceStationDisplay][value=" + data + "]").prop("checked", true); }; @@ -183,12 +183,12 @@ $(function() { // Set up the websocket back to the server. websocket = new CheesyWebsocket("/match_play/websocket", { - status: function(event) { handleStatus(event.data); }, - matchTiming: function(event) { handleMatchTiming(event.data); }, + allianceStationDisplayMode: function(event) { handleAllianceStationDisplayMode(event.data); }, + arenaStatus: function(event) { handleArenaStatus(event.data); }, + audienceDisplayMode: function(event) { handleAudienceDisplayMode(event.data); }, matchTime: function(event) { handleMatchTime(event.data); }, + matchTiming: function(event) { handleMatchTiming(event.data); }, realtimeScore: function(event) { handleRealtimeScore(event.data); }, - setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); }, - scoringStatus: function(event) { handleScoringStatus(event.data); }, - setAllianceStationDisplay: function(event) { handleSetAllianceStationDisplay(event.data); } + scoringStatus: function(event) { handleScoringStatus(event.data); } }); }); diff --git a/static/js/referee_display.js b/static/js/referee_display.js index 0843d60..f8b6d59 100644 --- a/static/js/referee_display.js +++ b/static/js/referee_display.js @@ -6,6 +6,7 @@ var websocket; var foulTeamButton; var foulRuleButton; +var firstMatchLoad = true; // Handles a click on a team button. var setFoulTeam = function(teamButton) { @@ -94,12 +95,22 @@ var commitMatch = function() { websocket.send("commitMatch"); }; +// Handles a websocket message to update the teams for the current match. +var handleMatchLoad = function(data) { + // Since the server always sends a matchLoad message upon establishing the websocket connection, ignore the first one. + if (!firstMatchLoad) { + location.reload(); + } + firstMatchLoad = false; +}; + $(function() { // Activate tooltips above the rule buttons. $("[data-toggle=tooltip]").tooltip({"placement": "top"}); // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/referee/websocket", { + matchLoad: function(event) { handleMatchLoad(event.data) } }); clearFoul(); diff --git a/static/js/scoring_display.js b/static/js/scoring_display.js index e71ef18..7bbf476 100644 --- a/static/js/scoring_display.js +++ b/static/js/scoring_display.js @@ -5,22 +5,30 @@ var websocket; var scoreCommitted = false; +var alliance; // Handles a websocket message to update the realtime scoring fields. -var handleScore = function(data) { +var handleRealtimeScore = function(data) { + var realtimeScore; + if (alliance === "red") { + realtimeScore = data.Red.RealtimeScore; + } else { + realtimeScore = data.Blue.RealtimeScore; + } + // Update autonomous period values. - var score = data.Score.CurrentScore; + var score = realtimeScore.CurrentScore; $("#autoRuns").text(score.AutoRuns); $("#climbs").text(score.Climbs); $("#parks").text(score.Parks); // Update component visibility. - if (!data.AutoCommitted) { + if (!realtimeScore.AutoCommitted) { $("#autoScoring").fadeTo(0, 1); $("#teleopScoring").hide(); $("#waitingMessage").hide(); scoreCommitted = false; - } else if (!data.Score.TeleopCommitted) { + } else if (!realtimeScore.TeleopCommitted) { $("#autoScoring").fadeTo(0, 0.25); $("#teleopScoring").show(); $("#waitingMessage").hide(); @@ -54,10 +62,12 @@ var commitMatchScore = function() { }; $(function() { + alliance = window.location.href.split("/").slice(-1)[0]; + // Set up the websocket back to the server. websocket = new CheesyWebsocket("/displays/scoring/" + alliance + "/websocket", { - score: function(event) { handleScore(event.data); }, - matchTime: function(event) { handleMatchTime(event.data); } + matchTime: function(event) { handleMatchTime(event.data); }, + realtimeScore: function(event) { handleRealtimeScore(event.data); } }); $(document).keypress(handleKeyPress); diff --git a/templates/announcer_display.html b/templates/announcer_display.html index 0d9d47e..6cef4f3 100644 --- a/templates/announcer_display.html +++ b/templates/announcer_display.html @@ -62,32 +62,32 @@