Broke displays out into separate files.

This commit is contained in:
Patrick Fairbank
2014-09-06 17:06:06 -07:00
parent 454316401f
commit d9badad8ca
17 changed files with 1836 additions and 1693 deletions

215
alliance_station_display.go Normal file
View File

@@ -0,0 +1,215 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web handlers for the alliance station display.
package main
import (
"fmt"
"io"
"log"
"net/http"
"text/template"
)
// 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
}
displayId := ""
if _, ok := r.URL.Query()["displayId"]; ok {
displayId = r.URL.Query()["displayId"][0]
}
data := struct {
*EventSettings
DisplayId string
}{eventSettings, displayId}
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
}
allianceStationDisplayListener := mainArena.allianceStationDisplayNotifier.Listen()
defer close(allianceStationDisplayListener)
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
defer close(matchLoadTeamsListener)
robotStatusListener := mainArena.robotStatusNotifier.Listen()
defer close(robotStatusListener)
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
defer close(realtimeScoreListener)
hotGoalLightListener := mainArena.hotGoalLightNotifier.Listen()
defer close(hotGoalLightListener)
reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen()
defer close(reloadDisplaysListener)
// Send the various notifications immediately upon connection.
var data interface{}
err = websocket.Write("setAllianceStationDisplay", mainArena.allianceStationDisplayScreen)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
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 := <-allianceStationDisplayListener:
if !ok {
return
}
websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)})
messageType = "setAllianceStationDisplay"
message = mainArena.allianceStationDisplayScreen
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 _, ok := <-robotStatusListener:
if !ok {
return
}
messageType = "status"
message = mainArena
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}
case side, ok := <-hotGoalLightListener:
if !ok {
return
}
if !eventSettings.AllianceDisplayHotGoals {
continue
}
messageType = "hotGoalLight"
message = side
case _, ok := <-reloadDisplaysListener:
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 "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))
}
}
}

View File

@@ -0,0 +1,85 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestAllianceStationDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/alliance_station")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Alliance Station Display - Untitled Event - Cheesy Arena")
}
func TestAllianceStationDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
eventSettings.AllianceDisplayHotGoals = true
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/alliance_station/websocket?displayId=1", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "setAllianceStationDisplay")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "setMatch")
readWebsocketType(t, ws, "realtimeScore")
// Change to a different screen.
mainArena.allianceStationDisplayScreen = "logo"
mainArena.allianceStationDisplayNotifier.Notify(nil)
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "setAllianceStationDisplay")
// Inform the server what display ID this is.
assert.Equal(t, "", mainArena.allianceStationDisplays["1"])
ws.Write("setAllianceStation", "R3")
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
assert.Equal(t, "R3", mainArena.allianceStationDisplays["1"])
// Run through a match cycle.
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "setMatch")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
mainArena.StartMatch()
mainArena.Update()
messages := readWebsocketMultiple(t, ws, 4)
_, ok := messages["status"]
assert.True(t, ok)
_, ok = messages["matchTime"]
assert.True(t, ok)
_, ok = messages["hotGoalLight"]
assert.True(t, ok)
mainArena.realtimeScoreNotifier.Notify(nil)
readWebsocketType(t, ws, "realtimeScore")
}

198
announcer_display.go Normal file
View File

@@ -0,0 +1,198 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web handlers for announcer display.
package main
import (
"fmt"
"io"
"log"
"net/http"
"text/template"
)
// Renders the announcer display which shows team info and scores for the current match.
func AnnouncerDisplayHandler(w http.ResponseWriter, r *http.Request) {
template := template.New("").Funcs(templateHelpers)
_, err := template.ParseFiles("templates/announcer_display.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
}{eventSettings}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// The websocket endpoint for the announcer display client to send control commands and receive status updates.
func AnnouncerDisplayWebsocketHandler(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)
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
defer close(realtimeScoreListener)
scorePostedListener := mainArena.scorePostedNotifier.Listen()
defer close(scorePostedListener)
audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen()
defer close(audienceDisplayListener)
reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen()
defer close(reloadDisplaysListener)
// Send the various notifications immediately upon connection.
var data interface{}
data = struct {
MatchType string
MatchDisplayName string
Red1 *Team
Red2 *Team
Red3 *Team
Blue1 *Team
Blue2 *Team
Blue3 *Team
}{mainArena.currentMatch.CapitalizedType(), mainArena.currentMatch.DisplayName,
mainArena.AllianceStations["R1"].team, mainArena.AllianceStations["R2"].team,
mainArena.AllianceStations["R3"].team, mainArena.AllianceStations["B1"].team,
mainArena.AllianceStations["B2"].team, mainArena.AllianceStations["B3"].team}
err = websocket.Write("setMatch", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
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 {
RedScore int
BlueScore int
}{mainArena.redRealtimeScore.Score(mainArena.blueRealtimeScore.Fouls),
mainArena.blueRealtimeScore.Score(mainArena.redRealtimeScore.Fouls)}
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"
message = struct {
MatchType string
MatchDisplayName string
Red1 *Team
Red2 *Team
Red3 *Team
Blue1 *Team
Blue2 *Team
Blue3 *Team
}{mainArena.currentMatch.CapitalizedType(), mainArena.currentMatch.DisplayName,
mainArena.AllianceStations["R1"].team, mainArena.AllianceStations["R2"].team,
mainArena.AllianceStations["R3"].team, mainArena.AllianceStations["B1"].team,
mainArena.AllianceStations["B2"].team, 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
BlueScore int
}{mainArena.redRealtimeScore.Score(mainArena.blueRealtimeScore.Fouls),
mainArena.blueRealtimeScore.Score(mainArena.redRealtimeScore.Fouls)}
case _, ok := <-scorePostedListener:
if !ok {
return
}
messageType = "setFinalScore"
message = struct {
MatchType string
MatchDisplayName string
RedScoreSummary *ScoreSummary
BlueScoreSummary *ScoreSummary
RedFouls []Foul
BlueFouls []Foul
}{mainArena.savedMatch.CapitalizedType(), mainArena.savedMatch.DisplayName,
mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary(),
mainArena.savedMatchResult.RedFouls, mainArena.savedMatchResult.BlueFouls}
case _, ok := <-audienceDisplayListener:
if !ok {
return
}
messageType = "setAudienceDisplay"
message = mainArena.audienceDisplayScreen
case _, ok := <-reloadDisplaysListener:
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 "setAudienceDisplay":
screen, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
mainArena.audienceDisplayScreen = screen
mainArena.audienceDisplayNotifier.Notify(nil)
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
}
}
}

