mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Add functionality to trigger a timeout and show the countdown on the audience display (fixes #51).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -110,8 +110,22 @@
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-lg-9 well">
|
||||
<div class="col-lg-4">
|
||||
<div class="col-lg-12 well">
|
||||
<div class="col-lg-3">
|
||||
<p>Scoring Status</p>
|
||||
<p><span class="label label-scoring" id="refereeScoreStatus">Referee</span><br />
|
||||
<span class="label label-scoring" id="redScoreStatus">Red Scoring</span><br />
|
||||
<span class="label label-scoring" id="blueScoreStatus">Blue Scoring</span></p>
|
||||
<br />
|
||||
{{if .EventSettings.PlcAddress}}
|
||||
<p>PLC Status</p>
|
||||
<p>
|
||||
<span class="label label-scoring" id="plcStatus"></span><br />
|
||||
<span class="label label-scoring" id="fieldEstop">E-Stop</span>
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
Audience Display
|
||||
<div class="form-group">
|
||||
<div class="radio">
|
||||
@@ -150,55 +164,53 @@
|
||||
onclick="setAudienceDisplay();">Alliance Selection
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="audienceDisplay" value="timeout" onclick="setAudienceDisplay();">Timeout
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<p>Scoring Status</p>
|
||||
<p><span class="label label-scoring" id="refereeScoreStatus">Referee</span><br />
|
||||
<span class="label label-scoring" id="redScoreStatus">Red Scoring</span><br />
|
||||
<span class="label label-scoring" id="blueScoreStatus">Blue Scoring</span></p>
|
||||
<br />
|
||||
<p>Match Sounds</p>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="muteMatchSounds">
|
||||
Mute
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="col-lg-3">
|
||||
<p>Alliance Station Display</p>
|
||||
<div class="form-group">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="allianceStationDisplay" value="blank"
|
||||
onclick="setAllianceStationDisplay();">Blank
|
||||
onclick="setAllianceStationDisplay();">Blank
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="allianceStationDisplay" value="match"
|
||||
onclick="setAllianceStationDisplay();">Match
|
||||
onclick="setAllianceStationDisplay();">Match
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="allianceStationDisplay" value="logo"
|
||||
onclick="setAllianceStationDisplay();">Logo
|
||||
onclick="setAllianceStationDisplay();">Logo
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<p>Game-Specific Data</p>
|
||||
<input type="text" id="gameSpecificData" size="10"
|
||||
{{if eq .CurrentMatchType "qualification" }}disabled{{end}} />
|
||||
</div>
|
||||
{{if .EventSettings.PlcAddress}}
|
||||
PLC Status
|
||||
<p>
|
||||
<span class="label label-scoring" id="plcStatus"></span><br />
|
||||
<span class="label label-scoring" id="fieldEstop">E-Stop</span>
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<p>Game-Specific Data</p>
|
||||
<input type="text" id="gameSpecificData" size="10"
|
||||
{{if or (eq .CurrentMatchType "qualification") (eq .CurrentMatchType "elimination") }}disabled{{end}} />
|
||||
<br /><br />
|
||||
<p>Match Sounds</p>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="muteMatchSounds">
|
||||
Mute
|
||||
</label>
|
||||
</div>
|
||||
<p>Timeout</p>
|
||||
<input type="text" id="timeoutDuration" size="4" value="6:00" />
|
||||
<button type="button" id="startTimeout" class="btn btn-info btn-xs" onclick="startTimeout();">
|
||||
Start
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user