mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Added referee display.
This commit is contained in:
2
arena.go
2
arena.go
@@ -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
|
||||
}
|
||||
|
||||
154
displays.go
154
displays.go
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ type Cycle struct {
|
||||
type Foul struct {
|
||||
TeamId int
|
||||
Rule string
|
||||
TimeInMatchSec float32
|
||||
TimeInMatchSec float64
|
||||
IsTechnical bool
|
||||
}
|
||||
|
||||
|
||||
41
static/css/referee_display.css
Normal file
41
static/css/referee_display.css
Normal 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;
|
||||
}
|
||||
78
static/js/referee_display.js
Normal file
78
static/js/referee_display.js
Normal 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();
|
||||
});
|
||||
@@ -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 – Red</a></li>
|
||||
<li><a href="/displays/scoring/blue">Scoring – Blue</a></li>
|
||||
<li class="disabled"><a href="#">Field Monitor</a></li>
|
||||
|
||||
87
templates/referee_display.html
Normal file
87
templates/referee_display.html
Normal 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
2
web.go
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user