75
announcer_display_test.go Normal file
View File

@@ -0,0 +1,75 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestAnnouncerDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/announcer")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Announcer Display - Untitled Event - Cheesy Arena")
}
func TestAnnouncerDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/announcer/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "setMatch")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "realtimeScore")
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "setMatch")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
mainArena.StartMatch()
mainArena.Update()
messages := readWebsocketMultiple(t, ws, 2)
_, ok := messages["setAudienceDisplay"]
assert.True(t, ok)
_, ok = messages["matchTime"]
assert.True(t, ok)
mainArena.realtimeScoreNotifier.Notify(nil)
readWebsocketType(t, ws, "realtimeScore")
mainArena.scorePostedNotifier.Notify(nil)
readWebsocketType(t, ws, "setFinalScore")
// Test triggering the final score screen.
ws.Write("setAudienceDisplay", "score")
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
assert.Equal(t, "score", mainArena.audienceDisplayScreen)
}

View File

@@ -68,8 +68,10 @@ func TestArenaMatchFlow(t *testing.T) {
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
db.CreateTeam(&Team{Id: 254})
eventSettings, _ = db.GetEventSettings()
mainArena = Arena{}
mainArena.Setup()
db.CreateTeam(&Team{Id: 254})
err = mainArena.AssignTeam(254, "B3")
assert.Nil(t, err)

218
audience_display.go Normal file
View File

@@ -0,0 +1,218 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web handlers for audience screen display.
package main
import (
"io"
"log"
"net/http"
"text/template"
)
// Renders the audience display to be chroma keyed over the video feed.
func AudienceDisplayHandler(w http.ResponseWriter, r *http.Request) {
template := template.New("").Funcs(templateHelpers)
_, err := template.ParseFiles("templates/audience_display.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
}{eventSettings}
err = template.ExecuteTemplate(w, "audience_display.html", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// The websocket endpoint for the audience display client to receive status updates.
func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer websocket.Close()
audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen()
defer close(audienceDisplayListener)
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
defer close(matchLoadTeamsListener)
matchTimeListener := mainArena.matchTimeNotifier.Listen()
defer close(matchTimeListener)
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
defer close(realtimeScoreListener)
scorePostedListener := mainArena.scorePostedNotifier.Listen()
defer close(scorePostedListener)
playSoundListener := mainArena.playSoundNotifier.Listen()
defer close(playSoundListener)
allianceSelectionListener := mainArena.allianceSelectionNotifier.Listen()
defer close(allianceSelectionListener)
lowerThirdListener := mainArena.lowerThirdNotifier.Listen()
defer close(lowerThirdListener)
reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen()
defer close(reloadDisplaysListener)
// 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
}
err = websocket.Write("setAudienceDisplay", mainArena.audienceDisplayScreen)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
data = struct {
Match *Match
MatchName string
}{mainArena.currentMatch, mainArena.currentMatch.CapitalizedType()}
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
}
data = struct {
Match *Match
MatchName string
RedScore *ScoreSummary
BlueScore *ScoreSummary
}{mainArena.savedMatch, mainArena.savedMatch.CapitalizedType(),
mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary()}
err = websocket.Write("setFinalScore", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
err = websocket.Write("allianceSelection", cachedAlliances)
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 := <-audienceDisplayListener:
if !ok {
return
}
messageType = "setAudienceDisplay"
message = mainArena.audienceDisplayScreen
case _, ok := <-matchLoadTeamsListener:
if !ok {
return
}
messageType = "setMatch"
message = struct {
Match *Match
MatchName string
}{mainArena.currentMatch, mainArena.currentMatch.CapitalizedType()}
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}
case _, ok := <-scorePostedListener:
if !ok {
return
}
messageType = "setFinalScore"
message = struct {
Match *Match
MatchName string
RedScore *ScoreSummary
BlueScore *ScoreSummary
}{mainArena.savedMatch, mainArena.savedMatch.CapitalizedType(),
mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary()}
case sound, ok := <-playSoundListener:
if !ok {
return
}
messageType = "playSound"
message = sound
case _, ok := <-allianceSelectionListener:
if !ok {
return
}
messageType = "allianceSelection"
message = cachedAlliances
case lowerThird, ok := <-lowerThirdListener:
if !ok {
return
}
messageType = "lowerThird"
message = lowerThird
case _, ok := <-reloadDisplaysListener:
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 {
_, _, 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
}
}
}

