Refactor displays to allow for centralized remote configuration.

This commit is contained in:
Patrick Fairbank
2018-09-09 22:42:38 -07:00
parent 6cfdcc924d
commit 833bd32ab2
46 changed files with 1018 additions and 201 deletions

View File

@@ -6,6 +6,7 @@
package web
import (
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/websocket"
@@ -19,22 +20,19 @@ func (web *Web) allianceStationDisplayHandler(w http.ResponseWriter, r *http.Req
return
}
if !web.enforceDisplayConfiguration(w, r, map[string]string{"station": "R1"}) {
return
}
template, err := web.parseFiles("templates/alliance_station_display.html")
if err != nil {
handleWebErr(w, err)
return
}
displayId := ""
if _, ok := r.URL.Query()["displayId"]; ok {
// Register the display in memory by its ID so that it can be configured to a certain station.
displayId = r.URL.Query()["displayId"][0]
}
data := struct {
*model.EventSettings
DisplayId string
}{web.arena.EventSettings, displayId}
}{web.arena.EventSettings}
err = template.ExecuteTemplate(w, "alliance_station_display.html", data)
if err != nil {
handleWebErr(w, err)
@@ -48,12 +46,13 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r
return
}
displayId := r.URL.Query()["displayId"][0]
station, ok := web.arena.AllianceStationDisplays[displayId]
if !ok {
station = ""
web.arena.AllianceStationDisplays[displayId] = station
display, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
if err != nil {
handleWebErr(w, err)
return
}
web.arena.RegisterDisplay(display)
defer web.arena.MarkDisplayDisconnected(display)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -62,13 +61,6 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r
}
defer ws.Close()
// Inform the client which alliance station it should represent.
err = ws.Write("allianceStation", station)
if err != nil {
log.Println(err)
return
}
// Inform the client what the match period timing parameters are configured to.
err = ws.Write("matchTiming", game.MatchTiming)
if err != nil {
@@ -79,5 +71,5 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.AllianceStationDisplayModeNotifier, web.arena.ArenaStatusNotifier,
web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
web.arena.ReloadDisplaysNotifier)
web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -16,6 +16,11 @@ func TestAllianceStationDisplay(t *testing.T) {
web := setupTestWeb(t)
recorder := web.getHttpResponse("/displays/alliance_station")
assert.Equal(t, 302, recorder.Code)
assert.Contains(t, recorder.Header().Get("Location"), "displayId=874")
assert.Contains(t, recorder.Header().Get("Location"), "station=R1")
recorder = web.getHttpResponse("/displays/alliance_station?displayId=1&station=B1")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Alliance Station Display - Untitled Event - Cheesy Arena")
}
@@ -31,13 +36,13 @@ func TestAllianceStationDisplayWebsocket(t *testing.T) {
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "allianceStation")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "allianceStationDisplayMode")
readWebsocketType(t, ws, "arenaStatus")
readWebsocketType(t, ws, "matchLoad")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "realtimeScore")
readWebsocketType(t, ws, "displayConfiguration")
// Change to a different screen.
web.arena.AllianceStationDisplayMode = "logo"

View File

@@ -6,6 +6,7 @@
package web
import (
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/websocket"
@@ -19,6 +20,10 @@ func (web *Web) announcerDisplayHandler(w http.ResponseWriter, r *http.Request)
return
}
if !web.enforceDisplayConfiguration(w, r, nil) {
return
}
template, err := web.parseFiles("templates/announcer_display.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
@@ -41,6 +46,14 @@ func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http.
return
}
display, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
if err != nil {
handleWebErr(w, err)
return
}
web.arena.RegisterDisplay(display)
defer web.arena.MarkDisplayDisconnected(display)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
@@ -57,5 +70,6 @@ func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http.
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
web.arena.ScorePostedNotifier, web.arena.AudienceDisplayModeNotifier, web.arena.ReloadDisplaysNotifier)
web.arena.ScorePostedNotifier, web.arena.AudienceDisplayModeNotifier, web.arena.DisplayConfigurationNotifier,
web.arena.ReloadDisplaysNotifier)
}

