Added scoring interface.

This commit is contained in:
Patrick Fairbank
2014-07-29 17:07:30 -07:00
parent b954dcd152
commit 16769c864b
7 changed files with 514 additions and 1 deletions

View File

@@ -42,12 +42,24 @@ type MatchTiming struct {
EndgameTimeLeftSec int
}
type RealtimeScore struct {
CurrentScore Score
CurrentCycle Cycle
AutoPreloadedBalls int
AutoCommitted bool
TeleopCommitted bool
undoAutoScores []Score
undoCycles []Cycle
}
type Arena struct {
AllianceStations map[string]*AllianceStation
MatchState int
CanStartMatch bool
matchTiming MatchTiming
currentMatch *Match
redRealtimeScore *RealtimeScore
blueRealtimeScore *RealtimeScore
matchStartTime time.Time
lastDsPacketTime time.Time
matchStateNotifier *Notifier
@@ -164,6 +176,11 @@ func (arena *Arena) LoadMatch(match *Match) error {
if err != nil {
return err
}
// Reset the realtime scores.
arena.redRealtimeScore = new(RealtimeScore)
arena.blueRealtimeScore = new(RealtimeScore)
arena.matchLoadTeamsNotifier.Notify(nil)
return nil
}

View File

@@ -7,9 +7,11 @@ package main
import (
"fmt"
"github.com/gorilla/mux"
"io"
"log"
"net/http"
"strconv"
"text/template"
)
@@ -166,3 +168,205 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
}
}
}
// Renders the scoring interface which enables input of scores in real-time.
func ScoringDisplayHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
alliance := vars["alliance"]
if alliance != "red" && alliance != "blue" {
handleWebErr(w, fmt.Errorf("Invalid alliance '%s'.", alliance))
return
}
template, err := template.ParseFiles("templates/scoring_display.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
Alliance string
}{eventSettings, alliance}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// The websocket endpoint for the scoring interface client to send control commands and receive status updates.
func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
alliance := vars["alliance"]
if alliance != "red" && alliance != "blue" {
handleWebErr(w, fmt.Errorf("Invalid alliance '%s'.", alliance))
return
}
var score **RealtimeScore
if alliance == "red" {
score = &mainArena.redRealtimeScore
} else {
score = &mainArena.blueRealtimeScore
}
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer websocket.Close()
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
defer close(matchLoadTeamsListener)
// Send the various notifications immediately upon connection.
err = websocket.Write("score", *score)
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 = "score"
message = *score
}
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 "preload":
if !(*score).AutoCommitted {
preloadedBallsStr, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
preloadedBalls, err := strconv.Atoi(preloadedBallsStr)
(*score).AutoPreloadedBalls = preloadedBalls
if err != nil {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
}
case "mobility":
if !(*score).AutoCommitted {
(*score).undoAutoScores = append((*score).undoAutoScores, (*score).CurrentScore)
(*score).CurrentScore.AutoMobilityBonuses += 1
}
case "scoredHighHot":
if !(*score).AutoCommitted {
(*score).undoAutoScores = append((*score).undoAutoScores, (*score).CurrentScore)
(*score).CurrentScore.AutoHighHot += 1
}
case "scoredHigh":
if !(*score).AutoCommitted {
(*score).undoAutoScores = append((*score).undoAutoScores, (*score).CurrentScore)
(*score).CurrentScore.AutoHigh += 1
} else if !(*score).TeleopCommitted && !(*score).CurrentCycle.ScoredHigh {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.ScoredHigh = true
(*score).CurrentCycle.ScoredLow = false
(*score).CurrentCycle.DeadBall = false
}
case "scoredLowHot":
if !(*score).AutoCommitted {
(*score).undoAutoScores = append((*score).undoAutoScores, (*score).CurrentScore)
(*score).CurrentScore.AutoLowHot += 1
}
case "scoredLow":
if !(*score).AutoCommitted {
(*score).undoAutoScores = append((*score).undoAutoScores, (*score).CurrentScore)
(*score).CurrentScore.AutoLow += 1
} else if !(*score).TeleopCommitted && !(*score).CurrentCycle.ScoredLow {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.ScoredHigh = false
(*score).CurrentCycle.ScoredLow = true
(*score).CurrentCycle.DeadBall = false
}
case "assist":
if !(*score).TeleopCommitted && (*score).CurrentCycle.Assists < 3 {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.Assists += 1
}
case "truss":
if !(*score).TeleopCommitted && !(*score).CurrentCycle.Truss {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.Truss = true
}
case "catch":
if !(*score).TeleopCommitted && !(*score).CurrentCycle.Catch && (*score).CurrentCycle.Truss {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.Catch = true
}
case "deadBall":
if !(*score).TeleopCommitted && !(*score).CurrentCycle.DeadBall {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.ScoredHigh = false
(*score).CurrentCycle.ScoredLow = false
(*score).CurrentCycle.DeadBall = true
}
case "commit":
if !(*score).AutoCommitted {
(*score).AutoCommitted = true
} else if !(*score).TeleopCommitted {
if (*score).CurrentCycle.ScoredLow || (*score).CurrentCycle.ScoredHigh ||
(*score).CurrentCycle.DeadBall {
(*score).CurrentScore.Cycles = append((*score).CurrentScore.Cycles, (*score).CurrentCycle)
(*score).CurrentCycle = Cycle{}
(*score).undoCycles = []Cycle{}
}
}
case "commitMatch":
(*score).AutoCommitted = true
(*score).TeleopCommitted = true
if (*score).CurrentCycle != (Cycle{}) {
// Commit last cycle.
(*score).CurrentScore.Cycles = append((*score).CurrentScore.Cycles, (*score).CurrentCycle)
}
case "undo":
if !(*score).AutoCommitted && len((*score).undoAutoScores) > 0 {
(*score).CurrentScore = (*score).undoAutoScores[len((*score).undoAutoScores)-1]
(*score).undoAutoScores = (*score).undoAutoScores[0 : len((*score).undoAutoScores)-1]
} else if !(*score).TeleopCommitted && len((*score).undoCycles) > 0 {
(*score).CurrentCycle = (*score).undoCycles[len((*score).undoCycles)-1]
(*score).undoCycles = (*score).undoCycles[0 : len((*score).undoCycles)-1]
}
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
continue
}
// Send out the score again after handling the command, as it most likely changed as a result.
err = websocket.Write("score", *score)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
}
}

