mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Added alliance station display.
This commit is contained in:
2
arena.go
2
arena.go
@@ -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.
|
||||
|
||||
156
displays.go
156
displays.go
@@ -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
38
setup_field.go
Normal 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)
|
||||
}
|
||||
39
static/css/alliance_station_display.css
Normal file
39
static/css/alliance_station_display.css
Normal 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;
|
||||
}
|
||||
79
static/js/alliance_station_display.js
Normal file
79
static/js/alliance_station_display.js
Normal 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); }
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
// Client-side methods for the audience display.
|
||||
|
||||
var websocket;
|
||||
var transitionMap;
|
||||
var currentScreen = "blank";
|
||||
|
||||
|
||||
32
templates/alliance_station_display.html
Normal file
32
templates/alliance_station_display.html
Normal 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>
|
||||
@@ -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 – Red</a></li>
|
||||
<li><a href="/displays/scoring/blue">Scoring – Blue</a></li>
|
||||
<li><a href="/displays/alliance_station">Alliance Station</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
31
templates/field.html
Normal file
31
templates/field.html
Normal 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
4
web.go
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user