Refactor display registry to not broadcast all config changes to all displays.

This commit is contained in:
Patrick Fairbank
2020-03-29 16:55:53 -07:00
parent 305db8e0f2
commit f12d37778e
24 changed files with 188 additions and 177 deletions

View File

@@ -33,11 +33,6 @@ type ArenaNotifiers struct {
ControlPanelColorNotifier *websocket.Notifier
}
type DisplayConfigurationMessage struct {
Displays map[string]*Display
DisplayUrls map[string]string
}
type MatchTimeMessage struct {
MatchState
MatchTimeSec int
@@ -107,16 +102,14 @@ func (arena *Arena) generateAudienceDisplayModeMessage() interface{} {
}
func (arena *Arena) generateDisplayConfigurationMessage() interface{} {
// Notify() for this notifier must always called from a method that has a lock on the display mutex.
// Make a copy of the map to avoid potential data races; otherwise the same map would get iterated through as it is
// serialized to JSON.
displaysCopy := make(map[string]*Display)
displayUrls := make(map[string]string)
// serialized to JSON, outside the mutex lock.
displaysCopy := make(map[string]Display)
for displayId, display := range arena.Displays {
displaysCopy[displayId] = display
displayUrls[displayId] = display.ToUrl()
displaysCopy[displayId] = *display
}
return &DisplayConfigurationMessage{displaysCopy, displayUrls}
return displaysCopy
}
func (arena *Arena) generateEventStatusMessage() interface{} {

View File

@@ -7,6 +7,7 @@ package field
import (
"fmt"
"github.com/Team254/cheesy-arena/websocket"
"net/url"
"reflect"
"sort"
@@ -60,64 +61,69 @@ var displayTypePaths = map[DisplayType]string{
var displayRegistryMutex sync.Mutex
type Display struct {
Id string
Nickname string
Type DisplayType
Configuration map[string]string
IpAddress string
ConnectionCount int
lastConnectedTime time.Time
DisplayConfiguration DisplayConfiguration
IpAddress string
ConnectionCount int
Notifier *websocket.Notifier
lastConnectedTime time.Time
}
type DisplayConfiguration struct {
Id string
Nickname string
Type DisplayType
Configuration map[string]string
}
// Parses the given display URL path and query string to extract the configuration.
func DisplayFromUrl(path string, query map[string][]string) (*Display, error) {
func DisplayFromUrl(path string, query map[string][]string) (*DisplayConfiguration, error) {
if _, ok := query["displayId"]; !ok {
return nil, fmt.Errorf("Display ID not present in request.")
}
var display Display
display.Id = query["displayId"][0]
var displayConfig DisplayConfiguration
displayConfig.Id = query["displayId"][0]
if nickname, ok := query["nickname"]; ok {
display.Nickname, _ = url.QueryUnescape(nickname[0])
displayConfig.Nickname, _ = url.QueryUnescape(nickname[0])
}
// Determine type from the websocket connection URL. This way of doing it isn't super efficient, but it's not really
// a concern since it should happen relatively infrequently.
for displayType, displayPath := range displayTypePaths {
if path == displayPath+"/websocket" {
display.Type = displayType
displayConfig.Type = displayType
break
}
}
if display.Type == InvalidDisplay {
if displayConfig.Type == InvalidDisplay {
return nil, fmt.Errorf("Could not determine display type from path %s.", path)
}
// Put any remaining query parameters into the per-type configuration map.
display.Configuration = make(map[string]string)
displayConfig.Configuration = make(map[string]string)
for key, value := range query {
if key != "displayId" && key != "nickname" {
display.Configuration[key], _ = url.QueryUnescape(value[0])
displayConfig.Configuration[key], _ = url.QueryUnescape(value[0])
}
}
return &display, nil
return &displayConfig, nil
}
// Returns the URL string for the given display that includes all of its configuration parameters.
func (display *Display) ToUrl() string {
var builder strings.Builder
builder.WriteString(displayTypePaths[display.Type])
builder.WriteString(displayTypePaths[display.DisplayConfiguration.Type])
builder.WriteString("?displayId=")
builder.WriteString(url.QueryEscape(display.Id))
if display.Nickname != "" {
builder.WriteString(url.QueryEscape(display.DisplayConfiguration.Id))
if display.DisplayConfiguration.Nickname != "" {
builder.WriteString("&nickname=")
builder.WriteString(url.QueryEscape(display.Nickname))
builder.WriteString(url.QueryEscape(display.DisplayConfiguration.Nickname))
}
// Sort the keys so that the URL generated is deterministic.
var keys []string
for key := range display.Configuration {
for key := range display.DisplayConfiguration.Configuration {
keys = append(keys, key)
}
sort.Strings(keys)
@@ -125,11 +131,15 @@ func (display *Display) ToUrl() string {
builder.WriteString("&")
builder.WriteString(url.QueryEscape(key))
builder.WriteString("=")
builder.WriteString(url.QueryEscape(display.Configuration[key]))
builder.WriteString(url.QueryEscape(display.DisplayConfiguration.Configuration[key]))
}
return builder.String()
}
func (display *Display) generateDisplayConfigurationMessage() interface{} {
return display.ToUrl()
}
// Returns an unused ID that can be used for a new display.
func (arena *Arena) NextDisplayId() string {
displayRegistryMutex.Lock()
@@ -146,55 +156,63 @@ func (arena *Arena) NextDisplayId() string {
}
}
// Adds the given display to the arena registry and triggers a notification.
func (arena *Arena) RegisterDisplay(display *Display) {
// Creates or gets the given display in the arena registry and triggers a notification.
func (arena *Arena) RegisterDisplay(displayConfig *DisplayConfiguration, ipAddress string) *Display {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
existingDisplay, ok := arena.Displays[display.Id]
if ok && display.Type == PlaceholderDisplay {
display, ok := arena.Displays[displayConfig.Id]
if ok && displayConfig.Type == PlaceholderDisplay {
// Don't rewrite the registered configuration if the new one is a placeholder -- if it is reconnecting after a
// restart, it should adopt the existing configuration.
arena.Displays[display.Id].ConnectionCount++
arena.Displays[displayConfig.Id].ConnectionCount++
arena.Displays[displayConfig.Id].IpAddress = ipAddress
} else {
if ok {
display.ConnectionCount = existingDisplay.ConnectionCount + 1
} else {
display.ConnectionCount = 1
if !ok {
display = new(Display)
display.Notifier = websocket.NewNotifier("displayConfiguration",
display.generateDisplayConfigurationMessage)
arena.Displays[displayConfig.Id] = display
}
display.DisplayConfiguration = *displayConfig
display.IpAddress = ipAddress
display.ConnectionCount += 1
display.lastConnectedTime = time.Now()
arena.Displays[display.Id] = display
display.Notifier.Notify()
}
arena.DisplayConfigurationNotifier.Notify()
return display
}
// Updates the given display in the arena registry. Triggers a notification if the display configuration changed.
func (arena *Arena) UpdateDisplay(display *Display) error {
func (arena *Arena) UpdateDisplay(displayConfig DisplayConfiguration) error {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
existingDisplay, ok := arena.Displays[display.Id]
display, ok := arena.Displays[displayConfig.Id]
if !ok {
return fmt.Errorf("Display %s doesn't exist.", display.Id)
return fmt.Errorf("Display %s doesn't exist.", displayConfig.Id)
}
display.ConnectionCount = existingDisplay.ConnectionCount
if !reflect.DeepEqual(existingDisplay, display) {
arena.Displays[display.Id] = display
if !reflect.DeepEqual(displayConfig, display.DisplayConfiguration) {
display.DisplayConfiguration = displayConfig
display.Notifier.Notify()
arena.DisplayConfigurationNotifier.Notify()
}
return nil
}
// Marks the given display as having disconnected in the arena registry and triggers a notification.
func (arena *Arena) MarkDisplayDisconnected(display *Display) {
func (arena *Arena) MarkDisplayDisconnected(displayId string) {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
if existingDisplay, ok := arena.Displays[display.Id]; ok {
if existingDisplay.Type == PlaceholderDisplay && existingDisplay.Nickname == "" &&
len(existingDisplay.Configuration) == 0 {
if existingDisplay, ok := arena.Displays[displayId]; ok {
if existingDisplay.DisplayConfiguration.Type == PlaceholderDisplay &&
existingDisplay.DisplayConfiguration.Nickname == "" &&
len(existingDisplay.DisplayConfiguration.Configuration) == 0 {
// If the display is an unconfigured placeholder, just remove it entirely to prevent clutter.
delete(arena.Displays, existingDisplay.Id)
delete(arena.Displays, existingDisplay.DisplayConfiguration.Id)
} else {
existingDisplay.ConnectionCount -= 1
}
@@ -210,7 +228,7 @@ func (arena *Arena) purgeDisconnectedDisplays() {
deleted := false
for id, display := range arena.Displays {
if display.ConnectionCount == 0 && display.Nickname == "" &&
if display.ConnectionCount == 0 && display.DisplayConfiguration.Nickname == "" &&
time.Now().Sub(display.lastConnectedTime).Minutes() >= displayPurgeTtlMin {
delete(arena.Displays, id)
deleted = true

View File

@@ -52,8 +52,8 @@ func TestDisplayFromUrl(t *testing.T) {
}
func TestDisplayToUrl(t *testing.T) {
display := &Display{Id: "254", Nickname: "Test Nickname", Type: PitDisplay,
Configuration: map[string]string{"f": "1", "z": "#fff", "a": "3", "c": "4"}}
display := &Display{DisplayConfiguration: DisplayConfiguration{Id: "254", Nickname: "Test Nickname",
Type: PitDisplay, Configuration: map[string]string{"f": "1", "z": "#fff", "a": "3", "c": "4"}}}
assert.Equal(t, "/displays/pit?displayId=254&nickname=Test+Nickname&a=3&c=4&f=1&z=%23fff", display.ToUrl())
}
@@ -62,51 +62,57 @@ func TestNextDisplayId(t *testing.T) {
assert.Equal(t, "100", arena.NextDisplayId())
display := &Display{Id: "100"}
arena.RegisterDisplay(display)
displayConfig := &DisplayConfiguration{Id: "100"}
arena.RegisterDisplay(displayConfig, "")
assert.Equal(t, "101", arena.NextDisplayId())
}
func TestDisplayRegisterUnregister(t *testing.T) {
arena := setupTestArena(t)
display := &Display{Id: "254", Nickname: "Placeholder", Type: PlaceholderDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(display)
displayConfig := &DisplayConfiguration{Id: "254", Nickname: "Placeholder", Type: PlaceholderDisplay,
Configuration: map[string]string{}}
arena.RegisterDisplay(displayConfig, "1.2.3.4")
if assert.Contains(t, arena.Displays, "254") {
assert.Equal(t, "Placeholder", arena.Displays["254"].Nickname)
assert.Equal(t, PlaceholderDisplay, arena.Displays["254"].Type)
assert.Equal(t, "Placeholder", arena.Displays["254"].DisplayConfiguration.Nickname)
assert.Equal(t, PlaceholderDisplay, arena.Displays["254"].DisplayConfiguration.Type)
assert.Equal(t, 1, arena.Displays["254"].ConnectionCount)
assert.Equal(t, "1.2.3.4", arena.Displays["254"].IpAddress)
}
notifier := arena.Displays["254"].Notifier
// Register a second instance of the same display.
display2 := &Display{Id: "254", Nickname: "Pit", Type: PitDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(display2)
displayConfig2 := &DisplayConfiguration{Id: "254", Nickname: "Pit", Type: PitDisplay,
Configuration: map[string]string{}}
arena.RegisterDisplay(displayConfig2, "2.3.4.5")
if assert.Contains(t, arena.Displays, "254") {
assert.Equal(t, "Pit", arena.Displays["254"].Nickname)
assert.Equal(t, PitDisplay, arena.Displays["254"].Type)
assert.Equal(t, "Pit", arena.Displays["254"].DisplayConfiguration.Nickname)
assert.Equal(t, PitDisplay, arena.Displays["254"].DisplayConfiguration.Type)
assert.Equal(t, 2, arena.Displays["254"].ConnectionCount)
assert.Equal(t, "2.3.4.5", arena.Displays["254"].IpAddress)
assert.Same(t, notifier, arena.Displays["254"].Notifier)
}
// Register a second display.
display3 := &Display{Id: "148", Type: FieldMonitorDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(display3)
displayConfig3 := &DisplayConfiguration{Id: "148", Type: FieldMonitorDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(displayConfig3, "3.4.5.6")
if assert.Contains(t, arena.Displays, "148") {
assert.Equal(t, 1, arena.Displays["148"].ConnectionCount)
}
// Update the first display.
display4 := &Display{Id: "254", Nickname: "Alliance", Type: AllianceStationDisplay,
displayConfig4 := DisplayConfiguration{Id: "254", Nickname: "Alliance", Type: AllianceStationDisplay,
Configuration: map[string]string{"station": "B2"}}
arena.UpdateDisplay(display4)
arena.UpdateDisplay(displayConfig4)
if assert.Contains(t, arena.Displays, "254") {
assert.Equal(t, "Alliance", arena.Displays["254"].Nickname)
assert.Equal(t, AllianceStationDisplay, arena.Displays["254"].Type)
assert.Equal(t, "Alliance", arena.Displays["254"].DisplayConfiguration.Nickname)
assert.Equal(t, AllianceStationDisplay, arena.Displays["254"].DisplayConfiguration.Type)
assert.Equal(t, 2, arena.Displays["254"].ConnectionCount)
}
// Disconnect both displays.
arena.MarkDisplayDisconnected(display)
arena.MarkDisplayDisconnected(display3)
arena.MarkDisplayDisconnected(displayConfig.Id)
arena.MarkDisplayDisconnected(displayConfig3.Id)
if assert.Contains(t, arena.Displays, "148") {
assert.Equal(t, 0, arena.Displays["148"].ConnectionCount)
}
@@ -118,8 +124,8 @@ func TestDisplayRegisterUnregister(t *testing.T) {
func TestDisplayUpdateError(t *testing.T) {
arena := setupTestArena(t)
display := &Display{Id: "254", Configuration: map[string]string{}}
err := arena.UpdateDisplay(display)
displayConfig := DisplayConfiguration{Id: "254", Configuration: map[string]string{}}
err := arena.UpdateDisplay(displayConfig)
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "doesn't exist")
}
@@ -129,41 +135,41 @@ func TestDisplayPurge(t *testing.T) {
arena := setupTestArena(t)
// Unnamed placeholder gets immediately purged upon disconnection.
display := &Display{Id: "254", Type: PlaceholderDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(display)
displayConfig := &DisplayConfiguration{Id: "254", Type: PlaceholderDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "254")
arena.MarkDisplayDisconnected(display)
arena.MarkDisplayDisconnected(displayConfig.Id)
assert.NotContains(t, arena.Displays, "254")
// Named placeholder does not get immediately purged upon disconnection.
display.Nickname = "Bob"
arena.RegisterDisplay(display)
displayConfig.Nickname = "Bob"
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "254")
arena.MarkDisplayDisconnected(display)
arena.MarkDisplayDisconnected(displayConfig.Id)
assert.Contains(t, arena.Displays, "254")
// Unnamed configured display does not get immediately purged upon disconnection.
display = &Display{Id: "1114", Type: FieldMonitorDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(display)
// Unnamed configured displayConfig does not get immediately purged upon disconnection.
displayConfig = &DisplayConfiguration{Id: "1114", Type: FieldMonitorDisplay, Configuration: map[string]string{}}
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "1114")
arena.MarkDisplayDisconnected(display)
arena.MarkDisplayDisconnected(displayConfig.Id)
assert.Contains(t, arena.Displays, "1114")
arena.purgeDisconnectedDisplays()
assert.Contains(t, arena.Displays, "1114")
// Unnamed configured display gets purged by periodic task.
arena.RegisterDisplay(display)
// Unnamed configured displayConfig gets purged by periodic task.
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "1114")
arena.MarkDisplayDisconnected(display)
arena.MarkDisplayDisconnected(displayConfig.Id)
arena.Displays["1114"].lastConnectedTime = time.Now().Add(-displayPurgeTtlMin * time.Minute)
arena.purgeDisconnectedDisplays()
assert.NotContains(t, arena.Displays, "1114")
// Named configured display does not get purged by periodic task.
display.Nickname = "Brunhilda"
arena.RegisterDisplay(display)
// Named configured displayConfig does not get purged by periodic task.
displayConfig.Nickname = "Brunhilda"
arena.RegisterDisplay(displayConfig, "1.2.3.4")
assert.Contains(t, arena.Displays, "1114")
arena.MarkDisplayDisconnected(display)
arena.MarkDisplayDisconnected(displayConfig.Id)
arena.Displays["1114"].lastConnectedTime = time.Now().Add(-displayPurgeTtlMin * time.Minute)
arena.purgeDisconnectedDisplays()
assert.Contains(t, arena.Displays, "1114")

View File

@@ -40,13 +40,11 @@ var CheesyWebsocket = function(path, events) {
// Insert an event to allow reconfiguration if this is a display.
if (!events.hasOwnProperty("displayConfiguration")) {
events.displayConfiguration = function (event) {
if (displayId in event.data.DisplayUrls) {
var newUrl = event.data.DisplayUrls[displayId];
var newUrl = event.data;
// Reload the display if the configuration has changed.
if (newUrl !== window.location.pathname + window.location.search) {
window.location = newUrl;
}
// Reload the display if the configuration has changed.
if (newUrl !== window.location.pathname + window.location.search) {
window.location = newUrl;
}
};
}

View File

@@ -38,14 +38,14 @@ var reloadAllDisplays = function() {
var handleDisplayConfiguration = function(data) {
$("#displayContainer").empty();
$.each(data.Displays, function(displayId, display) {
$.each(data, function(displayId, display) {
var displayRow = displayTemplate(display);
$("#displayContainer").append(displayRow);
$("#displayNickname" + displayId).val(display.Nickname);
$("#displayType" + displayId).val(display.Type);
$("#displayNickname" + displayId).val(display.DisplayConfiguration.Nickname);
$("#displayType" + displayId).val(display.DisplayConfiguration.Type);
// Convert configuration map to query string format.
var configurationString = $.map(Object.entries(display.Configuration), function(entry) {
var configurationString = $.map(Object.entries(display.DisplayConfiguration.Configuration), function(entry) {
return entry.join("=");
}).join("&");
$("#displayConfiguration" + displayId).val(configurationString);

View File

@@ -31,30 +31,30 @@
<script id="displayTemplate" type="text/x-handlebars-template">
<tr{{"{{#unless ConnectionCount}}"}} class="danger"{{"{{/unless}}"}}>
<td>{{"{{Id}}"}}</td>
<td>{{"{{DisplayConfiguration.Id}}"}}</td>
<td>{{"{{ConnectionCount}}"}}</td>
<td>{{"{{IpAddress}}"}}</td>
<td><input type="text" id="displayNickname{{"{{Id}}"}}" size="30" /></td>
<td><input type="text" id="displayNickname{{"{{DisplayConfiguration.Id}}"}}" size="30" /></td>
<td>
<select id="displayType{{"{{Id}}"}}">
<select id="displayType{{"{{DisplayConfiguration.Id}}"}}">
{{range $type, $typeName := .DisplayTypeNames}}
<option value="{{$type}}">{{$typeName}}</option>
{{end}}
</select>
</td>
<td>
<input type="text" id="displayConfiguration{{"{{Id}}"}}" size="50" />
<input type="text" id="displayConfiguration{{"{{DisplayConfiguration.Id}}"}}" size="50" />
</td>
<td>
<button type="button" class="btn btn-info btn-xs" title="Save Changes"
onclick="configureDisplay('{{"{{Id}}"}}');">
onclick="configureDisplay('{{"{{DisplayConfiguration.Id}}"}}');">
<i class="glyphicon glyphicon-ok"></i>
</button>
<button type="button" class="btn btn-primary btn-xs" title="Undo Changes" onclick="undoChanges();">
<i class="glyphicon glyphicon-arrow-left"></i>
</button>
<button type="button" class="btn btn-success btn-xs" title="Reload Display"
onclick="reloadDisplay('{{"{{Id}}"}}');">
onclick="reloadDisplay('{{"{{DisplayConfiguration.Id}}"}}');">
<i class="glyphicon glyphicon-refresh"></i>
</button>
</td>

View File

@@ -40,7 +40,7 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r
handleWebErr(w, err)
return
}
defer web.arena.MarkDisplayDisconnected(display)
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -50,7 +50,7 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r
defer ws.Close()
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.AllianceStationDisplayModeNotifier,
ws.HandleNotifiers(display.Notifier, web.arena.MatchTimingNotifier, web.arena.AllianceStationDisplayModeNotifier,
web.arena.ArenaStatusNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier,
web.arena.RealtimeScoreNotifier, web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
web.arena.RealtimeScoreNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -36,13 +36,13 @@ func TestAllianceStationDisplayWebsocket(t *testing.T) {
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "displayConfiguration")
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

@@ -40,7 +40,7 @@ func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http.
handleWebErr(w, err)
return
}
defer web.arena.MarkDisplayDisconnected(display)
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -50,7 +50,7 @@ func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http.
defer ws.Close()
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier,
web.arena.RealtimeScoreNotifier, web.arena.ScorePostedNotifier, web.arena.AudienceDisplayModeNotifier,
web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
ws.HandleNotifiers(display.Notifier, web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier,
web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier, web.arena.ScorePostedNotifier,
web.arena.AudienceDisplayModeNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -29,13 +29,13 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) {
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "displayConfiguration")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchLoad")
readWebsocketType(t, ws, "matchTime")
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

@@ -43,7 +43,7 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
handleWebErr(w, err)
return
}
defer web.arena.MarkDisplayDisconnected(display)
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -53,8 +53,8 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
defer ws.Close()
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.AudienceDisplayModeNotifier,
ws.HandleNotifiers(display.Notifier, web.arena.MatchTimingNotifier, 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.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
web.arena.LowerThirdNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -36,6 +36,7 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "displayConfiguration")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "audienceDisplayMode")
readWebsocketType(t, ws, "matchLoad")
@@ -44,7 +45,6 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
readWebsocketType(t, ws, "scorePosted")
readWebsocketType(t, ws, "allianceSelection")
readWebsocketType(t, ws, "lowerThird")
readWebsocketType(t, ws, "displayConfiguration")
// Run through a match cycle.
web.arena.MatchLoadNotifier.Notify()

View File

@@ -54,18 +54,16 @@ func (web *Web) enforceDisplayConfiguration(w http.ResponseWriter, r *http.Reque
// Constructs, registers, and returns the display object for the given incoming websocket request.
func (web *Web) registerDisplay(r *http.Request) (*field.Display, error) {
display, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
displayConfig, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
if err != nil {
return nil, err
}
// Extract the source IP address of the request and store it in the display object.
if ipAddress := r.Header.Get("X-Real-IP"); ipAddress != "" {
display.IpAddress = ipAddress
} else {
display.IpAddress = regexp.MustCompile("(.*):\\d+$").FindStringSubmatch(r.RemoteAddr)[1]
var ipAddress string
if ipAddress = r.Header.Get("X-Real-IP"); ipAddress == "" {
ipAddress = regexp.MustCompile("(.*):\\d+$").FindStringSubmatch(r.RemoteAddr)[1]
}
web.arena.RegisterDisplay(display)
return display, nil
return web.arena.RegisterDisplay(displayConfig, ipAddress), nil
}

View File

@@ -39,7 +39,7 @@ func (web *Web) fieldMonitorDisplayWebsocketHandler(w http.ResponseWriter, r *ht
handleWebErr(w, err)
return
}
defer web.arena.MarkDisplayDisconnected(display)
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -49,6 +49,5 @@ func (web *Web) fieldMonitorDisplayWebsocketHandler(w http.ResponseWriter, r *ht
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.DisplayConfigurationNotifier,
web.arena.ReloadDisplaysNotifier)
ws.HandleNotifiers(display.Notifier, web.arena.ArenaStatusNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -29,6 +29,6 @@ func TestFieldMonitorDisplayWebsocket(t *testing.T) {
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "arenaStatus")
readWebsocketType(t, ws, "displayConfiguration")
readWebsocketType(t, ws, "arenaStatus")
}

View File

@@ -39,7 +39,7 @@ func (web *Web) pitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reques
handleWebErr(w, err)
return
}
defer web.arena.MarkDisplayDisconnected(display)
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -49,6 +49,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.EventStatusNotifier, web.arena.DisplayConfigurationNotifier,
web.arena.ReloadDisplaysNotifier)
ws.HandleNotifiers(display.Notifier, web.arena.EventStatusNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -29,8 +29,8 @@ func TestPitDisplayWebsocket(t *testing.T) {
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "eventStatus")
readWebsocketType(t, ws, "displayConfiguration")
readWebsocketType(t, ws, "eventStatus")
// Check forced reloading as that is the only purpose the pit websocket serves.
web.arena.ReloadDisplaysNotifier.Notify()

View File

@@ -39,7 +39,7 @@ func (web *Web) placeholderDisplayWebsocketHandler(w http.ResponseWriter, r *htt
handleWebErr(w, err)
return
}
defer web.arena.MarkDisplayDisconnected(display)
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -49,5 +49,5 @@ func (web *Web) placeholderDisplayWebsocketHandler(w http.ResponseWriter, r *htt
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)
ws.HandleNotifiers(display.Notifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -37,15 +37,15 @@ func TestPlaceholderDisplayWebsocket(t *testing.T) {
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"])
assert.Equal(t, "blop", web.arena.Displays["123"].DisplayConfiguration.Nickname)
if assert.Equal(t, 1, len(web.arena.Displays["123"].DisplayConfiguration.Configuration)) {
assert.Equal(t, "b", web.arena.Displays["123"].DisplayConfiguration.Configuration["a"])
}
}
// Reconfigure the display and verify that the new configuration is received.
display := &field.Display{Id: "123", Nickname: "Alliance", Type: field.AllianceStationDisplay,
displayConfig := field.DisplayConfiguration{Id: "123", Nickname: "Alliance", Type: field.AllianceStationDisplay,
Configuration: map[string]string{"station": "B2"}}
web.arena.UpdateDisplay(display)
web.arena.UpdateDisplay(displayConfig)
readWebsocketType(t, ws, "displayConfiguration")
}

View File

@@ -69,7 +69,7 @@ func (web *Web) queueingDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
handleWebErr(w, err)
return
}
defer web.arena.MarkDisplayDisconnected(display)
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -79,6 +79,6 @@ func (web *Web) queueingDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
defer ws.Close()
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier,
web.arena.EventStatusNotifier, web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
ws.HandleNotifiers(display.Notifier, web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier,
web.arena.MatchTimeNotifier, web.arena.EventStatusNotifier, web.arena.ReloadDisplaysNotifier)
}

View File

@@ -29,9 +29,9 @@ func TestQueueingDisplayWebsocket(t *testing.T) {
ws := websocket.NewTestWebsocket(conn)
// Should get a few status updates right after connection.
readWebsocketType(t, ws, "displayConfiguration")
readWebsocketType(t, ws, "matchTiming")
readWebsocketType(t, ws, "matchLoad")
readWebsocketType(t, ws, "matchTime")
readWebsocketType(t, ws, "eventStatus")
readWebsocketType(t, ws, "displayConfiguration")
}

View File

@@ -68,13 +68,13 @@ func (web *Web) displaysWebsocketHandler(w http.ResponseWriter, r *http.Request)
switch messageType {
case "configureDisplay":
var display field.Display
err = mapstructure.Decode(data, &display)
var displayConfig field.DisplayConfiguration
err = mapstructure.Decode(data, &displayConfig)
if err != nil {
ws.WriteError(err.Error())
continue
}
if err = web.arena.UpdateDisplay(&display); err != nil {
if err = web.arena.UpdateDisplay(displayConfig); err != nil {
ws.WriteError(err.Error())
continue
}

View File

@@ -32,37 +32,37 @@ func TestSetupDisplaysWebsocket(t *testing.T) {
// Should get a few status updates right after connection.
message := readDisplayConfiguration(t, ws)
assert.Empty(t, message.Displays)
assert.Empty(t, message.DisplayUrls)
assert.Empty(t, message)
// Connect a couple of displays and verify the resulting configuration messages.
displayConn1, _, _ := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/display/websocket?displayId=1", nil)
defer displayConn1.Close()
displayWs1 := websocket.NewTestWebsocket(displayConn1)
assert.Equal(t, "/display?displayId=1", readWebsocketType(t, displayWs1, "displayConfiguration"))
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, IpAddress: "127.0.0.1"}
expectedDisplay2 := &field.Display{Id: "2", Type: field.AllianceStationDisplay,
Configuration: map[string]string{"station": "R2"}, ConnectionCount: 1, IpAddress: "127.0.0.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"])
if assert.Equal(t, 2, len(message)) {
assert.Equal(t, field.DisplayConfiguration{"1", "", field.PlaceholderDisplay, map[string]string{}},
message["1"].DisplayConfiguration)
assert.Equal(t, 1, message["1"].ConnectionCount)
assert.Equal(t, "127.0.0.1", message["1"].IpAddress)
assert.Equal(t, field.DisplayConfiguration{"2", "", field.AllianceStationDisplay,
map[string]string{"station": "R2"}}, message["2"].DisplayConfiguration)
assert.Equal(t, 1, message["2"].ConnectionCount)
assert.Equal(t, "127.0.0.1", message["2"].IpAddress)
}
// 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)
displayConfig := field.DisplayConfiguration{Id: "1", Nickname: "Audience Display", Type: field.AudienceDisplay,
Configuration: map[string]string{"background": "#00f", "reversed": "true"}}
ws.Write("configureDisplay", displayConfig)
message = readDisplayConfiguration(t, ws)
assert.Equal(t, expectedDisplay1, message.Displays["1"])
assert.Equal(t, expectedDisplay1.ToUrl(), message.DisplayUrls["1"])
assert.Equal(t, displayConfig, message["1"].DisplayConfiguration)
assert.Equal(t, "/displays/audience?displayId=1&nickname=Audience+Display&background=%2300f&reversed=true",
readWebsocketType(t, displayWs1, "displayConfiguration"))
}
func TestSetupDisplaysWebsocketReloadDisplays(t *testing.T) {
@@ -82,7 +82,7 @@ func TestSetupDisplaysWebsocketReloadDisplays(t *testing.T) {
displayConn, _, _ := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/display/websocket?displayId=1", nil)
defer displayConn.Close()
displayWs := websocket.NewTestWebsocket(displayConn)
readDisplayConfiguration(t, displayWs)
assert.Equal(t, "/display?displayId=1", readWebsocketType(t, displayWs, "displayConfiguration"))
readDisplayConfiguration(t, ws)
// Reset a display selectively and verify the resulting message.
@@ -92,10 +92,10 @@ func TestSetupDisplaysWebsocketReloadDisplays(t *testing.T) {
assert.Equal(t, nil, readWebsocketType(t, displayWs, "reload"))
}
func readDisplayConfiguration(t *testing.T, ws *websocket.Websocket) *field.DisplayConfigurationMessage {
func readDisplayConfiguration(t *testing.T, ws *websocket.Websocket) map[string]field.Display {
message := readWebsocketType(t, ws, "displayConfiguration")
var displayConfigurationMessage field.DisplayConfigurationMessage
var displayConfigurationMessage map[string]field.Display
err := mapstructure.Decode(message, &displayConfigurationMessage)
assert.Nil(t, err)
return &displayConfigurationMessage
return displayConfigurationMessage
}

View File

@@ -39,7 +39,7 @@ func (web *Web) twitchDisplayWebsocketHandler(w http.ResponseWriter, r *http.Req
handleWebErr(w, err)
return
}
defer web.arena.MarkDisplayDisconnected(display)
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
ws, err := websocket.NewWebsocket(w, r)
if err != nil {
@@ -49,5 +49,5 @@ func (web *Web) twitchDisplayWebsocketHandler(w http.ResponseWriter, r *http.Req
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)
ws.HandleNotifiers(display.Notifier, web.arena.ReloadDisplaysNotifier)
}