85
audience_display_test.go Normal file
View File

@@ -0,0 +1,85 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
)
func TestAudienceDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/audience")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Audience Display - Untitled Event - Cheesy Arena")
}
func TestAudienceDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/audience/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "setAudienceDisplay")
readWebsocketType(t, ws, "setMatch")
readWebsocketType(t, ws, "realtimeScore")
readWebsocketType(t, ws, "setFinalScore")
readWebsocketType(t, ws, "allianceSelection")
// Run through a match cycle.
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "setMatch")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
mainArena.StartMatch()
mainArena.Update()
messages := readWebsocketMultiple(t, ws, 3)
screen, ok := messages["setAudienceDisplay"]
if assert.True(t, ok) {
assert.Equal(t, "match", screen)
}
sound, ok := messages["playSound"]
if assert.True(t, ok) {
assert.Equal(t, "match-start", sound)
}
_, ok = messages["matchTime"]
assert.True(t, ok)
mainArena.realtimeScoreNotifier.Notify(nil)
readWebsocketType(t, ws, "realtimeScore")
mainArena.scorePostedNotifier.Notify(nil)
readWebsocketType(t, ws, "setFinalScore")
// Test other overlays.
mainArena.allianceSelectionNotifier.Notify(nil)
readWebsocketType(t, ws, "allianceSelection")
mainArena.lowerThirdNotifier.Notify(nil)
readWebsocketType(t, ws, "lowerThird")
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,543 +0,0 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestAudienceDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/audience")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Audience Display - Untitled Event - Cheesy Arena")
}
func TestAudienceDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/audience/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "setAudienceDisplay")
readWebsocketType(t, ws, "setMatch")
readWebsocketType(t, ws, "realtimeScore")
readWebsocketType(t, ws, "setFinalScore")
readWebsocketType(t, ws, "allianceSelection")
// Run through a match cycle.
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "setMatch")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
mainArena.StartMatch()
mainArena.Update()
messages := readWebsocketMultiple(t, ws, 3)
screen, ok := messages["setAudienceDisplay"]
if assert.True(t, ok) {
assert.Equal(t, "match", screen)
}
sound, ok := messages["playSound"]
if assert.True(t, ok) {
assert.Equal(t, "match-start", sound)
}
_, ok = messages["matchTime"]
assert.True(t, ok)
mainArena.realtimeScoreNotifier.Notify(nil)
readWebsocketType(t, ws, "realtimeScore")
mainArena.scorePostedNotifier.Notify(nil)
readWebsocketType(t, ws, "setFinalScore")
// Test other overlays.
mainArena.allianceSelectionNotifier.Notify(nil)
readWebsocketType(t, ws, "allianceSelection")
mainArena.lowerThirdNotifier.Notify(nil)
readWebsocketType(t, ws, "lowerThird")
}
func TestPitDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
recorder := getHttpResponse("/displays/pit")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Pit Display - Untitled Event - Cheesy Arena")
}
func TestPitDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/pit/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Check forced reloading as that is the only purpose the pit websocket serves.
recorder := getHttpResponse("/setup/field/reload_displays")
assert.Equal(t, 302, recorder.Code)
readWebsocketType(t, ws, "reload")
}
func TestAnnouncerDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/announcer")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Announcer Display - Untitled Event - Cheesy Arena")
}
func TestAnnouncerDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/announcer/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "setMatch")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "realtimeScore")
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "setMatch")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
mainArena.StartMatch()
mainArena.Update()
messages := readWebsocketMultiple(t, ws, 2)
_, ok := messages["setAudienceDisplay"]
assert.True(t, ok)
_, ok = messages["matchTime"]
assert.True(t, ok)
mainArena.realtimeScoreNotifier.Notify(nil)
readWebsocketType(t, ws, "realtimeScore")
mainArena.scorePostedNotifier.Notify(nil)
readWebsocketType(t, ws, "setFinalScore")
// Test triggering the final score screen.
ws.Write("setAudienceDisplay", "score")
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
assert.Equal(t, "score", mainArena.audienceDisplayScreen)
}
func TestScoringDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/scoring/invalidalliance")
assert.Equal(t, 500, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Invalid alliance")
recorder = getHttpResponse("/displays/scoring/red")
assert.Equal(t, 200, recorder.Code)
recorder = getHttpResponse("/displays/scoring/blue")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Scoring - Untitled Event - Cheesy Arena")
}
func TestScoringDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
_, _, err = websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blorpy/websocket", nil)
assert.NotNil(t, err)
redConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/red/websocket", nil)
assert.Nil(t, err)
defer redConn.Close()
redWs := &Websocket{redConn}
blueConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blue/websocket", nil)
assert.Nil(t, err)
defer blueConn.Close()
blueWs := &Websocket{blueConn}
// Should a score update right after connection.
readWebsocketType(t, redWs, "score")
readWebsocketType(t, blueWs, "score")
// Send a match worth of scoring commands in.
redWs.Write("preload", "3")
blueWs.Write("preload", "3")
redWs.Write("mobility", nil)
blueWs.Write("mobility", nil)
blueWs.Write("mobility", nil)
blueWs.Write("mobility", nil)
blueWs.Write("scoredHighHot", nil)
blueWs.Write("scoredHigh", nil)
blueWs.Write("scoredLowHot", nil)
blueWs.Write("scoredLow", nil)
blueWs.Write("undo", nil)
redWs.Write("commit", nil)
blueWs.Write("commit", nil)
redWs.Write("deadBall", nil)
redWs.Write("commit", nil)
redWs.Write("scoredLow", nil)
redWs.Write("commit", nil)
redWs.Write("scoredHigh", nil)
redWs.Write("commit", nil)
redWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("scoredLow", nil)
blueWs.Write("scoredHigh", nil)
blueWs.Write("commit", nil)
blueWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("truss", nil)
blueWs.Write("catch", nil)
blueWs.Write("undo", nil)
blueWs.Write("scoredLow", nil)
blueWs.Write("commit", nil)
mainArena.MatchState = POST_MATCH
redWs.Write("commitMatch", nil)
for i := 0; i < 11; i++ {
readWebsocketType(t, redWs, "score")
}
for i := 0; i < 24; i++ {
readWebsocketType(t, blueWs, "score")
}
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoMobilityBonuses)
assert.Equal(t, 0, mainArena.redRealtimeScore.CurrentScore.AutoHighHot)
assert.Equal(t, 0, mainArena.redRealtimeScore.CurrentScore.AutoHigh)
assert.Equal(t, 0, mainArena.redRealtimeScore.CurrentScore.AutoLowHot)
assert.Equal(t, 0, mainArena.redRealtimeScore.CurrentScore.AutoLow)
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoClearHigh)
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoClearLow)
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoClearDead)
if assert.Equal(t, 1, len(mainArena.redRealtimeScore.CurrentScore.Cycles)) {
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.Cycles[0].Assists)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].Truss)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].Catch)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].ScoredHigh)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].ScoredLow)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].DeadBall)
}
assert.True(t, mainArena.redRealtimeScore.AutoCommitted)
assert.True(t, mainArena.redRealtimeScore.TeleopCommitted)
assert.Equal(t, 3, mainArena.blueRealtimeScore.CurrentScore.AutoMobilityBonuses)
assert.Equal(t, 1, mainArena.blueRealtimeScore.CurrentScore.AutoHighHot)
assert.Equal(t, 1, mainArena.blueRealtimeScore.CurrentScore.AutoHigh)
assert.Equal(t, 1, mainArena.blueRealtimeScore.CurrentScore.AutoLowHot)
assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.AutoLow)
assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.AutoClearHigh)
assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.AutoClearLow)
assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.AutoClearDead)
if assert.Equal(t, 2, len(mainArena.blueRealtimeScore.CurrentScore.Cycles)) {
assert.Equal(t, 3, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].Assists)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].Truss)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].Catch)
assert.True(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].ScoredHigh)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].ScoredLow)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].DeadBall)
assert.Equal(t, 2, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].Assists)
assert.True(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].Truss)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].Catch)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].ScoredHigh)
assert.True(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].ScoredLow)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].DeadBall)
}
assert.True(t, mainArena.blueRealtimeScore.AutoCommitted)
assert.False(t, mainArena.blueRealtimeScore.TeleopCommitted)
// Load another match to reset the results.
mainArena.ResetMatch()
mainArena.LoadTestMatch()
readWebsocketType(t, redWs, "score")
readWebsocketType(t, blueWs, "score")
assert.Equal(t, *NewRealtimeScore(), *mainArena.redRealtimeScore)
assert.Equal(t, *NewRealtimeScore(), *mainArena.blueRealtimeScore)
}
func TestRefereeDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/referee")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Referee Display - Untitled Event - Cheesy Arena")
}
func TestRefereeDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/referee/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Test foul addition.
foulData := struct {
Alliance string
TeamId int
Rule string
TimeInMatchSec float64
IsTechnical bool
}{"red", 256, "G22", 0, false}
ws.Write("addFoul", foulData)
foulData.TeamId = 359
foulData.IsTechnical = true
ws.Write("addFoul", foulData)
foulData.Alliance = "blue"
foulData.TeamId = 1680
ws.Write("addFoul", foulData)
readWebsocketType(t, ws, "reload")
readWebsocketType(t, ws, "reload")
readWebsocketType(t, ws, "reload")
if assert.Equal(t, 2, len(mainArena.redRealtimeScore.Fouls)) {
assert.Equal(t, 256, mainArena.redRealtimeScore.Fouls[0].TeamId)
assert.Equal(t, "G22", mainArena.redRealtimeScore.Fouls[0].Rule)
assert.Equal(t, 0, mainArena.redRealtimeScore.Fouls[0].TimeInMatchSec)
assert.Equal(t, false, mainArena.redRealtimeScore.Fouls[0].IsTechnical)
assert.Equal(t, 359, mainArena.redRealtimeScore.Fouls[1].TeamId)
assert.Equal(t, "G22", mainArena.redRealtimeScore.Fouls[1].Rule)
assert.Equal(t, true, mainArena.redRealtimeScore.Fouls[1].IsTechnical)
}
if assert.Equal(t, 1, len(mainArena.blueRealtimeScore.Fouls)) {
assert.Equal(t, 1680, mainArena.blueRealtimeScore.Fouls[0].TeamId)
assert.Equal(t, "G22", mainArena.blueRealtimeScore.Fouls[0].Rule)
assert.Equal(t, 0, mainArena.blueRealtimeScore.Fouls[0].TimeInMatchSec)
assert.Equal(t, true, mainArena.blueRealtimeScore.Fouls[0].IsTechnical)
}
assert.False(t, mainArena.redRealtimeScore.FoulsCommitted)
assert.False(t, mainArena.blueRealtimeScore.FoulsCommitted)
// Test foul deletion.
ws.Write("deleteFoul", foulData)
readWebsocketType(t, ws, "reload")
assert.Equal(t, 0, len(mainArena.blueRealtimeScore.Fouls))
foulData.Alliance = "red"
foulData.TeamId = 359
foulData.TimeInMatchSec = 29 // Make it not match.
ws.Write("deleteFoul", foulData)
readWebsocketType(t, ws, "reload")
assert.Equal(t, 2, len(mainArena.redRealtimeScore.Fouls))
foulData.TimeInMatchSec = 0
ws.Write("deleteFoul", foulData)
readWebsocketType(t, ws, "reload")
assert.Equal(t, 1, len(mainArena.redRealtimeScore.Fouls))
// Test card setting.
cardData := struct {
Alliance string
TeamId int
Card string
}{"red", 256, "yellow"}
ws.Write("card", cardData)
cardData.Alliance = "blue"
cardData.TeamId = 1680
cardData.Card = "red"
ws.Write("card", cardData)
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
if assert.Equal(t, 1, len(mainArena.redRealtimeScore.Cards)) {
assert.Equal(t, "yellow", mainArena.redRealtimeScore.Cards["256"])
}
if assert.Equal(t, 1, len(mainArena.blueRealtimeScore.Cards)) {
assert.Equal(t, "red", mainArena.blueRealtimeScore.Cards["1680"])
}
// Test field reset and match committing.
mainArena.MatchState = POST_MATCH
ws.Write("signalReset", nil)
time.Sleep(time.Millisecond * 10)
assert.True(t, mainArena.redRealtimeScore.FieldReset)
assert.True(t, mainArena.blueRealtimeScore.FieldReset)
assert.False(t, mainArena.redRealtimeScore.FoulsCommitted)
assert.False(t, mainArena.blueRealtimeScore.FoulsCommitted)
mainArena.redRealtimeScore.FieldReset = false
mainArena.blueRealtimeScore.FieldReset = false
ws.Write("commitMatch", nil)
readWebsocketType(t, ws, "reload")
assert.True(t, mainArena.redRealtimeScore.FieldReset)
assert.True(t, mainArena.blueRealtimeScore.FieldReset)
assert.True(t, mainArena.redRealtimeScore.FoulsCommitted)
assert.True(t, mainArena.blueRealtimeScore.FoulsCommitted)
// Should refresh the page when the next match is loaded.
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "reload")
}
func TestAllianceStationDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/alliance_station")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Alliance Station Display - Untitled Event - Cheesy Arena")
}
func TestAllianceStationDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
eventSettings.AllianceDisplayHotGoals = true
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/alliance_station/websocket?displayId=1", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "setAllianceStationDisplay")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "setMatch")
readWebsocketType(t, ws, "realtimeScore")
// Change to a different screen.
mainArena.allianceStationDisplayScreen = "logo"
mainArena.allianceStationDisplayNotifier.Notify(nil)
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "setAllianceStationDisplay")
// Inform the server what display ID this is.
assert.Equal(t, "", mainArena.allianceStationDisplays["1"])
ws.Write("setAllianceStation", "R3")
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
assert.Equal(t, "R3", mainArena.allianceStationDisplays["1"])
// Run through a match cycle.
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "setMatch")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
mainArena.StartMatch()
mainArena.Update()
messages := readWebsocketMultiple(t, ws, 4)
_, ok := messages["status"]
assert.True(t, ok)
_, ok = messages["matchTime"]
assert.True(t, ok)
_, ok = messages["hotGoalLight"]
assert.True(t, ok)
mainArena.realtimeScoreNotifier.Notify(nil)
readWebsocketType(t, ws, "realtimeScore")
}
func TestFtaDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
recorder := getHttpResponse("/displays/fta")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "FTA Display - Untitled Event - Cheesy Arena")
}