View File

@@ -60,3 +60,14 @@
.nowrap {
white-space: nowrap;
}
.scoring {
font-size: 20px;
font-weight: bold;
color: #333;
}
.scoring-comment {
font-size: 20px;
}
.scoring-message {
color: #f00;
}

View File

@@ -0,0 +1,122 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Client-side logic for the scoring interface.
var websocket;
var handleScore = function(data) {
// Update autonomous period values.
var score = data.CurrentScore;
$("#autoPreloadedBalls").text(data.AutoPreloadedBalls);
$("#autoMobilityBonuses").text(score.AutoMobilityBonuses);
$("#autoHighHot").text(score.AutoHighHot);
$("#autoHigh").text(score.AutoHigh);
$("#autoLowHot").text(score.AutoLowHot);
$("#autoLow").text(score.AutoLow);
var unscoredBalls = data.AutoPreloadedBalls - score.AutoHighHot - score.AutoHigh - score.AutoLowHot -
score.AutoLow;
$("#autoUnscoredBalls").text(unscoredBalls);
// Update teleoperated period current cycle values.
var cycle = data.CurrentCycle;
$("#assists").text(cycle.Assists);
$("#truss").text(cycle.Truss ? "X" : "");
$("#catch").text(cycle.Catch ? "X" : "");
$("#scoredHigh").text(cycle.ScoredHigh ? "X" : "");
$("#scoredLow").text(cycle.ScoredLow ? "X" : "");
$("#deadBall").text(cycle.DeadBall ? "X" : "");
if (cycle.ScoredHigh || cycle.ScoredLow || cycle.DeadBall) {
$("#teleopMessage").html("Press Enter to commit cycle and light pedestal.<br />This cannot be undone.");
} else {
$("#teleopMessage").text("");
}
// Update component visibility.
if (!data.AutoCommitted) {
$("#autoCommands").show();
$("#autoScore").show();
$("#teleopCommands").hide();
$("#teleopScore").hide();
$("#commitMatchScore").show();
$("#waitingMessage").hide();
} else if (!data.TeleopCommitted) {
$("#autoCommands").hide();
$("#autoScore").hide();
$("#teleopCommands").show();
$("#teleopScore").show();
$("#commitMatchScore").show();
$("#waitingMessage").hide();
} else {
$("#autoCommands").hide();
$("#autoScore").hide();
$("#teleopCommands").hide();
$("#teleopScore").hide();
$("#commitMatchScore").hide();
$("#waitingMessage").show();
}
};
var handleKeyPress = function(event) {
var key = String.fromCharCode(event.keyCode);
switch(key) {
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
websocket.send("preload", key);
break;
case "m":
websocket.send("mobility");
break;
case "H":
websocket.send("scoredHighHot");
break;
case "h":
websocket.send("scoredHigh");
break;
case "L":
websocket.send("scoredLowHot");
break;
case "l":
websocket.send("scoredLow");
break;
case "a":
websocket.send("assist");
break;
case "t":
websocket.send("truss");
break;
case "c":
websocket.send("catch");
break;
case "d":
websocket.send("deadBall");
break;
case "\r":
websocket.send("commit");
break;
case "u":
websocket.send("undo");
break;
}
};
var commitMatchScore = function() {
websocket.send("commitMatch");
};
$(function() {
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/scoring/" + alliance + "/websocket", {
score: function(event) { handleScore(event.data); }
});
$(document).keypress(handleKeyPress);
});

