Refined announcer display.

This commit is contained in:
Patrick Fairbank
2014-08-03 01:35:23 -07:00
parent 59b6da1574
commit aa9d7a06a1
7 changed files with 208 additions and 123 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ _testmain.go
*.test
*.db
*.out
.DS_Store

View File

@@ -217,40 +217,9 @@ func AnnouncerDisplayHandler(w http.ResponseWriter, r *http.Request) {
handleWebErr(w, err)
return
}
// Assemble info about the current match.
matchType := mainArena.currentMatch.CapitalizedType()
red1 := mainArena.AllianceStations["R1"].team
red2 := mainArena.AllianceStations["R2"].team
red3 := mainArena.AllianceStations["R3"].team
blue1 := mainArena.AllianceStations["B1"].team
blue2 := mainArena.AllianceStations["B2"].team
blue3 := mainArena.AllianceStations["B3"].team
// Assemble info about the saved match result.
var redScoreSummary, blueScoreSummary *ScoreSummary
var savedMatchType, savedMatchDisplayName string
savedMatchType = mainArena.savedMatch.CapitalizedType()
savedMatchDisplayName = mainArena.savedMatch.DisplayName
redScoreSummary = mainArena.savedMatchResult.RedScoreSummary()
blueScoreSummary = mainArena.savedMatchResult.BlueScoreSummary()
data := struct {
*EventSettings
MatchType string
MatchDisplayName string
Red1 *Team
Red2 *Team
Red3 *Team
Blue1 *Team
Blue2 *Team
Blue3 *Team
SavedMatchResult *MatchResult
SavedMatchType string
SavedMatchDisplayName string
RedScoreSummary *ScoreSummary
BlueScoreSummary *ScoreSummary
}{eventSettings, matchType, mainArena.currentMatch.DisplayName, red1, red2, red3, blue1, blue2, blue3,
mainArena.savedMatchResult, savedMatchType, savedMatchDisplayName, redScoreSummary, blueScoreSummary}
}{eventSettings}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
@@ -271,10 +240,33 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
defer close(matchLoadTeamsListener)
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
defer close(realtimeScoreListener)
scorePostedListener := mainArena.scorePostedNotifier.Listen()
defer close(scorePostedListener)
audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen()
defer close(audienceDisplayListener)
// Send the various notifications immediately upon connection.
var data interface{}
data = struct {
MatchType string
MatchDisplayName string
Red1 *Team
Red2 *Team
Red3 *Team
Blue1 *Team
Blue2 *Team
Blue3 *Team
}{mainArena.currentMatch.CapitalizedType(), mainArena.currentMatch.DisplayName,
mainArena.AllianceStations["R1"].team, mainArena.AllianceStations["R2"].team,
mainArena.AllianceStations["R3"].team, mainArena.AllianceStations["B1"].team,
mainArena.AllianceStations["B2"].team, mainArena.AllianceStations["B3"].team}
err = websocket.Write("setMatch", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("matchTiming", mainArena.matchTiming)
if err != nil {
log.Printf("Websocket error: %s", err)
@@ -285,6 +277,15 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Websocket error: %s", err)
return
}
data = struct {
RedScore int
BlueScore int
}{mainArena.redRealtimeScore.Score(), mainArena.blueRealtimeScore.Score()}
err = websocket.Write("realtimeScore", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
go func() {
@@ -296,20 +297,56 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
messageType = "reload"
message = nil
messageType = "setMatch"
message = struct {
MatchType string
MatchDisplayName string
Red1 *Team
Red2 *Team
Red3 *Team
Blue1 *Team
Blue2 *Team
Blue3 *Team
}{mainArena.currentMatch.CapitalizedType(), mainArena.currentMatch.DisplayName,
mainArena.AllianceStations["R1"].team, mainArena.AllianceStations["R2"].team,
mainArena.AllianceStations["R3"].team, mainArena.AllianceStations["B1"].team,
mainArena.AllianceStations["B2"].team, mainArena.AllianceStations["B3"].team}
case matchTimeSec, ok := <-matchTimeListener:
if !ok {
return
}
messageType = "matchTime"
message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)}
case _, ok := <-realtimeScoreListener:
if !ok {
return
}
messageType = "realtimeScore"
message = struct {
RedScore int
BlueScore int
}{mainArena.redRealtimeScore.Score(), mainArena.blueRealtimeScore.Score()}
case _, ok := <-scorePostedListener:
if !ok {
return
}
messageType = "reload"
message = nil
messageType = "setFinalScore"
message = struct {
MatchType string
MatchDisplayName string
RedScoreSummary *ScoreSummary
BlueScoreSummary *ScoreSummary
RedFouls []Foul
BlueFouls []Foul
}{mainArena.savedMatch.CapitalizedType(), mainArena.savedMatch.DisplayName,
mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary(),
mainArena.savedMatchResult.RedFouls, mainArena.savedMatchResult.BlueFouls}
case _, ok := <-audienceDisplayListener:
if !ok {
return
}
messageType = "setAudienceDisplay"
message = mainArena.audienceDisplayScreen
}
err = websocket.Write(messageType, message)
if err != nil {

View File

@@ -123,11 +123,13 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) {
ws := &Websocket{conn}
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "setMatch")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "realtimeScore")
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "reload")
readWebsocketType(t, ws, "setMatch")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
@@ -136,9 +138,15 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) {
mainArena.AllianceStations["B3"].Bypass = true
mainArena.StartMatch()
mainArena.Update()
readWebsocketType(t, ws, "matchTime")
messages := readWebsocketMultiple(t, ws, 2)
_, ok := messages["setAudienceDisplay"]
assert.True(t, ok)
_, ok = messages["matchTime"]
assert.True(t, ok)
mainArena.realtimeScoreNotifier.Notify(nil)
readWebsocketType(t, ws, "realtimeScore")
mainArena.scorePostedNotifier.Notify(nil)
readWebsocketType(t, ws, "reload")
readWebsocketType(t, ws, "setFinalScore")
// Test triggering the final score screen.
ws.Write("setAudienceDisplay", "score")

View File

@@ -38,6 +38,9 @@
padding-left: 0;
padding-right: 0;
}
.modal-large {
width: 60%;
}
.ds-status, .robot-status, .battery-status, .bypass-status {
background-color: #aaa;
color: #000;

View File

@@ -4,20 +4,47 @@
// Client-side logic for the announcer display.
var websocket;
var blinkTimeout;
var teamTemplate = Handlebars.compile($("#teamTemplate").html());
var matchResultTemplate = Handlebars.compile($("#matchResultTemplate").html());
var handleSetAudienceDisplay = function(targetScreen) {
// Hide the final results so that they aren't blocking the current teams when the announcer needs them most.
if (targetScreen == "intro" || targetScreen == "match") {
$("#matchResult").modal("hide");
}
};
var handleSetMatch = function(data) {
$("#matchName").text(data.MatchType + " Match " + data.MatchDisplayName);
$("#red1").html(teamTemplate(data.Red1));
$("#red2").html(teamTemplate(data.Red2));
$("#red3").html(teamTemplate(data.Red3));
$("#blue1").html(teamTemplate(data.Blue1));
$("#blue2").html(teamTemplate(data.Blue2));
$("#blue3").html(teamTemplate(data.Blue3));
};
var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
$("#matchState").text(matchStateText);
$("#matchTime").text(getCountdown(data.MatchState, data.MatchTimeSec));
if (matchState == "PRE_MATCH" || matchState == "POST_MATCH") {
$("#savedMatchResult").show();
}
});
};
var handleRealtimeScore = function(data) {
$("#redScore").text(data.RedScore);
$("#blueScore").text(data.BlueScore);
};
var handleSetFinalScore = function(data) {
console.log(data);
$("#scoreMatchName").text(data.MatchType + " Match " + data.MatchDisplayName);
$("#redScoreDetails").html(matchResultTemplate({score: data.RedScoreSummary, fouls: data.RedFouls}));
$("#blueScoreDetails").html(matchResultTemplate({score: data.BlueScoreSummary, fouls: data.BlueFouls}));
$("#matchResult").modal("show");
};
var postMatchResult = function(data) {
clearTimeout(blinkTimeout);
$("#savedMatchResult").attr("data-blink", false);
websocket.send("setAudienceDisplay", "score");
}
@@ -25,12 +52,16 @@ var postMatchResult = function(data) {
$(function() {
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/announcer/websocket", {
setMatch: function(event) { handleSetMatch(event.data); },
matchTiming: function(event) { handleMatchTiming(event.data); },
matchTime: function(event) { handleMatchTime(event.data); }
matchTime: function(event) { handleMatchTime(event.data); },
realtimeScore: function(event) { handleRealtimeScore(event.data); },
setFinalScore: function(event) { handleSetFinalScore(event.data); },
setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); }
});
// Make the score blink.
blinkTimeout = setInterval(function() {
setInterval(function() {
var blinkOn = $("#savedMatchResult").attr("data-blink") == "true";
$("#savedMatchResult").attr("data-blink", !blinkOn);
}, 500);

View File

@@ -1,6 +1,6 @@
{{define "title"}}Announcer Display{{end}}
{{define "body"}}
<h3>{{.MatchType}} Match {{.MatchDisplayName}}</h3>
<h3 id="matchName"></h3>
<table class="table">
<thead>
<tr>
@@ -12,14 +12,14 @@
<th class="nowrap">Rookie Year</th>
<th class="nowrap">Recent Accomplishments</th>
</tr>
<tr class="well-darkred">{{template "announcerDisplayTeam" .Red1}}</tr>
<tr class="well-darkred">{{template "announcerDisplayTeam" .Red2}}</tr>
<tr class="well-darkred">{{template "announcerDisplayTeam" .Red3}}</tr>
<tr class="well-darkblue">{{template "announcerDisplayTeam" .Blue1}}</tr>
<tr class="well-darkblue">{{template "announcerDisplayTeam" .Blue2}}</tr>
<tr class="well-darkblue">{{template "announcerDisplayTeam" .Blue3}}</tr>
</thead>
<tbody>
<tr class="well-darkred" id="red1"></tr>
<tr class="well-darkred" id="red2"></tr>
<tr class="well-darkred" id="red3"></tr>
<tr class="well-darkblue" id="blue1"></tr>
<tr class="well-darkblue" id="blue2"></tr>
<tr class="well-darkblue" id="blue3"></tr>
</tbody>
</table>
<div class="row">
@@ -30,80 +30,86 @@
</div>
<div class="row">
</div>
{{if .SavedMatchResult}}
<div class="col-lg-8 col-lg-offset-2 well well-sm" id="savedMatchResult" style="display: none;" data-blink="off">
<div class="col-lg-8">
<h3>Final Results &ndash; {{.SavedMatchType}} Match {{.SavedMatchDisplayName}}</h3>
</div>
<div class="col-lg-6">
<div class="well well-darkred">
{{template "announcerDisplayResult" dict "fouls" .SavedMatchResult.RedFouls "summary" .RedScoreSummary}}
<div id="matchResult" class="modal" style="top: 10%;">
<div class="modal-dialog modal-large">
<div class="modal-content">
<div class="modal-header" id="savedMatchResult">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Final Results &ndash; <span id="scoreMatchName"></span></h4>
</div>
<div class="modal-body row">
<div class="col-lg-6">
<div class="well well-darkred" id="redScoreDetails"></div>
</div>
<div class="col-lg-6">
<div class="well well-darkblue" id="blueScoreDetails"></div>
</div>
</div>
<div class="modal-footer">
<form class="form-horizontal" action="/setup/teams/clear" method="POST">
<button type="button" class="btn btn-info" onclick="postMatchResult();">
Post Score to Audience Display
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Dismiss</button>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="well well-darkblue">
{{template "announcerDisplayResult" dict "fouls" .SavedMatchResult.BlueFouls "summary" .BlueScoreSummary}}
</div>
</div>
<div class="text-center col-lg-12">
<button type="button" class="btn btn-info" onclick="postMatchResult();">Post Score to Audience Display</button>
</div>
</div>
{{end}}
<script id="teamTemplate" type="text/x-handlebars-template">
{{"{{#if this}}"}}
<td><b>{{"{{Id}}"}}</b></td>
<td class="nowrap">{{"{{Nickname}}"}}</td>
<td class="nowrap">{{"{{City}}"}}, {{"{{StateProv}}"}}, {{"{{Country}}"}}</td>
<td>{{"{{Name}}"}}</td>
<td class="nowrap">{{"{{RobotName}}"}}</td>
<td>{{"{{RookieYear}}"}}</td>
<td class="nowrap">2014 Central Valley Regional - Gracious Professionalism<br />Placeholder</td>
{{"{{else}}"}}
<td colspan="100">No team present</td>
{{"{{/if}}"}}
</script>
<script id="matchResultTemplate" type="text/x-handlebars-template">
<h4>Score</h4>
<div class="row">
<div class="col-lg-7 col-lg-offset-1 control-label">Auto Points</div>
<div class="col-lg-2">{{"{{score.AutoPoints}}"}}</div>
</div>
<div class="row">
<div class="col-lg-7 col-lg-offset-1 control-label">Teleop Points</div>
<div class="col-lg-2">{{"{{score.TeleopPoints}}"}}</div>
</div>
<div class="row">
<div class="col-lg-6 col-lg-offset-2 control-label">Assist Points</div>
<div class="col-lg-2">{{"{{score.AssistPoints}}"}}</div>
</div>
<div class="row">
<div class="col-lg-6 col-lg-offset-2 control-label">Truss/Catch Points</div>
<div class="col-lg-2">{{"{{score.TrussCatchPoints}}"}}</div>
</div>
<div class="row">
<div class="col-lg-6 col-lg-offset-2 control-label">Goal Points</div>
<div class="col-lg-2">{{"{{score.GoalPoints}}"}}</div>
</div>
<div class="row">
<div class="col-lg-7 col-lg-offset-1 control-label">Foul Points</div>
<div class="col-lg-2">{{"{{score.FoulPoints}}"}}</div>
</div>
<div class="row">
<div class="col-lg-7 col-lg-offset-1 control-label"><b>Final Score</b></div>
<div class="col-lg-2"><b>{{"{{score.Score}}"}}</b></div>
</div>
<h4>Fouls</h4>
{{"{{#each fouls}}"}}
<div class="row">
<div class="col-lg-4 col-lg-offset-1">{{"{{#if IsTechnical}}"}}Tech {{"{{/if}}"}}Foul</div>
<div class="col-lg-3">{{"{{TeamId}}"}}</div>
<div class="col-lg-3">{{"{{Rule}}"}}</div>
</div>
{{"{{/each}}"}}
</script>
{{end}}
{{define "script"}}
<script src="/static/js/match_timing.js"></script>
<script src="/static/js/announcer_display.js"></script>
{{end}}
{{define "announcerDisplayTeam"}}
{{if .}}
<td><b>{{.Id}}</b></td>
<td class="nowrap">{{.Nickname}}</td>
<td class="nowrap">{{.City}}, {{.StateProv}}, {{.Country}}</td>
<td>{{.Name}}</td>
<td class="nowrap">{{.RobotName}}</td>
<td>{{.RookieYear}}</td>
<td class="nowrap">2014 Central Valley Regional - Gracious Professionalism<br />Placeholder</td>
{{else}}
<td colspan="100">No team present</td>
{{end}}
{{end}}
{{define "announcerDisplayResult"}}
<h4>Score</h4>
<div class="row">
<div class="col-lg-7 col-lg-offset-1 control-label">Auto Points</div>
<div class="col-lg-2">{{.summary.AutoPoints}}</div>
</div>
<div class="row">
<div class="col-lg-7 col-lg-offset-1 control-label">Teleop Points</div>
<div class="col-lg-2">{{.summary.TeleopPoints}}</div>
</div>
<div class="row">
<div class="col-lg-6 col-lg-offset-2 control-label">Assist Points</div>
<div class="col-lg-2">{{.summary.AssistPoints}}</div>
</div>
<div class="row">
<div class="col-lg-6 col-lg-offset-2 control-label">Truss/Catch Points</div>
<div class="col-lg-2">{{.summary.TrussCatchPoints}}</div>
</div>
<div class="row">
<div class="col-lg-6 col-lg-offset-2 control-label">Goal Points</div>
<div class="col-lg-2">{{.summary.GoalPoints}}</div>
</div>
<div class="row">
<div class="col-lg-7 col-lg-offset-1 control-label">Foul Points</div>
<div class="col-lg-2">{{.summary.FoulPoints}}</div>
</div>
<div class="row">
<div class="col-lg-7 col-lg-offset-1 control-label"><b>Final Score</b></div>
<div class="col-lg-2"><b>{{.summary.Score}}</b></div>
</div>
<h4>Fouls</h4>
{{range $foul := .fouls}}
<div class="row">
<div class="col-lg-4 col-lg-offset-1">{{if $foul.IsTechnical}}Tech {{end}}Foul</div>
<div class="col-lg-3">{{$foul.TeamId}}</div>
<div class="col-lg-3">{{$foul.Rule}}</div>
</div>
{{end}}
{{end}}

View File

@@ -103,7 +103,6 @@
<audio id="match-abort" src="/static/audio/match_abort.mp3" type="audio/mp3" preload="auto" />
<audio id="match-resume" src="/static/audio/match_resume.wav" type="audio/wav" preload="auto" />
<audio id="match-endgame" src="/static/audio/match_endgame.wav" type="audio/wav" preload="auto" />
<script src="/static/js/lib/handlebars-1.3.0.js"></script>
<script src="/static/js/lib/jquery.min.js"></script>
<script src="/static/js/lib/jquery.json-2.4.min.js"></script>
<script src="/static/js/lib/jquery.websocket-0.0.1.js"></script>