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 *.test
*.db *.db
*.out *.out
.DS_Store

View File

@@ -217,40 +217,9 @@ func AnnouncerDisplayHandler(w http.ResponseWriter, r *http.Request) {
handleWebErr(w, err) handleWebErr(w, err)
return 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 { data := struct {
*EventSettings *EventSettings
MatchType string }{eventSettings}
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}
err = template.ExecuteTemplate(w, "base", data) err = template.ExecuteTemplate(w, "base", data)
if err != nil { if err != nil {
handleWebErr(w, err) handleWebErr(w, err)
@@ -271,10 +240,33 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
defer close(matchLoadTeamsListener) defer close(matchLoadTeamsListener)
matchTimeListener := mainArena.matchTimeNotifier.Listen() matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener) defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
defer close(realtimeScoreListener)
scorePostedListener := mainArena.scorePostedNotifier.Listen() scorePostedListener := mainArena.scorePostedNotifier.Listen()
defer close(scorePostedListener) defer close(scorePostedListener)
audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen()
defer close(audienceDisplayListener)
// Send the various notifications immediately upon connection. // 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) err = websocket.Write("matchTiming", mainArena.matchTiming)
if err != nil { if err != nil {
log.Printf("Websocket error: %s", err) log.Printf("Websocket error: %s", err)
@@ -285,6 +277,15 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Websocket error: %s", err) log.Printf("Websocket error: %s", err)
return 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. // Spin off a goroutine to listen for notifications and pass them on through the websocket.
go func() { go func() {
@@ -296,20 +297,56 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
if !ok { if !ok {
return return
} }
messageType = "reload" messageType = "setMatch"
message = nil 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: case matchTimeSec, ok := <-matchTimeListener:
if !ok { if !ok {
return return
} }
messageType = "matchTime" messageType = "matchTime"
message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)} 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: case _, ok := <-scorePostedListener:
if !ok { if !ok {
return return
} }
messageType = "reload" messageType = "setFinalScore"
message = nil 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) err = websocket.Write(messageType, message)
if err != nil { if err != nil {

View File

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

View File

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

View File

@@ -4,20 +4,47 @@
// Client-side logic for the announcer display. // Client-side logic for the announcer display.
var websocket; 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) { var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) { translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
$("#matchState").text(matchStateText); $("#matchState").text(matchStateText);
$("#matchTime").text(getCountdown(data.MatchState, data.MatchTimeSec)); $("#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) { var postMatchResult = function(data) {
clearTimeout(blinkTimeout);
$("#savedMatchResult").attr("data-blink", false); $("#savedMatchResult").attr("data-blink", false);
websocket.send("setAudienceDisplay", "score"); websocket.send("setAudienceDisplay", "score");
} }
@@ -25,12 +52,16 @@ var postMatchResult = function(data) {
$(function() { $(function() {
// Set up the websocket back to the server. // Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/announcer/websocket", { websocket = new CheesyWebsocket("/displays/announcer/websocket", {
setMatch: function(event) { handleSetMatch(event.data); },
matchTiming: function(event) { handleMatchTiming(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. // Make the score blink.
blinkTimeout = setInterval(function() { setInterval(function() {
var blinkOn = $("#savedMatchResult").attr("data-blink") == "true"; var blinkOn = $("#savedMatchResult").attr("data-blink") == "true";
$("#savedMatchResult").attr("data-blink", !blinkOn); $("#savedMatchResult").attr("data-blink", !blinkOn);
}, 500); }, 500);

View File

@@ -1,6 +1,6 @@
{{define "title"}}Announcer Display{{end}} {{define "title"}}Announcer Display{{end}}
{{define "body"}} {{define "body"}}
<h3>{{.MatchType}} Match {{.MatchDisplayName}}</h3> <h3 id="matchName"></h3>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@@ -12,14 +12,14 @@
<th class="nowrap">Rookie Year</th> <th class="nowrap">Rookie Year</th>
<th class="nowrap">Recent Accomplishments</th> <th class="nowrap">Recent Accomplishments</th>
</tr> </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> </thead>
<tbody> <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> </tbody>
</table> </table>
<div class="row"> <div class="row">
@@ -30,80 +30,86 @@
</div> </div>
<div class="row"> <div class="row">
</div> </div>
{{if .SavedMatchResult}} <div id="matchResult" class="modal" style="top: 10%;">
<div class="col-lg-8 col-lg-offset-2 well well-sm" id="savedMatchResult" style="display: none;" data-blink="off"> <div class="modal-dialog modal-large">
<div class="col-lg-8"> <div class="modal-content">
<h3>Final Results &ndash; {{.SavedMatchType}} Match {{.SavedMatchDisplayName}}</h3> <div class="modal-header" id="savedMatchResult">
</div> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<div class="col-lg-6"> <h4 class="modal-title">Final Results &ndash; <span id="scoreMatchName"></span></h4>
<div class="well well-darkred"> </div>
{{template "announcerDisplayResult" dict "fouls" .SavedMatchResult.RedFouls "summary" .RedScoreSummary}} <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> </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> </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}} {{end}}
{{define "script"}} {{define "script"}}
<script src="/static/js/match_timing.js"></script> <script src="/static/js/match_timing.js"></script>
<script src="/static/js/announcer_display.js"></script> <script src="/static/js/announcer_display.js"></script>
{{end}} {{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-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-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" /> <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.min.js"></script>
<script src="/static/js/lib/jquery.json-2.4.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> <script src="/static/js/lib/jquery.websocket-0.0.1.js"></script>