29
fta_display.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web handlers for the FTA diagnostic display.
package main
import (
"net/http"
"text/template"
)
// Renders the FTA diagnostic display.
func FtaDisplayHandler(w http.ResponseWriter, r *http.Request) {
template := template.New("").Funcs(templateHelpers)
_, err := template.ParseFiles("templates/fta_display.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
}{eventSettings}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
return
}
}

23
fta_display_test.go Normal file
View File

@@ -0,0 +1,23 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestFtaDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
recorder := getHttpResponse("/displays/fta")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "FTA Display - Untitled Event - Cheesy Arena")
}

77
pit_display.go Normal file
View File

@@ -0,0 +1,77 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web handlers for the pit rankings display.
package main
import (
"io"
"log"
"net/http"
"text/template"
)
// Renders the pit display which shows scrolling rankings.
func PitDisplayHandler(w http.ResponseWriter, r *http.Request) {
template, err := template.ParseFiles("templates/pit_display.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*EventSettings
}{eventSettings}
err = template.Execute(w, data)
if err != nil {
handleWebErr(w, err)
return
}
}
// The websocket endpoint for the pit display, used only to force reloads remotely.
func PitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer websocket.Close()
reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen()
defer close(reloadDisplaysListener)
// 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 := <-reloadDisplaysListener:
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 {
_, _, 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
}
}
}

