mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Refactor displays to allow for centralized remote configuration.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
49
web/display_utils.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
67
web/placeholder_display.go
Normal file
67
web/placeholder_display.go
Normal 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)
|
||||
}
|
||||
51
web/placeholder_display_test.go
Normal file
51
web/placeholder_display_test.go
Normal 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")
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user