Added alliance station display.

This commit is contained in:
Patrick Fairbank
2014-08-04 00:52:46 -07:00
parent 4b8d72fe15
commit 35d6cc7e47
10 changed files with 384 additions and 0 deletions

View File

@@ -75,6 +75,7 @@ type Arena struct {
audienceDisplayNotifier *Notifier
playSoundNotifier *Notifier
audienceDisplayScreen string
allianceStationDisplays map[string]string
lastMatchState int
lastMatchTimeSec float64
savedMatch *Match
@@ -118,6 +119,7 @@ func (arena *Arena) Setup() {
arena.audienceDisplayScreen = "blank"
arena.savedMatch = &Match{}
arena.savedMatchResult = &MatchResult{}
arena.allianceStationDisplays = make(map[string]string)
}
// Loads a team into an alliance station, cleaning up the previous team there if there is one.

View File

@@ -762,3 +762,159 @@ func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
}
}
}
// Renders the team number and status display shown above each alliance station.
func AllianceStationDisplayHandler(w http.ResponseWriter, r *http.Request) {
template := template.New("").Funcs(templateHelpers)
_, err := template.ParseFiles("templates/alliance_station_display.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
}{eventSettings}
err = template.ExecuteTemplate(w, "alliance_station_display.html", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// The websocket endpoint for the alliance station display client to receive status updates.
func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer websocket.Close()
displayId := r.URL.Query()["displayId"][0]
station, ok := mainArena.allianceStationDisplays[displayId]
if !ok {
station = ""
mainArena.allianceStationDisplays[displayId] = station
}
defer delete(mainArena.allianceStationDisplays, displayId)
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
defer close(matchLoadTeamsListener)
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
defer close(realtimeScoreListener)
// Send the various notifications immediately upon connection.
var data interface{}
err = websocket.Write("matchTiming", mainArena.matchTiming)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)})
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
data = struct {
AllianceStation string
Teams map[string]*Team
}{station, map[string]*Team{"R1": mainArena.AllianceStations["R1"].team,
"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}}
err = websocket.Write("setMatch", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
data = struct {
RedScore int
RedCycle Cycle
BlueScore int
BlueCycle Cycle
}{mainArena.redRealtimeScore.Score(mainArena.blueRealtimeScore.Fouls),
mainArena.redRealtimeScore.CurrentCycle,
mainArena.blueRealtimeScore.Score(mainArena.redRealtimeScore.Fouls),
mainArena.blueRealtimeScore.CurrentCycle}
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() {
for {
var messageType string
var message interface{}
select {
case _, ok := <-matchLoadTeamsListener:
if !ok {
return
}
messageType = "setMatch"
station = mainArena.allianceStationDisplays[displayId]
message = struct {
AllianceStation string
Teams map[string]*Team
}{station, map[string]*Team{station: mainArena.AllianceStations["R1"].team,
"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 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
RedCycle Cycle
BlueScore int
BlueCycle Cycle
}{mainArena.redRealtimeScore.Score(mainArena.blueRealtimeScore.Fouls),
mainArena.redRealtimeScore.CurrentCycle,
mainArena.blueRealtimeScore.Score(mainArena.redRealtimeScore.Fouls),
mainArena.blueRealtimeScore.CurrentCycle}
}
err = websocket.Write(messageType, message)
if err != nil {
// The client has probably closed the connection; nothing to do here.
return
}
}
}()
// Loop, waiting for commands and responding to them, until the client closes the connection.
for {
messageType, data, err := websocket.Read()
if err != nil {
if err == io.EOF {
// Client has closed the connection; nothing to do here.
return
}
log.Printf("Websocket error: %s", err)
return
}
switch messageType {
case "setAllianceStation":
station, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
mainArena.allianceStationDisplays[displayId] = station
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
}
}
}

38
setup_field.go Normal file
View File

@@ -0,0 +1,38 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web routes for configuring the field components.
package main
import (
"html/template"
"net/http"
)
// Shows the field configuration page.
func FieldGetHandler(w http.ResponseWriter, r *http.Request) {
template, err := template.ParseFiles("templates/field.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
AllianceStationDisplays map[string]string
}{eventSettings, mainArena.allianceStationDisplays}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// Updates the display-station mapping for a single display.
func FieldPostHandler(w http.ResponseWriter, r *http.Request) {
displayId := r.PostFormValue("displayId")
allianceStation := r.PostFormValue("allianceStation")
mainArena.allianceStationDisplays[displayId] = allianceStation
mainArena.matchLoadTeamsNotifier.Notify(nil)
http.Redirect(w, r, "/setup/field", 302)
}

View File

@@ -0,0 +1,39 @@
html {
cursor: none;
-webkit-user-select: none;
-moz-user-select: none;
overflow: hidden;
}
body {
background-color: #000;
font-family: "FuturaLTBold";
}
#displayId {
color: #ff0;
font-size: 500px;
}
#teamId {
font-size: 500px;
line-height: 500px;
margin: 50px 0px;
}
#teamName {
font-family: "FuturaLT";
font-size: 120px;
}
[data-alliance=R] {
color: #f00;
}
[data-alliance=B] {
color: #00f;
}
.match-info {
color: #fff;
font-size: 190px;
}
#redScore {
color: #f00;
}
#blueScore {
color: #00f;
}

View File

