Added referee display.

This commit is contained in:
Patrick Fairbank
2014-07-31 19:43:02 -07:00
parent ecd78a3c8b
commit 86ac6df4ec
8 changed files with 366 additions and 2 deletions

View File

@@ -46,8 +46,10 @@ type RealtimeScore struct {
CurrentScore Score
CurrentCycle Cycle
AutoPreloadedBalls int
Fouls []Foul
AutoCommitted bool
TeleopCommitted bool
FoulsCommitted bool
undoAutoScores []Score
undoCycles []Cycle
}

View File

@@ -8,6 +8,7 @@ package main
import (
"fmt"
"github.com/gorilla/mux"
"github.com/mitchellh/mapstructure"
"io"
"log"
"net/http"
@@ -15,6 +16,10 @@ import (
"text/template"
)
var rules = []string{"G3", "G5", "G10", "G11", "G12", "G14", "G15", "G16", "G17", "G18", "G19", "G21", "G22",
"G23", "G24", "G25", "G26", "G26-1", "G27", "G28", "G29", "G30", "G31", "G32", "G34", "G35", "G36", "G37",
"G38", "G39", "G40", "G41", "G42"}
// Renders the pit display which shows scrolling rankings.
func PitDisplayHandler(w http.ResponseWriter, r *http.Request) {
template, err := template.ParseFiles("templates/pit_display.html")
@@ -102,6 +107,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
scorePostedListener := mainArena.scorePostedNotifier.Listen()
defer close(scorePostedListener)
// Send the various notifications immediately upon connection.
err = websocket.Write("matchTiming", mainArena.matchTiming)
@@ -384,3 +390,151 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
}
}
}
// Renders the referee interface for assigning fouls.
func RefereeDisplayHandler(w http.ResponseWriter, r *http.Request) {
template := template.New("").Funcs(templateHelpers)
_, err := template.ParseFiles("templates/referee_display.html")
if err != nil {
handleWebErr(w, err)
return
}
match := mainArena.currentMatch
matchType := match.CapitalizedType()
data := struct {
*EventSettings
MatchType string
MatchDisplayName string
Red1 int
Red2 int
Red3 int
Blue1 int
Blue2 int
Blue3 int
RedFouls []Foul
BlueFouls []Foul
Rules []string
EntryEnabled bool
}{eventSettings, matchType, match.DisplayName, match.Red1, match.Red2, match.Red3, match.Blue1, match.Blue2,
match.Blue3, mainArena.redRealtimeScore.Fouls, mainArena.blueRealtimeScore.Fouls, rules,
!(mainArena.redRealtimeScore.FoulsCommitted && mainArena.blueRealtimeScore.FoulsCommitted)}
err = template.ExecuteTemplate(w, "referee_display.html", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// The websocket endpoint for the refereee interface client to send control commands and receive status updates.
func RefereeDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer websocket.Close()
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
defer close(matchLoadTeamsListener)
// 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 = "reload"
message = nil
}
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 "addFoul":
args := struct {
Alliance string
TeamId int
Rule string
IsTechnical bool
}{}
err = mapstructure.Decode(data, &args)
if err != nil {
websocket.WriteError(err.Error())
continue
}
// Add the foul to the correct alliance's list.
foul := Foul{TeamId: args.TeamId, Rule: args.Rule, IsTechnical: args.IsTechnical,
TimeInMatchSec: mainArena.MatchTimeSec()}
if args.Alliance == "red" {
mainArena.redRealtimeScore.Fouls = append(mainArena.redRealtimeScore.Fouls, foul)
} else {
mainArena.blueRealtimeScore.Fouls = append(mainArena.blueRealtimeScore.Fouls, foul)
}
case "deleteFoul":
args := struct {
Alliance string
TeamId int
Rule string
TimeInMatchSec float64
IsTechnical bool
}{}
err = mapstructure.Decode(data, &args)
if err != nil {
websocket.WriteError(err.Error())
continue
}
// Remove the foul from the correct alliance's list.
deleteFoul := Foul{TeamId: args.TeamId, Rule: args.Rule, IsTechnical: args.IsTechnical,
TimeInMatchSec: args.TimeInMatchSec}
var fouls *[]Foul
if args.Alliance == "red" {
fouls = &mainArena.redRealtimeScore.Fouls
} else {
fouls = &mainArena.blueRealtimeScore.Fouls
}
for i, foul := range *fouls {
if foul == deleteFoul {
*fouls = append((*fouls)[:i], (*fouls)[i+1:]...)
break
}
}
case "commitMatch":
mainArena.redRealtimeScore.FoulsCommitted = true
mainArena.blueRealtimeScore.FoulsCommitted = true
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
continue
}
// Force a reload of the client to render the updated foul list.
err = websocket.Write("reload", nil)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
}
}

