Refined alliance station display.

This commit is contained in:
Patrick Fairbank
2014-08-08 12:39:08 -07:00
parent 35d6cc7e47
commit 6f2d3f0364
9 changed files with 247 additions and 65 deletions

View File

@@ -56,30 +56,32 @@ type RealtimeScore struct {
}
type Arena struct {
AllianceStations map[string]*AllianceStation
MatchState int
CanStartMatch bool
matchTiming MatchTiming
currentMatch *Match
redRealtimeScore *RealtimeScore
blueRealtimeScore *RealtimeScore
matchStartTime time.Time
lastDsPacketTime time.Time
matchStateNotifier *Notifier
matchTimeNotifier *Notifier
robotStatusNotifier *Notifier
matchLoadTeamsNotifier *Notifier
scoringStatusNotifier *Notifier
realtimeScoreNotifier *Notifier
scorePostedNotifier *Notifier
audienceDisplayNotifier *Notifier
playSoundNotifier *Notifier
audienceDisplayScreen string
allianceStationDisplays map[string]string
lastMatchState int
lastMatchTimeSec float64
savedMatch *Match
savedMatchResult *MatchResult
AllianceStations map[string]*AllianceStation
MatchState int
CanStartMatch bool
matchTiming MatchTiming
currentMatch *Match
redRealtimeScore *RealtimeScore
blueRealtimeScore *RealtimeScore
matchStartTime time.Time
lastDsPacketTime time.Time
matchStateNotifier *Notifier
matchTimeNotifier *Notifier
robotStatusNotifier *Notifier
matchLoadTeamsNotifier *Notifier
scoringStatusNotifier *Notifier
realtimeScoreNotifier *Notifier
scorePostedNotifier *Notifier
audienceDisplayNotifier *Notifier
playSoundNotifier *Notifier
allianceStationDisplayNotifier *Notifier
audienceDisplayScreen string
allianceStationDisplays map[string]string
allianceStationDisplayScreen string
lastMatchState int
lastMatchTimeSec float64
savedMatch *Match
savedMatchResult *MatchResult
}
var mainArena Arena // Named thusly to avoid polluting the global namespace with something more generic.
@@ -108,6 +110,7 @@ func (arena *Arena) Setup() {
arena.scorePostedNotifier = NewNotifier()
arena.audienceDisplayNotifier = NewNotifier()
arena.playSoundNotifier = NewNotifier()
arena.allianceStationDisplayNotifier = NewNotifier()
// Load empty match as current.
arena.MatchState = PRE_MATCH
@@ -120,6 +123,7 @@ func (arena *Arena) Setup() {
arena.savedMatch = &Match{}
arena.savedMatchResult = &MatchResult{}
arena.allianceStationDisplays = make(map[string]string)
arena.allianceStationDisplayScreen = "blank"
}
// Loads a team into an alliance station, cleaning up the previous team there if there is one.

View File

@@ -772,9 +772,14 @@ func AllianceStationDisplayHandler(w http.ResponseWriter, r *http.Request) {
return
}
displayId := ""
if _, ok := r.URL.Query()["displayId"]; ok {
displayId = r.URL.Query()["displayId"][0]
}
data := struct {
*EventSettings
}{eventSettings}
DisplayId string
}{eventSettings, displayId}
err = template.ExecuteTemplate(w, "alliance_station_display.html", data)
if err != nil {
handleWebErr(w, err)
@@ -797,10 +802,13 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
station = ""
mainArena.allianceStationDisplays[displayId] = station
}
defer delete(mainArena.allianceStationDisplays, displayId)
allianceStationDisplayListener := mainArena.allianceStationDisplayNotifier.Listen()
defer close(allianceStationDisplayListener)
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
defer close(matchLoadTeamsListener)
robotStatusListener := mainArena.robotStatusNotifier.Listen()
defer close(robotStatusListener)
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
@@ -808,6 +816,11 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
// Send the various notifications immediately upon connection.
var data interface{}
err = websocket.Write("setAllianceStationDisplay", mainArena.allianceStationDisplayScreen)
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)
@@ -851,6 +864,12 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
var messageType string
var message interface{}
select {
case _, ok := <-allianceStationDisplayListener:
if !ok {
return
}
messageType = "setAllianceStationDisplay"
message = mainArena.allianceStationDisplayScreen
case _, ok := <-matchLoadTeamsListener:
if !ok {
return
@@ -864,6 +883,12 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
"R2": mainArena.AllianceStations["R2"].team, "R3": mainArena.AllianceStations["R3"].team,
"B1": mainArena.AllianceStations["B1"].team, "B2": mainArena.AllianceStations["B2"].team,
"B3": mainArena.AllianceStations["B3"].team}}
case _, ok := <-robotStatusListener:
if !ok {
return
}
messageType = "status"
message = mainArena
case matchTimeSec, ok := <-matchTimeListener:
if !ok {
return

View File

@@ -161,6 +161,8 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
defer close(audienceDisplayListener)
scoringStatusListener := mainArena.scoringStatusNotifier.Listen()
defer close(scoringStatusListener)
allianceStationDisplayListener := mainArena.allianceStationDisplayNotifier.Listen()
defer close(allianceStationDisplayListener)
// Send the various notifications immediately upon connection.
var data interface{}
@@ -196,6 +198,11 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("setAllianceStationDisplay", mainArena.allianceStationDisplayScreen)
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() {
@@ -232,6 +239,12 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
BlueScoreReady bool
}{mainArena.redRealtimeScore.FoulsCommitted && mainArena.blueRealtimeScore.FoulsCommitted,
mainArena.redRealtimeScore.TeleopCommitted, mainArena.blueRealtimeScore.TeleopCommitted}
case _, ok := <-allianceStationDisplayListener:
if !ok {
return
}
messageType = "setAllianceStationDisplay"
message = mainArena.allianceStationDisplayScreen
}
err = websocket.Write(messageType, message)
if err != nil {
@@ -340,6 +353,15 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
mainArena.audienceDisplayScreen = screen
mainArena.audienceDisplayNotifier.Notify(nil)
continue
case "setAllianceStationDisplay":
screen, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
mainArena.allianceStationDisplayScreen = screen
mainArena.allianceStationDisplayNotifier.Notify(nil)
continue
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
continue

View File

@@ -8,28 +8,60 @@ body {
background-color: #000;
font-family: "FuturaLTBold";
}
#match {
position: absolute;
width: 100%;
height: 100%;
}
#displayId {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto auto;
height: 1px;
line-height: 1px;
text-align: center;
color: #ff0;
font-size: 500px;
}
#teamId {
position: absolute;
top: 7%;
left: 0;
right: 0;
margin: 0 auto;
font-size: 500px;
line-height: 500px;
text-align: center;
margin: 50px 0px;
color: #fff;
}
#teamName {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
height: 200px;
line-height: 200px;
text-align: center;
font-family: "FuturaLT";
font-size: 120px;
}
[data-alliance=R] {
color: #f00;
}
[data-alliance=B] {
color: #00f;
}
.match-info {
color: #fff;
}
#matchInfo {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
height: 200px;
line-height: 200px;
text-align: center;
font-size: 190px;
color: #fff;
}
#redScore {
color: #f00;
@@ -37,3 +69,21 @@ body {
#blueScore {
color: #00f;
}
#logo {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 90%;
margin: auto auto;
}
[data-alliance=R], [data-status=R] {
background-color: #f00;
}
[data-alliance=B], [data-status=B] {
background-color: #00f;
}
[data-status=bypass] {
background-color: #999;
}

