From d8c4b92f57d6fa7eb43fe6d01028c0a8e5427ee3 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Tue, 18 Sep 2018 23:58:33 -0700 Subject: [PATCH] Add functionality to trigger a timeout and show the countdown on the audience display (fixes #51). --- field/arena.go | 53 ++++++++++- field/arena_notifiers.go | 6 ++ field/arena_test.go | 48 ++++++++++ field/driver_station_connection.go | 4 + game/match_timing.go | 3 +- static/css/alliance_station_display.css | 3 +- static/css/audience_display.css | 3 +- static/js/audience_display.js | 111 +++++++++++++++++++----- static/js/fta_display.js | 3 +- static/js/match_play.js | 32 ++++++- static/js/match_timing.js | 12 ++- templates/match_play.html | 74 +++++++++------- web/alliance_station_display.go | 15 +--- web/announcer_display.go | 15 +--- web/audience_display.go | 17 +--- web/match_play.go | 23 ++--- web/scoring_panel.go | 3 +- web/setup_led_plc.go | 3 +- 18 files changed, 314 insertions(+), 114 deletions(-) diff --git a/field/arena.go b/field/arena.go index 278527f..bacb0db 100644 --- a/field/arena.go +++ b/field/arena.go @@ -22,6 +22,7 @@ const ( arenaLoopPeriodMs = 10 dsPacketPeriodMs = 250 matchEndScoreDwellSec = 3 + postTimeoutSec = 4 ) // Progression of match states. @@ -36,6 +37,8 @@ const ( TeleopPeriod EndgamePeriod PostMatch + TimeoutActive + PostTimeout ) type Arena struct { @@ -342,11 +345,18 @@ func (arena *Arena) StartMatch() error { return err } -// Kills the current match if it is underway. +// Kills the current match or timeout if it is underway. func (arena *Arena) AbortMatch() error { - if arena.MatchState == PreMatch || arena.MatchState == PostMatch { + if arena.MatchState == PreMatch || arena.MatchState == PostMatch || arena.MatchState == PostTimeout { return fmt.Errorf("Cannot abort match when it is not in progress.") } + + if arena.MatchState == TimeoutActive { + // Handle by advancing the timeout clock to the end and letting the regular logic deal with it. + arena.MatchStartTime = time.Now().Add(-time.Second * time.Duration(game.MatchTiming.TimeoutDurationSec)) + return nil + } + if !arena.MuteMatchSounds && arena.MatchState != WarmupPeriod { arena.PlaySoundNotifier.NotifyWithMessage("match-abort") } @@ -376,6 +386,23 @@ func (arena *Arena) ResetMatch() error { return nil } +// Starts a timeout of the given duration. +func (arena *Arena) StartTimeout(durationSec int) error { + if arena.MatchState != PreMatch { + return fmt.Errorf("Cannot start timeout while there is a match still in progress or with results pending.") + } + + game.MatchTiming.TimeoutDurationSec = durationSec + arena.MatchTimingNotifier.Notify() + arena.MatchState = TimeoutActive + arena.MatchStartTime = time.Now() + arena.LastMatchTimeSec = -1 + arena.AudienceDisplayMode = "timeout" + arena.AudienceDisplayModeNotifier.Notify() + + return nil +} + // Returns the fractional number of seconds since the start of the match. func (arena *Arena) MatchTimeSec() float64 { if arena.MatchState == PreMatch || arena.MatchState == StartMatch || arena.MatchState == PostMatch { @@ -483,6 +510,21 @@ func (arena *Arena) Update() { arena.PlaySoundNotifier.NotifyWithMessage("match-end") } } + case TimeoutActive: + if matchTimeSec >= float64(game.MatchTiming.TimeoutDurationSec) { + arena.MatchState = PostTimeout + arena.PlaySoundNotifier.NotifyWithMessage("match-end") + go func() { + // Leave the timer on the screen briefly at the end of the timeout period. + time.Sleep(time.Second * matchEndScoreDwellSec) + arena.AudienceDisplayMode = "blank" + arena.AudienceDisplayModeNotifier.Notify() + }() + } + case PostTimeout: + if matchTimeSec >= float64(game.MatchTiming.TimeoutDurationSec+postTimeoutSec) { + arena.MatchState = PreMatch + } } // Send a match tick notification if passing an integer second threshold or if the match state changed. @@ -679,7 +721,8 @@ func (arena *Arena) handlePlcInput() { arena.handleEstop("B2", blueEstops[1]) arena.handleEstop("B3", blueEstops[2]) - if arena.MatchState == PreMatch || arena.MatchState == PostMatch { + if arena.MatchState == PreMatch || arena.MatchState == PostMatch || arena.MatchState == TimeoutActive || + arena.MatchState == PostTimeout { // Don't do anything if we're outside the match, otherwise we may overwrite manual edits. return } @@ -746,6 +789,10 @@ func (arena *Arena) handlePlcInput() { func (arena *Arena) handleLeds() { switch arena.MatchState { case PreMatch: + fallthrough + case TimeoutActive: + fallthrough + case PostTimeout: // Set the stack light state -- blinking green if ready, or solid alliance color(s) if not. redAllianceReady := arena.checkAllianceStationsReady("R1", "R2", "R3") == nil blueAllianceReady := arena.checkAllianceStationsReady("B1", "B2", "B3") == nil diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index a4ec9d3..13b7a6a 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -25,6 +25,7 @@ type ArenaNotifiers struct { LowerThirdNotifier *websocket.Notifier MatchLoadNotifier *websocket.Notifier MatchTimeNotifier *websocket.Notifier + MatchTimingNotifier *websocket.Notifier PlaySoundNotifier *websocket.Notifier RealtimeScoreNotifier *websocket.Notifier ReloadDisplaysNotifier *websocket.Notifier @@ -70,6 +71,7 @@ func (arena *Arena) configureNotifiers() { arena.LowerThirdNotifier = websocket.NewNotifier("lowerThird", nil) arena.MatchLoadNotifier = websocket.NewNotifier("matchLoad", arena.generateMatchLoadMessage) arena.MatchTimeNotifier = websocket.NewNotifier("matchTime", arena.generateMatchTimeMessage) + arena.MatchTimingNotifier = websocket.NewNotifier("matchTiming", arena.generateMatchTimingMessage) arena.PlaySoundNotifier = websocket.NewNotifier("playSound", nil) arena.RealtimeScoreNotifier = websocket.NewNotifier("realtimeScore", arena.generateRealtimeScoreMessage) arena.ReloadDisplaysNotifier = websocket.NewNotifier("reload", nil) @@ -135,6 +137,10 @@ func (arena *Arena) generateMatchTimeMessage() interface{} { return MatchTimeMessage{int(arena.MatchState), int(arena.MatchTimeSec())} } +func (arena *Arena) generateMatchTimingMessage() interface{} { + return &game.MatchTiming +} + func (arena *Arena) generateRealtimeScoreMessage() interface{} { fields := struct { Red *audienceAllianceScoreFields diff --git a/field/arena_test.go b/field/arena_test.go index a6fcb33..da37793 100644 --- a/field/arena_test.go +++ b/field/arena_test.go @@ -547,3 +547,51 @@ func TestAstop(t *testing.T) { assert.Equal(t, true, arena.AllianceStations["R1"].DsConn.Enabled) assert.Equal(t, false, arena.AllianceStations["R2"].DsConn.Enabled) } + +func TestArenaTimeout(t *testing.T) { + arena := setupTestArena(t) + + // Test regular ending of timeout. + timeoutDurationSec := 9 + assert.Nil(t, arena.StartTimeout(timeoutDurationSec)) + assert.Equal(t, timeoutDurationSec, game.MatchTiming.TimeoutDurationSec) + assert.Equal(t, TimeoutActive, arena.MatchState) + arena.MatchStartTime = time.Now().Add(-time.Duration(timeoutDurationSec) * time.Second) + arena.Update() + assert.Equal(t, PostTimeout, arena.MatchState) + arena.MatchStartTime = time.Now().Add(-time.Duration(timeoutDurationSec+postTimeoutSec) * time.Second) + arena.Update() + assert.Equal(t, PreMatch, arena.MatchState) + + // Test early cancellation of timeout. + timeoutDurationSec = 28 + assert.Nil(t, arena.StartTimeout(timeoutDurationSec)) + assert.Equal(t, timeoutDurationSec, game.MatchTiming.TimeoutDurationSec) + assert.Equal(t, TimeoutActive, arena.MatchState) + assert.Nil(t, arena.AbortMatch()) + arena.Update() + assert.Equal(t, PostTimeout, arena.MatchState) + arena.MatchStartTime = time.Now().Add(-time.Duration(timeoutDurationSec+postTimeoutSec) * time.Second) + arena.Update() + assert.Equal(t, PreMatch, arena.MatchState) + + // Test that timeout can't be started during a match. + arena.AllianceStations["R1"].Bypass = true + arena.AllianceStations["R2"].Bypass = true + arena.AllianceStations["R3"].Bypass = true + arena.AllianceStations["B1"].Bypass = true + arena.AllianceStations["B2"].Bypass = true + arena.AllianceStations["B3"].Bypass = true + assert.Nil(t, arena.StartMatch()) + arena.Update() + assert.NotNil(t, arena.StartTimeout(1)) + assert.NotEqual(t, TimeoutActive, arena.MatchState) + assert.Equal(t, timeoutDurationSec, game.MatchTiming.TimeoutDurationSec) + arena.MatchStartTime = time.Now().Add(-time.Duration(game.MatchTiming.WarmupDurationSec+ + game.MatchTiming.AutoDurationSec+game.MatchTiming.PauseDurationSec+game.MatchTiming.TeleopDurationSec) * + time.Second) + for arena.MatchState != PostMatch { + arena.Update() + assert.NotNil(t, arena.StartTimeout(1)) + } +} diff --git a/field/driver_station_connection.go b/field/driver_station_connection.go index 3765488..fa81198 100644 --- a/field/driver_station_connection.go +++ b/field/driver_station_connection.go @@ -220,6 +220,10 @@ func (dsConn *DriverStationConnection) encodeControlPacket(arena *Arena) [22]byt switch arena.MatchState { case PreMatch: fallthrough + case TimeoutActive: + fallthrough + case PostTimeout: + matchSecondsRemaining = game.MatchTiming.AutoDurationSec case StartMatch: fallthrough case AutoPeriod: diff --git a/game/match_timing.go b/game/match_timing.go index 5207063..33d78c7 100644 --- a/game/match_timing.go +++ b/game/match_timing.go @@ -13,7 +13,8 @@ var MatchTiming = struct { PauseDurationSec int TeleopDurationSec int EndgameTimeLeftSec int -}{3, 15, 2, 135, 30} + TimeoutDurationSec int +}{3, 15, 2, 135, 30, 0} func GetAutoEndTime(matchStartTime time.Time) time.Time { return matchStartTime.Add(time.Duration(MatchTiming.WarmupDurationSec+MatchTiming.AutoDurationSec) * time.Second) diff --git a/static/css/alliance_station_display.css b/static/css/alliance_station_display.css index 6626055..49f30b6 100644 --- a/static/css/alliance_station_display.css +++ b/static/css/alliance_station_display.css @@ -73,7 +73,8 @@ body[data-mode=fieldReset] .mode#fieldReset { #preMatch, #inMatch { display: none; } -#match[data-state=PRE_MATCH] #preMatch { +#match[data-state=PRE_MATCH] #preMatch, #match[data-state=TIMEOUT_ACTIVE] #preMatch, + #match[data-state=POST_TIMEOUT] #preMatch { display: block; } #match[data-state=WARMUP_PERIOD] #inMatch, #match[data-state=AUTO_PERIOD] #inMatch, diff --git a/static/css/audience_display.css b/static/css/audience_display.css index 24ae5a1..97aa781 100644 --- a/static/css/audience_display.css +++ b/static/css/audience_display.css @@ -28,10 +28,11 @@ html { .teams { width: 40px; height: 100%; - line-height: 26px; + line-height: 29px; text-align: center; display: table; font-family: "FuturaLT"; + font-size: 21px; } .avatars { display: none; diff --git a/static/js/audience_display.js b/static/js/audience_display.js index 58864e9..90c9937 100644 --- a/static/js/audience_display.js +++ b/static/js/audience_display.js @@ -13,6 +13,17 @@ var allianceSelectionTemplate = Handlebars.compile($("#allianceSelectionTemplate var sponsorImageTemplate = Handlebars.compile($("#sponsorImageTemplate").html()); var sponsorTextTemplate = Handlebars.compile($("#sponsorTextTemplate").html()); +// Constants for overlay positioning. The CSS is the source of truth for the values that represent initial state. +var centeringDown = $("#centering").css("bottom"); +var centeringUp = "0px"; +var logoUp = "10px"; +var logoDown = $("#logo").css("top"); +var scoreIn = $(".score").css("width"); +var scoreMid = "120px"; +var scoreOut = "275px"; +var teamsIn = $(".teams").css("width"); +var teamsOut = "65px"; + // Handles a websocket message to change which screen is displayed. var handleAudienceDisplayMode = function(targetScreen) { if (targetScreen === currentScreen) { @@ -175,11 +186,11 @@ var handleLowerThird = function(data) { }; var transitionBlankToIntro = function(callback) { - $(".avatars").show(); - $(".avatars").css("opacity", 1); - $("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() { - $(".teams").transition({queue: false, width: "65px"}, 100, "linear", function() { - $(".score").transition({queue: false, width: "120px"}, 500, "ease", function() { + $("#centering").transition({queue: false, bottom: centeringUp}, 500, "ease", function() { + $(".avatars").show(); + $(".avatars").css("opacity", 1); + $(".teams").transition({queue: false, width: teamsOut}, 100, "linear", function() { + $(".score").transition({queue: false, width: scoreMid}, 500, "ease", function() { $("#eventMatchInfo").show(); var height = -$("#eventMatchInfo").height(); $("#eventMatchInfo").transition({queue: false, bottom: height + "px"}, 500, "ease", callback); @@ -192,8 +203,8 @@ var transitionIntroToInMatch = function(callback) { $(".avatars").transition({queue: false, opacity: 0}, 500, "ease", function() { $(".avatars").hide(); }); - $("#logo").transition({queue: false, top: "10px"}, 500, "ease"); - $(".score").transition({queue: false, width: "275px"}, 500, "ease", function() { + $("#logo").transition({queue: false, top: logoUp}, 500, "ease"); + $(".score").transition({queue: false, width: scoreOut}, 500, "ease", function() { $(".score-number").transition({queue: false, opacity: 1}, 750, "ease"); $(".score-fields").transition({queue: false, opacity: 1}, 750, "ease"); $(".seesaw-indicator").transition({queue: false, opacity: 1}, 750, "ease"); @@ -204,25 +215,25 @@ var transitionIntroToInMatch = function(callback) { var transitionIntroToBlank = function(callback) { $("#eventMatchInfo").transition({queue: false, bottom: "0px"}, 500, "ease", function() { $("#eventMatchInfo").hide(); - $(".score").transition({queue: false, width: "0px"}, 500, "ease"); - $(".teams").transition({queue: false, width: "40px"}, 500, "ease", function() { + $(".score").transition({queue: false, width: scoreIn}, 500, "ease"); + $(".teams").transition({queue: false, width: teamsIn}, 500, "ease", function() { $(".avatars").css("opacity", 0); $(".avatars").hide(); - $("#centering").transition({queue: false, bottom: "-340px"}, 1000, "ease", callback); + $("#centering").transition({queue: false, bottom: centeringDown}, 1000, "ease", callback); }); }); }; var transitionBlankToInMatch = function(callback) { - $("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() { - $(".teams").transition({queue: false, width: "65px"}, 100, "linear", function() { - $("#logo").transition({queue: false, top: "10px"}, 500, "ease"); - $(".score").transition({queue: false, width: "275px"}, 500, "ease", function() { + $("#centering").transition({queue: false, bottom: centeringUp}, 500, "ease", function() { + $(".teams").transition({queue: false, width: teamsOut}, 100, "linear", function() { + $("#logo").transition({queue: false, top: logoUp}, 500, "ease"); + $(".score").transition({queue: false, width: scoreOut}, 500, "ease", function() { $("#eventMatchInfo").show(); $(".score-number").transition({queue: false, opacity: 1}, 750, "ease"); $(".score-fields").transition({queue: false, opacity: 1}, 750, "ease"); $(".seesaw-indicator").transition({queue: false, opacity: 1}, 750, "ease"); - $("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback); + $("#matchTime").transition({queue: false, opacity: 1}, 750, "ease"); var height = -$("#eventMatchInfo").height(); $("#eventMatchInfo").transition({queue: false, bottom: height + "px"}, 500, "ease", callback); }); @@ -235,9 +246,9 @@ var transitionInMatchToIntro = function(callback) { $(".score-fields").transition({queue: false, opacity: 0}, 300, "linear"); $(".seesaw-indicator").transition({queue: false, opacity: 0}, 300, "linear"); $("#matchTime").transition({queue: false, opacity: 0}, 300, "linear", function() { - $("#logo").transition({queue: false, top: "35px"}, 500, "ease"); - $(".score").transition({queue: false, width: "120px"}, 500, "ease"); - $(".teams").transition({queue: false, width: "65px"}, 500, "ease", function() { + $("#logo").transition({queue: false, top: logoDown}, 500, "ease"); + $(".score").transition({queue: false, width: scoreMid}, 500, "ease"); + $(".teams").transition({queue: false, width: teamsOut}, 500, "ease", function() { $(".avatars").show(); $(".avatars").transition({queue: false, opacity: 1}, 500, "ease", callback); }); @@ -251,10 +262,10 @@ var transitionInMatchToBlank = function(callback) { $(".seesaw-indicator").transition({queue: false, opacity: 0}, 300, "linear"); $(".score-number").transition({queue: false, opacity: 0}, 300, "linear", function() { $("#eventMatchInfo").hide(); - $("#logo").transition({queue: false, top: "35px"}, 500, "ease"); - $(".score").transition({queue: false, width: "0px"}, 500, "ease"); - $(".teams").transition({queue: false, width: "40px"}, 500, "ease", function() { - $("#centering").transition({queue: false, bottom: "-340px"}, 1000, "ease", callback); + $("#logo").transition({queue: false, top: logoDown}, 500, "ease"); + $(".score").transition({queue: false, width: scoreIn}, 500, "ease"); + $(".teams").transition({queue: false, width: teamsIn}, 500, "ease", function() { + $("#centering").transition({queue: false, bottom: centeringDown}, 1000, "ease", callback); }); }); }; @@ -384,6 +395,52 @@ var transitionSponsorToScore = function(callback) { }); }; +var transitionBlankToTimeout = function(callback) { + $("#centering").transition({queue: false, bottom: centeringUp}, 500, "ease", function () { + $("#logo").transition({queue: false, top: logoUp}, 500, "ease", function() { + $("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback); + }); + }); +}; + +var transitionIntroToTimeout = function(callback) { + $("#eventMatchInfo").transition({queue: false, bottom: "0px"}, 500, "ease", function() { + $("#eventMatchInfo").hide(); + $(".score").transition({queue: false, width: scoreIn}, 500, "ease"); + $(".teams").transition({queue: false, width: teamsIn}, 500, "ease", function() { + $(".avatars").css("opacity", 0); + $(".avatars").hide(); + $("#logo").transition({queue: false, top: logoUp}, 500, "ease", function() { + $("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback); + }); + }); + }); +}; + +var transitionTimeoutToBlank = function(callback) { + $("#matchTime").transition({queue: false, opacity: 0}, 300, "linear", function() { + $("#logo").transition({queue: false, top: logoDown}, 500, "ease", function() { + $("#centering").transition({queue: false, bottom: centeringDown}, 1000, "ease", callback); + }); + }); +}; + +var transitionTimeoutToIntro = function(callback) { + $("#matchTime").transition({queue: false, opacity: 0}, 300, "linear", function() { + $("#logo").transition({queue: false, top: logoDown}, 500, "ease", function() { + $(".avatars").show(); + $(".avatars").css("opacity", 1); + $(".teams").transition({queue: false, width: teamsOut}, 100, "linear", function () { + $(".score").transition({queue: false, width: scoreMid}, 500, "ease", function () { + $("#eventMatchInfo").show(); + var height = -$("#eventMatchInfo").height(); + $("#eventMatchInfo").transition({queue: false, bottom: height + "px"}, 500, "ease", callback); + }); + }); + }); + }); +}; + // Loads sponsor slide data and builds the slideshow HTML. var initializeSponsorDisplay = function() { $.getJSON("/api/sponsor_slides", function(slides) { @@ -472,11 +529,13 @@ $(function() { logo: transitionBlankToLogo, sponsor: transitionBlankToSponsor, allianceSelection: transitionBlankToAllianceSelection, - lowerThird: transitionBlankToLowerThird + lowerThird: transitionBlankToLowerThird, + timeout: transitionBlankToTimeout }, intro: { blank: transitionIntroToBlank, - match: transitionIntroToInMatch + match: transitionIntroToInMatch, + timeout: transitionIntroToTimeout }, match: { blank: transitionInMatchToBlank, @@ -502,6 +561,10 @@ $(function() { }, lowerThird: { blank: transitionLowerThirdToBlank + }, + timeout: { + blank: transitionTimeoutToBlank, + intro: transitionTimeoutToIntro } } }); diff --git a/static/js/fta_display.js b/static/js/fta_display.js index c989cb2..96441d8 100644 --- a/static/js/fta_display.js +++ b/static/js/fta_display.js @@ -27,7 +27,8 @@ var handleArenaStatus = function(data) { $("#status" + station + " .robot-status").text(""); } var lowBatteryThreshold = 6; - if (matchStates[data.MatchState] === "PRE_MATCH") { + if (matchStates[data.MatchState] === "PRE_MATCH" || matchStates[data.MatchState] === "TIMEOUT_ACTIVE" || + matchStates[data.MatchState] === "POST_TIMEOUT") { lowBatteryThreshold = 12; } $("#status" + station + " .battery-status").attr("data-status-ok", diff --git a/static/js/match_play.js b/static/js/match_play.js index 7277dff..10d0682 100644 --- a/static/js/match_play.js +++ b/static/js/match_play.js @@ -47,6 +47,16 @@ var setAllianceStationDisplay = function() { websocket.send("setAllianceStationDisplay", $("input[name=allianceStationDisplay]:checked").val()); }; +// Sends a websocket message to start the timeout. +var startTimeout = function() { + var duration = $("#timeoutDuration").val().split(":"); + var durationSec = parseFloat(duration[0]); + if (duration.length > 1) { + durationSec = durationSec * 60 + parseFloat(duration[1]); + } + websocket.send("startTimeout", durationSec); +}; + var confirmCommit = function(isReplay) { if (isReplay || !scoreIsReady) { // Show the appropriate message(s) in the confirmation dialog. @@ -72,7 +82,8 @@ var handleArenaStatus = function(data) { $("#status" + station + " .robot-status").text(""); } var lowBatteryThreshold = 6; - if (matchStates[data.MatchState] === "PRE_MATCH") { + if (matchStates[data.MatchState] === "PRE_MATCH" || matchStates[data.MatchState] === "TIMEOUT_ACTIVE" || + matchStates[data.MatchState] === "POST_TIMEOUT") { lowBatteryThreshold = 12; } $("#status" + station + " .battery-status").attr("data-status-ok", @@ -107,6 +118,7 @@ var handleArenaStatus = function(data) { $("#commitResults").prop("disabled", true); $("#discardResults").prop("disabled", true); $("#editResults").prop("disabled", true); + $("#startTimeout").prop("disabled", false); break; case "START_MATCH": case "AUTO_PERIOD": @@ -118,6 +130,7 @@ var handleArenaStatus = function(data) { $("#commitResults").prop("disabled", true); $("#discardResults").prop("disabled", true); $("#editResults").prop("disabled", true); + $("#startTimeout").prop("disabled", true); break; case "POST_MATCH": $("#startMatch").prop("disabled", true); @@ -125,6 +138,23 @@ var handleArenaStatus = function(data) { $("#commitResults").prop("disabled", false); $("#discardResults").prop("disabled", false); $("#editResults").prop("disabled", false); + $("#startTimeout").prop("disabled", true); + break; + case "TIMEOUT_ACTIVE": + $("#startMatch").prop("disabled", true); + $("#abortMatch").prop("disabled", false); + $("#commitResults").prop("disabled", true); + $("#discardResults").prop("disabled", true); + $("#editResults").prop("disabled", true); + $("#startTimeout").prop("disabled", true); + break; + case "POST_TIMEOUT": + $("#startMatch").prop("disabled", true); + $("#abortMatch").prop("disabled", true); + $("#commitResults").prop("disabled", true); + $("#discardResults").prop("disabled", true); + $("#editResults").prop("disabled", true); + $("#startTimeout").prop("disabled", true); break; } diff --git a/static/js/match_timing.js b/static/js/match_timing.js index a7b3ecb..a0e0ff4 100644 --- a/static/js/match_timing.js +++ b/static/js/match_timing.js @@ -11,7 +11,9 @@ var matchStates = { 4: "PAUSE_PERIOD", 5: "TELEOP_PERIOD", 6: "ENDGAME_PERIOD", - 7: "POST_MATCH" + 7: "POST_MATCH", + 8: "TIMEOUT_ACTIVE", + 9: "POST_TIMEOUT" }; var matchTiming; @@ -45,6 +47,10 @@ var translateMatchTime = function(data, callback) { case "POST_MATCH": matchStateText = "POST-MATCH"; break; + case "TIMEOUT_ACTIVE": + case "POST_TIMEOUT": + matchStateText = "TIMEOUT"; + break; } callback(matchStates[data.MatchState], matchStateText, getCountdown(data.MatchState, data.MatchTimeSec)); }; @@ -55,13 +61,15 @@ var getCountdown = function(matchState, matchTimeSec) { case "PRE_MATCH": case "START_MATCH": case "WARMUP_PERIOD": - return matchTiming.AutoDurationSec; + return matchTiming.AutoDurationSec; case "AUTO_PERIOD": return matchTiming.WarmupDurationSec + matchTiming.AutoDurationSec - matchTimeSec; case "TELEOP_PERIOD": case "ENDGAME_PERIOD": return matchTiming.WarmupDurationSec + matchTiming.AutoDurationSec + matchTiming.TeleopDurationSec + matchTiming.PauseDurationSec - matchTimeSec; + case "TIMEOUT_ACTIVE": + return matchTiming.TimeoutDurationSec - matchTimeSec; default: return 0; } diff --git a/templates/match_play.html b/templates/match_play.html index 70aaa58..8368c89 100644 --- a/templates/match_play.html +++ b/templates/match_play.html @@ -110,8 +110,22 @@
-
-
+
+
+

Scoring Status

+

Referee
+ Red Scoring
+ Blue Scoring

+
+ {{if .EventSettings.PlcAddress}} +

PLC Status

+

+
+ E-Stop +

+ {{end}} +
+
Audience Display
@@ -150,55 +164,53 @@ onclick="setAudienceDisplay();">Alliance Selection
+
+ +
-
-

Scoring Status

-

Referee
- Red Scoring
- Blue Scoring

-
-

Match Sounds

-
- -
-
-
+

Alliance Station Display

-
-

Game-Specific Data

-
- {{if .EventSettings.PlcAddress}} - PLC Status -

-
- E-Stop -

- {{end}} +
+
+

Game-Specific Data

+ +

+

Match Sounds

+
+ +
+

Timeout

+ +
diff --git a/web/alliance_station_display.go b/web/alliance_station_display.go index cd51f31..12acf3b 100644 --- a/web/alliance_station_display.go +++ b/web/alliance_station_display.go @@ -6,10 +6,8 @@ package web import ( - "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/websocket" - "log" "net/http" ) @@ -59,15 +57,8 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r } defer ws.Close() - // 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.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier) + ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.AllianceStationDisplayModeNotifier, + web.arena.ArenaStatusNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, + web.arena.RealtimeScoreNotifier, web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier) } diff --git a/web/announcer_display.go b/web/announcer_display.go index dff0f14..1b7674d 100644 --- a/web/announcer_display.go +++ b/web/announcer_display.go @@ -6,10 +6,8 @@ package web import ( - "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/websocket" - "log" "net/http" ) @@ -59,15 +57,8 @@ func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http. } defer ws.Close() - // 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.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier, - web.arena.ScorePostedNotifier, web.arena.AudienceDisplayModeNotifier, web.arena.DisplayConfigurationNotifier, - web.arena.ReloadDisplaysNotifier) + ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, + web.arena.RealtimeScoreNotifier, web.arena.ScorePostedNotifier, web.arena.AudienceDisplayModeNotifier, + web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier) } diff --git a/web/audience_display.go b/web/audience_display.go index 1994445..f0bf5ec 100644 --- a/web/audience_display.go +++ b/web/audience_display.go @@ -6,10 +6,8 @@ package web import ( - "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/websocket" - "log" "net/http" ) @@ -59,16 +57,9 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R } defer ws.Close() - // 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.AudienceDisplayModeNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, - web.arena.RealtimeScoreNotifier, web.arena.PlaySoundNotifier, web.arena.ScorePostedNotifier, - web.arena.AllianceSelectionNotifier, web.arena.LowerThirdNotifier, web.arena.DisplayConfigurationNotifier, - web.arena.ReloadDisplaysNotifier) + ws.HandleNotifiers(web.arena.MatchTimingNotifier, 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.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier) } diff --git a/web/match_play.go b/web/match_play.go index 1c9a93c..13ad46b 100644 --- a/web/match_play.go +++ b/web/match_play.go @@ -7,7 +7,6 @@ package web import ( "fmt" - "github.com/Team254/cheesy-arena/game" "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/tournament" "github.com/Team254/cheesy-arena/websocket" @@ -167,16 +166,9 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request } defer ws.Close() - // 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, in a separate goroutine. - go ws.HandleNotifiers(web.arena.ArenaStatusNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier, - web.arena.ScoringStatusNotifier, web.arena.AudienceDisplayModeNotifier, + go ws.HandleNotifiers(web.arena.MatchTimingNotifier, 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. @@ -298,6 +290,17 @@ func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request web.arena.AllianceStationDisplayMode = screen web.arena.AllianceStationDisplayModeNotifier.Notify() continue + case "startTimeout": + durationSec, ok := data.(float64) + if !ok { + ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType)) + continue + } + err = web.arena.StartTimeout(int(durationSec)) + if err != nil { + ws.WriteError(err.Error()) + continue + } default: ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) continue diff --git a/web/scoring_panel.go b/web/scoring_panel.go index b69c810..141bceb 100644 --- a/web/scoring_panel.go +++ b/web/scoring_panel.go @@ -132,7 +132,8 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ } } case "\r": - if (web.arena.MatchState != field.PreMatch || web.arena.CurrentMatch.Type == "test") && + if (web.arena.MatchState != field.PreMatch && web.arena.MatchState != field.TimeoutActive && + web.arena.MatchState != field.PostTimeout || web.arena.CurrentMatch.Type == "test") && !(*score).AutoCommitted { (*score).AutoCommitted = true scoreChanged = true diff --git a/web/setup_led_plc.go b/web/setup_led_plc.go index 75fd7cb..003b6b8 100644 --- a/web/setup_led_plc.go +++ b/web/setup_led_plc.go @@ -76,7 +76,8 @@ func (web *Web) ledPlcWebsocketHandler(w http.ResponseWriter, r *http.Request) { switch messageType { case "setLedMode": - if web.arena.MatchState != field.PreMatch { + if web.arena.MatchState != field.PreMatch && web.arena.MatchState != field.TimeoutActive && + web.arena.MatchState != field.PostTimeout { ws.WriteError("Arena must be in pre-match state") continue }