View File

@@ -59,7 +59,8 @@
<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 class="disabled"><a href="#">Scoring</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>
</ul>
</li>

View File

@@ -0,0 +1,156 @@
{{define "title"}}Scoring{{end}}
{{define "body"}}
<div class="row">
<div class="col-lg-12 well well-{{.Alliance}}">
<div class="text-center" id="waitingMessage" style="display: none;">
<h3>Waiting for the next match...</h3>
</div>
<div class="col-lg-6">
<div id="autoCommands" style="display: none;">
<h2>Autonomous Period</h2>
<p>Use the following keyboard shortcuts:</p>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">0-9</div>
<div class="col-lg-8 scoring-comment">Set the number of pre-loaded balls</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">m</div>
<div class="col-lg-8 scoring-comment">Add mobility bonus</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">H</div>
<div class="col-lg-8 scoring-comment">High hot goal</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">h</div>
<div class="col-lg-8 scoring-comment">High non-hot goal</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">L</div>
<div class="col-lg-8 scoring-comment">Low hot goal</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">l</div>
<div class="col-lg-8 scoring-comment">Low non-hot goal</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">Enter</div>
<div class="col-lg-8 scoring-comment">Commit autonomous score</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">u</div>
<div class="col-lg-8 scoring-comment">Undo last scoring action</div>
</div>
</div>
<div id="teleopCommands" style="display: none;">
<h2>Teleoperated Period</h2>
<p>Use the following keyboard shortcuts:</p>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">a</div>
<div class="col-lg-8 scoring-comment">Add assist</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">t</div>
<div class="col-lg-8 scoring-comment">Truss</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">c</div>
<div class="col-lg-8 scoring-comment">Catch</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">h</div>
<div class="col-lg-8 scoring-comment">High goal</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">l</div>
<div class="col-lg-8 scoring-comment">Low goal</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">d</div>
<div class="col-lg-8 scoring-comment">Dead ball</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">Enter</div>
<div class="col-lg-8 scoring-comment">Commit cycle</div>
</div>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 scoring">u</div>
<div class="col-lg-8 scoring-comment">Undo last scoring action</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div id="autoScore" style="display: none;">
<h2>Autonomous Score</h2>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Preloads</div>
<div class="col-lg-2 scoring-comment" id="autoPreloadedBalls">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Mobility</div>
<div class="col-lg-2 scoring-comment" id="autoMobilityBonuses">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">High hot</div>
<div class="col-lg-2 scoring-comment" id="autoHighHot">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">High non-hot</div>
<div class="col-lg-2 scoring-comment" id="autoHigh">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Low hot</div>
<div class="col-lg-2 scoring-comment" id="autoLowHot">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Low non-hot</div>
<div class="col-lg-2 scoring-comment" id="autoLow">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Unscored balls</div>
<div class="col-lg-2 scoring-comment" id="autoUnscoredBalls">0</div>
</div>
<h3 class="text-center scoring-message">Press Enter to commit autonomous score.<br />This cannot be undone.</h3>
</div>
<div id="teleopScore" style="display: none;">
<h2>Current Cycle</h2>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Assists</div>
<div class="col-lg-2 scoring-comment" id="assists">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Truss</div>
<div class="col-lg-2 scoring-comment" id="truss">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Catch</div>
<div class="col-lg-2 scoring-comment" id="catch">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Scored high</div>
<div class="col-lg-2 scoring-comment" id="scoredHigh">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Scored low</div>
<div class="col-lg-2 scoring-comment" id="scoredLow">0</div>
</div>
<div class="row">
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Dead ball</div>
<div class="col-lg-2 scoring-comment" id="deadBall">0</div>
</div>
<h3 class="text-center scoring-message" id="teleopMessage">Hit Enter to commit cycle</h3>
</div>
</div>
</div>
<div class="text-center col-lg-12">
<button type="button" class="btn btn-info" id="commitMatchScore" onclick="commitMatchScore();"
style="display: none;">Commit Final Match Score</button>
</div>
</div>
{{end}}
{{define "script"}}
<script>
var alliance = "{{.Alliance}}";
</script>
<script src="/static/js/scoring_display.js"></script>
{{end}}

2
web.go
View File

@@ -135,6 +135,8 @@ func newHandler() http.Handler {
router.HandleFunc("/displays/pit", PitDisplayHandler).Methods("GET")
router.HandleFunc("/displays/announcer", AnnouncerDisplayHandler).Methods("GET")
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("/api/rankings", RankingsApiHandler).Methods("GET")
router.HandleFunc("/", IndexHandler).Methods("GET")
return router