Files
cheesy-arena-lite/field/display.go
2018-09-16 22:27:28 -07:00

186 lines
5.4 KiB
Go

// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Model representing and methods for controlling a remote web display.
package field
import (
"fmt"
"math/rand"
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"sync"
)
const (
minDisplayId = 100
maxDisplayId = 999
)
type DisplayType int
const (
InvalidDisplay DisplayType = iota
PlaceholderDisplay
AllianceStationDisplay
AnnouncerDisplay
AudienceDisplay
FieldMonitorDisplay
PitDisplay
)
var DisplayTypeNames = map[DisplayType]string{
PlaceholderDisplay: "Placeholder",
AllianceStationDisplay: "Alliance Station",
AnnouncerDisplay: "Announcer",
AudienceDisplay: "Audience",
FieldMonitorDisplay: "Field Monitor",
PitDisplay: "Pit",
}
var displayTypePaths = map[DisplayType]string{
PlaceholderDisplay: "/display",
AllianceStationDisplay: "/displays/alliance_station",
AnnouncerDisplay: "/displays/announcer",
AudienceDisplay: "/displays/audience",
FieldMonitorDisplay: "/displays/fta",
PitDisplay: "/displays/pit",
}
var displayRegistryMutex sync.Mutex
type Display struct {
Id string
Nickname string
Type DisplayType
Configuration map[string]string
IpAddress string
ConnectionCount int
}
// Parses the given display URL path and query string to extract the configuration.
func DisplayFromUrl(path string, query map[string][]string) (*Display, error) {
if _, ok := query["displayId"]; !ok {
return nil, fmt.Errorf("Display ID not present in request.")
}
var display Display
display.Id = query["displayId"][0]
if nickname, ok := query["nickname"]; ok {
display.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
break
}
}
if display.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)
for key, value := range query {
if key != "displayId" && key != "nickname" {
display.Configuration[key], _ = url.QueryUnescape(value[0])
}
}
return &display, 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("?displayId=")
builder.WriteString(url.QueryEscape(display.Id))
if display.Nickname != "" {
builder.WriteString("&nickname=")
builder.WriteString(url.QueryEscape(display.Nickname))
}
// Sort the keys so that the URL generated is deterministic.
var keys []string
for key := range display.Configuration {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
builder.WriteString("&")
builder.WriteString(url.QueryEscape(key))
builder.WriteString("=")
builder.WriteString(url.QueryEscape(display.Configuration[key]))
}
return builder.String()
}
// Returns an unused ID that can be used for a new display.
func (arena *Arena) NextDisplayId() string {
// Loop until we get an ID that isn't already used. This is inefficient if there is a large number of displays, but
// that should never be the case.
for {
candidateId := strconv.Itoa(rand.Intn(maxDisplayId+1-minDisplayId) + minDisplayId)
if _, ok := arena.Displays[candidateId]; !ok {
return candidateId
}
}
}
// Adds the given display to the arena registry and triggers a notification.
func (arena *Arena) RegisterDisplay(display *Display) {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
existingDisplay, ok := arena.Displays[display.Id]
if ok && display.Type == PlaceholderDisplay && existingDisplay.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++
} else {
if ok {
display.ConnectionCount = existingDisplay.ConnectionCount + 1
} else {
display.ConnectionCount = 1
}
arena.Displays[display.Id] = display
}
arena.DisplayConfigurationNotifier.Notify()
}
// Updates the given display in the arena registry. Triggers a notification if the display configuration changed.
func (arena *Arena) UpdateDisplay(display *Display) error {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
existingDisplay, ok := arena.Displays[display.Id]
if !ok {
return fmt.Errorf("Display %s doesn't exist.", display.Id)
}
display.ConnectionCount = existingDisplay.ConnectionCount
if !reflect.DeepEqual(existingDisplay, display) {
arena.Displays[display.Id] = display
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) {
displayRegistryMutex.Lock()
defer displayRegistryMutex.Unlock()
if display, ok := arena.Displays[display.Id]; ok {
display.ConnectionCount -= 1
arena.DisplayConfigurationNotifier.Notify()
}
}