mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 05:36:45 -04:00
Refactor websocket model to reduce duplicated code.
This commit is contained in:
2
coverage
2
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
|
||||
|
||||
177
field/arena.go
177
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:
|
||||
|
||||
184
field/arena_notifiers.go
Normal file
184
field/arena_notifiers.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
104
field/plc.go
104
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++ {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -62,32 +62,32 @@
|
||||
<script id="matchResultTemplate" type="text/x-handlebars-template">
|
||||
<h4>Score</h4>
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Auto Mobility Points</div>
|
||||
<div class="col-lg-2">{{"{{score.AutoMobilityPoints}}"}}</div>
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Auto Run Points</div>
|
||||
<div class="col-lg-2">{{"{{score.AutoRunPoints}}"}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Pressure Points</div>
|
||||
<div class="col-lg-2">{{"{{score.PressurePoints}}"}}</div>
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Ownership Points</div>
|
||||
<div class="col-lg-2">{{"{{score.OwnershipPoints}}"}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Rotor Points</div>
|
||||
<div class="col-lg-2">{{"{{score.RotorPoints}}"}}</div>
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Vault Points</div>
|
||||
<div class="col-lg-2">{{"{{score.VaultPoints}}"}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Takeoff Points</div>
|
||||
<div class="col-lg-2">{{"{{score.TakeoffPoints}}"}}</div>
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Park/Climb Points</div>
|
||||
<div class="col-lg-2">{{"{{score.ParkClimbPoints}}"}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Foul Points</div>
|
||||
<div class="col-lg-2">{{"{{score.FoulPoints}}"}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Pressure Bonus</div>
|
||||
<div class="col-lg-2">{{"{{score.PressureGoalReached}}"}}</div>
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Auto Quest</div>
|
||||
<div class="col-lg-2">{{"{{score.AutoQuest}}"}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Rotor Bonus</div>
|
||||
<div class="col-lg-2">{{"{{score.RotorGoalReached}}"}}</div>
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label">Face the Boss</div>
|
||||
<div class="col-lg-2">{{"{{score.FaceTheBoss}}"}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-lg-offset-1 control-label"><b>Final Score</b></div>
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"io"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Renders the team number and status display shown above each alliance station.
|
||||
@@ -50,175 +48,36 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
|
||||
displayId := r.URL.Query()["displayId"][0]
|
||||
station, ok := web.arena.AllianceStationDisplays[displayId]
|
||||
if !ok {
|
||||
station = ""
|
||||
web.arena.AllianceStationDisplays[displayId] = station
|
||||
}
|
||||
rankings := make(map[string]*game.Ranking)
|
||||
for _, allianceStation := range web.arena.AllianceStations {
|
||||
if allianceStation.Team != nil {
|
||||
rankings[strconv.Itoa(allianceStation.Team.Id)], _ =
|
||||
web.arena.Database.GetRankingForTeam(allianceStation.Team.Id)
|
||||
}
|
||||
}
|
||||
|
||||
allianceStationDisplayListener := web.arena.AllianceStationDisplayNotifier.Listen()
|
||||
defer close(allianceStationDisplayListener)
|
||||
matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen()
|
||||
defer close(matchLoadTeamsListener)
|
||||
robotStatusListener := web.arena.RobotStatusNotifier.Listen()
|
||||
defer close(robotStatusListener)
|
||||
matchTimeListener := web.arena.MatchTimeNotifier.Listen()
|
||||
defer close(matchTimeListener)
|
||||
realtimeScoreListener := web.arena.RealtimeScoreNotifier.Listen()
|
||||
defer close(realtimeScoreListener)
|
||||
reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen()
|
||||
defer close(reloadDisplaysListener)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
// Send the various notifications immediately upon connection.
|
||||
var data interface{}
|
||||
err = websocket.Write("setAllianceStationDisplay", web.arena.AllianceStationDisplayScreen)
|
||||
// Inform the client which alliance station it should represent.
|
||||
err = ws.Write("allianceStation", station)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("matchTiming", game.MatchTiming)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("matchTime", MatchTimeMessage{int(web.arena.MatchState), int(web.arena.LastMatchTimeSec)})
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
AllianceStation string
|
||||
Teams map[string]*model.Team
|
||||
Rankings map[string]*game.Ranking
|
||||
}{station, map[string]*model.Team{"R1": web.arena.AllianceStations["R1"].Team,
|
||||
"R2": web.arena.AllianceStations["R2"].Team, "R3": web.arena.AllianceStations["R3"].Team,
|
||||
"B1": web.arena.AllianceStations["B1"].Team, "B2": web.arena.AllianceStations["B2"].Team,
|
||||
"B3": web.arena.AllianceStations["B3"].Team}, rankings}
|
||||
err = websocket.Write("setMatch", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
RedScore int
|
||||
BlueScore int
|
||||
}{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score}
|
||||
err = websocket.Write("realtimeScore", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-allianceStationDisplayListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
websocket.Write("matchTime",
|
||||
MatchTimeMessage{int(web.arena.MatchState), int(web.arena.LastMatchTimeSec)})
|
||||
messageType = "setAllianceStationDisplay"
|
||||
message = web.arena.AllianceStationDisplayScreen
|
||||
case _, ok := <-matchLoadTeamsListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setMatch"
|
||||
station = web.arena.AllianceStationDisplays[displayId]
|
||||
rankings := make(map[string]*game.Ranking)
|
||||
for _, allianceStation := range web.arena.AllianceStations {
|
||||
if allianceStation.Team != nil {
|
||||
rankings[strconv.Itoa(allianceStation.Team.Id)], _ =
|
||||
web.arena.Database.GetRankingForTeam(allianceStation.Team.Id)
|
||||
}
|
||||
}
|
||||
message = struct {
|
||||
AllianceStation string
|
||||
Teams map[string]*model.Team
|
||||
Rankings map[string]*game.Ranking
|
||||
MatchType string
|
||||
}{station, map[string]*model.Team{"R1": web.arena.AllianceStations["R1"].Team,
|
||||
"R2": web.arena.AllianceStations["R2"].Team, "R3": web.arena.AllianceStations["R3"].Team,
|
||||
"B1": web.arena.AllianceStations["B1"].Team, "B2": web.arena.AllianceStations["B2"].Team,
|
||||
"B3": web.arena.AllianceStations["B3"].Team}, rankings, web.arena.CurrentMatch.Type}
|
||||
case _, ok := <-robotStatusListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "status"
|
||||
message = web.arena.GetStatus()
|
||||
case matchTimeSec, ok := <-matchTimeListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "matchTime"
|
||||
message = MatchTimeMessage{int(web.arena.MatchState), matchTimeSec.(int)}
|
||||
case _, ok := <-realtimeScoreListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "realtimeScore"
|
||||
message = struct {
|
||||
RedScore int
|
||||
BlueScore int
|
||||
}{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score}
|
||||
case _, ok := <-reloadDisplaysListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, data, err := websocket.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch messageType {
|
||||
case "setAllianceStation":
|
||||
// The client knows what station it is (e.g. across a server restart) and is informing the server.
|
||||
station, ok := data.(string)
|
||||
if !ok {
|
||||
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
continue
|
||||
}
|
||||
web.arena.AllianceStationDisplays[displayId] = station
|
||||
default:
|
||||
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
}
|
||||
// Inform the client what the match period timing parameters are configured to.
|
||||
err = ws.Write("matchTiming", game.MatchTiming)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.AllianceStationDisplayModeNotifier, web.arena.ArenaStatusNotifier,
|
||||
web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
|
||||
web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ package web
|
||||
|
||||
import (
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
gorillawebsocket "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -25,33 +25,28 @@ func TestAllianceStationDisplayWebsocket(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/alliance_station/websocket?displayId=1", nil)
|
||||
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/alliance_station/websocket?displayId=1", nil)
|
||||
assert.Nil(t, err)
|
||||
defer conn.Close()
|
||||
ws := &Websocket{conn, new(sync.Mutex)}
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "setAllianceStationDisplay")
|
||||
readWebsocketType(t, ws, "allianceStation")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "allianceStationDisplayMode")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "setMatch")
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
|
||||
// Change to a different screen.
|
||||
web.arena.AllianceStationDisplayScreen = "logo"
|
||||
web.arena.AllianceStationDisplayNotifier.Notify(nil)
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "setAllianceStationDisplay")
|
||||
|
||||
// Inform the server what display ID this is.
|
||||
assert.Equal(t, "", web.arena.AllianceStationDisplays["1"])
|
||||
ws.Write("setAllianceStation", "R3")
|
||||
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
|
||||
assert.Equal(t, "R3", web.arena.AllianceStationDisplays["1"])
|
||||
web.arena.AllianceStationDisplayMode = "logo"
|
||||
web.arena.AllianceStationDisplayModeNotifier.Notify()
|
||||
readWebsocketType(t, ws, "allianceStationDisplayMode")
|
||||
|
||||
// Run through a match cycle.
|
||||
web.arena.MatchLoadTeamsNotifier.Notify(nil)
|
||||
readWebsocketType(t, ws, "setMatch")
|
||||
web.arena.MatchLoadNotifier.Notify()
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
web.arena.AllianceStations["R1"].Bypass = true
|
||||
web.arena.AllianceStations["R2"].Bypass = true
|
||||
web.arena.AllianceStations["R3"].Bypass = true
|
||||
@@ -60,14 +55,16 @@ func TestAllianceStationDisplayWebsocket(t *testing.T) {
|
||||
web.arena.AllianceStations["B3"].Bypass = true
|
||||
web.arena.StartMatch()
|
||||
web.arena.Update()
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
messages := readWebsocketMultiple(t, ws, 2)
|
||||
_, ok := messages["matchTime"]
|
||||
assert.True(t, ok)
|
||||
web.arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
|
||||
web.arena.Update()
|
||||
messages := readWebsocketMultiple(t, ws, 2)
|
||||
_, ok := messages["status"]
|
||||
messages = readWebsocketMultiple(t, ws, 2)
|
||||
_, ok = messages["arenaStatus"]
|
||||
assert.True(t, ok)
|
||||
_, ok = messages["matchTime"]
|
||||
assert.True(t, ok)
|
||||
web.arena.RealtimeScoreNotifier.Notify(nil)
|
||||
web.arena.RealtimeScoreNotifier.Notify()
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"io"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
@@ -25,6 +24,7 @@ func (web *Web) announcerDisplayHandler(w http.ResponseWriter, r *http.Request)
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
*model.EventSettings
|
||||
}{web.arena.EventSettings}
|
||||
@@ -41,182 +41,21 @@ func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen()
|
||||
defer close(matchLoadTeamsListener)
|
||||
matchTimeListener := web.arena.MatchTimeNotifier.Listen()
|
||||
defer close(matchTimeListener)
|
||||
realtimeScoreListener := web.arena.RealtimeScoreNotifier.Listen()
|
||||
defer close(realtimeScoreListener)
|
||||
scorePostedListener := web.arena.ScorePostedNotifier.Listen()
|
||||
defer close(scorePostedListener)
|
||||
audienceDisplayListener := web.arena.AudienceDisplayNotifier.Listen()
|
||||
defer close(audienceDisplayListener)
|
||||
reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen()
|
||||
defer close(reloadDisplaysListener)
|
||||
|
||||
// Send the various notifications immediately upon connection.
|
||||
var data interface{}
|
||||
data = struct {
|
||||
MatchType string
|
||||
MatchDisplayName string
|
||||
Red1 *model.Team
|
||||
Red2 *model.Team
|
||||
Red3 *model.Team
|
||||
Blue1 *model.Team
|
||||
Blue2 *model.Team
|
||||
Blue3 *model.Team
|
||||
}{web.arena.CurrentMatch.CapitalizedType(), web.arena.CurrentMatch.DisplayName,
|
||||
web.arena.AllianceStations["R1"].Team, web.arena.AllianceStations["R2"].Team,
|
||||
web.arena.AllianceStations["R3"].Team, web.arena.AllianceStations["B1"].Team,
|
||||
web.arena.AllianceStations["B2"].Team, web.arena.AllianceStations["B3"].Team}
|
||||
err = websocket.Write("setMatch", data)
|
||||
// Inform the client what the match period timing parameters are configured to.
|
||||
err = ws.Write("matchTiming", game.MatchTiming)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("matchTiming", game.MatchTiming)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("matchTime", MatchTimeMessage{int(web.arena.MatchState), int(web.arena.LastMatchTimeSec)})
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
RedScore int
|
||||
BlueScore int
|
||||
}{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score}
|
||||
err = websocket.Write("realtimeScore", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-matchLoadTeamsListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setMatch"
|
||||
message = struct {
|
||||
MatchType string
|
||||
MatchDisplayName string
|
||||
Red1 *model.Team
|
||||
Red2 *model.Team
|
||||
Red3 *model.Team
|
||||
Blue1 *model.Team
|
||||
Blue2 *model.Team
|
||||
Blue3 *model.Team
|
||||
}{web.arena.CurrentMatch.CapitalizedType(), web.arena.CurrentMatch.DisplayName,
|
||||
web.arena.AllianceStations["R1"].Team, web.arena.AllianceStations["R2"].Team,
|
||||
web.arena.AllianceStations["R3"].Team, web.arena.AllianceStations["B1"].Team,
|
||||
web.arena.AllianceStations["B2"].Team, web.arena.AllianceStations["B3"].Team}
|
||||
case matchTimeSec, ok := <-matchTimeListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "matchTime"
|
||||
message = MatchTimeMessage{int(web.arena.MatchState), matchTimeSec.(int)}
|
||||
case _, ok := <-realtimeScoreListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "realtimeScore"
|
||||
message = struct {
|
||||
RedScore int
|
||||
BlueScore int
|
||||
}{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score}
|
||||
case _, ok := <-scorePostedListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setFinalScore"
|
||||
message = struct {
|
||||
MatchType string
|
||||
MatchDisplayName string
|
||||
RedScoreSummary *game.ScoreSummary
|
||||
BlueScoreSummary *game.ScoreSummary
|
||||
RedFouls []game.Foul
|
||||
BlueFouls []game.Foul
|
||||
RedCards map[string]string
|
||||
BlueCards map[string]string
|
||||
}{web.arena.SavedMatch.CapitalizedType(), web.arena.SavedMatch.DisplayName,
|
||||
web.arena.SavedMatchResult.RedScoreSummary(), web.arena.SavedMatchResult.BlueScoreSummary(),
|
||||
populateFoulDescriptions(web.arena.SavedMatchResult.RedScore.Fouls),
|
||||
populateFoulDescriptions(web.arena.SavedMatchResult.BlueScore.Fouls),
|
||||
web.arena.SavedMatchResult.RedCards, web.arena.SavedMatchResult.BlueCards}
|
||||
case _, ok := <-audienceDisplayListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setAudienceDisplay"
|
||||
message = web.arena.AudienceDisplayScreen
|
||||
case _, ok := <-reloadDisplaysListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, data, err := websocket.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch messageType {
|
||||
case "setAudienceDisplay":
|
||||
// The announcer can make the final score screen show when they are ready to announce the score.
|
||||
screen, ok := data.(string)
|
||||
if !ok {
|
||||
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
continue
|
||||
}
|
||||
web.arena.AudienceDisplayScreen = screen
|
||||
web.arena.AudienceDisplayNotifier.Notify(nil)
|
||||
default:
|
||||
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
|
||||
web.arena.ScorePostedNotifier, web.arena.AudienceDisplayModeNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
gorillawebsocket "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAnnouncerDisplay(t *testing.T) {
|
||||
@@ -24,19 +23,21 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/announcer/websocket", nil)
|
||||
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/announcer/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer conn.Close()
|
||||
ws := &Websocket{conn, new(sync.Mutex)}
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "setMatch")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
readWebsocketType(t, ws, "scorePosted")
|
||||
readWebsocketType(t, ws, "audienceDisplayMode")
|
||||
|
||||
web.arena.MatchLoadTeamsNotifier.Notify(nil)
|
||||
readWebsocketType(t, ws, "setMatch")
|
||||
web.arena.MatchLoadNotifier.Notify()
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
web.arena.AllianceStations["R1"].Bypass = true
|
||||
web.arena.AllianceStations["R2"].Bypass = true
|
||||
web.arena.AllianceStations["R3"].Bypass = true
|
||||
@@ -46,17 +47,12 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) {
|
||||
web.arena.StartMatch()
|
||||
web.arena.Update()
|
||||
messages := readWebsocketMultiple(t, ws, 2)
|
||||
_, ok := messages["setAudienceDisplay"]
|
||||
_, ok := messages["audienceDisplayMode"]
|
||||
assert.True(t, ok)
|
||||
_, ok = messages["matchTime"]
|
||||
assert.True(t, ok)
|
||||
web.arena.RealtimeScoreNotifier.Notify(nil)
|
||||
web.arena.RealtimeScoreNotifier.Notify()
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
web.arena.ScorePostedNotifier.Notify(nil)
|
||||
readWebsocketType(t, ws, "setFinalScore")
|
||||
|
||||
// Test triggering the final score screen.
|
||||
ws.Write("setAudienceDisplay", "score")
|
||||
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
|
||||
assert.Equal(t, "score", web.arena.AudienceDisplayScreen)
|
||||
web.arena.ScorePostedNotifier.Notify()
|
||||
readWebsocketType(t, ws, "scorePosted")
|
||||
}
|
||||
|
||||
@@ -8,29 +8,11 @@ package web
|
||||
import (
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"io"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type audienceScoreFields struct {
|
||||
Red *audienceAllianceScoreFields
|
||||
Blue *audienceAllianceScoreFields
|
||||
ScaleOwnedBy game.Alliance
|
||||
}
|
||||
|
||||
type audienceAllianceScoreFields struct {
|
||||
Score int
|
||||
ForceCubes int
|
||||
LevitateCubes int
|
||||
BoostCubes int
|
||||
ForceState game.PowerUpState
|
||||
LevitateState game.PowerUpState
|
||||
BoostState game.PowerUpState
|
||||
SwitchOwnedBy game.Alliance
|
||||
}
|
||||
|
||||
// Renders the audience display to be chroma keyed over the video feed.
|
||||
func (web *Web) audienceDisplayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !web.userIsReader(w, r) {
|
||||
@@ -59,208 +41,22 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
audienceDisplayListener := web.arena.AudienceDisplayNotifier.Listen()
|
||||
defer close(audienceDisplayListener)
|
||||
matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen()
|
||||
defer close(matchLoadTeamsListener)
|
||||
matchTimeListener := web.arena.MatchTimeNotifier.Listen()
|
||||
defer close(matchTimeListener)
|
||||
realtimeScoreListener := web.arena.RealtimeScoreNotifier.Listen()
|
||||
defer close(realtimeScoreListener)
|
||||
scorePostedListener := web.arena.ScorePostedNotifier.Listen()
|
||||
defer close(scorePostedListener)
|
||||
playSoundListener := web.arena.PlaySoundNotifier.Listen()
|
||||
defer close(playSoundListener)
|
||||
allianceSelectionListener := web.arena.AllianceSelectionNotifier.Listen()
|
||||
defer close(allianceSelectionListener)
|
||||
lowerThirdListener := web.arena.LowerThirdNotifier.Listen()
|
||||
defer close(lowerThirdListener)
|
||||
reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen()
|
||||
defer close(reloadDisplaysListener)
|
||||
|
||||
// Send the various notifications immediately upon connection.
|
||||
var data interface{}
|
||||
err = websocket.Write("matchTiming", game.MatchTiming)
|
||||
// Inform the client what the match period timing parameters are configured to.
|
||||
err = ws.Write("matchTiming", game.MatchTiming)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("matchTime", MatchTimeMessage{int(web.arena.MatchState), int(web.arena.LastMatchTimeSec)})
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("setAudienceDisplay", web.arena.AudienceDisplayScreen)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
Match *model.Match
|
||||
MatchName string
|
||||
}{web.arena.CurrentMatch, web.arena.CurrentMatch.CapitalizedType()}
|
||||
err = websocket.Write("setMatch", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = web.getAudienceScoreFields()
|
||||
err = websocket.Write("realtimeScore", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
Match *model.Match
|
||||
MatchName string
|
||||
RedScore *game.ScoreSummary
|
||||
BlueScore *game.ScoreSummary
|
||||
}{web.arena.SavedMatch, web.arena.SavedMatch.CapitalizedType(),
|
||||
web.arena.SavedMatchResult.RedScoreSummary(), web.arena.SavedMatchResult.BlueScoreSummary()}
|
||||
err = websocket.Write("setFinalScore", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("allianceSelection", cachedAlliances)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-audienceDisplayListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setAudienceDisplay"
|
||||
message = web.arena.AudienceDisplayScreen
|
||||
case _, ok := <-matchLoadTeamsListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setMatch"
|
||||
message = struct {
|
||||
Match *model.Match
|
||||
MatchName string
|
||||
}{web.arena.CurrentMatch, web.arena.CurrentMatch.CapitalizedType()}
|
||||
case matchTimeSec, ok := <-matchTimeListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "matchTime"
|
||||
message = MatchTimeMessage{int(web.arena.MatchState), matchTimeSec.(int)}
|
||||
case _, ok := <-realtimeScoreListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "realtimeScore"
|
||||
message = web.getAudienceScoreFields()
|
||||
case _, ok := <-scorePostedListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setFinalScore"
|
||||
message = struct {
|
||||
Match *model.Match
|
||||
MatchName string
|
||||
RedScore *game.ScoreSummary
|
||||
BlueScore *game.ScoreSummary
|
||||
}{web.arena.SavedMatch, web.arena.SavedMatch.CapitalizedType(),
|
||||
web.arena.SavedMatchResult.RedScoreSummary(), web.arena.SavedMatchResult.BlueScoreSummary()}
|
||||
case sound, ok := <-playSoundListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "playSound"
|
||||
message = sound
|
||||
case _, ok := <-allianceSelectionListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "allianceSelection"
|
||||
message = cachedAlliances
|
||||
case lowerThird, ok := <-lowerThirdListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "lowerThird"
|
||||
message = lowerThird
|
||||
case _, ok := <-reloadDisplaysListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
_, _, err := websocket.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Constructs the data object sent to the audience display for the realtime scoring overlay.
|
||||
func (web *Web) getAudienceScoreFields() *audienceScoreFields {
|
||||
fields := new(audienceScoreFields)
|
||||
fields.Red = getAudienceAllianceScoreFields(&web.arena.RedRealtimeScore.CurrentScore, web.arena.RedScoreSummary(),
|
||||
web.arena.RedVault, web.arena.RedSwitch)
|
||||
fields.Blue = getAudienceAllianceScoreFields(&web.arena.BlueRealtimeScore.CurrentScore,
|
||||
web.arena.BlueScoreSummary(), web.arena.BlueVault, web.arena.BlueSwitch)
|
||||
fields.ScaleOwnedBy = web.arena.Scale.GetOwnedBy()
|
||||
return fields
|
||||
}
|
||||
|
||||
// Constructs the data object for one alliance sent to the audience display for the realtime scoring overlay.
|
||||
func getAudienceAllianceScoreFields(allianceScore *game.Score, allianceScoreSummary *game.ScoreSummary,
|
||||
allianceVault *game.Vault, allianceSwitch *game.Seesaw) *audienceAllianceScoreFields {
|
||||
fields := new(audienceAllianceScoreFields)
|
||||
fields.Score = allianceScoreSummary.Score
|
||||
fields.ForceCubes = allianceScore.ForceCubes
|
||||
fields.LevitateCubes = allianceScore.LevitateCubes
|
||||
fields.BoostCubes = allianceScore.BoostCubes
|
||||
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
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.AudienceDisplayModeNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier,
|
||||
web.arena.RealtimeScoreNotifier, web.arena.PlaySoundNotifier, web.arena.ScorePostedNotifier,
|
||||
web.arena.AllianceSelectionNotifier, web.arena.LowerThirdNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
gorillawebsocket "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -23,23 +23,22 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/audience/websocket", nil)
|
||||
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/audience/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer conn.Close()
|
||||
ws := &Websocket{conn, new(sync.Mutex)}
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "audienceDisplayMode")
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "setAudienceDisplay")
|
||||
readWebsocketType(t, ws, "setMatch")
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
readWebsocketType(t, ws, "setFinalScore")
|
||||
readWebsocketType(t, ws, "allianceSelection")
|
||||
readWebsocketType(t, ws, "scorePosted")
|
||||
|
||||
// Run through a match cycle.
|
||||
web.arena.MatchLoadTeamsNotifier.Notify(nil)
|
||||
readWebsocketType(t, ws, "setMatch")
|
||||
web.arena.MatchLoadNotifier.Notify()
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
web.arena.AllianceStations["R1"].Bypass = true
|
||||
web.arena.AllianceStations["R2"].Bypass = true
|
||||
web.arena.AllianceStations["R3"].Bypass = true
|
||||
@@ -49,7 +48,7 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
|
||||
web.arena.StartMatch()
|
||||
web.arena.Update()
|
||||
messages := readWebsocketMultiple(t, ws, 3)
|
||||
screen, ok := messages["setAudienceDisplay"]
|
||||
screen, ok := messages["audienceDisplayMode"]
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, "match", screen)
|
||||
}
|
||||
@@ -59,14 +58,14 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
|
||||
}
|
||||
_, ok = messages["matchTime"]
|
||||
assert.True(t, ok)
|
||||
web.arena.RealtimeScoreNotifier.Notify(nil)
|
||||
web.arena.RealtimeScoreNotifier.Notify()
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
web.arena.ScorePostedNotifier.Notify(nil)
|
||||
readWebsocketType(t, ws, "setFinalScore")
|
||||
web.arena.ScorePostedNotifier.Notify()
|
||||
readWebsocketType(t, ws, "scorePosted")
|
||||
|
||||
// Test other overlays.
|
||||
web.arena.AllianceSelectionNotifier.Notify(nil)
|
||||
web.arena.AllianceSelectionNotifier.Notify()
|
||||
readWebsocketType(t, ws, "allianceSelection")
|
||||
web.arena.LowerThirdNotifier.Notify(nil)
|
||||
web.arena.LowerThirdNotifier.Notify()
|
||||
readWebsocketType(t, ws, "lowerThird")
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ package web
|
||||
|
||||
import (
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"io"
|
||||
"log"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -37,62 +36,13 @@ func (web *Web) ftaDisplayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (web *Web) ftaDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(patrick): Enable authentication once Safari (for iPad) supports it over Websocket.
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
robotStatusListener := web.arena.RobotStatusNotifier.Listen()
|
||||
defer close(robotStatusListener)
|
||||
reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen()
|
||||
defer close(reloadDisplaysListener)
|
||||
|
||||
// Send the various notifications immediately upon connection.
|
||||
err = websocket.Write("status", web.arena.GetStatus())
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-robotStatusListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "status"
|
||||
message = web.arena.GetStatus()
|
||||
case _, ok := <-reloadDisplaysListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
_, _, err := websocket.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.ArenaStatusNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/Team254/cheesy-arena/tournament"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"io"
|
||||
@@ -30,11 +31,6 @@ type MatchPlayListItem struct {
|
||||
|
||||
type MatchPlayList []MatchPlayListItem
|
||||
|
||||
type MatchTimeMessage struct {
|
||||
MatchState int
|
||||
MatchTimeSec int
|
||||
}
|
||||
|
||||
// Global var to hold the current active tournament so that its matches are displayed by default.
|
||||
var currentMatchType string
|
||||
|
||||
@@ -153,7 +149,7 @@ func (web *Web) matchPlayShowResultHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
web.arena.SavedMatch = match
|
||||
web.arena.SavedMatchResult = matchResult
|
||||
web.arena.ScorePostedNotifier.Notify(nil)
|
||||
web.arena.ScorePostedNotifier.Notify()
|
||||
|
||||
http.Redirect(w, r, "/match_play", 303)
|
||||
}
|
||||
@@ -164,143 +160,34 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
matchTimeListener := web.arena.MatchTimeNotifier.Listen()
|
||||
defer close(matchTimeListener)
|
||||
realtimeScoreListener := web.arena.RealtimeScoreNotifier.Listen()
|
||||
defer close(realtimeScoreListener)
|
||||
robotStatusListener := web.arena.RobotStatusNotifier.Listen()
|
||||
defer close(robotStatusListener)
|
||||
audienceDisplayListener := web.arena.AudienceDisplayNotifier.Listen()
|
||||
defer close(audienceDisplayListener)
|
||||
scoringStatusListener := web.arena.ScoringStatusNotifier.Listen()
|
||||
defer close(scoringStatusListener)
|
||||
allianceStationDisplayListener := web.arena.AllianceStationDisplayNotifier.Listen()
|
||||
defer close(allianceStationDisplayListener)
|
||||
|
||||
// Send the various notifications immediately upon connection.
|
||||
var data interface{}
|
||||
err = websocket.Write("status", web.arena.GetStatus())
|
||||
// Inform the client what the match period timing parameters are configured to.
|
||||
err = ws.Write("matchTiming", game.MatchTiming)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("matchTiming", game.MatchTiming)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = MatchTimeMessage{int(web.arena.MatchState), int(web.arena.LastMatchTimeSec)}
|
||||
err = websocket.Write("matchTime", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
RedScore int
|
||||
BlueScore int
|
||||
}{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score}
|
||||
err = websocket.Write("realtimeScore", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("setAudienceDisplay", web.arena.AudienceDisplayScreen)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
RefereeScoreReady bool
|
||||
RedScoreReady bool
|
||||
BlueScoreReady bool
|
||||
}{web.arena.RedRealtimeScore.FoulsCommitted && web.arena.BlueRealtimeScore.FoulsCommitted,
|
||||
web.arena.RedRealtimeScore.TeleopCommitted, web.arena.BlueRealtimeScore.TeleopCommitted}
|
||||
err = websocket.Write("scoringStatus", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("setAllianceStationDisplay", web.arena.AllianceStationDisplayScreen)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case matchTimeSec, ok := <-matchTimeListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "matchTime"
|
||||
message = MatchTimeMessage{int(web.arena.MatchState), matchTimeSec.(int)}
|
||||
case _, ok := <-realtimeScoreListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "realtimeScore"
|
||||
message = struct {
|
||||
RedScore int
|
||||
BlueScore int
|
||||
}{web.arena.RedScoreSummary().Score, web.arena.BlueScoreSummary().Score}
|
||||
case _, ok := <-robotStatusListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "status"
|
||||
message = web.arena.GetStatus()
|
||||
case _, ok := <-audienceDisplayListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setAudienceDisplay"
|
||||
message = web.arena.AudienceDisplayScreen
|
||||
case _, ok := <-scoringStatusListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "scoringStatus"
|
||||
message = struct {
|
||||
RefereeScoreReady bool
|
||||
RedScoreReady bool
|
||||
BlueScoreReady bool
|
||||
}{web.arena.RedRealtimeScore.FoulsCommitted && web.arena.BlueRealtimeScore.FoulsCommitted,
|
||||
web.arena.RedRealtimeScore.TeleopCommitted, web.arena.BlueRealtimeScore.TeleopCommitted}
|
||||
case _, ok := <-allianceStationDisplayListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setAllianceStationDisplay"
|
||||
message = web.arena.AllianceStationDisplayScreen
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine.
|
||||
go ws.HandleNotifiers(web.arena.ArenaStatusNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
|
||||
web.arena.ScoringStatusNotifier, web.arena.AudienceDisplayModeNotifier,
|
||||
web.arena.AllianceStationDisplayModeNotifier)
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, data, err := websocket.Read()
|
||||
messageType, data, err := ws.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -312,22 +199,22 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request
|
||||
}{}
|
||||
err = mapstructure.Decode(data, &args)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
err = web.arena.SubstituteTeam(args.Team, args.Position)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
case "toggleBypass":
|
||||
station, ok := data.(string)
|
||||
if !ok {
|
||||
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
continue
|
||||
}
|
||||
if _, ok := web.arena.AllianceStations[station]; !ok {
|
||||
websocket.WriteError(fmt.Sprintf("Invalid alliance station '%s'.", station))
|
||||
ws.WriteError(fmt.Sprintf("Invalid alliance station '%s'.", station))
|
||||
continue
|
||||
}
|
||||
web.arena.AllianceStations[station].Bypass = !web.arena.AllianceStations[station].Bypass
|
||||
@@ -338,88 +225,88 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request
|
||||
}{}
|
||||
err = mapstructure.Decode(data, &args)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
web.arena.MuteMatchSounds = args.MuteMatchSounds
|
||||
web.arena.CurrentMatch.GameSpecificData = args.GameSpecificData
|
||||
err = web.arena.StartMatch()
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
case "abortMatch":
|
||||
err = web.arena.AbortMatch()
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
case "commitResults":
|
||||
err = web.commitCurrentMatchScore()
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
err = web.arena.ResetMatch()
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
err = web.arena.LoadNextMatch()
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
err = websocket.Write("reload", nil)
|
||||
err = ws.WriteNotifier(web.arena.ReloadDisplaysNotifier)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
continue // Skip sending the status update, as the client is about to terminate and reload.
|
||||
case "discardResults":
|
||||
err = web.arena.ResetMatch()
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
err = web.arena.LoadNextMatch()
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
err = websocket.Write("reload", nil)
|
||||
err = ws.WriteNotifier(web.arena.ReloadDisplaysNotifier)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
continue // Skip sending the status update, as the client is about to terminate and reload.
|
||||
case "setAudienceDisplay":
|
||||
screen, ok := data.(string)
|
||||
if !ok {
|
||||
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
continue
|
||||
}
|
||||
web.arena.AudienceDisplayScreen = screen
|
||||
web.arena.AudienceDisplayNotifier.Notify(nil)
|
||||
web.arena.AudienceDisplayMode = screen
|
||||
web.arena.AudienceDisplayModeNotifier.Notify()
|
||||
continue
|
||||
case "setAllianceStationDisplay":
|
||||
screen, ok := data.(string)
|
||||
if !ok {
|
||||
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
continue
|
||||
}
|
||||
web.arena.AllianceStationDisplayScreen = screen
|
||||
web.arena.AllianceStationDisplayNotifier.Notify(nil)
|
||||
web.arena.AllianceStationDisplayMode = screen
|
||||
web.arena.AllianceStationDisplayModeNotifier.Notify()
|
||||
continue
|
||||
default:
|
||||
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
continue
|
||||
}
|
||||
|
||||
// Send out the status again after handling the command, as it most likely changed as a result.
|
||||
err = websocket.Write("status", web.arena.GetStatus())
|
||||
err = ws.WriteNotifier(web.arena.ArenaStatusNotifier)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -436,7 +323,7 @@ func (web *Web) commitMatchScore(match *model.Match, matchResult *model.MatchRes
|
||||
// Store the result in the buffer to be shown in the audience display.
|
||||
web.arena.SavedMatch = match
|
||||
web.arena.SavedMatchResult = matchResult
|
||||
web.arena.ScorePostedNotifier.Notify(nil)
|
||||
web.arena.ScorePostedNotifier.Notify()
|
||||
}
|
||||
|
||||
if match.Type == "test" {
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/Team254/cheesy-arena/tournament"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
gorillawebsocket "github.com/gorilla/websocket"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"log"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -100,7 +100,7 @@ func TestMatchPlayShowResult(t *testing.T) {
|
||||
recorder = web.getHttpResponse(fmt.Sprintf("/match_play/%d/show_result", match.Id))
|
||||
assert.Equal(t, 500, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "No result found")
|
||||
web.arena.Database.CreateMatchResult(&model.MatchResult{MatchId: match.Id})
|
||||
web.arena.Database.CreateMatchResult(model.BuildTestMatchResult(match.Id, 1))
|
||||
recorder = web.getHttpResponse(fmt.Sprintf("/match_play/%d/show_result", match.Id))
|
||||
assert.Equal(t, 303, recorder.Code)
|
||||
assert.Equal(t, match.Id, web.arena.SavedMatch.Id)
|
||||
@@ -241,19 +241,19 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket", nil)
|
||||
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer conn.Close()
|
||||
ws := &Websocket{conn, new(sync.Mutex)}
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
readWebsocketType(t, ws, "setAudienceDisplay")
|
||||
readWebsocketType(t, ws, "scoringStatus")
|
||||
readWebsocketType(t, ws, "setAllianceStationDisplay")
|
||||
readWebsocketType(t, ws, "audienceDisplayMode")
|
||||
readWebsocketType(t, ws, "allianceStationDisplayMode")
|
||||
|
||||
// Test that a server-side error is communicated to the client.
|
||||
ws.Write("nonexistenttype", nil)
|
||||
@@ -265,20 +265,20 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
|
||||
ws.Write("substituteTeam", map[string]interface{}{"team": 254, "position": "B5"})
|
||||
assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station")
|
||||
ws.Write("substituteTeam", map[string]interface{}{"team": 254, "position": "B1"})
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
assert.Equal(t, 254, web.arena.CurrentMatch.Blue1)
|
||||
ws.Write("substituteTeam", map[string]interface{}{"team": 0, "position": "B1"})
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
assert.Equal(t, 0, web.arena.CurrentMatch.Blue1)
|
||||
ws.Write("toggleBypass", nil)
|
||||
assert.Contains(t, readWebsocketError(t, ws), "Failed to parse")
|
||||
ws.Write("toggleBypass", "R4")
|
||||
assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station")
|
||||
ws.Write("toggleBypass", "R3")
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
assert.Equal(t, true, web.arena.AllianceStations["R3"].Bypass)
|
||||
ws.Write("toggleBypass", "R3")
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
assert.Equal(t, false, web.arena.AllianceStations["R3"].Bypass)
|
||||
|
||||
// Go through match flow.
|
||||
@@ -293,15 +293,16 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
|
||||
web.arena.AllianceStations["B2"].Bypass = true
|
||||
web.arena.AllianceStations["B3"].Bypass = true
|
||||
ws.Write("startMatch", nil)
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
assert.Equal(t, field.StartMatch, web.arena.MatchState)
|
||||
ws.Write("commitResults", nil)
|
||||
assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match")
|
||||
ws.Write("discardResults", nil)
|
||||
assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match")
|
||||
ws.Write("abortMatch", nil)
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "setAudienceDisplay")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
readWebsocketType(t, ws, "audienceDisplayMode")
|
||||
readWebsocketType(t, ws, "allianceStationDisplayMode")
|
||||
assert.Equal(t, field.PostMatch, web.arena.MatchState)
|
||||
web.arena.RedRealtimeScore.CurrentScore.AutoRuns = 1
|
||||
web.arena.BlueRealtimeScore.CurrentScore.BoostCubes = 2
|
||||
@@ -316,11 +317,11 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
|
||||
|
||||
// Test changing the displays.
|
||||
ws.Write("setAudienceDisplay", "logo")
|
||||
readWebsocketType(t, ws, "setAudienceDisplay")
|
||||
assert.Equal(t, "logo", web.arena.AudienceDisplayScreen)
|
||||
readWebsocketType(t, ws, "audienceDisplayMode")
|
||||
assert.Equal(t, "logo", web.arena.AudienceDisplayMode)
|
||||
ws.Write("setAllianceStationDisplay", "logo")
|
||||
readWebsocketType(t, ws, "setAllianceStationDisplay")
|
||||
assert.Equal(t, "logo", web.arena.AllianceStationDisplayScreen)
|
||||
readWebsocketType(t, ws, "allianceStationDisplayMode")
|
||||
assert.Equal(t, "logo", web.arena.AllianceStationDisplayMode)
|
||||
}
|
||||
|
||||
func TestMatchPlayWebsocketNotifications(t *testing.T) {
|
||||
@@ -330,18 +331,19 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket", nil)
|
||||
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer conn.Close()
|
||||
ws := &Websocket{conn, new(sync.Mutex)}
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
readWebsocketType(t, ws, "setAudienceDisplay")
|
||||
readWebsocketType(t, ws, "scoringStatus")
|
||||
readWebsocketType(t, ws, "audienceDisplayMode")
|
||||
readWebsocketType(t, ws, "allianceStationDisplayMode")
|
||||
|
||||
web.arena.AllianceStations["R1"].Bypass = true
|
||||
web.arena.AllianceStations["R2"].Bypass = true
|
||||
@@ -349,14 +351,15 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
|
||||
web.arena.AllianceStations["B1"].Bypass = true
|
||||
web.arena.AllianceStations["B2"].Bypass = true
|
||||
web.arena.AllianceStations["B3"].Bypass = true
|
||||
web.arena.StartMatch()
|
||||
assert.Nil(t, web.arena.StartMatch())
|
||||
web.arena.Update()
|
||||
messages := readWebsocketMultiple(t, ws, 3)
|
||||
_, ok := messages["matchTime"]
|
||||
assert.True(t, ok)
|
||||
_, ok = messages["setAudienceDisplay"]
|
||||
_, ok = messages["audienceDisplayMode"]
|
||||
assert.True(t, ok)
|
||||
_, ok = messages["allianceStationDisplayMode"]
|
||||
assert.True(t, ok)
|
||||
_, ok = messages["setAllianceStationDisplay"]
|
||||
web.arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec) * time.Second)
|
||||
web.arena.Update()
|
||||
messages = readWebsocketMultiple(t, ws, 2)
|
||||
@@ -365,7 +368,7 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
|
||||
assert.Equal(t, 3, matchTime.MatchState)
|
||||
assert.Equal(t, 3, matchTime.MatchTimeSec)
|
||||
assert.True(t, ok)
|
||||
web.arena.ScoringStatusNotifier.Notify(nil)
|
||||
web.arena.ScoringStatusNotifier.Notify()
|
||||
readWebsocketType(t, ws, "scoringStatus")
|
||||
|
||||
// Should get a tick notification when an integer second threshold is crossed.
|
||||
@@ -395,14 +398,14 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
|
||||
}
|
||||
|
||||
// Handles the status and matchTime messages arriving in either order.
|
||||
func readWebsocketStatusMatchTime(t *testing.T, ws *Websocket) (bool, MatchTimeMessage) {
|
||||
func readWebsocketStatusMatchTime(t *testing.T, ws *websocket.Websocket) (bool, field.MatchTimeMessage) {
|
||||
return getStatusMatchTime(t, readWebsocketMultiple(t, ws, 2))
|
||||
}
|
||||
|
||||
func getStatusMatchTime(t *testing.T, messages map[string]interface{}) (bool, MatchTimeMessage) {
|
||||
_, statusReceived := messages["status"]
|
||||
func getStatusMatchTime(t *testing.T, messages map[string]interface{}) (bool, field.MatchTimeMessage) {
|
||||
_, statusReceived := messages["arenaStatus"]
|
||||
message, ok := messages["matchTime"]
|
||||
var matchTime MatchTimeMessage
|
||||
var matchTime field.MatchTimeMessage
|
||||
if assert.True(t, ok) {
|
||||
err := mapstructure.Decode(message, &matchTime)
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -7,8 +7,7 @@ package web
|
||||
|
||||
import (
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"io"
|
||||
"log"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -39,47 +38,13 @@ func (web *Web) pitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen()
|
||||
defer close(reloadDisplaysListener)
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-reloadDisplaysListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
_, _, err := websocket.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
gorillawebsocket "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -23,10 +23,10 @@ func TestPitDisplayWebsocket(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/pit/websocket", nil)
|
||||
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/pit/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer conn.Close()
|
||||
ws := &Websocket{conn, new(sync.Mutex)}
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Check forced reloading as that is the only purpose the pit websocket serves.
|
||||
recorder := web.getHttpResponse("/setup/field/reload_displays")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/Team254/cheesy-arena/field"
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"io"
|
||||
"log"
|
||||
@@ -86,54 +87,25 @@ func (web *Web) refereeDisplayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(patrick): Enable authentication once Safari (for iPad) supports it over Websocket.
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen()
|
||||
defer close(matchLoadTeamsListener)
|
||||
reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen()
|
||||
defer close(reloadDisplaysListener)
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-matchLoadTeamsListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
case _, ok := <-reloadDisplaysListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// 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.ReloadDisplaysNotifier)
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, data, err := websocket.Read()
|
||||
messageType, data, err := ws.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -147,7 +119,7 @@ func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Re
|
||||
}{}
|
||||
err = mapstructure.Decode(data, &args)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -161,7 +133,7 @@ func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Re
|
||||
web.arena.BlueRealtimeScore.CurrentScore.Fouls =
|
||||
append(web.arena.BlueRealtimeScore.CurrentScore.Fouls, foul)
|
||||
}
|
||||
web.arena.RealtimeScoreNotifier.Notify(nil)
|
||||
web.arena.RealtimeScoreNotifier.Notify()
|
||||
case "deleteFoul":
|
||||
args := struct {
|
||||
Alliance string
|
||||
@@ -172,7 +144,7 @@ func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Re
|
||||
}{}
|
||||
err = mapstructure.Decode(data, &args)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -191,7 +163,7 @@ func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Re
|
||||
break
|
||||
}
|
||||
}
|
||||
web.arena.RealtimeScoreNotifier.Notify(nil)
|
||||
web.arena.RealtimeScoreNotifier.Notify()
|
||||
case "card":
|
||||
args := struct {
|
||||
Alliance string
|
||||
@@ -200,7 +172,7 @@ func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Re
|
||||
}{}
|
||||
err = mapstructure.Decode(data, &args)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -226,8 +198,8 @@ func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Re
|
||||
continue
|
||||
}
|
||||
web.arena.FieldReset = true
|
||||
web.arena.AllianceStationDisplayScreen = "fieldReset"
|
||||
web.arena.AllianceStationDisplayNotifier.Notify(nil)
|
||||
web.arena.AllianceStationDisplayMode = "fieldReset"
|
||||
web.arena.AllianceStationDisplayModeNotifier.Notify()
|
||||
continue // Don't reload.
|
||||
case "commitMatch":
|
||||
if web.arena.MatchState != field.PostMatch {
|
||||
@@ -237,18 +209,18 @@ func (web *Web) refereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Re
|
||||
web.arena.RedRealtimeScore.FoulsCommitted = true
|
||||
web.arena.BlueRealtimeScore.FoulsCommitted = true
|
||||
web.arena.FieldReset = true
|
||||
web.arena.AllianceStationDisplayScreen = "fieldReset"
|
||||
web.arena.AllianceStationDisplayNotifier.Notify(nil)
|
||||
web.arena.ScoringStatusNotifier.Notify(nil)
|
||||
web.arena.AllianceStationDisplayMode = "fieldReset"
|
||||
web.arena.AllianceStationDisplayModeNotifier.Notify()
|
||||
web.arena.ScoringStatusNotifier.Notify()
|
||||
default:
|
||||
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
continue
|
||||
}
|
||||
|
||||
// Force a reload of the client to render the updated foul list.
|
||||
err = websocket.Write("reload", nil)
|
||||
err = ws.WriteNotifier(web.arena.ReloadDisplaysNotifier)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ package web
|
||||
|
||||
import (
|
||||
"github.com/Team254/cheesy-arena/field"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
gorillawebsocket "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -25,10 +25,13 @@ func TestRefereeDisplayWebsocket(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/referee/websocket", nil)
|
||||
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/referee/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer conn.Close()
|
||||
ws := &Websocket{conn, new(sync.Mutex)}
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
|
||||
// Test foul addition.
|
||||
foulData := struct {
|
||||
@@ -104,17 +107,17 @@ func TestRefereeDisplayWebsocket(t *testing.T) {
|
||||
web.arena.MatchState = field.PostMatch
|
||||
ws.Write("signalReset", nil)
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
assert.Equal(t, "fieldReset", web.arena.AllianceStationDisplayScreen)
|
||||
assert.Equal(t, "fieldReset", web.arena.AllianceStationDisplayMode)
|
||||
assert.False(t, web.arena.RedRealtimeScore.FoulsCommitted)
|
||||
assert.False(t, web.arena.BlueRealtimeScore.FoulsCommitted)
|
||||
web.arena.AllianceStationDisplayScreen = "logo"
|
||||
web.arena.AllianceStationDisplayMode = "logo"
|
||||
ws.Write("commitMatch", nil)
|
||||
readWebsocketType(t, ws, "reload")
|
||||
assert.Equal(t, "fieldReset", web.arena.AllianceStationDisplayScreen)
|
||||
assert.Equal(t, "fieldReset", web.arena.AllianceStationDisplayMode)
|
||||
assert.True(t, web.arena.RedRealtimeScore.FoulsCommitted)
|
||||
assert.True(t, web.arena.BlueRealtimeScore.FoulsCommitted)
|
||||
|
||||
// Should refresh the page when the next match is loaded.
|
||||
web.arena.MatchLoadTeamsNotifier.Notify(nil)
|
||||
readWebsocketType(t, ws, "reload")
|
||||
web.arena.MatchLoadNotifier.Notify()
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ package web
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Team254/cheesy-arena/field"
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
"log"
|
||||
@@ -58,162 +58,110 @@ func (web *Web) scoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
var score **field.RealtimeScore
|
||||
var scoreSummaryFunc func() *game.ScoreSummary
|
||||
if alliance == "red" {
|
||||
score = &web.arena.RedRealtimeScore
|
||||
scoreSummaryFunc = web.arena.RedScoreSummary
|
||||
} else {
|
||||
score = &web.arena.BlueRealtimeScore
|
||||
scoreSummaryFunc = web.arena.BlueScoreSummary
|
||||
}
|
||||
autoCommitted := false
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
matchLoadTeamsListener := web.arena.MatchLoadTeamsNotifier.Listen()
|
||||
defer close(matchLoadTeamsListener)
|
||||
matchTimeListener := web.arena.MatchTimeNotifier.Listen()
|
||||
defer close(matchTimeListener)
|
||||
reloadDisplaysListener := web.arena.ReloadDisplaysNotifier.Listen()
|
||||
defer close(reloadDisplaysListener)
|
||||
|
||||
// Send the various notifications immediately upon connection.
|
||||
data := struct {
|
||||
Score *field.RealtimeScore
|
||||
ScoreSummary *game.ScoreSummary
|
||||
AutoCommitted bool
|
||||
}{*score, scoreSummaryFunc(), autoCommitted}
|
||||
err = websocket.Write("score", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("matchTime", MatchTimeMessage{int(web.arena.MatchState), int(web.arena.LastMatchTimeSec)})
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-matchLoadTeamsListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
case matchTimeSec, ok := <-matchTimeListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "matchTime"
|
||||
message = MatchTimeMessage{int(web.arena.MatchState), matchTimeSec.(int)}
|
||||
case _, ok := <-reloadDisplaysListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "reload"
|
||||
message = nil
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine.
|
||||
go ws.HandleNotifiers(web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
|
||||
web.arena.ReloadDisplaysNotifier)
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, data, err := websocket.Read()
|
||||
messageType, _, err := ws.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
scoreChanged := false
|
||||
switch messageType {
|
||||
case "r":
|
||||
if !autoCommitted {
|
||||
if !(*score).AutoCommitted {
|
||||
if (*score).CurrentScore.AutoRuns < 3 {
|
||||
(*score).CurrentScore.AutoRuns++
|
||||
scoreChanged = true
|
||||
}
|
||||
}
|
||||
case "R":
|
||||
if !autoCommitted {
|
||||
if !(*score).AutoCommitted {
|
||||
if (*score).CurrentScore.AutoRuns > 0 {
|
||||
(*score).CurrentScore.AutoRuns--
|
||||
scoreChanged = true
|
||||
}
|
||||
}
|
||||
case "c":
|
||||
if autoCommitted {
|
||||
if (*score).AutoCommitted {
|
||||
if (*score).CurrentScore.Climbs+(*score).CurrentScore.Parks < 3 {
|
||||
(*score).CurrentScore.Climbs++
|
||||
scoreChanged = true
|
||||
}
|
||||
}
|
||||
case "C":
|
||||
if autoCommitted {
|
||||
if (*score).AutoCommitted {
|
||||
if (*score).CurrentScore.Climbs > 0 {
|
||||
(*score).CurrentScore.Climbs--
|
||||
scoreChanged = true
|
||||
}
|
||||
}
|
||||
case "p":
|
||||
if autoCommitted {
|
||||
if (*score).AutoCommitted {
|
||||
if (*score).CurrentScore.Climbs+(*score).CurrentScore.Parks < 3 {
|
||||
(*score).CurrentScore.Parks++
|
||||
scoreChanged = true
|
||||
}
|
||||
}
|
||||
case "P":
|
||||
if autoCommitted {
|
||||
if (*score).AutoCommitted {
|
||||
if (*score).CurrentScore.Parks > 0 {
|
||||
(*score).CurrentScore.Parks--
|
||||
scoreChanged = true
|
||||
}
|
||||
}
|
||||
case "\r":
|
||||
if web.arena.MatchState != field.PreMatch || web.arena.CurrentMatch.Type == "test" {
|
||||
autoCommitted = true
|
||||
if (web.arena.MatchState != field.PreMatch || web.arena.CurrentMatch.Type == "test") &&
|
||||
!(*score).AutoCommitted {
|
||||
(*score).AutoCommitted = true
|
||||
scoreChanged = true
|
||||
}
|
||||
case "a":
|
||||
autoCommitted = false
|
||||
if (*score).AutoCommitted {
|
||||
(*score).AutoCommitted = false
|
||||
scoreChanged = true
|
||||
}
|
||||
case "commitMatch":
|
||||
if web.arena.MatchState != field.PostMatch {
|
||||
// Don't allow committing the score until the match is over.
|
||||
websocket.WriteError("Cannot commit score: Match is not over.")
|
||||
ws.WriteError("Cannot commit score: Match is not over.")
|
||||
continue
|
||||
}
|
||||
|
||||
autoCommitted = true
|
||||
(*score).TeleopCommitted = true
|
||||
web.arena.ScoringStatusNotifier.Notify(nil)
|
||||
if !(*score).TeleopCommitted {
|
||||
(*score).AutoCommitted = true
|
||||
(*score).TeleopCommitted = true
|
||||
web.arena.ScoringStatusNotifier.Notify()
|
||||
scoreChanged = true
|
||||
}
|
||||
default:
|
||||
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
// Unknown keypress; just swallow the message without doing anything.
|
||||
continue
|
||||
}
|
||||
|
||||
web.arena.RealtimeScoreNotifier.Notify(nil)
|
||||
|
||||
// Send out the score again after handling the command, as it most likely changed as a result.
|
||||
data = struct {
|
||||
Score *field.RealtimeScore
|
||||
ScoreSummary *game.ScoreSummary
|
||||
AutoCommitted bool
|
||||
}{*score, scoreSummaryFunc(), autoCommitted}
|
||||
err = websocket.Write("score", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
if scoreChanged {
|
||||
web.arena.RealtimeScoreNotifier.Notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ package web
|
||||
|
||||
import (
|
||||
"github.com/Team254/cheesy-arena/field"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
gorillawebsocket "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestScoringDisplay(t *testing.T) {
|
||||
@@ -29,22 +30,22 @@ func TestScoringDisplayWebsocket(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
_, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blorpy/websocket", nil)
|
||||
_, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blorpy/websocket", nil)
|
||||
assert.NotNil(t, err)
|
||||
redConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/red/websocket", nil)
|
||||
redConn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/red/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer redConn.Close()
|
||||
redWs := &Websocket{redConn, new(sync.Mutex)}
|
||||
blueConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blue/websocket", nil)
|
||||
redWs := websocket.NewTestWebsocket(redConn)
|
||||
blueConn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blue/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer blueConn.Close()
|
||||
blueWs := &Websocket{blueConn, new(sync.Mutex)}
|
||||
blueWs := websocket.NewTestWebsocket(blueConn)
|
||||
|
||||
// Should receive a score update right after connection.
|
||||
readWebsocketType(t, redWs, "score")
|
||||
readWebsocketType(t, redWs, "matchTime")
|
||||
readWebsocketType(t, blueWs, "score")
|
||||
readWebsocketType(t, redWs, "realtimeScore")
|
||||
readWebsocketType(t, blueWs, "matchTime")
|
||||
readWebsocketType(t, blueWs, "realtimeScore")
|
||||
|
||||
// Send a match worth of scoring commands in.
|
||||
redWs.Write("r", nil)
|
||||
@@ -53,27 +54,23 @@ func TestScoringDisplayWebsocket(t *testing.T) {
|
||||
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, "score")
|
||||
}
|
||||
for i := 0; i < 6; i++ {
|
||||
readWebsocketType(t, blueWs, "score")
|
||||
readWebsocketType(t, redWs, "realtimeScore")
|
||||
readWebsocketType(t, blueWs, "realtimeScore")
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, web.arena.RedRealtimeScore.CurrentScore.AutoRuns)
|
||||
assert.Equal(t, 2, web.arena.BlueRealtimeScore.CurrentScore.AutoRuns)
|
||||
|
||||
redWs.Write("r", nil)
|
||||
for i := 0; i < 1; i++ {
|
||||
readWebsocketType(t, redWs, "score")
|
||||
}
|
||||
for i := 0; i < 0; i++ {
|
||||
readWebsocketType(t, blueWs, "score")
|
||||
}
|
||||
|
||||
// Make sure auto scores haven't changed in teleop.
|
||||
assert.Equal(t, 1, web.arena.RedRealtimeScore.CurrentScore.AutoRuns)
|
||||
@@ -82,17 +79,22 @@ func TestScoringDisplayWebsocket(t *testing.T) {
|
||||
// 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)
|
||||
web.arena.MatchState = field.PostMatch
|
||||
redWs.Write("commitMatch", nil)
|
||||
blueWs.Write("commitMatch", nil)
|
||||
readWebsocketType(t, redWs, "score")
|
||||
readWebsocketType(t, blueWs, "score")
|
||||
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)
|
||||
|
||||
// Load another match to reset the results.
|
||||
web.arena.ResetMatch()
|
||||
web.arena.LoadTestMatch()
|
||||
readWebsocketType(t, redWs, "reload")
|
||||
readWebsocketType(t, blueWs, "reload")
|
||||
readWebsocketType(t, redWs, "realtimeScore")
|
||||
readWebsocketType(t, blueWs, "realtimeScore")
|
||||
assert.Equal(t, field.NewRealtimeScore(), web.arena.RedRealtimeScore)
|
||||
assert.Equal(t, field.NewRealtimeScore(), web.arena.BlueRealtimeScore)
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func (web *Web) allianceSelectionPostHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
cachedRankedTeams = newRankedTeams
|
||||
|
||||
web.arena.AllianceSelectionNotifier.Notify(nil)
|
||||
web.arena.AllianceSelectionNotifier.NotifyWithMessage(cachedAlliances)
|
||||
http.Redirect(w, r, "/setup/alliance_selection", 303)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (web *Web) allianceSelectionStartHandler(w http.ResponseWriter, r *http.Req
|
||||
cachedRankedTeams[i] = &RankedTeam{i + 1, ranking.TeamId, false}
|
||||
}
|
||||
|
||||
web.arena.AllianceSelectionNotifier.Notify(nil)
|
||||
web.arena.AllianceSelectionNotifier.NotifyWithMessage(cachedAlliances)
|
||||
http.Redirect(w, r, "/setup/alliance_selection", 303)
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func (web *Web) allianceSelectionResetHandler(w http.ResponseWriter, r *http.Req
|
||||
|
||||
cachedAlliances = [][]model.AllianceTeam{}
|
||||
cachedRankedTeams = []*RankedTeam{}
|
||||
web.arena.AllianceSelectionNotifier.Notify(nil)
|
||||
web.arena.AllianceSelectionNotifier.NotifyWithMessage(cachedAlliances)
|
||||
http.Redirect(w, r, "/setup/alliance_selection", 303)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/Team254/cheesy-arena/led"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/Team254/cheesy-arena/vaultled"
|
||||
"log"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
@@ -56,7 +56,7 @@ func (web *Web) fieldPostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
displayId := r.PostFormValue("displayId")
|
||||
allianceStation := r.PostFormValue("allianceStation")
|
||||
web.arena.AllianceStationDisplays[displayId] = allianceStation
|
||||
web.arena.MatchLoadTeamsNotifier.Notify(nil)
|
||||
web.arena.MatchLoadNotifier.Notify()
|
||||
http.Redirect(w, r, "/setup/field", 303)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func (web *Web) fieldReloadDisplaysHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
web.arena.ReloadDisplaysNotifier.Notify(nil)
|
||||
web.arena.ReloadDisplaysNotifier.Notify()
|
||||
http.Redirect(w, r, "/setup/field", 303)
|
||||
}
|
||||
|
||||
@@ -101,47 +101,13 @@ func (web *Web) fieldWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
plcIoChangeListener := web.arena.Plc.IoChangeNotifier.Listen()
|
||||
defer close(plcIoChangeListener)
|
||||
|
||||
// Send the PLC status immediately upon connection.
|
||||
data := struct {
|
||||
Inputs []bool
|
||||
Registers []uint16
|
||||
Coils []bool
|
||||
}{web.arena.Plc.Inputs[:], web.arena.Plc.Registers[:], web.arena.Plc.Coils[:]}
|
||||
err = websocket.Write("plcIoChange", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-plcIoChangeListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "plcIoChange"
|
||||
message = struct {
|
||||
Inputs []bool
|
||||
Registers []uint16
|
||||
Coils []bool
|
||||
}{web.arena.Plc.Inputs[:], web.arena.Plc.Registers[:], web.arena.Plc.Coils[:]}
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.Plc.IoChangeNotifier)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package web
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"io"
|
||||
"log"
|
||||
@@ -47,22 +48,22 @@ func (web *Web) lowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
defer ws.Close()
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, data, err := websocket.Read()
|
||||
messageType, data, err := ws.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ func (web *Web) lowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Reque
|
||||
var lowerThird model.LowerThird
|
||||
err = mapstructure.Decode(data, &lowerThird)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
web.saveLowerThird(&lowerThird)
|
||||
@@ -79,36 +80,36 @@ func (web *Web) lowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Reque
|
||||
var lowerThird model.LowerThird
|
||||
err = mapstructure.Decode(data, &lowerThird)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
err = web.arena.Database.DeleteLowerThird(&lowerThird)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
case "showLowerThird":
|
||||
var lowerThird model.LowerThird
|
||||
err = mapstructure.Decode(data, &lowerThird)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
web.saveLowerThird(&lowerThird)
|
||||
web.arena.LowerThirdNotifier.Notify(lowerThird)
|
||||
web.arena.AudienceDisplayScreen = "lowerThird"
|
||||
web.arena.AudienceDisplayNotifier.Notify(nil)
|
||||
web.arena.LowerThirdNotifier.NotifyWithMessage(lowerThird)
|
||||
web.arena.AudienceDisplayMode = "lowerThird"
|
||||
web.arena.AudienceDisplayModeNotifier.Notify()
|
||||
continue
|
||||
case "hideLowerThird":
|
||||
var lowerThird model.LowerThird
|
||||
err = mapstructure.Decode(data, &lowerThird)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
web.saveLowerThird(&lowerThird)
|
||||
web.arena.AudienceDisplayScreen = "blank"
|
||||
web.arena.AudienceDisplayNotifier.Notify(nil)
|
||||
web.arena.AudienceDisplayMode = "blank"
|
||||
web.arena.AudienceDisplayModeNotifier.Notify()
|
||||
continue
|
||||
case "reorderLowerThird":
|
||||
args := struct {
|
||||
@@ -117,23 +118,23 @@ func (web *Web) lowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Reque
|
||||
}{}
|
||||
err = mapstructure.Decode(data, &args)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
err = web.reorderLowerThird(args.Id, args.MoveUp)
|
||||
if err != nil {
|
||||
websocket.WriteError(err.Error())
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
default:
|
||||
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
continue
|
||||
}
|
||||
|
||||
// Force a reload of the client to render the updated lower thirds list.
|
||||
err = websocket.Write("reload", nil)
|
||||
err = ws.WriteNotifier(web.arena.ReloadDisplaysNotifier)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ package web
|
||||
|
||||
import (
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
gorillawebsocket "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -26,10 +26,10 @@ func TestSetupLowerThirds(t *testing.T) {
|
||||
|
||||
server, wsUrl := web.startTestServer()
|
||||
defer server.Close()
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/setup/lower_thirds/websocket", nil)
|
||||
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/setup/lower_thirds/websocket", nil)
|
||||
assert.Nil(t, err)
|
||||
defer conn.Close()
|
||||
ws := &Websocket{conn, new(sync.Mutex)}
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
ws.Write("saveLowerThird", model.LowerThird{1, "Top Text 4", "Bottom Text 1", 0})
|
||||
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
|
||||
@@ -41,18 +41,18 @@ func TestSetupLowerThirds(t *testing.T) {
|
||||
lowerThird, _ = web.arena.Database.GetLowerThirdById(1)
|
||||
assert.Nil(t, lowerThird)
|
||||
|
||||
assert.Equal(t, "blank", web.arena.AudienceDisplayScreen)
|
||||
assert.Equal(t, "blank", web.arena.AudienceDisplayMode)
|
||||
ws.Write("showLowerThird", model.LowerThird{2, "Top Text 5", "Bottom Text 1", 0})
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
lowerThird, _ = web.arena.Database.GetLowerThirdById(2)
|
||||
assert.Equal(t, "Top Text 5", lowerThird.TopText)
|
||||
assert.Equal(t, "lowerThird", web.arena.AudienceDisplayScreen)
|
||||
assert.Equal(t, "lowerThird", web.arena.AudienceDisplayMode)
|
||||
|
||||
ws.Write("hideLowerThird", model.LowerThird{2, "Top Text 6", "Bottom Text 1", 0})
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
lowerThird, _ = web.arena.Database.GetLowerThirdById(2)
|
||||
assert.Equal(t, "Top Text 6", lowerThird.TopText)
|
||||
assert.Equal(t, "blank", web.arena.AudienceDisplayScreen)
|
||||
assert.Equal(t, "blank", web.arena.AudienceDisplayMode)
|
||||
|
||||
ws.Write("reorderLowerThird", map[string]interface{}{"Id": 2, "moveUp": false})
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
@@ -5,11 +5,13 @@ package web
|
||||
|
||||
import (
|
||||
"github.com/Team254/cheesy-arena/field"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
@@ -42,7 +44,7 @@ func (web *Web) startTestServer() (*httptest.Server, string) {
|
||||
}
|
||||
|
||||
// Receives the next websocket message and asserts that it is an error.
|
||||
func readWebsocketError(t *testing.T, ws *Websocket) string {
|
||||
func readWebsocketError(t *testing.T, ws *websocket.Websocket) string {
|
||||
messageType, data, err := ws.Read()
|
||||
if assert.Nil(t, err) && assert.Equal(t, "error", messageType) {
|
||||
return data.(string)
|
||||
@@ -51,18 +53,18 @@ func readWebsocketError(t *testing.T, ws *Websocket) string {
|
||||
}
|
||||
|
||||
// Receives the next websocket message and asserts that it is of the given type.
|
||||
func readWebsocketType(t *testing.T, ws *Websocket, expectedMessageType string) interface{} {
|
||||
messageType, message, err := ws.Read()
|
||||
func readWebsocketType(t *testing.T, ws *websocket.Websocket, expectedMessageType string) interface{} {
|
||||
messageType, message, err := ws.ReadWithTimeout(time.Second)
|
||||
if assert.Nil(t, err) {
|
||||
assert.Equal(t, expectedMessageType, messageType)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func readWebsocketMultiple(t *testing.T, ws *Websocket, count int) map[string]interface{} {
|
||||
func readWebsocketMultiple(t *testing.T, ws *websocket.Websocket, count int) map[string]interface{} {
|
||||
messages := make(map[string]interface{})
|
||||
for i := 0; i < count; i++ {
|
||||
messageType, message, err := ws.Read()
|
||||
messageType, message, err := ws.ReadWithTimeout(time.Second)
|
||||
if assert.Nil(t, err) {
|
||||
messages[messageType] = message
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
//
|
||||
// Functions for the server side of handling websockets.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Wraps the Gorilla Websocket module so that we can define additional functions on it.
|
||||
type Websocket struct {
|
||||
conn *websocket.Conn
|
||||
writeMutex *sync.Mutex
|
||||
}
|
||||
|
||||
type WebsocketMessage struct {
|
||||
Type string `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
var websocketUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 2014}
|
||||
|
||||
// Upgrades the given HTTP request to a websocket connection.
|
||||
func NewWebsocket(w http.ResponseWriter, r *http.Request) (*Websocket, error) {
|
||||
conn, err := websocketUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Websocket{conn, new(sync.Mutex)}, nil
|
||||
}
|
||||
|
||||
func (ws *Websocket) Close() {
|
||||
ws.conn.Close()
|
||||
}
|
||||
|
||||
func (ws *Websocket) Read() (string, interface{}, error) {
|
||||
var message WebsocketMessage
|
||||
err := ws.conn.ReadJSON(&message)
|
||||
if websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
|
||||
// This error indicates that the browser terminated the connection normally; rewwrite it so that clients don't
|
||||
// log it.
|
||||
return "", nil, io.EOF
|
||||
}
|
||||
return message.Type, message.Data, err
|
||||
}
|
||||
|
||||
func (ws *Websocket) Write(messageType string, data interface{}) error {
|
||||
ws.writeMutex.Lock()
|
||||
defer ws.writeMutex.Unlock()
|
||||
return ws.conn.WriteJSON(WebsocketMessage{messageType, data})
|
||||
}
|
||||
|
||||
func (ws *Websocket) WriteError(errorMessage string) error {
|
||||
ws.writeMutex.Lock()
|
||||
defer ws.writeMutex.Unlock()
|
||||
return ws.conn.WriteJSON(WebsocketMessage{"error", errorMessage})
|
||||
}
|
||||
|
||||
func (ws *Websocket) ShowDialog(message string) error {
|
||||
ws.writeMutex.Lock()
|
||||
defer ws.writeMutex.Unlock()
|
||||
return ws.conn.WriteJSON(WebsocketMessage{"dialog", message})
|
||||
}
|
||||
80
websocket/notifier.go
Normal file
80
websocket/notifier.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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 websocket
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// Allow the listeners to buffer a small number of notifications to streamline delivery.
|
||||
const notifyBufferSize = 5
|
||||
|
||||
type Notifier struct {
|
||||
messageType string
|
||||
messageProducer func() interface{}
|
||||
listeners map[chan messageEnvelope]struct{} // The map is essentially a set; the value is ignored.
|
||||
}
|
||||
|
||||
type messageEnvelope struct {
|
||||
messageType string
|
||||
messageBody interface{}
|
||||
}
|
||||
|
||||
func NewNotifier(messageType string, messageProducer func() interface{}) *Notifier {
|
||||
notifier := &Notifier{messageType: messageType, messageProducer: messageProducer}
|
||||
notifier.listeners = make(map[chan messageEnvelope]struct{})
|
||||
return notifier
|
||||
}
|
||||
|
||||
// Calls the messageProducer function and sends a message containing the results to all registered listeners, and cleans
|
||||
// up any listeners that have closed.
|
||||
func (notifier *Notifier) Notify() {
|
||||
notifier.NotifyWithMessage(notifier.getMessageBody())
|
||||
}
|
||||
|
||||
// Sends the given message to all registered listeners, and cleans up any listeners that have closed. If there is a
|
||||
// messageProducer function defined it is ignored.
|
||||
func (notifier *Notifier) NotifyWithMessage(messageBody interface{}) {
|
||||
message := messageEnvelope{messageType: notifier.messageType, messageBody: messageBody}
|
||||
for listener := range notifier.listeners {
|
||||
notifier.notifyListener(listener, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (notifier *Notifier) notifyListener(listener chan messageEnvelope, message messageEnvelope) {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 messageEnvelope {
|
||||
listener := make(chan messageEnvelope, notifyBufferSize)
|
||||
notifier.listeners[listener] = struct{}{}
|
||||
return listener
|
||||
}
|
||||
|
||||
// Invokes the message producer to get the message, or returns nil if no producer is defined.
|
||||
func (notifier *Notifier) getMessageBody() interface{} {
|
||||
if notifier.messageProducer == nil {
|
||||
return nil
|
||||
} else {
|
||||
return notifier.messageProducer()
|
||||
}
|
||||
}
|
||||
90
websocket/notifier_test.go
Normal file
90
websocket/notifier_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNotifier(t *testing.T) {
|
||||
notifier := NewNotifier("testMessageType", generateTestMessage)
|
||||
|
||||
// Should do nothing when there are no listeners.
|
||||
notifier.Notify()
|
||||
notifier.NotifyWithMessage(12345)
|
||||
notifier.NotifyWithMessage(struct{}{})
|
||||
|
||||
listener := notifier.listen()
|
||||
notifier.Notify()
|
||||
message := <-listener
|
||||
assert.Equal(t, "testMessageType", message.messageType)
|
||||
assert.Equal(t, "test message", message.messageBody)
|
||||
notifier.NotifyWithMessage(12345)
|
||||
assert.Equal(t, 12345, (<-listener).messageBody)
|
||||
|
||||
// Should allow multiple messages without blocking.
|
||||
notifier.NotifyWithMessage("message1")
|
||||
notifier.NotifyWithMessage("message2")
|
||||
notifier.Notify()
|
||||
assert.Equal(t, "message1", (<-listener).messageBody)
|
||||
assert.Equal(t, "message2", (<-listener).messageBody)
|
||||
assert.Equal(t, "test message", (<-listener).messageBody)
|
||||
|
||||
// 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.NotifyWithMessage(i)
|
||||
}
|
||||
var value messageEnvelope
|
||||
var lastValue interface{}
|
||||
for lastValue == nil {
|
||||
select {
|
||||
case value = <-listener:
|
||||
default:
|
||||
lastValue = value.messageBody
|
||||
return
|
||||
}
|
||||
}
|
||||
notifier.NotifyWithMessage("next message")
|
||||
assert.True(t, lastValue.(int) < 10)
|
||||
assert.Equal(t, "next message", (<-listener).messageBody)
|
||||
}
|
||||
|
||||
func TestNotifyMultipleListeners(t *testing.T) {
|
||||
notifier := NewNotifier("testMessageType2", nil)
|
||||
listeners := [50]chan messageEnvelope{}
|
||||
for i := 0; i < len(listeners); i++ {
|
||||
listeners[i] = notifier.listen()
|
||||
}
|
||||
|
||||
notifier.Notify()
|
||||
notifier.NotifyWithMessage(12345)
|
||||
for listener := range notifier.listeners {
|
||||
assert.Equal(t, nil, (<-listener).messageBody)
|
||||
assert.Equal(t, 12345, (<-listener).messageBody)
|
||||
}
|
||||
|
||||
// Should reap closed channels automatically.
|
||||
close(listeners[4])
|
||||
notifier.NotifyWithMessage("message1")
|
||||
assert.Equal(t, 49, len(notifier.listeners))
|
||||
for listener := range notifier.listeners {
|
||||
assert.Equal(t, "message1", (<-listener).messageBody)
|
||||
}
|
||||
close(listeners[16])
|
||||
close(listeners[21])
|
||||
close(listeners[49])
|
||||
notifier.NotifyWithMessage("message2")
|
||||
assert.Equal(t, 46, len(notifier.listeners))
|
||||
for listener := range notifier.listeners {
|
||||
assert.Equal(t, "message2", (<-listener).messageBody)
|
||||
}
|
||||
}
|
||||
|
||||
func generateTestMessage() interface{} {
|
||||
return "test message"
|
||||
}
|
||||
148
websocket/websocket.go
Normal file
148
websocket/websocket.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
//
|
||||
// Functions for the server side of handling websockets.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Wraps the Gorilla Websocket module so that we can define additional functions on it.
|
||||
type Websocket struct {
|
||||
conn *websocket.Conn
|
||||
writeMutex *sync.Mutex
|
||||
}
|
||||
|
||||
type WebsocketMessage struct {
|
||||
Type string `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
var websocketUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 2014}
|
||||
|
||||
// Upgrades the given HTTP request to a websocket connection.
|
||||
func NewWebsocket(w http.ResponseWriter, r *http.Request) (*Websocket, error) {
|
||||
conn, err := websocketUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Websocket{conn, new(sync.Mutex)}, nil
|
||||
}
|
||||
|
||||
func NewTestWebsocket(conn *websocket.Conn) *Websocket {
|
||||
return &Websocket{conn, new(sync.Mutex)}
|
||||
}
|
||||
|
||||
func (ws *Websocket) Close() error {
|
||||
return ws.conn.Close()
|
||||
}
|
||||
|
||||
func (ws *Websocket) Read() (string, interface{}, error) {
|
||||
var message WebsocketMessage
|
||||
err := ws.conn.ReadJSON(&message)
|
||||
if websocket.IsCloseError(err, websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure) {
|
||||
// This error indicates that the browser terminated the connection normally; rewrite it so that clients don't
|
||||
// log it.
|
||||
return "", nil, io.EOF
|
||||
}
|
||||
if err != nil {
|
||||
// Include the caller of this method in the error message.
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
filePathParts := strings.Split(file, "/")
|
||||
return "", nil, fmt.Errorf("[%s:%d] Websocket read error: %v", filePathParts[len(filePathParts)-1], line, err)
|
||||
}
|
||||
return message.Type, message.Data, nil
|
||||
}
|
||||
|
||||
func (ws *Websocket) ReadWithTimeout(timeout time.Duration) (string, interface{}, error) {
|
||||
type wsReadResult struct {
|
||||
messageType string
|
||||
message interface{}
|
||||
err error
|
||||
}
|
||||
readChan := make(chan wsReadResult, 1)
|
||||
go func() {
|
||||
messageType, message, err := ws.Read()
|
||||
readChan <- wsReadResult{messageType, message, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-readChan:
|
||||
return result.messageType, result.message, result.err
|
||||
case <-time.After(timeout):
|
||||
return "", nil, fmt.Errorf("Websocket read timed out after waiting for %v", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Websocket) Write(messageType string, data interface{}) error {
|
||||
ws.writeMutex.Lock()
|
||||
defer ws.writeMutex.Unlock()
|
||||
err := ws.conn.WriteJSON(WebsocketMessage{messageType, data})
|
||||
if err != nil {
|
||||
// Include the caller of this method in the error message.
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
filePathParts := strings.Split(file, "/")
|
||||
return fmt.Errorf("[%s:%d] Websocket write error: %v", filePathParts[len(filePathParts)-1], line, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *Websocket) WriteNotifier(notifier *Notifier) error {
|
||||
return ws.Write(notifier.messageType, notifier.getMessageBody())
|
||||
}
|
||||
|
||||
func (ws *Websocket) WriteError(errorMessage string) error {
|
||||
return ws.Write("error", errorMessage)
|
||||
}
|
||||
|
||||
// Creates listeners for the given notifiers and loops forever to pass their output directly through to the websocket.
|
||||
func (ws *Websocket) HandleNotifiers(notifiers ...*Notifier) {
|
||||
// Use reflection to dynamically build a select/case structure for all the notifiers.
|
||||
listeners := make([]reflect.SelectCase, len(notifiers))
|
||||
for i, notifier := range notifiers {
|
||||
listener := notifier.listen()
|
||||
defer close(listener)
|
||||
listeners[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(listener)}
|
||||
|
||||
// Send each notifier's respective data immediately upon connection to bootstrap the client state.
|
||||
if notifier.messageProducer != nil {
|
||||
err := ws.WriteNotifier(notifier)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error writing inital value for notifier %v: %v", notifier, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// Block until a message is available on any of the channels.
|
||||
chosenIndex, value, ok := reflect.Select(listeners)
|
||||
if !ok {
|
||||
log.Printf("Channel for notifier %v closed unexpectedly.", notifiers[chosenIndex])
|
||||
return
|
||||
}
|
||||
message, ok := value.Interface().(messageEnvelope)
|
||||
if !ok {
|
||||
log.Printf("Channel for notifier %v sent unexpected value %v.", notifiers[chosenIndex], value)
|
||||
continue
|
||||
}
|
||||
|
||||
// Forward the message verbatim on to the websocket.
|
||||
err := ws.Write(message.messageType, message.messageBody)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
116
websocket/websocket_test.go
Normal file
116
websocket/websocket_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright 2018 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWebsocket(t *testing.T) {
|
||||
// Set up some fake notifiers.
|
||||
notifier1 := NewNotifier("messageType1", func() interface{} { return "test message" })
|
||||
notifier2 := NewNotifier("messageType2", nil)
|
||||
changingValue := 123.45
|
||||
notifier3 := NewNotifier("messageType3", func() interface{} { return changingValue })
|
||||
|
||||
// Start up a fake server with a trivial websocket handler.
|
||||
testWebsocketHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := NewWebsocket(w, r)
|
||||
assert.Nil(t, err)
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on, in a separate goroutine.
|
||||
go ws.HandleNotifiers(notifier3, notifier2, notifier1)
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, data, err := ws.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
return
|
||||
} else {
|
||||
assert.Fail(t, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch messageType {
|
||||
case "sendMessageType1":
|
||||
ws.WriteNotifier(notifier1)
|
||||
case "sendError":
|
||||
ws.WriteError("error message")
|
||||
default:
|
||||
// Echo the commands back out.
|
||||
err = ws.Write(messageType, data)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/", testWebsocketHandler)
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
wsUrl := "ws" + server.URL[len("http"):]
|
||||
|
||||
// Create a client connection to the websocket handler on the server.
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsUrl, nil)
|
||||
assert.Nil(t, err)
|
||||
ws := NewTestWebsocket(conn)
|
||||
|
||||
// Ensure the initial messages are sent upon connection.
|
||||
assertMessage(t, ws, "messageType3", changingValue)
|
||||
assertMessage(t, ws, "messageType1", "test message")
|
||||
|
||||
// Trigger and read notifications.
|
||||
notifier2.Notify()
|
||||
assertMessage(t, ws, "messageType2", nil)
|
||||
notifier1.Notify()
|
||||
assertMessage(t, ws, "messageType1", "test message")
|
||||
notifier3.Notify()
|
||||
assertMessage(t, ws, "messageType3", changingValue)
|
||||
changingValue = 254.254
|
||||
notifier3.Notify()
|
||||
assertMessage(t, ws, "messageType3", changingValue)
|
||||
notifier1.NotifyWithMessage("test message 2")
|
||||
assertMessage(t, ws, "messageType1", "test message 2")
|
||||
notifier3.NotifyWithMessage("test message 3")
|
||||
assertMessage(t, ws, "messageType3", "test message 3")
|
||||
|
||||
// Test sending commands back.
|
||||
ws.Write("sendMessageType1", nil)
|
||||
assertMessage(t, ws, "messageType1", "test message")
|
||||
ws.Write("messageType4", "test message 4")
|
||||
assertMessage(t, ws, "messageType4", "test message 4")
|
||||
ws.Write("sendError", nil)
|
||||
assertMessage(t, ws, "error", "error message")
|
||||
|
||||
// Ensure the read times out if there is nothing to read.
|
||||
_, _, err = ws.ReadWithTimeout(time.Millisecond)
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "timed out")
|
||||
}
|
||||
|
||||
// Test that closing the connection eliminates the listeners once another message is sent.
|
||||
assert.Nil(t, ws.Close())
|
||||
time.Sleep(time.Millisecond)
|
||||
notifier1.Notify()
|
||||
time.Sleep(time.Millisecond)
|
||||
notifier1.Notify()
|
||||
assert.Equal(t, 0, len(notifier1.listeners))
|
||||
}
|
||||
|
||||
func assertMessage(t *testing.T, ws *Websocket, expectedMessageType string, expectedMessageBody interface{}) {
|
||||
messageType, messageBody, err := ws.ReadWithTimeout(time.Second)
|
||||
if assert.Nil(t, err) {
|
||||
assert.Equal(t, expectedMessageType, messageType)
|
||||
assert.Equal(t, expectedMessageBody, messageBody)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user