diff --git a/arena.go b/arena.go index c8b7427..b3c7fc9 100644 --- a/arena.go +++ b/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 } diff --git a/displays.go b/displays.go index 700f84f..8f11e92 100644 --- a/displays.go +++ b/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 + } + } +} diff --git a/match_result.go b/match_result.go index 838ee7c..045bbb5 100644 --- a/match_result.go +++ b/match_result.go @@ -55,7 +55,7 @@ type Cycle struct { type Foul struct { TeamId int Rule string - TimeInMatchSec float32 + TimeInMatchSec float64 IsTechnical bool } diff --git a/static/css/referee_display.css b/static/css/referee_display.css new file mode 100644 index 0000000..83d82a3 --- /dev/null +++ b/static/css/referee_display.css @@ -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; +} diff --git a/static/js/referee_display.js b/static/js/referee_display.js new file mode 100644 index 0000000..8fef939 --- /dev/null +++ b/static/js/referee_display.js @@ -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(); +}); diff --git a/templates/base.html b/templates/base.html index 41de336..c24bf66 100644 --- a/templates/base.html +++ b/templates/base.html @@ -58,7 +58,7 @@