View File

@@ -13,7 +13,7 @@ import (
func TestAnnouncerDisplay(t *testing.T) {
web := setupTestWeb(t)
recorder := web.getHttpResponse("/displays/announcer")
recorder := web.getHttpResponse("/displays/announcer?displayId=1")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Announcer Display - Untitled Event - Cheesy Arena")
}
@@ -23,7 +23,7 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) {
server, wsUrl := web.startTestServer()
defer server.Close()
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/announcer/websocket", nil)
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/announcer/websocket?displayId=1", nil)
assert.Nil(t, err)
defer conn.Close()
ws := websocket.NewTestWebsocket(conn)
@@ -35,6 +35,7 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) {
readWebsocketType(t, ws, "realtimeScore")
readWebsocketType(t, ws, "scorePosted")
readWebsocketType(t, ws, "audienceDisplayMode")
readWebsocketType(t, ws, "displayConfiguration")
web.arena.MatchLoadNotifier.Notify()
readWebsocketType(t, ws, "matchLoad")

View File

@@ -6,6 +6,7 @@
package web
import (
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/game"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/websocket"
@@ -19,6 +20,10 @@ func (web *Web) audienceDisplayHandler(w http.ResponseWriter, r *http.Request) {
return
}
if !web.enforceDisplayConfiguration(w, r, map[string]string{"background": "#0f0", "reversed": "false"}) {
return
}
template, err := web.parseFiles("templates/audience_display.html")
if err != nil {
handleWebErr(w, err)
@@ -41,6 +46,14 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
return
}
display, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
if err != nil {
handleWebErr(w, err)
return
}
web.arena.RegisterDisplay(display)
defer web.arena.MarkDisplayDisconnected(display)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
@@ -58,5 +71,6 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.AudienceDisplayModeNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier,
web.arena.RealtimeScoreNotifier, web.arena.PlaySoundNotifier, web.arena.ScorePostedNotifier,
web.arena.AllianceSelectionNotifier, web.arena.LowerThirdNotifier, web.arena.ReloadDisplaysNotifier)
web.arena.AllianceSelectionNotifier, web.arena.LowerThirdNotifier, web.arena.DisplayConfigurationNotifier,
web.arena.ReloadDisplaysNotifier)
}

View File

@@ -14,6 +14,12 @@ func TestAudienceDisplay(t *testing.T) {
web := setupTestWeb(t)
recorder := web.getHttpResponse("/displays/audience")
assert.Equal(t, 302, recorder.Code)
assert.Contains(t, recorder.Header().Get("Location"), "displayId=874")
assert.Contains(t, recorder.Header().Get("Location"), "background=%230f0")
assert.Contains(t, recorder.Header().Get("Location"), "reversed=false")
recorder = web.getHttpResponse("/displays/audience?displayId=1&background=%23000&reversed=false")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Audience Display - Untitled Event - Cheesy Arena")
}
@@ -23,7 +29,7 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
server, wsUrl := web.startTestServer()
defer server.Close()
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/audience/websocket", nil)
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/audience/websocket?displayId=1", nil)
assert.Nil(t, err)
defer conn.Close()
ws := websocket.NewTestWebsocket(conn)
@@ -35,6 +41,7 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "realtimeScore")
readWebsocketType(t, ws, "scorePosted")
readWebsocketType(t, ws, "displayConfiguration")
// Run through a match cycle.
web.arena.MatchLoadNotifier.Notify()

49
web/display_utils.go Normal file
View File