View File

@@ -55,7 +55,7 @@ type Cycle struct {
type Foul struct {
TeamId int
Rule string
TimeInMatchSec float32
TimeInMatchSec float64
IsTechnical bool
}

View File

@@ -0,0 +1,41 @@
html {
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
}
body {
background-color: #333;
padding: 50px;
color: #fff;
}
h3, h4 {
color: #fff;
}
.btn-referee {
width: 120px;
margin: 5px 5px;
font-size: 30px;
cursor: default;
}
.btn-referee-wide {
width: 254px;
margin: 5px 5px;
font-size: 30px;
}
.btn {
cursor: default;
}
.btn-referee[data-selected="true"] {
border: 5px solid #fc0;
margin: 0px 5px;
background-color: #fc0;
}
tr {
font-size: 15px;
}
.row-red {
background-color: #cb210e;
}
.row-blue {
background-color: #028fc0;
}

View File

@@ -0,0 +1,78 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Client-side logic for the referee interface.
var websocket;
var foulAlliance;
var foulTeam;
var foulIsTech;
var foulRule;
// Handles a click on a team button.
var setFoulTeam = function(teamButton) {
foulAlliance = $(teamButton).attr("data-alliance");
foulTeam = $(teamButton).attr("data-team");
setSelections();
};
// Handles a click on the non-tech/tech foul buttons.
var setFoulIsTech = function(isTech) {
foulIsTech = isTech;
setSelections();
};
// Handles a click on a rule button.
var setFoulRule = function(ruleButton) {
foulRule = $(ruleButton).attr("data-rule");
setSelections();
};
// Sets button styles to match the selection cached in the global variables.
var setSelections = function() {
$("[data-team]").each(function(i, teamButton) {
$(teamButton).attr("data-selected", $(teamButton).attr("data-team") == foulTeam);
});
$("#foul").attr("data-selected", !foulIsTech);
$("#techFoul").attr("data-selected", foulIsTech);
$("[data-rule]").each(function(i, ruleButton) {
$(ruleButton).attr("data-selected", $(ruleButton).attr("data-rule") == foulRule);
});
$("#commit").prop("disabled", (foulTeam == "" || foulRule == ""));
};
// Resets the buttons to their default selections.
var clearFoul = function() {
foulTeam = "";
foulIsTech = false;
foulRule = "";
setSelections();
};
// Sends the foul to the server to add it to the list.
var commitFoul = function() {
websocket.send("addFoul", {Alliance: foulAlliance, TeamId: parseInt(foulTeam), Rule: foulRule,
IsTechnical: foulIsTech});
};
// Removes the foul with the given parameters from the list.
var deleteFoul = function(alliance, team, rule, timeSec, isTech) {
websocket.send("deleteFoul", {Alliance: alliance, TeamId: parseInt(team), Rule: rule,
TimeInMatchSec: timeSec, IsTechnical: isTech});
};
// Signals the scorekeeper that foul entry is complete for this match.
var commitMatch = function() {
websocket.send("commitMatch");
};
$(function() {
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/referee/websocket", {
});
clearFoul();
});

View File

@@ -58,7 +58,7 @@
<li class="disabled"><a href="#">Audience</a></li>
<li><a target="_blank" href="/displays/pit">Pit</a></li>
<li><a href="/displays/announcer">Announcer</a></li>
<li class="disabled"><a href="#">Referee</a></li>
<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 class="disabled"><a href="#">Field Monitor</a></li>

