Add functionality to trigger a timeout and show the countdown on the audience display (fixes #51).

This commit is contained in:
Patrick Fairbank
2018-09-18 23:58:33 -07:00
parent 27f0134ff7
commit d8c4b92f57
18 changed files with 314 additions and 114 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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:

View File

@@ -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)

View File

@@ -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,

View File

@@ -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;

View File

@@ -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
}
}
});

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}