47
pit_display_test.go Normal file
View File

@@ -0,0 +1,47 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
)
func TestPitDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
recorder := getHttpResponse("/displays/pit")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Pit Display - Untitled Event - Cheesy Arena")
}
func TestPitDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/pit/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Check forced reloading as that is the only purpose the pit websocket serves.
recorder := getHttpResponse("/setup/field/reload_displays")
assert.Equal(t, 302, recorder.Code)
readWebsocketType(t, ws, "reload")
}

241
referee_display.go Normal file
View File

@@ -0,0 +1,241 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web handlers for the referee interface.
package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
"io"
"log"
"net/http"
"strconv"
"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 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()
red1 := mainArena.AllianceStations["R1"].team
if red1 == nil {
red1 = &Team{}
}
red2 := mainArena.AllianceStations["R2"].team
if red2 == nil {
red2 = &Team{}
}
red3 := mainArena.AllianceStations["R3"].team
if red3 == nil {
red3 = &Team{}
}
blue1 := mainArena.AllianceStations["B1"].team
if blue1 == nil {
blue1 = &Team{}
}
blue2 := mainArena.AllianceStations["B2"].team
if blue2 == nil {
blue2 = &Team{}
}
blue3 := mainArena.AllianceStations["B3"].team
if blue3 == nil {
blue3 = &Team{}
}
data := struct {
*EventSettings
MatchType string
MatchDisplayName string
Red1 *Team
Red2 *Team
Red3 *Team
Blue1 *Team
Blue2 *Team
Blue3 *Team
RedFouls []Foul
BlueFouls []Foul
RedCards map[string]string
BlueCards map[string]string
Rules []string
EntryEnabled bool
}{eventSettings, matchType, match.DisplayName, red1, red2, red3, blue1, blue2, blue3,
mainArena.redRealtimeScore.Fouls, mainArena.blueRealtimeScore.Fouls, mainArena.redRealtimeScore.Cards,
mainArena.blueRealtimeScore.Cards, 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)
reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen()
defer close(reloadDisplaysListener)
// 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
case _, ok := <-reloadDisplaysListener:
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)
}
mainArena.realtimeScoreNotifier.Notify(nil)
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
}
}
mainArena.realtimeScoreNotifier.Notify(nil)
case "card":
args := struct {
Alliance string
TeamId int
Card string
}{}
err = mapstructure.Decode(data, &args)
if err != nil {
websocket.WriteError(err.Error())
continue
}
// Set the card in the correct alliance's score.
var cards map[string]string
if args.Alliance == "red" {
cards = mainArena.redRealtimeScore.Cards
} else {
cards = mainArena.blueRealtimeScore.Cards
}
cards[strconv.Itoa(args.TeamId)] = args.Card
continue
case "signalReset":
if mainArena.MatchState != POST_MATCH {
// Don't allow clearing the field until the match is over.
continue
}
mainArena.redRealtimeScore.FieldReset = true
mainArena.blueRealtimeScore.FieldReset = true
continue // Don't reload.
case "commitMatch":
if mainArena.MatchState != POST_MATCH {
// Don't allow committing the fouls until the match is over.
continue
}
mainArena.redRealtimeScore.FoulsCommitted = true
mainArena.blueRealtimeScore.FoulsCommitted = true
mainArena.redRealtimeScore.FieldReset = true
mainArena.blueRealtimeScore.FieldReset = true
mainArena.scoringStatusNotifier.Notify(nil)
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
}
}
}