@@ -0,0 +1,49 @@
// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Common utility methods for display web routes.
package web
import (
"fmt"
"net/http"
"net/url"
"strings"
)
// Returns true if the given required parameters are present; otherwise redirects to the defaults and returns false.
func (web *Web) enforceDisplayConfiguration(w http.ResponseWriter, r *http.Request, defaults map[string]string) bool {
allPresent := true
configuration := make(map[string]string)
// Get display ID and nickname from the query parameters.
var displayId string
if displayId = r.URL.Query().Get("displayId"); displayId == "" {
displayId = web.arena.NextDisplayId()
allPresent = false
}
configuration["nickname"] = r.URL.Query().Get("nickname")
// Get display-specific fields from the query parameters.
if defaults != nil {
for key, defaultValue := range defaults {
if configuration[key] = r.URL.Query().Get(key); configuration[key] == "" {
configuration[key] = defaultValue
allPresent = false
}
}
}
if !allPresent {
var builder strings.Builder
for key, value := range configuration {
builder.WriteString("&")
builder.WriteString(url.QueryEscape(key))
builder.WriteString("=")
builder.WriteString(url.QueryEscape(value))
}
http.Redirect(w, r, fmt.Sprintf("%s?displayId=%s%s", r.URL.Path, displayId, builder.String()), 302)
}
return allPresent
}

View File