BIN
static/img/logo-black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -3,11 +3,27 @@
//
// Client-side methods for the alliance station display.
// A unique id to differentiate this station's display from its peers.
var displayId;
var allianceStation = "";
var blinkInterval;
var websocket;
var handleSetAllianceStationDisplay = function(targetScreen) {
switch (targetScreen) {
case "logo":
$("#match").hide();
$("#logo").show();
break;
case "blank":
$("#match").hide();
$("#logo").hide();
break;
case "match":
$("#match").show();
$("#logo").hide();
break;
}
};
var handleSetMatch = function(data) {
if (allianceStation != "" && data.AllianceStation == "") {
// The client knows better what display this should be; let the server know.
@@ -22,22 +38,49 @@ var handleSetMatch = function(data) {
if (team == null) {
$("#teamId").text("");
$("#teamName").text("");
$("#teamName").attr("data-alliance", "");
} else {
$("#teamId").attr("data-alliance", allianceStation[0]);
$("#teamName").attr("data-alliance", allianceStation[0]);
$("#teamId").text(data.Teams[allianceStation].Id);
$("#teamName").text(data.Teams[allianceStation].Nickname);
}
$("#displayIdRow").hide();
$("#teamIdRow").show();
$("#teamNameRow").show();
$("#displayId").hide();
$("#teamId").show();
$("#teamName").show();
} else {
// Show the display ID so that someone can assign it to a station from the configuration interface.
$("#teamId").text("");
$("#teamName").text("");
$("#displayIdRow").show();
$("#teamIdRow").hide();
$("#teamNameRow").hide();
$("#displayId").show();
$("#teamId").hide();
$("#teamName").hide();
}
};
var handleStatus = function(data) {
stationStatus = data.AllianceStations[allianceStation];
var blink = false;
if (stationStatus.Bypass) {
$("#match").attr("data-status", "bypass");
} else if (stationStatus.DsConn) {
if (!stationStatus.DsConn.DriverStationStatus.DsLinked) {
$("#match").attr("data-status", allianceStation[0]);
} else if (!stationStatus.DsConn.DriverStationStatus.RobotLinked) {
blink = true;
if (!blinkInterval) {
blinkInterval = setInterval(function() {
var status = $("#match").attr("data-status");
$("#match").attr("data-status", (status == "") ? allianceStation[0] : "");
}, 250);
}
}
} else {
$("#match").attr("data-status", "");
}
if (!blink && blinkInterval) {
clearInterval(blinkInterval);
blinkInterval = null;
}
};
@@ -51,11 +94,11 @@ var handleMatchTime = function(data) {
$("#matchTime").text(countdownString);
if (matchState == "PRE_MATCH" || matchState == "POST_MATCH") {
$("#teamNameRow").show();
$("#matchInfoRow").hide();
$("#teamName").show();
$("#matchInfo").hide();
} else {
$("#teamNameRow").hide();
$("#matchInfoRow").show();
$("#teamName").hide();
$("#matchInfo").show();
}
});
};
@@ -66,12 +109,17 @@ var handleRealtimeScore = function(data) {
};
$(function() {
displayId = Math.floor(Math.random() * 10000);
if (displayId == "") {
displayId = Math.floor(Math.random() * 10000);
window.location = "/displays/alliance_station?displayId=" + displayId;
}
$("#displayId").text(displayId);
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/alliance_station/websocket?displayId=" + displayId, {
setAllianceStationDisplay: function(event) { handleSetAllianceStationDisplay(event.data); },
setMatch: function(event) { handleSetMatch(event.data); },
status: function(event) { handleStatus(event.data); },
matchTiming: function(event) { handleMatchTiming(event.data); },
matchTime: function(event) { handleMatchTime(event.data); },
realtimeScore: function(event) { handleRealtimeScore(event.data); }

View File

@@ -34,6 +34,10 @@ var setAudienceDisplay = function() {
websocket.send("setAudienceDisplay", $("input[name=audienceDisplay]:checked").val());
};
var setAllianceStationDisplay = function() {
websocket.send("setAllianceStationDisplay", $("input[name=allianceStationDisplay]:checked").val());
};
var confirmCommit = function(isReplay) {
if (isReplay || !scoreIsReady) {
// Show the appropriate message(s) in the confirmation dialog.
@@ -118,6 +122,10 @@ var handleScoringStatus = function(data) {
$("#blueScoreStatus").attr("data-ready", data.BlueScoreReady);
};
var handleSetAllianceStationDisplay = function(data) {
$("input[name=allianceStationDisplay][value=" + data + "]").prop("checked", true);
};
$(function() {
// Activate tooltips above the status headers.
$("[data-toggle=tooltip]").tooltip({"placement": "top"});
@@ -128,6 +136,7 @@ $(function() {
matchTiming: function(event) { handleMatchTiming(event.data); },
matchTime: function(event) { handleMatchTime(event.data); },
setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); },
scoringStatus: function(event) { handleScoringStatus(event.data); }
scoringStatus: function(event) { handleScoringStatus(event.data); },
setAllianceStationDisplay: function(event) { handleSetAllianceStationDisplay(event.data); }
});
});

View File

@@ -8,20 +8,21 @@
<link rel="stylesheet" href="/static/css/alliance_station_display.css" />
</head>
<body>
<div class="row" id="displayIdRow" style="display: none;">>
<div class="col-lg-12 text-center" id="displayId">1718</div>
</div>
<div class="row" id="teamIdRow" style="display: none;">>
<div class="col-lg-12 text-center" id="teamId">254</div>
</div>
<div class="row" id="teamNameRow" style="display: none;">
<div class="col-lg-12 text-center" id="teamName">Buchanan Bird Brains</div>
</div>
<div class="row" id="matchInfoRow" style="display: none;">
<div class="col-lg-4 text-center match-info" id="redScore"></div>
<div class="col-lg-4 text-center match-info" id="matchTime"></div>
<div class="col-lg-4 text-center match-info" id="blueScore"></div>
<div id="displayId" style="display: none;"></div>
<div id="match" style="display: none;">
<div id="teamId" style="display: none;"></div>
<div id="teamName" style="display: none;"></div>
<div class="row" id="matchInfo" style="display: none;">
<div class="col-lg-4 text-center" id="redScore"></div>
<div class="col-lg-4 text-center" id="matchTime"></div>
<div class="col-lg-4 text-center" id="blueScore"></div>
</div>
</div>
<img id="logo" src="/static/img/logo-black.png" style="display: none;"/>
<script>
// A unique id to differentiate this station's display from its peers.
var displayId = "{{.DisplayId}}";
</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>

View File

@@ -97,8 +97,8 @@
</div>
<br />
<div class="row">
<div class="col-lg-6 well">
<div class="col-lg-6">
<div class="col-lg-9 well">
<div class="col-lg-4">
Audience Display
<div class="form-group">
<div class="radio">
@@ -128,12 +128,35 @@
</div>
</div>
</div>
<div class="col-lg-6">
<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>
</div>
<div class="col-lg-4">
Alliance Station Display
<div class="form-group">
<div class="radio">
<label>
<input type="radio" name="allianceStationDisplay" value="blank"
onclick="setAllianceStationDisplay();">Blank
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="allianceStationDisplay" value="match"
onclick="setAllianceStationDisplay();">Match
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="allianceStationDisplay" value="logo"
onclick="setAllianceStationDisplay();">Logo
</label>
</div>
</div>
</div>
</div>
</div>
</div>