135
referee_display_test.go Normal file
View File

@@ -0,0 +1,135 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestRefereeDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/referee")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Referee Display - Untitled Event - Cheesy Arena")
}
func TestRefereeDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/referee/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Test foul addition.
foulData := struct {
Alliance string
TeamId int
Rule string
TimeInMatchSec float64
IsTechnical bool
}{"red", 256, "G22", 0, false}
ws.Write("addFoul", foulData)
foulData.TeamId = 359
foulData.IsTechnical = true
ws.Write("addFoul", foulData)
foulData.Alliance = "blue"
foulData.TeamId = 1680
ws.Write("addFoul", foulData)
readWebsocketType(t, ws, "reload")
readWebsocketType(t, ws, "reload")
readWebsocketType(t, ws, "reload")
if assert.Equal(t, 2, len(mainArena.redRealtimeScore.Fouls)) {
assert.Equal(t, 256, mainArena.redRealtimeScore.Fouls[0].TeamId)
assert.Equal(t, "G22", mainArena.redRealtimeScore.Fouls[0].Rule)
assert.Equal(t, 0, mainArena.redRealtimeScore.Fouls[0].TimeInMatchSec)
assert.Equal(t, false, mainArena.redRealtimeScore.Fouls[0].IsTechnical)
assert.Equal(t, 359, mainArena.redRealtimeScore.Fouls[1].TeamId)
assert.Equal(t, "G22", mainArena.redRealtimeScore.Fouls[1].Rule)
assert.Equal(t, true, mainArena.redRealtimeScore.Fouls[1].IsTechnical)
}
if assert.Equal(t, 1, len(mainArena.blueRealtimeScore.Fouls)) {
assert.Equal(t, 1680, mainArena.blueRealtimeScore.Fouls[0].TeamId)
assert.Equal(t, "G22", mainArena.blueRealtimeScore.Fouls[0].Rule)
assert.Equal(t, 0, mainArena.blueRealtimeScore.Fouls[0].TimeInMatchSec)
assert.Equal(t, true, mainArena.blueRealtimeScore.Fouls[0].IsTechnical)
}
assert.False(t, mainArena.redRealtimeScore.FoulsCommitted)
assert.False(t, mainArena.blueRealtimeScore.FoulsCommitted)
// Test foul deletion.
ws.Write("deleteFoul", foulData)
readWebsocketType(t, ws, "reload")
assert.Equal(t, 0, len(mainArena.blueRealtimeScore.Fouls))
foulData.Alliance = "red"
foulData.TeamId = 359
foulData.TimeInMatchSec = 29 // Make it not match.
ws.Write("deleteFoul", foulData)
readWebsocketType(t, ws, "reload")
assert.Equal(t, 2, len(mainArena.redRealtimeScore.Fouls))
foulData.TimeInMatchSec = 0
ws.Write("deleteFoul", foulData)
readWebsocketType(t, ws, "reload")
assert.Equal(t, 1, len(mainArena.redRealtimeScore.Fouls))
// Test card setting.
cardData := struct {
Alliance string
TeamId int
Card string
}{"red", 256, "yellow"}
ws.Write("card", cardData)
cardData.Alliance = "blue"
cardData.TeamId = 1680
cardData.Card = "red"
ws.Write("card", cardData)
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
if assert.Equal(t, 1, len(mainArena.redRealtimeScore.Cards)) {
assert.Equal(t, "yellow", mainArena.redRealtimeScore.Cards["256"])
}
if assert.Equal(t, 1, len(mainArena.blueRealtimeScore.Cards)) {
assert.Equal(t, "red", mainArena.blueRealtimeScore.Cards["1680"])
}
// Test field reset and match committing.
mainArena.MatchState = POST_MATCH
ws.Write("signalReset", nil)
time.Sleep(time.Millisecond * 10)
assert.True(t, mainArena.redRealtimeScore.FieldReset)
assert.True(t, mainArena.blueRealtimeScore.FieldReset)
assert.False(t, mainArena.redRealtimeScore.FoulsCommitted)
assert.False(t, mainArena.blueRealtimeScore.FoulsCommitted)
mainArena.redRealtimeScore.FieldReset = false
mainArena.blueRealtimeScore.FieldReset = false
ws.Write("commitMatch", nil)
readWebsocketType(t, ws, "reload")
assert.True(t, mainArena.redRealtimeScore.FieldReset)
assert.True(t, mainArena.blueRealtimeScore.FieldReset)
assert.True(t, mainArena.redRealtimeScore.FoulsCommitted)
assert.True(t, mainArena.blueRealtimeScore.FoulsCommitted)
// Should refresh the page when the next match is loaded.
mainArena.matchLoadTeamsNotifier.Notify(nil)
readWebsocketType(t, ws, "reload")
}