@@ -6,6 +6,7 @@
package web
import (
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/websocket"
"net/http"
@@ -17,6 +18,10 @@ func (web *Web) ftaDisplayHandler(w http.ResponseWriter, r *http.Request) {
return
}
if !web.enforceDisplayConfiguration(w, r, nil) {
return
}
template, err := web.parseFiles("templates/fta_display.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
@@ -34,7 +39,17 @@ func (web *Web) ftaDisplayHandler(w http.ResponseWriter, r *http.Request) {
// The websocket endpoint for the FTA display client to receive status updates.
func (web *Web) ftaDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
// TODO(patrick): Enable authentication once Safari (for iPad) supports it over Websocket.
if !web.userIsReader(w, r) {
return
}
display, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
if err != nil {
handleWebErr(w, err)
return
}
web.arena.RegisterDisplay(display)
defer web.arena.MarkDisplayDisconnected(display)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -44,5 +59,6 @@ func (web *Web) ftaDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reques
defer ws.Close()
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.ArenaStatusNotifier, web.arena.ReloadDisplaysNotifier)
ws.HandleNotifiers(web.arena.ArenaStatusNotifier, web.arena.DisplayConfigurationNotifier,
web.arena.ReloadDisplaysNotifier)
}

View File

@@ -11,7 +11,7 @@ import (
func TestFtaDisplay(t *testing.T) {
web := setupTestWeb(t)
recorder := web.getHttpResponse("/displays/fta")
recorder := web.getHttpResponse("/displays/fta?displayId=1")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Field Monitor - Untitled Event - Cheesy Arena")
}

View File

@@ -6,6 +6,7 @@
package web
import (
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/websocket"
"net/http"
@@ -17,6 +18,10 @@ func (web *Web) pitDisplayHandler(w http.ResponseWriter, r *http.Request) {
return
}
if !web.enforceDisplayConfiguration(w, r, nil) {
return
}
template, err := web.parseFiles("templates/pit_display.html")
if err != nil {
handleWebErr(w, err)
@@ -38,6 +43,14 @@ func (web *Web) pitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reques
return
}
display, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
if err != nil {
handleWebErr(w, err)
return
}
web.arena.RegisterDisplay(display)
defer web.arena.MarkDisplayDisconnected(display)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
@@ -46,5 +59,5 @@ func (web *Web) pitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reques
defer ws.Close()
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.ReloadDisplaysNotifier)
ws.HandleNotifiers(web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -13,7 +13,7 @@ import (
func TestPitDisplay(t *testing.T) {
web := setupTestWeb(t)
recorder := web.getHttpResponse("/displays/pit")
recorder := web.getHttpResponse("/displays/pit?displayId=1")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Pit Display - Untitled Event - Cheesy Arena")
}
@@ -23,13 +23,15 @@ func TestPitDisplayWebsocket(t *testing.T) {
server, wsUrl := web.startTestServer()
defer server.Close()
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/pit/websocket", nil)
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/displays/pit/websocket?displayId=1", nil)
assert.Nil(t, err)
defer conn.Close()
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "displayConfiguration")
// Check forced reloading as that is the only purpose the pit websocket serves.
recorder := web.getHttpResponse("/setup/displays/reload")
assert.Equal(t, 303, recorder.Code)
web.arena.ReloadDisplaysNotifier.Notify()
readWebsocketType(t, ws, "reload")
}

View File

@@ -0,0 +1,67 @@
// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Web routes for a placeholder display to be later configured by the server.
package web
import (
"fmt"
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/websocket"
"net/http"
)
// Shows a random ID to visually identify the display so that it can be configured on the server.
func (web *Web) placeholderDisplayHandler(w http.ResponseWriter, r *http.Request) {
if !web.userIsReader(w, r) {
return
}
// Generate a display ID and redirect if the client doesn't already have one.
displayId := r.URL.Query().Get("displayId")
if displayId == "" {
http.Redirect(w, r, fmt.Sprintf(r.URL.Path+"?displayId=%s", web.arena.NextDisplayId()), 302)
return
}
template, err := web.parseFiles("templates/placeholder_display.html")
if err != nil {
handleWebErr(w, err)
return
}
data := struct {
*model.EventSettings
}{web.arena.EventSettings}
err = template.ExecuteTemplate(w, "placeholder_display.html", data)
if err != nil {
handleWebErr(w, err)
return
}
}
// The websocket endpoint for sending configuration commands to the display.
func (web *Web) placeholderDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
if !web.userIsReader(w, r) {
return
}
display, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
if err != nil {
handleWebErr(w, err)
return
}
web.arena.RegisterDisplay(display)
defer web.arena.MarkDisplayDisconnected(display)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer ws.Close()
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -0,0 +1,51 @@
// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
package web
import (
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/websocket"
gorillawebsocket "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
)
func TestPlaceholderDisplay(t *testing.T) {
web := setupTestWeb(t)
recorder := web.getHttpResponse("/displays/audience")
assert.Equal(t, 302, recorder.Code)
assert.Contains(t, recorder.Header().Get("Location"), "displayId=874")
recorder = web.getHttpResponse("/display?displayId=1")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Placeholder Display - Untitled Event - Cheesy Arena")
}
func TestPlaceholderDisplayWebsocket(t *testing.T) {
web := setupTestWeb(t)
server, wsUrl := web.startTestServer()
defer server.Close()
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/display/websocket?displayId=123&nickname=blop&a=b", nil)
assert.Nil(t, err)
defer conn.Close()
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "displayConfiguration")
if assert.Contains(t, web.arena.Displays, "123") {
assert.Equal(t, "blop", web.arena.Displays["123"].Nickname)
if assert.Equal(t, 1, len(web.arena.Displays["123"].Configuration)) {
assert.Equal(t, "b", web.arena.Displays["123"].Configuration["a"])
}
}
// Reconfigure the display and verify that the new configuration is received.
display := &field.Display{Id: "123", Nickname: "Alliance", Type: field.AllianceStationDisplay,
Configuration: map[string]string{"station": "B2"}}
web.arena.UpdateDisplay(display)
readWebsocketType(t, ws, "displayConfiguration")
}

View File

@@ -85,7 +85,9 @@ func (web *Web) refereePanelHandler(w http.ResponseWriter, r *http.Request) {
// The websocket endpoint for the refereee interface client to send control commands and receive status updates.
func (web *Web) refereePanelWebsocketHandler(w http.ResponseWriter, r *http.Request) {
// TODO(patrick): Enable authentication once Safari (for iPad) supports it over Websocket.
if !web.userIsAdmin(w, r) {
return
}
ws, err := websocket.NewWebsocket(w, r)
if err != nil {

View File

@@ -6,7 +6,13 @@
package web
import (
"fmt"
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/model"
"github.com/Team254/cheesy-arena/websocket"
"github.com/mitchellh/mapstructure"
"io"
"log"
"net/http"
)
@@ -23,8 +29,8 @@ func (web *Web) displaysGetHandler(w http.ResponseWriter, r *http.Request) {
}
data := struct {
*model.EventSettings
AllianceStationDisplays map[string]string
}{web.arena.EventSettings, web.arena.AllianceStationDisplays}
DisplayTypeNames map[field.DisplayType]string
}{web.arena.EventSettings, field.DisplayTypeNames}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
@@ -32,25 +38,57 @@ func (web *Web) displaysGetHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Updates the display-station mapping for a single display.
func (web *Web) displaysPostHandler(w http.ResponseWriter, r *http.Request) {
// The websocket endpoint for the display configuration page to send control commands and receive status updates.
func (web *Web) displaysWebsocketHandler(w http.ResponseWriter, r *http.Request) {
if !web.userIsAdmin(w, r) {
return
}
displayId := r.PostFormValue("displayId")
allianceStation := r.PostFormValue("allianceStation")
web.arena.AllianceStationDisplays[displayId] = allianceStation
web.arena.MatchLoadNotifier.Notify()
http.Redirect(w, r, "/setup/displays", 303)
}
// Force-reloads all the websocket-connected displays.
func (web *Web) displaysReloadHandler(w http.ResponseWriter, r *http.Request) {
if !web.userIsAdmin(w, r) {
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer ws.Close()
web.arena.ReloadDisplaysNotifier.Notify()
http.Redirect(w, r, "/setup/displays", 303)
// Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine.
go ws.HandleNotifiers(web.arena.DisplayConfigurationNotifier)
// Loop, waiting for commands and responding to them, until the client closes the connection.
for {
messageType, data, err := ws.Read()
if err != nil {
if err == io.EOF {
// Client has closed the connection; nothing to do here.
return
}
log.Println(err)
return
}
switch messageType {
case "configureDisplay":
var display field.Display
err = mapstructure.Decode(data, &display)
if err != nil {
ws.WriteError(err.Error())
continue
}
if err = web.arena.UpdateDisplay(&display); err != nil {
ws.WriteError(err.Error())
continue
}
case "reloadDisplay":
displayId, ok := data.(string)
if !ok {
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
web.arena.ReloadDisplaysNotifier.NotifyWithMessage(displayId)
case "reloadAllDisplays":
web.arena.ReloadDisplaysNotifier.Notify()
default:
ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
}
}
}

View File

@@ -4,6 +4,10 @@
package web
import (
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/websocket"
gorillawebsocket "github.com/gorilla/websocket"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/assert"
"testing"
)
@@ -11,15 +15,87 @@ import (
func TestSetupDisplays(t *testing.T) {
web := setupTestWeb(t)
web.arena.AllianceStationDisplays["12345"] = ""
recorder := web.getHttpResponse("/setup/displays")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "12345")
assert.NotContains(t, recorder.Body.String(), "selected")
recorder = web.postHttpResponse("/setup/displays", "displayId=12345&allianceStation=B1")
assert.Equal(t, 303, recorder.Code)
recorder = web.getHttpResponse("/setup/displays")
assert.Contains(t, recorder.Body.String(), "12345")
assert.Contains(t, recorder.Body.String(), "selected")
assert.Contains(t, recorder.Body.String(), "Display Configuration - Untitled Event - Cheesy Arena")
}
func TestSetupDisplaysWebsocket(t *testing.T) {
web := setupTestWeb(t)
server, wsUrl := web.startTestServer()
defer server.Close()
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/setup/displays/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
message := readDisplayConfiguration(t, ws)
assert.Empty(t, message.Displays)
assert.Empty(t, message.DisplayUrls)
// Connect a couple of displays and verify the resulting configuration messages.
displayConn1, _, _ := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/display/websocket?displayId=1", nil)
defer displayConn1.Close()
readDisplayConfiguration(t, ws)
displayConn2, _, _ := gorillawebsocket.DefaultDialer.Dial(wsUrl+
"/displays/alliance_station/websocket?displayId=2&station=R2", nil)
defer displayConn2.Close()
expectedDisplay1 := &field.Display{Id: "1", Type: field.PlaceholderDisplay, Configuration: map[string]string{},
ConnectionCount: 1}
expectedDisplay2 := &field.Display{Id: "2", Type: field.AllianceStationDisplay,
Configuration: map[string]string{"station": "R2"}, ConnectionCount: 1}
message = readDisplayConfiguration(t, ws)
if assert.Equal(t, 2, len(message.Displays)) {
assert.Equal(t, expectedDisplay1, message.Displays["1"])
assert.Equal(t, expectedDisplay2, message.Displays["2"])
assert.Equal(t, expectedDisplay1.ToUrl(), message.DisplayUrls["1"])
assert.Equal(t, expectedDisplay2.ToUrl(), message.DisplayUrls["2"])
}
// Reconfigure a display and verify the result.
expectedDisplay1.Nickname = "Audience Display"
expectedDisplay1.Type = field.AudienceDisplay
expectedDisplay1.Configuration["background"] = "#00f"
expectedDisplay1.Configuration["reversed"] = "true"
ws.Write("configureDisplay", expectedDisplay1)
message = readDisplayConfiguration(t, ws)
assert.Equal(t, expectedDisplay1, message.Displays["1"])
assert.Equal(t, expectedDisplay1.ToUrl(), message.DisplayUrls["1"])
}
func TestSetupDisplaysWebsocketReloadDisplays(t *testing.T) {
web := setupTestWeb(t)
server, wsUrl := web.startTestServer()
defer server.Close()
conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/setup/displays/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readDisplayConfiguration(t, ws)
// Connect a display and verify the resulting configuration messages.
displayConn, _, _ := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/display/websocket?displayId=1", nil)
defer displayConn.Close()
displayWs := websocket.NewTestWebsocket(displayConn)
readDisplayConfiguration(t, displayWs)
readDisplayConfiguration(t, ws)
// Reset a display selectively and verify the resulting message.
ws.Write("reloadDisplay", "1")
assert.Equal(t, "1", readWebsocketType(t, displayWs, "reload"))
ws.Write("reloadAllDisplays", nil)
assert.Equal(t, nil, readWebsocketType(t, displayWs, "reload"))
}
func readDisplayConfiguration(t *testing.T, ws *websocket.Websocket) *field.DisplayConfigurationMessage {
message := readWebsocketType(t, ws, "displayConfiguration")
var displayConfigurationMessage field.DisplayConfigurationMessage
err := mapstructure.Decode(message, &displayConfigurationMessage)
assert.Nil(t, err)
return &displayConfigurationMessage
}

View File

@@ -61,6 +61,7 @@ func (web *Web) ledPlcWebsocketHandler(w http.ResponseWriter, r *http.Request) {
// Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine.
go ws.HandleNotifiers(web.arena.LedModeNotifier, web.arena.Plc.IoChangeNotifier)
// Loop, waiting for commands and responding to them, until the client closes the connection.
for {
messageType, data, err := ws.Read()

View File

@@ -12,7 +12,6 @@ import (
"io/ioutil"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -35,12 +34,6 @@ func (web *Web) settingsPostHandler(w http.ResponseWriter, r *http.Request) {
eventSettings := web.arena.EventSettings
eventSettings.Name = r.PostFormValue("name")
match, _ := regexp.MatchString("^#([0-9A-Fa-f]{3}){1,2}$", r.PostFormValue("displayBackgroundColor"))
if !match {
web.renderSettings(w, r, "Display background color must be a valid hex color value.")
return
}
eventSettings.DisplayBackgroundColor = r.PostFormValue("displayBackgroundColor")
numAlliances, _ := strconv.Atoi(r.PostFormValue("numElimAlliances"))
if numAlliances < 2 || numAlliances > 16 {
web.renderSettings(w, r, "Number of alliances must be between 2 and 16.")

View File

@@ -23,17 +23,15 @@ func TestSetupSettings(t *testing.T) {
recorder := web.getHttpResponse("/setup/settings")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Untitled Event")
assert.Contains(t, recorder.Body.String(), "#00ff00")
assert.Contains(t, recorder.Body.String(), "8")
assert.NotContains(t, recorder.Body.String(), "tbaPublishingEnabled\" checked")
// Change the settings and check the response.
recorder = web.postHttpResponse("/setup/settings", "name=Chezy Champs&code=CC&displayBackgroundColor=#ff00ff&"+
"numElimAlliances=16&tbaPublishingEnabled=on&tbaEventCode=2014cc&tbaSecretId=secretId&tbaSecret=tbasec")
recorder = web.postHttpResponse("/setup/settings", "name=Chezy Champs&code=CC&numElimAlliances=16&"+
"tbaPublishingEnabled=on&tbaEventCode=2014cc&tbaSecretId=secretId&tbaSecret=tbasec")
assert.Equal(t, 303, recorder.Code)
recorder = web.getHttpResponse("/setup/settings")
assert.Contains(t, recorder.Body.String(), "Chezy Champs")
assert.Contains(t, recorder.Body.String(), "#ff00ff")
assert.Contains(t, recorder.Body.String(), "16")
assert.Contains(t, recorder.Body.String(), "tbaPublishingEnabled\" checked")
assert.Contains(t, recorder.Body.String(), "2014cc")
@@ -44,12 +42,8 @@ func TestSetupSettings(t *testing.T) {
func TestSetupSettingsInvalidValues(t *testing.T) {
web := setupTestWeb(t)
// Invalid color value.
recorder := web.postHttpResponse("/setup/settings", "numAlliances=8&displayBackgroundColor=blorpy")
assert.Contains(t, recorder.Body.String(), "must be a valid hex color value")
// Invalid number of alliances.
recorder = web.postHttpResponse("/setup/settings", "numAlliances=1&displayBackgroundColor=#000")
recorder := web.postHttpResponse("/setup/settings", "numAlliances=1")
assert.Contains(t, recorder.Body.String(), "must be between 2 and 16")
}

View File

@@ -136,6 +136,8 @@ func (web *Web) newHandler() http.Handler {
router.HandleFunc("/api/matches/{type}", web.matchesApiHandler).Methods("GET")
router.HandleFunc("/api/rankings", web.rankingsApiHandler).Methods("GET")
router.HandleFunc("/api/sponsor_slides", web.sponsorSlidesApiHandler).Methods("GET")
router.HandleFunc("/display", web.placeholderDisplayHandler).Methods("GET")
router.HandleFunc("/display/websocket", web.placeholderDisplayWebsocketHandler).Methods("GET")
router.HandleFunc("/displays/alliance_station", web.allianceStationDisplayHandler).Methods("GET")
router.HandleFunc("/displays/alliance_station/websocket", web.allianceStationDisplayWebsocketHandler).Methods("GET")
router.HandleFunc("/displays/announcer", web.announcerDisplayHandler).Methods("GET")
@@ -168,8 +170,7 @@ func (web *Web) newHandler() http.Handler {
router.HandleFunc("/setup/db/restore", web.restoreDbHandler).Methods("POST")
router.HandleFunc("/setup/db/save", web.saveDbHandler).Methods("GET")
router.HandleFunc("/setup/displays", web.displaysGetHandler).Methods("GET")
router.HandleFunc("/setup/displays", web.displaysPostHandler).Methods("POST")
router.HandleFunc("/setup/displays/reload", web.displaysReloadHandler).Methods("GET")
router.HandleFunc("/setup/displays/websocket", web.displaysWebsocketHandler).Methods("GET")
router.HandleFunc("/setup/led_plc", web.ledPlcGetHandler).Methods("GET")
router.HandleFunc("/setup/led_plc/websocket", web.ledPlcWebsocketHandler).Methods("GET")
router.HandleFunc("/setup/lower_thirds", web.lowerThirdsGetHandler).Methods("GET")