@@ -0,0 +1,79 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// 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 websocket;
var handleSetMatch = function(data) {
if (allianceStation != "" && data.AllianceStation == "") {
// The client knows better what display this should be; let the server know.
websocket.send("setAllianceStation", allianceStation);
} else if (allianceStation != data.AllianceStation) {
// The server knows better what display this should be; sync up.
allianceStation = data.AllianceStation;
}
if (allianceStation != "") {
team = data.Teams[allianceStation];
if (team == null) {
$("#teamId").text("");
$("#teamName").text("");
} 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();
} 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();
}
};
var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
var countdownString = String(countdownSec % 60);
if (countdownString.length == 1) {
countdownString = "0" + countdownString;
}
countdownString = Math.floor(countdownSec / 60) + ":" + countdownString;
$("#matchTime").text(countdownString);
if (matchState == "PRE_MATCH" || matchState == "POST_MATCH") {
$("#teamNameRow").show();
$("#matchInfoRow").hide();
} else {
$("#teamNameRow").hide();
$("#matchInfoRow").show();
}
});
};
var handleRealtimeScore = function(data) {
$("#redScore").text(data.RedScore);
$("#blueScore").text(data.BlueScore);
};
$(function() {
displayId = Math.floor(Math.random() * 10000);
$("#displayId").text(displayId);
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/alliance_station/websocket?displayId=" + displayId, {
setMatch: function(event) { handleSetMatch(event.data); },
matchTiming: function(event) { handleMatchTiming(event.data); },
matchTime: function(event) { handleMatchTime(event.data); },
realtimeScore: function(event) { handleRealtimeScore(event.data); }
});
});

View File

@@ -3,6 +3,7 @@
//
// Client-side methods for the audience display.
var websocket;
var transitionMap;
var currentScreen = "blank";

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<title>Alliance Station Display - {{.EventSettings.Name}} - Cheesy Arena </title>
<link rel="shortcut icon" href="/static/img/favicon32.png">
<link rel="stylesheet" href="/static/css/lib/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/cheesy-arena.css" />
<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>
<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>
<script src="/static/js/cheesy-websocket.js"></script>
<script src="/static/js/match_timing.js"></script>
<script src="/static/js/alliance_station_display.js"></script>
</body>
</html>

View File

@@ -21,6 +21,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Setup</a>
<ul class="dropdown-menu">
<li><a href="/setup/settings">Settings</a></li>
<li><a href="/setup/field">Field Configuration</a></li>
<li><a href="/setup/teams">Team List</a></li>
<li><a href="/setup/schedule">Match Scheduling</a></li>
<li><a href="/setup/alliance_selection">Alliance Selection</a></li>
@@ -60,6 +61,7 @@
<li><a href="/displays/referee">Referee</a></li>
<li><a href="/displays/scoring/red">Scoring &ndash; Red</a></li>
<li><a href="/displays/scoring/blue">Scoring &ndash; Blue</a></li>
<li><a href="/displays/alliance_station">Alliance Station</a></li>
</ul>
</li>
</ul>

31
templates/field.html Normal file
View File

@@ -0,0 +1,31 @@
{{define "title"}}Field Configuration{{end}}
{{define "body"}}
<div class="row">
<div class="col-lg-4 col-lg-offset-4">
<div class="well">
<legend>Alliance Station Displays</legend>
{{range $displayId, $station := .AllianceStationDisplays}}
<form class="form-horizontal" action="/setup/field" method="POST">
<div class="form-group">
<label class="col-lg-5 control-label">Display {{$displayId}}</label>
<div class="col-lg-7">
<input type="hidden" name="displayId" value="{{$displayId}}" />
<select class="form-control" name="allianceStation" onchange="this.form.submit();">
<option value=""></option>
<option value="R1"{{if eq $station "R1"}} selected{{end}}>Red 1</option>
<option value="R2"{{if eq $station "R2"}} selected{{end}}>Red 2</option>
<option value="R3"{{if eq $station "R3"}} selected{{end}}>Red 3</option>
<option value="B1"{{if eq $station "B1"}} selected{{end}}>Blue 1</option>
<option value="B2"{{if eq $station "B2"}} selected{{end}}>Blue 2</option>
<option value="B3"{{if eq $station "B3"}} selected{{end}}>Blue 3</option>
</select>
</div>
</div>
</form>
{{end}}
</div>
</div>
</div>
{{end}}
{{define "script"}}
{{end}}

4
web.go
View File

@@ -120,6 +120,8 @@ func newHandler() http.Handler {
router.HandleFunc("/setup/alliance_selection/start", AllianceSelectionStartHandler).Methods("POST")
router.HandleFunc("/setup/alliance_selection/reset", AllianceSelectionResetHandler).Methods("POST")
router.HandleFunc("/setup/alliance_selection/finalize", AllianceSelectionFinalizeHandler).Methods("POST")
router.HandleFunc("/setup/field", FieldGetHandler).Methods("GET")
router.HandleFunc("/setup/field", FieldPostHandler).Methods("POST")
router.HandleFunc("/match_play", MatchPlayHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/load", MatchPlayLoadHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/show_result", MatchPlayShowResultHandler).Methods("GET")
@@ -142,6 +144,8 @@ func newHandler() http.Handler {
router.HandleFunc("/displays/scoring/{alliance}/websocket", ScoringDisplayWebsocketHandler).Methods("GET")
router.HandleFunc("/displays/referee", RefereeDisplayHandler).Methods("GET")
router.HandleFunc("/displays/referee/websocket", RefereeDisplayWebsocketHandler).Methods("GET")
router.HandleFunc("/displays/alliance_station", AllianceStationDisplayHandler).Methods("GET")
router.HandleFunc("/displays/alliance_station/websocket", AllianceStationDisplayWebsocketHandler).Methods("GET")
router.HandleFunc("/api/matches/{type}", MatchesApiHandler).Methods("GET")
router.HandleFunc("/api/rankings", RankingsApiHandler).Methods("GET")
router.HandleFunc("/", IndexHandler).Methods("GET")