251
scoring_display.go Normal file
View File

@@ -0,0 +1,251 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web handlers for scoring interface.
package main
import (
"fmt"
"github.com/gorilla/mux"
"io"
"log"
"net/http"
"strconv"
"text/template"
)
// 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)
reloadDisplaysListener := mainArena.reloadDisplaysNotifier.Listen()
defer close(reloadDisplaysListener)
// 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
case _, ok := <-reloadDisplaysListener:
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 "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).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
(*score).CurrentCycle.Assists < 3 {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.Assists += 1
}
case "truss":
if (*score).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
!(*score).CurrentCycle.Truss {
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
(*score).CurrentCycle.Truss = true
}
case "catch":
if (*score).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
!(*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).AutoLeftoverBalls = (*score).AutoPreloadedBalls - (*score).CurrentScore.AutoHighHot -
(*score).CurrentScore.AutoHigh - (*score).CurrentScore.AutoLowHot -
(*score).CurrentScore.AutoLow
(*score).AutoCommitted = true
} else if !(*score).TeleopCommitted {
if (*score).CurrentCycle.ScoredHigh || (*score).CurrentCycle.ScoredLow ||
(*score).CurrentCycle.DeadBall {
// Check whether this is a leftover ball from autonomous.
if (*score).AutoLeftoverBalls > 0 {
if (*score).CurrentCycle.ScoredHigh {
(*score).CurrentScore.AutoClearHigh += 1
} else if (*score).CurrentCycle.ScoredLow {
(*score).CurrentScore.AutoClearLow += 1
} else {
(*score).CurrentScore.AutoClearDead += 1
}
(*score).AutoLeftoverBalls -= 1
} else {
(*score).CurrentScore.Cycles = append((*score).CurrentScore.Cycles, (*score).CurrentCycle)
}
(*score).CurrentCycle = Cycle{}
(*score).undoCycles = []Cycle{}
}
}
case "commitMatch":
if mainArena.MatchState != POST_MATCH {
// Don't allow committing the score until the match is over.
continue
}
(*score).AutoCommitted = true
(*score).TeleopCommitted = true
if (*score).CurrentCycle != (Cycle{}) {
// Commit last cycle.
(*score).CurrentScore.Cycles = append((*score).CurrentScore.Cycles, (*score).CurrentCycle)
}
mainArena.scoringStatusNotifier.Notify(nil)
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
}
mainArena.realtimeScoreNotifier.Notify(nil)
// 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
}
}
}