View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<title>Referee Display - {{.EventSettings.Name}} - Cheesy Arena</title>
<link rel="shortcut icon" href="/static/img/favicon32.png">
<link href="/static/css/lib/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/lib/bootstrap-colorpicker.min.css" rel="stylesheet">
<link href="/static/css/lib/bootstrap-datetimepicker.min.css" rel="stylesheet">
<link href="/static/css/cheesy-arena.css" rel="stylesheet">
<link href="/static/css/referee_display.css" rel="stylesheet">
</head>
<body>
{{if .EntryEnabled}}
<h3 style="margin-top: 0">{{.MatchType}} Match {{.MatchDisplayName}}</h3>
<div class="row">
<div class="col-lg-3">
<h4>Fouls</h4>
<table class="table">
{{range $foul := .RedFouls}}
{{template "foul" dict "foul" $foul "color" "red"}}
{{end}}
{{range $foul := .BlueFouls}}
{{template "foul" dict "foul" $foul "color" "blue"}}
{{end}}
</table>
</div>
<div class="col-lg-9">
<h4>Add/Edit Foul</h4>
<div class="row">
<a class="btn btn-lg btn-primary btn-referee" data-alliance="red" data-team="{{.Red1}}"
onclick="setFoulTeam(this);">{{.Red1}}</a>
<a class="btn btn-lg btn-primary btn-referee" data-alliance="red" data-team="{{.Red2}}"
onclick="setFoulTeam(this);">{{.Red2}}</a>
<a class="btn btn-lg btn-primary btn-referee" data-alliance="red" data-team="{{.Red3}}"
onclick="setFoulTeam(this);">{{.Red3}}</a>
<a class="btn btn-lg btn-info btn-referee" data-alliance="blue" data-team="{{.Blue1}}"
onclick="setFoulTeam(this);">{{.Blue1}}</a>
<a class="btn btn-lg btn-info btn-referee" data-alliance="blue" data-team="{{.Blue2}}"
onclick="setFoulTeam(this);">{{.Blue2}}</a>
<a class="btn btn-lg btn-info btn-referee" data-alliance="blue" data-team="{{.Blue3}}"
onclick="setFoulTeam(this);">{{.Blue3}}</a>
</div>
<div class="row">
<a class="btn btn-lg btn-default btn-referee btn-referee-wide" id="foul"
onclick="setFoulIsTech(false);">Foul</a>
<a class="btn btn-lg btn-default btn-referee btn-referee-wide" id="techFoul"
onclick="setFoulIsTech(true);">Tech Foul</a>
</div>
<div class="row">
{{range $rule := .Rules}}
<a class="btn btn-lg btn-warning btn-referee" data-rule="{{$rule}}"
onclick="setFoulRule(this);">{{$rule}}</a>
{{end}}
</div>
<div class="row text-center">
<a class="btn btn-lg btn-default btn-referee btn-referee-wide" onclick="clearFoul();">Clear</a>
<button type="button" class="btn btn-lg btn-success btn-referee btn-referee-wide" id="commit"
onclick="commitFoul();">Add Foul</button>
</div>
<div class="row text-center">
<a class="btn btn-lg btn-info btn-referee btn-referee-wide"
onclick="commitMatch();">Commit Match</a>
</div>
</div>
</div>
{{else}}
<div class="text-center">
<h3>Waiting for the next match...</h3>
</div>
{{end}}
<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/lib/bootstrap.min.js"></script>
<script src="/static/js/cheesy-websocket.js"></script>
<script src="/static/js/referee_display.js"></script>
</body>
</html>
{{define "foul"}}
<tr class="row-{{.color}}">
<td>{{.foul.TeamId}}</td>
<td>{{if .foul.IsTechnical}}Technical {{end}}Foul</td>
<td>{{.foul.Rule}}</td>
<td><a class="btn btn-sm btn-danger" onclick="deleteFoul('{{.color}}', {{.foul.TeamId}}, '{{.foul.Rule}}',
{{.foul.TimeInMatchSec}}, {{.foul.IsTechnical}});">Delete</a></td>
</tr>
{{end}}

2
web.go
View File

@@ -138,6 +138,8 @@ func newHandler() http.Handler {
router.HandleFunc("/displays/announcer/websocket", AnnouncerDisplayWebsocketHandler).Methods("GET")
router.HandleFunc("/displays/scoring/{alliance}", ScoringDisplayHandler).Methods("GET")
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("/api/matches/{type}", MatchesApiHandler).Methods("GET")
router.HandleFunc("/api/rankings", RankingsApiHandler).Methods("GET")
router.HandleFunc("/", IndexHandler).Methods("GET")