154
scoring_display_test.go Normal file
View File

@@ -0,0 +1,154 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package main
import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
)
func TestScoringDisplay(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
recorder := getHttpResponse("/displays/scoring/invalidalliance")
assert.Equal(t, 500, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Invalid alliance")
recorder = getHttpResponse("/displays/scoring/red")
assert.Equal(t, 200, recorder.Code)
recorder = getHttpResponse("/displays/scoring/blue")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Scoring - Untitled Event - Cheesy Arena")
}
func TestScoringDisplayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
_, _, err = websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blorpy/websocket", nil)
assert.NotNil(t, err)
redConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/red/websocket", nil)
assert.Nil(t, err)
defer redConn.Close()
redWs := &Websocket{redConn}
blueConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blue/websocket", nil)
assert.Nil(t, err)
defer blueConn.Close()
blueWs := &Websocket{blueConn}
// Should a score update right after connection.
readWebsocketType(t, redWs, "score")
readWebsocketType(t, blueWs, "score")
// Send a match worth of scoring commands in.
redWs.Write("preload", "3")
blueWs.Write("preload", "3")
redWs.Write("mobility", nil)
blueWs.Write("mobility", nil)
blueWs.Write("mobility", nil)
blueWs.Write("mobility", nil)
blueWs.Write("scoredHighHot", nil)
blueWs.Write("scoredHigh", nil)
blueWs.Write("scoredLowHot", nil)
blueWs.Write("scoredLow", nil)
blueWs.Write("undo", nil)
redWs.Write("commit", nil)
blueWs.Write("commit", nil)
redWs.Write("deadBall", nil)
redWs.Write("commit", nil)
redWs.Write("scoredLow", nil)
redWs.Write("commit", nil)
redWs.Write("scoredHigh", nil)
redWs.Write("commit", nil)
redWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("scoredLow", nil)
blueWs.Write("scoredHigh", nil)
blueWs.Write("commit", nil)
blueWs.Write("assist", nil)
blueWs.Write("assist", nil)
blueWs.Write("truss", nil)
blueWs.Write("catch", nil)
blueWs.Write("undo", nil)
blueWs.Write("scoredLow", nil)
blueWs.Write("commit", nil)
mainArena.MatchState = POST_MATCH
redWs.Write("commitMatch", nil)
for i := 0; i < 11; i++ {
readWebsocketType(t, redWs, "score")
}
for i := 0; i < 24; i++ {
readWebsocketType(t, blueWs, "score")
}
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoMobilityBonuses)
assert.Equal(t, 0, mainArena.redRealtimeScore.CurrentScore.AutoHighHot)
assert.Equal(t, 0, mainArena.redRealtimeScore.CurrentScore.AutoHigh)
assert.Equal(t, 0, mainArena.redRealtimeScore.CurrentScore.AutoLowHot)
assert.Equal(t, 0, mainArena.redRealtimeScore.CurrentScore.AutoLow)
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoClearHigh)
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoClearLow)
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoClearDead)
if assert.Equal(t, 1, len(mainArena.redRealtimeScore.CurrentScore.Cycles)) {
assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.Cycles[0].Assists)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].Truss)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].Catch)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].ScoredHigh)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].ScoredLow)
assert.False(t, mainArena.redRealtimeScore.CurrentScore.Cycles[0].DeadBall)
}
assert.True(t, mainArena.redRealtimeScore.AutoCommitted)
assert.True(t, mainArena.redRealtimeScore.TeleopCommitted)
assert.Equal(t, 3, mainArena.blueRealtimeScore.CurrentScore.AutoMobilityBonuses)
assert.Equal(t, 1, mainArena.blueRealtimeScore.CurrentScore.AutoHighHot)
assert.Equal(t, 1, mainArena.blueRealtimeScore.CurrentScore.AutoHigh)
assert.Equal(t, 1, mainArena.blueRealtimeScore.CurrentScore.AutoLowHot)
assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.AutoLow)
assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.AutoClearHigh)
assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.AutoClearLow)
assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.AutoClearDead)
if assert.Equal(t, 2, len(mainArena.blueRealtimeScore.CurrentScore.Cycles)) {
assert.Equal(t, 3, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].Assists)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].Truss)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].Catch)
assert.True(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].ScoredHigh)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].ScoredLow)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[0].DeadBall)
assert.Equal(t, 2, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].Assists)
assert.True(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].Truss)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].Catch)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].ScoredHigh)
assert.True(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].ScoredLow)
assert.False(t, mainArena.blueRealtimeScore.CurrentScore.Cycles[1].DeadBall)
}
assert.True(t, mainArena.blueRealtimeScore.AutoCommitted)
assert.False(t, mainArena.blueRealtimeScore.TeleopCommitted)
// Load another match to reset the results.
mainArena.ResetMatch()
mainArena.LoadTestMatch()
readWebsocketType(t, redWs, "score")
readWebsocketType(t, blueWs, "score")
assert.Equal(t, *NewRealtimeScore(), *mainArena.redRealtimeScore)
assert.Equal(t, *NewRealtimeScore(), *mainArena.blueRealtimeScore)
}