mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 21:56:50 -04:00
Added audience display.
This commit is contained in:
64
arena.go
64
arena.go
@@ -46,6 +46,7 @@ type RealtimeScore struct {
|
||||
CurrentScore Score
|
||||
CurrentCycle Cycle
|
||||
AutoPreloadedBalls int
|
||||
AutoLeftoverBalls int
|
||||
Fouls []Foul
|
||||
AutoCommitted bool
|
||||
TeleopCommitted bool
|
||||
@@ -55,23 +56,27 @@ type RealtimeScore struct {
|
||||
}
|
||||
|
||||
type Arena struct {
|
||||
AllianceStations map[string]*AllianceStation
|
||||
MatchState int
|
||||
CanStartMatch bool
|
||||
matchTiming MatchTiming
|
||||
currentMatch *Match
|
||||
redRealtimeScore *RealtimeScore
|
||||
blueRealtimeScore *RealtimeScore
|
||||
matchStartTime time.Time
|
||||
lastDsPacketTime time.Time
|
||||
matchStateNotifier *Notifier
|
||||
matchTimeNotifier *Notifier
|
||||
robotStatusNotifier *Notifier
|
||||
matchLoadTeamsNotifier *Notifier
|
||||
scorePostedNotifier *Notifier
|
||||
lastMatchState int
|
||||
lastMatchTimeSec float64
|
||||
savedMatchResult *MatchResult
|
||||
AllianceStations map[string]*AllianceStation
|
||||
MatchState int
|
||||
CanStartMatch bool
|
||||
matchTiming MatchTiming
|
||||
currentMatch *Match
|
||||
redRealtimeScore *RealtimeScore
|
||||
blueRealtimeScore *RealtimeScore
|
||||
matchStartTime time.Time
|
||||
lastDsPacketTime time.Time
|
||||
matchStateNotifier *Notifier
|
||||
matchTimeNotifier *Notifier
|
||||
robotStatusNotifier *Notifier
|
||||
matchLoadTeamsNotifier *Notifier
|
||||
realtimeScoreNotifier *Notifier
|
||||
scorePostedNotifier *Notifier
|
||||
audienceDisplayNotifier *Notifier
|
||||
audienceDisplayScreen string
|
||||
lastMatchState int
|
||||
lastMatchTimeSec float64
|
||||
savedMatch *Match
|
||||
savedMatchResult *MatchResult
|
||||
}
|
||||
|
||||
var mainArena Arena // Named thusly to avoid polluting the global namespace with something more generic.
|
||||
@@ -95,13 +100,20 @@ func (arena *Arena) Setup() {
|
||||
arena.matchTimeNotifier = NewNotifier()
|
||||
arena.robotStatusNotifier = NewNotifier()
|
||||
arena.matchLoadTeamsNotifier = NewNotifier()
|
||||
arena.realtimeScoreNotifier = NewNotifier()
|
||||
arena.scorePostedNotifier = NewNotifier()
|
||||
arena.audienceDisplayNotifier = NewNotifier()
|
||||
|
||||
// Load empty match as current.
|
||||
arena.MatchState = PRE_MATCH
|
||||
arena.LoadTestMatch()
|
||||
arena.lastMatchState = -1
|
||||
arena.lastMatchTimeSec = 0
|
||||
|
||||
// Initialize display parameters.
|
||||
arena.audienceDisplayScreen = "blank"
|
||||
arena.savedMatch = &Match{}
|
||||
arena.savedMatchResult = &MatchResult{}
|
||||
}
|
||||
|
||||
// Loads a team into an alliance station, cleaning up the previous team there if there is one.
|
||||
@@ -184,6 +196,7 @@ func (arena *Arena) LoadMatch(match *Match) error {
|
||||
arena.blueRealtimeScore = new(RealtimeScore)
|
||||
|
||||
arena.matchLoadTeamsNotifier.Notify(nil)
|
||||
arena.realtimeScoreNotifier.Notify(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -274,6 +287,8 @@ func (arena *Arena) AbortMatch() error {
|
||||
return fmt.Errorf("Cannot abort match when it is not in progress.")
|
||||
}
|
||||
arena.MatchState = POST_MATCH
|
||||
arena.audienceDisplayScreen = "blank"
|
||||
arena.audienceDisplayNotifier.Notify(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -322,6 +337,8 @@ func (arena *Arena) Update() {
|
||||
auto = true
|
||||
enabled = true
|
||||
sendDsPacket = true
|
||||
arena.audienceDisplayScreen = "match"
|
||||
arena.audienceDisplayNotifier.Notify(nil)
|
||||
case AUTO_PERIOD:
|
||||
auto = true
|
||||
enabled = true
|
||||
@@ -357,6 +374,8 @@ func (arena *Arena) Update() {
|
||||
auto = false
|
||||
enabled = false
|
||||
sendDsPacket = true
|
||||
arena.audienceDisplayScreen = "blank"
|
||||
arena.audienceDisplayNotifier.Notify(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,3 +421,14 @@ func (arena *Arena) sendDsPacket(auto bool, enabled bool) {
|
||||
}
|
||||
arena.lastDsPacketTime = time.Now()
|
||||
}
|
||||
|
||||
func (realtimeScore *RealtimeScore) Score() int {
|
||||
score := scoreSummary(&realtimeScore.CurrentScore, []Foul{}).Score
|
||||
if realtimeScore.CurrentCycle.Truss {
|
||||
score += 10
|
||||
if realtimeScore.CurrentCycle.Catch {
|
||||
score += 10
|
||||
}
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
200
displays.go
200
displays.go
@@ -20,6 +20,157 @@ var rules = []string{"G3", "G5", "G10", "G11", "G12", "G14", "G15", "G16", "G17"
|
||||
"G23", "G24", "G25", "G26", "G26-1", "G27", "G28", "G29", "G30", "G31", "G32", "G34", "G35", "G36", "G37",
|
||||
"G38", "G39", "G40", "G41", "G42"}
|
||||
|
||||
// Renders the audience display to be chroma keyed over the video feed.
|
||||
func AudienceDisplayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
template := template.New("").Funcs(templateHelpers)
|
||||
_, err := template.ParseFiles("templates/audience_display.html")
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
*EventSettings
|
||||
}{eventSettings}
|
||||
err = template.ExecuteTemplate(w, "audience_display.html", data)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The websocket endpoint for the audience display client to receive status updates.
|
||||
func AudienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
websocket, err := NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer websocket.Close()
|
||||
|
||||
audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen()
|
||||
defer close(audienceDisplayListener)
|
||||
matchLoadTeamsListener := mainArena.matchLoadTeamsNotifier.Listen()
|
||||
defer close(matchLoadTeamsListener)
|
||||
matchTimeListener := mainArena.matchTimeNotifier.Listen()
|
||||
defer close(matchTimeListener)
|
||||
realtimeScoreListener := mainArena.realtimeScoreNotifier.Listen()
|
||||
defer close(realtimeScoreListener)
|
||||
scorePostedListener := mainArena.scorePostedNotifier.Listen()
|
||||
defer close(scorePostedListener)
|
||||
|
||||
// Send the various notifications immediately upon connection.
|
||||
var data interface{}
|
||||
err = websocket.Write("matchTiming", mainArena.matchTiming)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)})
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("setAudienceDisplay", mainArena.audienceDisplayScreen)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
Match *Match
|
||||
MatchName string
|
||||
}{mainArena.currentMatch, mainArena.currentMatch.CapitalizedType()}
|
||||
err = websocket.Write("setMatch", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
RedScore int
|
||||
RedCycle Cycle
|
||||
BlueScore int
|
||||
BlueCycle Cycle
|
||||
}{mainArena.redRealtimeScore.Score(), mainArena.redRealtimeScore.CurrentCycle,
|
||||
mainArena.blueRealtimeScore.Score(), mainArena.blueRealtimeScore.CurrentCycle}
|
||||
err = websocket.Write("realtimeScore", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data = struct {
|
||||
Match *Match
|
||||
MatchName string
|
||||
RedScore *ScoreSummary
|
||||
BlueScore *ScoreSummary
|
||||
}{mainArena.savedMatch, mainArena.savedMatch.CapitalizedType(),
|
||||
mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary()}
|
||||
err = websocket.Write("setFinalScore", data)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
for {
|
||||
var messageType string
|
||||
var message interface{}
|
||||
select {
|
||||
case _, ok := <-audienceDisplayListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setAudienceDisplay"
|
||||
message = mainArena.audienceDisplayScreen
|
||||
case _, ok := <-matchLoadTeamsListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setMatch"
|
||||
message = struct {
|
||||
Match *Match
|
||||
MatchName string
|
||||
}{mainArena.currentMatch, mainArena.currentMatch.CapitalizedType()}
|
||||
case matchTimeSec, ok := <-matchTimeListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "matchTime"
|
||||
message = MatchTimeMessage{mainArena.MatchState, matchTimeSec.(int)}
|
||||
case _, ok := <-realtimeScoreListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "realtimeScore"
|
||||
message = struct {
|
||||
RedScore int
|
||||
RedCycle Cycle
|
||||
BlueScore int
|
||||
BlueCycle Cycle
|
||||
}{mainArena.redRealtimeScore.Score(), mainArena.redRealtimeScore.CurrentCycle,
|
||||
mainArena.blueRealtimeScore.Score(), mainArena.blueRealtimeScore.CurrentCycle}
|
||||
case _, ok := <-scorePostedListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setFinalScore"
|
||||
message = struct {
|
||||
Match *Match
|
||||
MatchName string
|
||||
RedScore *ScoreSummary
|
||||
BlueScore *ScoreSummary
|
||||
}{mainArena.savedMatch, mainArena.savedMatch.CapitalizedType(),
|
||||
mainArena.savedMatchResult.RedScoreSummary(), mainArena.savedMatchResult.BlueScoreSummary()}
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
// The client has probably closed the connection; nothing to do here.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Renders the pit display which shows scrolling rankings.
|
||||
func PitDisplayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
template, err := template.ParseFiles("templates/pit_display.html")
|
||||
@@ -58,17 +209,10 @@ func AnnouncerDisplayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Assemble info about the saved match result.
|
||||
var redScoreSummary, blueScoreSummary *ScoreSummary
|
||||
var savedMatchType, savedMatchDisplayName string
|
||||
if mainArena.savedMatchResult != nil {
|
||||
redScoreSummary = mainArena.savedMatchResult.RedScoreSummary()
|
||||
blueScoreSummary = mainArena.savedMatchResult.BlueScoreSummary()
|
||||
match, err := db.GetMatchById(mainArena.savedMatchResult.MatchId)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
savedMatchType = match.CapitalizedType()
|
||||
savedMatchDisplayName = match.DisplayName
|
||||
}
|
||||
savedMatchType = mainArena.savedMatch.CapitalizedType()
|
||||
savedMatchDisplayName = mainArena.savedMatch.DisplayName
|
||||
redScoreSummary = mainArena.savedMatchResult.RedScoreSummary()
|
||||
blueScoreSummary = mainArena.savedMatchResult.BlueScoreSummary()
|
||||
data := struct {
|
||||
*EventSettings
|
||||
MatchType string
|
||||
@@ -115,8 +259,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
data := MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)}
|
||||
err = websocket.Write("matchTime", data)
|
||||
err = websocket.Write("matchTime", MatchTimeMessage{mainArena.MatchState, int(mainArena.lastMatchTimeSec)})
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
@@ -157,7 +300,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, _, err := websocket.Read()
|
||||
messageType, data, err := websocket.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
@@ -168,9 +311,16 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
switch messageType {
|
||||
case "setAudienceDisplay":
|
||||
screen, ok := data.(string)
|
||||
if !ok {
|
||||
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
continue
|
||||
}
|
||||
mainArena.audienceDisplayScreen = screen
|
||||
mainArena.audienceDisplayNotifier.Notify(nil)
|
||||
default:
|
||||
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,17 +466,20 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
(*score).CurrentCycle.DeadBall = false
|
||||
}
|
||||
case "assist":
|
||||
if !(*score).TeleopCommitted && (*score).CurrentCycle.Assists < 3 {
|
||||
if (*score).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
|
||||
(*score).CurrentCycle.Assists < 3 {
|
||||
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
|
||||
(*score).CurrentCycle.Assists += 1
|
||||
}
|
||||
case "truss":
|
||||
if !(*score).TeleopCommitted && !(*score).CurrentCycle.Truss {
|
||||
if (*score).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
|
||||
!(*score).CurrentCycle.Truss {
|
||||
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
|
||||
(*score).CurrentCycle.Truss = true
|
||||
}
|
||||
case "catch":
|
||||
if !(*score).TeleopCommitted && !(*score).CurrentCycle.Catch && (*score).CurrentCycle.Truss {
|
||||
if (*score).AutoCommitted && !(*score).TeleopCommitted && (*score).AutoLeftoverBalls == 0 &&
|
||||
!(*score).CurrentCycle.Catch && (*score).CurrentCycle.Truss {
|
||||
(*score).undoCycles = append((*score).undoCycles, (*score).CurrentCycle)
|
||||
(*score).CurrentCycle.Catch = true
|
||||
}
|
||||
@@ -339,15 +492,15 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
case "commit":
|
||||
if !(*score).AutoCommitted {
|
||||
(*score).AutoLeftoverBalls = (*score).AutoPreloadedBalls - (*score).CurrentScore.AutoHighHot -
|
||||
(*score).CurrentScore.AutoHigh - (*score).CurrentScore.AutoLowHot -
|
||||
(*score).CurrentScore.AutoLow
|
||||
(*score).AutoCommitted = true
|
||||
} else if !(*score).TeleopCommitted {
|
||||
if (*score).CurrentCycle.ScoredHigh || (*score).CurrentCycle.ScoredLow ||
|
||||
(*score).CurrentCycle.DeadBall {
|
||||
// Check whether this is a leftover ball from autonomous.
|
||||
if ((*score).AutoPreloadedBalls - (*score).CurrentScore.AutoHighHot -
|
||||
(*score).CurrentScore.AutoHigh - (*score).CurrentScore.AutoLowHot -
|
||||
(*score).CurrentScore.AutoLow - (*score).CurrentScore.AutoClearHigh -
|
||||
(*score).CurrentScore.AutoClearLow - (*score).CurrentScore.AutoClearDead) > 0 {
|
||||
if (*score).AutoLeftoverBalls > 0 {
|
||||
if (*score).CurrentCycle.ScoredHigh {
|
||||
(*score).CurrentScore.AutoClearHigh += 1
|
||||
} else if (*score).CurrentCycle.ScoredLow {
|
||||
@@ -355,6 +508,7 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
(*score).CurrentScore.AutoClearDead += 1
|
||||
}
|
||||
(*score).AutoLeftoverBalls -= 1
|
||||
} else {
|
||||
(*score).CurrentScore.Cycles = append((*score).CurrentScore.Cycles, (*score).CurrentCycle)
|
||||
}
|
||||
@@ -382,6 +536,8 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
|
||||
mainArena.realtimeScoreNotifier.Notify(nil)
|
||||
|
||||
// Send out the score again after handling the command, as it most likely changed as a result.
|
||||
err = websocket.Write("score", *score)
|
||||
if err != nil {
|
||||
|
||||
@@ -119,7 +119,16 @@ func MatchPlayLoadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func MatchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
matchId, _ := strconv.Atoi(vars["matchId"])
|
||||
matchResult, err := db.GetMatchResultForMatch(matchId)
|
||||
match, err := db.GetMatchById(matchId)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
if match == nil {
|
||||
handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId))
|
||||
return
|
||||
}
|
||||
matchResult, err := db.GetMatchResultForMatch(match.Id)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
@@ -128,6 +137,7 @@ func MatchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
handleWebErr(w, fmt.Errorf("No result found for match ID %d.", matchId))
|
||||
return
|
||||
}
|
||||
mainArena.savedMatch = match
|
||||
mainArena.savedMatchResult = matchResult
|
||||
mainArena.scorePostedNotifier.Notify(nil)
|
||||
|
||||
@@ -147,6 +157,8 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer close(matchTimeListener)
|
||||
robotStatusListener := mainArena.robotStatusNotifier.Listen()
|
||||
defer close(robotStatusListener)
|
||||
audienceDisplayListener := mainArena.audienceDisplayNotifier.Listen()
|
||||
defer close(audienceDisplayListener)
|
||||
|
||||
// Send the various notifications immediately upon connection.
|
||||
err = websocket.Write("status", mainArena)
|
||||
@@ -165,6 +177,11 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
err = websocket.Write("setAudienceDisplay", mainArena.audienceDisplayScreen)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Spin off a goroutine to listen for notifications and pass them on through the websocket.
|
||||
go func() {
|
||||
@@ -184,6 +201,12 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
messageType = "status"
|
||||
message = mainArena
|
||||
case _, ok := <-audienceDisplayListener:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messageType = "setAudienceDisplay"
|
||||
message = mainArena.audienceDisplayScreen
|
||||
}
|
||||
err = websocket.Write(messageType, message)
|
||||
if err != nil {
|
||||
@@ -284,6 +307,15 @@ func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
continue // Skip sending the status update, as the client is about to terminate and reload.
|
||||
case "setAudienceDisplay":
|
||||
screen, ok := data.(string)
|
||||
if !ok {
|
||||
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
||||
continue
|
||||
}
|
||||
mainArena.audienceDisplayScreen = screen
|
||||
mainArena.audienceDisplayNotifier.Notify(nil)
|
||||
continue
|
||||
default:
|
||||
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
||||
continue
|
||||
|
||||
@@ -172,6 +172,7 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "setAudienceDisplay")
|
||||
|
||||
// Test that a server-side error is communicated to the client.
|
||||
ws.Write("nonexistenttype", nil)
|
||||
@@ -219,6 +220,7 @@ func TestMatchPlayWebsocketCommands(t *testing.T) {
|
||||
assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match")
|
||||
ws.Write("abortMatch", nil)
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "setAudienceDisplay")
|
||||
assert.Equal(t, POST_MATCH, mainArena.MatchState)
|
||||
ws.Write("commitResults", nil)
|
||||
readWebsocketType(t, ws, "reload")
|
||||
@@ -249,6 +251,7 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
|
||||
readWebsocketType(t, ws, "status")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "setAudienceDisplay")
|
||||
|
||||
mainArena.AllianceStations["R1"].Bypass = true
|
||||
mainArena.AllianceStations["R2"].Bypass = true
|
||||
@@ -258,10 +261,13 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
|
||||
mainArena.AllianceStations["B3"].Bypass = true
|
||||
mainArena.StartMatch()
|
||||
mainArena.Update()
|
||||
statusReceived, matchTime := readWebsocketStatusMatchTime(t, ws)
|
||||
messages := readWebsocketMultiple(t, ws, 3)
|
||||
statusReceived, matchTime := getStatusMatchTime(t, messages)
|
||||
assert.Equal(t, true, statusReceived)
|
||||
assert.Equal(t, 2, matchTime.MatchState)
|
||||
assert.Equal(t, 0, matchTime.MatchTimeSec)
|
||||
_, ok := messages["setAudienceDisplay"]
|
||||
assert.True(t, ok)
|
||||
|
||||
// Should get a tick notification when an integer second threshold is crossed.
|
||||
mainArena.matchStartTime = time.Now().Add(-time.Second + 10*time.Millisecond) // Not crossed yet
|
||||
@@ -290,22 +296,18 @@ func TestMatchPlayWebsocketNotifications(t *testing.T) {
|
||||
assert.Equal(t, mainArena.matchTiming.AutoDurationSec, matchTime.MatchTimeSec)
|
||||
}
|
||||
|
||||
// Handles the status and matchTime messaegs arriving in either order.
|
||||
// Handles the status and matchTime messages arriving in either order.
|
||||
func readWebsocketStatusMatchTime(t *testing.T, ws *Websocket) (bool, MatchTimeMessage) {
|
||||
statusReceived := false
|
||||
return getStatusMatchTime(t, readWebsocketMultiple(t, ws, 2))
|
||||
}
|
||||
|
||||
func getStatusMatchTime(t *testing.T, messages map[string]interface{}) (bool, MatchTimeMessage) {
|
||||
_, statusReceived := messages["status"]
|
||||
message, ok := messages["matchTime"]
|
||||
var matchTime MatchTimeMessage
|
||||
for i := 0; i < 2; i++ {
|
||||
messageType, message, err := ws.Read()
|
||||
if assert.Nil(t, err) {
|
||||
if messageType == "status" {
|
||||
statusReceived = true
|
||||
} else {
|
||||
if assert.Equal(t, "matchTime", messageType) {
|
||||
err = mapstructure.Decode(message, &matchTime)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if assert.True(t, ok) {
|
||||
err := mapstructure.Decode(message, &matchTime)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
return statusReceived, matchTime
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<!--
|
||||
Copyright 2014 Team 254. All Rights Reserved.
|
||||
Author: nick@team254.com (Nick Eyre)
|
||||
-->
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Cheesy Arena - Audience Screen</title>
|
||||
<meta name="generator" content="Cheesy Arena" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/static/css/fonts/roboto-regular/stylesheet.css" type="text/css" charset="utf-8" />
|
||||
<link rel="stylesheet" href="/static/css/audience.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id='topcontainer'></div>
|
||||
|
||||
<div class='controlpanel'>
|
||||
<h3>Control Panel</h3>
|
||||
<button onclick="openScreen('logo');">Logo</button><br />
|
||||
<button onclick="openScreen('blank');">None</button><br />
|
||||
</button>
|
||||
|
||||
<script src="/static/lib/jquery.min.js"></script>
|
||||
<script src="/static/lib/jquery.transit.min.js"></script>
|
||||
<script src="/static/js/audience.js"></script>
|
||||
|
||||
<script type='text/x-template' class='template' id='blank'></script>
|
||||
<script type='text/x-template' class='template' id='logo'>
|
||||
<div class='blinds right background'> </div>
|
||||
<div class='blinds right center blank'> </div>
|
||||
<div class='blinds left background'> </div>
|
||||
<div class='blinds left center'> </div>
|
||||
<div class='blinds left center blank'> </div>
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Team 254. All Rights Reserved.
|
||||
Author: nick@team254.com (Nick Eyre)
|
||||
*/
|
||||
|
||||
/* Control Panel (Temporary) */
|
||||
.controlpanel {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
z-index: 99;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
/* Top Level Container */
|
||||
html,body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#topcontainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Design Element: Blinds */
|
||||
.blinds {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background-size: 200%;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
.blinds.left {
|
||||
background-position: left;
|
||||
left: -50%;
|
||||
}
|
||||
.blinds.right {
|
||||
background-position: right;
|
||||
right: -50%;
|
||||
}
|
||||
.blinds.full {
|
||||
width: 100%;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
/* Screen: Logo */
|
||||
#logo .blinds.background {
|
||||
background-image: url('/static/img/endofmatch-bg.png');
|
||||
}
|
||||
#logo .blinds.center {
|
||||
background-image: url('/static/img/endofmatch-center.png');
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
#logo .blinds.center.blank {
|
||||
background-image: url('/static/img/endofmatch-center-blank.png');
|
||||
}
|
||||
277
static/css/audience_display.css
Normal file
277
static/css/audience_display.css
Normal file
@@ -0,0 +1,277 @@
|
||||
html {
|
||||
cursor: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
#centering {
|
||||
position: absolute; left: 50%;
|
||||
bottom: -340px;
|
||||
}
|
||||
#matchOverlay {
|
||||
position: relative;
|
||||
left: -50%;
|
||||
bottom: 70px;
|
||||
margin: 0 auto;
|
||||
height: 104px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #000;
|
||||
color: #000;
|
||||
font-size: 22px;
|
||||
}
|
||||
.teams {
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
display: table;
|
||||
font-family: "FuturaLT";
|
||||
}
|
||||
.valign-cell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#redTeams {
|
||||
float: left;
|
||||
border-right: 1px solid #000;
|
||||
}
|
||||
#blueTeams {
|
||||
float: right;
|
||||
border-left: 1px solid #000;
|
||||
}
|
||||
.score {
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
}
|
||||
#redScore {
|
||||
float: left;
|
||||
background-color: #ff4444;
|
||||
}
|
||||
#blueScore {
|
||||
float: left;
|
||||
background-color: #2080ff;
|
||||
}
|
||||
#eventMatchInfo {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
bottom: 0px;
|
||||
line-height: 30px;
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
padding: 0px 5px;
|
||||
font-family: "FuturaLT";
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
z-index: -1;
|
||||
}
|
||||
#matchCircle {
|
||||
position: absolute;
|
||||
left: -75px;
|
||||
bottom: 50px;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
.score-number {
|
||||
width: 60%;
|
||||
margin: 0px 5px;
|
||||
text-align: center;
|
||||
font-family: "FuturaLTBold";
|
||||
font-size: 55px;
|
||||
line-height: 80px;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
}
|
||||
.score-fields {
|
||||
padding: 0px 15px;
|
||||
margin: -10px 0px;
|
||||
opacity: 0;
|
||||
}
|
||||
.assist {
|
||||
float: left;
|
||||
width: 22px;
|
||||
height: 12px;
|
||||
margin: 10px 4px;
|
||||
background-color: #fff;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.trussCatch {
|
||||
float: left;
|
||||
font-family: "FuturaLTBold";
|
||||
margin: 0px 5px;
|
||||
color: #fff;
|
||||
opacity: 0.3;
|
||||
}
|
||||
[data-on=true] {
|
||||
opacity: 1;
|
||||
}
|
||||
#logo {
|
||||
position: relative;
|
||||
top: 45px;
|
||||
height: 60px;
|
||||
}
|
||||
#matchTime {
|
||||
position: relative;
|
||||
top: 30px;
|
||||
height: 60px;
|
||||
color: #000;
|
||||
font-family: "FuturaLTBold";
|
||||
font-size: 32px;
|
||||
opacity: 0;
|
||||
}
|
||||
#blindsContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.blinds {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background-size: 200%;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
.blinds.left {
|
||||
background-position: left;
|
||||
left: -50%;
|
||||
}
|
||||
.blinds.right {
|
||||
background-position: right;
|
||||
right: -50%;
|
||||
}
|
||||
.blinds.full {
|
||||
width: 100%;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
}
|
||||
.blinds.background {
|
||||
background-image: url("/static/img/endofmatch-bg.png");
|
||||
}
|
||||
.blinds.center {
|
||||
position: fixed;
|
||||
background-image: url("/static/img/endofmatch-center.png");
|
||||
}
|
||||
.blinds.center-blank {
|
||||
background-image: url("/static/img/endofmatch-center-blank.png");
|
||||
-webkit-backface-visibility: hidden;
|
||||
z-index: 3;
|
||||
}
|
||||
#blindsCenter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto auto;
|
||||
border-radius: 50%;
|
||||
width: 310px;
|
||||
height: 310px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #333;
|
||||
box-shadow: 0 0 5px #666;
|
||||
text-align: center;
|
||||
-webkit-backface-visibility: hidden;
|
||||
z-index: 2;
|
||||
transform: rotateY(-180deg);
|
||||
}
|
||||
#blindsLogo {
|
||||
position: relative;
|
||||
top: 85px;
|
||||
height: 140px;
|
||||
}
|
||||
#finalScore {
|
||||
position: fixed;
|
||||
width: 800px;
|
||||
height: 450px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto auto;
|
||||
border: 2px solid #333;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
background-color: #444;
|
||||
}
|
||||
.final-score {
|
||||
float: left;
|
||||
width: 50%;
|
||||
height: 44%;
|
||||
line-height: 200px;
|
||||
border-bottom: 2px solid #333;
|
||||
color: #fff;
|
||||
font-family: "FuturaLTBold";
|
||||
font-size: 100px;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 3px #333;
|
||||
}
|
||||
#redFinalScore {
|
||||
background-color: #ff4444;
|
||||
padding-right: 150px;
|
||||
}
|
||||
#blueFinalScore {
|
||||
clear: right;
|
||||
background-color: #2080ff;
|
||||
padding-left: 150px;
|
||||
}
|
||||
.final-teams {
|
||||
float: left;
|
||||
width: 50%;
|
||||
height: 11%;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-family: "FuturaLT";
|
||||
font-size: 35px;
|
||||
}
|
||||
.final-teams span {
|
||||
margin: 0px 20px;
|
||||
}
|
||||
#redFinalTeams {
|
||||
clear: left;
|
||||
border-right: 2px solid #333;
|
||||
}
|
||||
#blueFinalTeam {
|
||||
}
|
||||
.final-breakdown {
|
||||
float: left;
|
||||
width: 33%;
|
||||
height: 34%;
|
||||
padding: 0px 20px;
|
||||
display: table;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
font-family: "FuturaLT";
|
||||
font-size: 30px;
|
||||
}
|
||||
#redFinalBreakdown {
|
||||
clear: left;
|
||||
text-align: right;
|
||||
}
|
||||
#blueFinalBreakdown {
|
||||
text-align: left;
|
||||
}
|
||||
#centerFinalBreakdown {
|
||||
width: 34%;
|
||||
border-left: 2px solid #333;
|
||||
border-right: 2px solid #333;
|
||||
}
|
||||
#finalEventMatchInfo {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
height: 11%;
|
||||
line-height: 50px;
|
||||
padding: 0px 25px;
|
||||
font-family: "FuturaLT";
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -8,6 +8,14 @@
|
||||
}
|
||||
|
||||
/* New styles. */
|
||||
@font-face {
|
||||
font-family: "FuturaLTBold";
|
||||
src: url("fonts/futura-lt-bold.otf") format("opentype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "FuturaLT";
|
||||
src: url("fonts/futura-lt.otf") format("opentype");
|
||||
}
|
||||
.red-text {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
BIN
static/css/fonts/futura-lt-bold.otf
Normal file
BIN
static/css/fonts/futura-lt-bold.otf
Normal file
Binary file not shown.
BIN
static/css/fonts/futura-lt.otf
Normal file
BIN
static/css/fonts/futura-lt.otf
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
16
static/img/logo-min.svg
Normal file
16
static/img/logo-min.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="1276.184px" height="982.562px" viewBox="0 0 1276.184 982.562" enable-background="new 0 0 1276.184 982.562"
|
||||
xml:space="preserve">
|
||||
<path fill="#231F20" d="M507.973,863.748c-39.596-39.314-71.906-85.14-96.543-135.638
|
||||
c-95.381-38.679-163.341-134.439-163.341-255.029c0-96.801,68.419-187.122,164.313-224.21
|
||||
c27.115-50.321,63.396-96.359,108.284-136.231c41.487-36.849,89.22-67.072,140.599-89.991C611.907,7.935,559.958,0,507.134,0
|
||||
C243.231,0,0,194.585,0,471.857C0,768.6,212.818,977.776,507.134,977.776c49.08,0,95.815-5.959,139.75-17.021
|
||||
C595.167,936.198,548.224,903.724,507.973,863.748z"/>
|
||||
<path fill="#0070FF" d="M910.72,751.506c-142.264,0-259.015-115.534-259.015-273.641c0-126.474,116.751-241.995,259.015-241.995
|
||||
c73.258,0,139.664,30.651,186.944,77.602l162.968-178.936C1167.11,52.621,1041.488,4.803,910.72,4.803
|
||||
c-263.872,0-507.116,194.579-507.116,471.851c0,296.73,212.83,505.908,507.116,505.908c148.021,0,275.382-52.934,365.464-143.273
|
||||
l-181.566-166.412C1047.551,722.176,982.388,751.506,910.72,751.506z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -8,9 +8,6 @@ var blinkTimeout;
|
||||
|
||||
var handleMatchTime = function(data) {
|
||||
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
|
||||
console.log(matchState);
|
||||
console.log(matchStateText);
|
||||
console.log(countdownSec);
|
||||
$("#matchState").text(matchStateText);
|
||||
$("#matchTime").text(getCountdown(data.MatchState, data.MatchTimeSec));
|
||||
if (matchState == "PRE_MATCH" || matchState == "POST_MATCH") {
|
||||
@@ -22,8 +19,7 @@ var handleMatchTime = function(data) {
|
||||
var postMatchResult = function(data) {
|
||||
clearTimeout(blinkTimeout);
|
||||
$("#savedMatchResult").attr("data-blink", false);
|
||||
|
||||
// TODO(patrick): Signal audience display.
|
||||
websocket.send("setAudienceDisplay", "score");
|
||||
}
|
||||
|
||||
$(function() {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: nick@team254.com (Nick Eyre)
|
||||
|
||||
var screens = {
|
||||
|
||||
blank: {
|
||||
init: function(cb){callback(cb);},
|
||||
open: function(cb){callback(cb);},
|
||||
close: function(cb){callback(cb);}
|
||||
},
|
||||
|
||||
logo: {
|
||||
init: function(cb){
|
||||
$('.blinds.center:not(.blank)').css({rotateY: '-180deg'});
|
||||
callback(cb);
|
||||
},
|
||||
open: function(cb){
|
||||
closeBlinds(function(){
|
||||
setTimeout(function(){
|
||||
$('.blinds.center.blank').transition({rotateY: '180deg'});
|
||||
$('.blinds.center:not(.blank)').transition({rotateY: '0deg'}, function(){
|
||||
callback(cb);
|
||||
});
|
||||
}, 400);
|
||||
});
|
||||
},
|
||||
close: function(cb){
|
||||
$('.blinds.center.blank').transition({rotateY: '360deg'});
|
||||
$('.blinds.center:not(.blank)').transition({rotateY: '180deg'}, function(){
|
||||
openBlinds(callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var currentScreen = 'blank';
|
||||
function openScreen(screen){
|
||||
|
||||
// If Screen Exists
|
||||
if(typeof(screens[screen]) == 'object' && $('.template#'+screen).length > 0 && currentScreen != screen){
|
||||
|
||||
// Initialize New Screen
|
||||
$('#topcontainer').append("<div class='container' id='"+screen+"'>"+$('.template#'+screen).html()+"</div>");
|
||||
screens[screen].init(function(){
|
||||
|
||||
// Close Current Screen
|
||||
screens[currentScreen].close(function(){
|
||||
|
||||
// Open New Screen
|
||||
currentScreen = screen;
|
||||
screens[screen].open();
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function callback(cb){
|
||||
if(typeof(cb) == 'function')
|
||||
cb();
|
||||
}
|
||||
|
||||
function closeBlinds(cb){
|
||||
$('.blinds.right').transition({right: 0});
|
||||
$('.blinds.left').transition({left: 0}, function(){
|
||||
$(this).addClass('full');
|
||||
callback(cb);
|
||||
});
|
||||
}
|
||||
|
||||
function openBlinds(cb){
|
||||
$('.blinds.right').show();
|
||||
$('.blinds.left').removeClass('full');
|
||||
$('.blinds.right').show().transition({right: '-50%'});
|
||||
$('.blinds.left').transition({left: '-50%'}, function(){
|
||||
callback(cb);
|
||||
});
|
||||
}
|
||||
239
static/js/audience_display.js
Normal file
239
static/js/audience_display.js
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
//
|
||||
// Client-side methods for the audience display.
|
||||
|
||||
var transitionMap;
|
||||
var currentScreen = "blank";
|
||||
|
||||
var handleSetAudienceDisplay = function(targetScreen) {
|
||||
if (targetScreen == currentScreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
transitions = transitionMap[currentScreen][targetScreen];
|
||||
if (transitions == null) {
|
||||
// There is no direct transition defined; need to go to the blank screen first.
|
||||
transitions = function() {
|
||||
transitionMap[currentScreen]["blank"](transitionMap["blank"][targetScreen]);
|
||||
};
|
||||
}
|
||||
transitions();
|
||||
|
||||
currentScreen = targetScreen;
|
||||
};
|
||||
|
||||
var handleSetMatch = function(data) {
|
||||
$("#redTeam1").text(data.Match.Red1)
|
||||
$("#redTeam2").text(data.Match.Red2)
|
||||
$("#redTeam3").text(data.Match.Red3)
|
||||
$("#blueTeam1").text(data.Match.Blue1)
|
||||
$("#blueTeam2").text(data.Match.Blue2)
|
||||
$("#blueTeam3").text(data.Match.Blue3)
|
||||
$("#matchName").text(data.MatchName + " " + data.Match.DisplayName);
|
||||
};
|
||||
|
||||
var handleMatchTime = function(data) {
|
||||
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
|
||||
var countdownString = String(countdownSec % 60);
|
||||
if (countdownString.length == 1) {
|
||||
countdownString = "0" + countdownString;
|
||||
}
|
||||
countdownString = Math.floor(countdownSec / 60) + ":" + countdownString;
|
||||
$("#matchTime").text(countdownString);
|
||||
});
|
||||
};
|
||||
|
||||
var handleRealtimeScore = function(data) {
|
||||
$("#redScoreNumber").text(data.RedScore);
|
||||
$("#redAssist1").attr("data-on", data.RedCycle.Assists >= 1);
|
||||
$("#redAssist2").attr("data-on", data.RedCycle.Assists >= 2);
|
||||
$("#redAssist3").attr("data-on", data.RedCycle.Assists >= 3);
|
||||
$("#redTruss").attr("data-on", data.RedCycle.Truss);
|
||||
$("#redCatch").attr("data-on", data.RedCycle.Catch);
|
||||
$("#blueScoreNumber").text(data.BlueScore);
|
||||
$("#blueAssist1").attr("data-on", data.BlueCycle.Assists >= 1);
|
||||
$("#blueAssist2").attr("data-on", data.BlueCycle.Assists >= 2);
|
||||
$("#blueAssist3").attr("data-on", data.BlueCycle.Assists >= 3);
|
||||
$("#blueTruss").attr("data-on", data.BlueCycle.Truss);
|
||||
$("#blueCatch").attr("data-on", data.BlueCycle.Catch);
|
||||
};
|
||||
|
||||
var handleSetFinalScore = function(data) {
|
||||
$("#redFinalScore").text(data.RedScore.Score);
|
||||
$("#redFinalTeam1").text(data.Match.Red1);
|
||||
$("#redFinalTeam2").text(data.Match.Red2);
|
||||
$("#redFinalTeam3").text(data.Match.Red3);
|
||||
$("#redFinalAuto").text(data.RedScore.AutoPoints);
|
||||
$("#redFinalTeleop").text(data.RedScore.TeleopPoints);
|
||||
$("#redFinalFoul").text(data.RedScore.FoulPoints);
|
||||
$("#blueFinalScore").text(data.BlueScore.Score);
|
||||
$("#blueFinalTeam1").text(data.Match.Blue1);
|
||||
$("#blueFinalTeam2").text(data.Match.Blue2);
|
||||
$("#blueFinalTeam3").text(data.Match.Blue3);
|
||||
$("#blueFinalAuto").text(data.BlueScore.AutoPoints);
|
||||
$("#blueFinalTeleop").text(data.BlueScore.TeleopPoints);
|
||||
$("#blueFinalFoul").text(data.BlueScore.FoulPoints);
|
||||
$("#finalMatchName").text(data.MatchName + " " + data.Match.DisplayName);
|
||||
};
|
||||
|
||||
var transitionBlankToIntro = function(callback) {
|
||||
$("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() {
|
||||
$(".teams").transition({queue: false, width: "75px"}, 100, "linear", function() {
|
||||
$(".score").transition({queue: false, width: "120px"}, 500, "ease", function() {
|
||||
$("#eventMatchInfo").show();
|
||||
var height = -$("#eventMatchInfo").height();
|
||||
$("#eventMatchInfo").transition({queue: false, bottom: height + "px"}, 500, "ease", callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var transitionIntroToInMatch = function(callback) {
|
||||
$("#logo").transition({queue: false, top: "25px"}, 500, "ease");
|
||||
$(".score").transition({queue: false, width: "230px"}, 500, "ease", function() {
|
||||
$(".score-number").transition({queue: false, opacity: 1}, 750, "ease");
|
||||
$(".score-fields").transition({queue: false, opacity: 1}, 750, "ease");
|
||||
$("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback);
|
||||
});
|
||||
};
|
||||
|
||||
var transitionIntroToBlank = function(callback) {
|
||||
$("#eventMatchInfo").transition({queue: false, bottom: "0px"}, 500, "ease", function() {
|
||||
$("#eventMatchInfo").hide();
|
||||
$(".score").transition({queue: false, width: "0px"}, 500, "ease");
|
||||
$(".teams").transition({queue: false, width: "40px"}, 500, "ease", function() {
|
||||
$("#centering").transition({queue: false, bottom: "-340px"}, 1000, "ease", callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var transitionBlankToInMatch = function(callback) {
|
||||
$("#centering").transition({queue: false, bottom: "0px"}, 500, "ease", function() {
|
||||
$(".teams").transition({queue: false, width: "75px"}, 100, "linear", function() {
|
||||
$("#logo").transition({queue: false, top: "25px"}, 500, "ease");
|
||||
$(".score").transition({queue: false, width: "230px"}, 500, "ease", function() {
|
||||
$("#eventMatchInfo").show();
|
||||
$(".score-number").transition({queue: false, opacity: 1}, 750, "ease");
|
||||
$(".score-fields").transition({queue: false, opacity: 1}, 750, "ease");
|
||||
$("#matchTime").transition({queue: false, opacity: 1}, 750, "ease", callback);
|
||||
var height = -$("#eventMatchInfo").height();
|
||||
$("#eventMatchInfo").transition({queue: false, bottom: height + "px"}, 500, "ease", callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var transitionInMatchToIntro = function(callback) {
|
||||
$(".score-number").transition({queue: false, opacity: 0}, 300, "linear");
|
||||
$(".score-fields").transition({queue: false, opacity: 0}, 300, "linear");
|
||||
$("#matchTime").transition({queue: false, opacity: 0}, 300, "linear", function() {
|
||||
$("#logo").transition({queue: false, top: "45px"}, 500, "ease");
|
||||
$(".score").transition({queue: false, width: "120px"}, 500, "ease");
|
||||
$(".teams").transition({queue: false, width: "75px"}, 500, "ease", callback);
|
||||
});
|
||||
};
|
||||
|
||||
var transitionInMatchToBlank = function(callback) {
|
||||
$("#eventMatchInfo").transition({queue: false, bottom: "0px"}, 500, "ease");
|
||||
$("#matchTime").transition({queue: false, opacity: 0}, 300, "linear");
|
||||
$(".score-number").transition({queue: false, opacity: 0}, 300, "linear");
|
||||
$(".score-fields").transition({queue: false, opacity: 0}, 300, "linear", function() {
|
||||
$("#eventMatchInfo").hide();
|
||||
$("#logo").transition({queue: false, top: "45px"}, 500, "ease");
|
||||
$(".score").transition({queue: false, width: "0px"}, 500, "ease");
|
||||
$(".teams").transition({queue: false, width: "40px"}, 500, "ease", function() {
|
||||
$("#centering").transition({queue: false, bottom: "-340px"}, 1000, "ease", callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var transitionBlankToLogo = function(callback) {
|
||||
$(".blinds.right").transition({queue: false, right: 0}, 1000, "ease");
|
||||
$(".blinds.left").transition({queue: false, left: 0}, 1000, "ease", function() {
|
||||
$(".blinds.left").addClass("full");
|
||||
$(".blinds.right").hide();
|
||||
$(".blinds.center-blank").css({rotateY: "0deg"});
|
||||
setTimeout(function() {
|
||||
$(".blinds.center-blank").transition({queue: false, rotateY: "180deg"}, 500, "ease");
|
||||
$("#blindsCenter").transition({queue: false, rotateY: "0deg"}, 500, "ease", callback);
|
||||
}, 200);
|
||||
});
|
||||
};
|
||||
|
||||
var transitionLogoToBlank = function(callback) {
|
||||
$(".blinds.center-blank").transition({queue: false, rotateY: "360deg"}, 500, "ease");
|
||||
$("#blindsCenter").transition({queue: false, rotateY: "180deg"}, 500, "ease", function() {
|
||||
setTimeout(function() {
|
||||
$(".blinds.left").removeClass("full");
|
||||
$(".blinds.right").show();
|
||||
$(".blinds.right").transition({queue: false, right: "-50%"}, 1000, "ease");
|
||||
$(".blinds.left").transition({queue: false, left: "-50%"}, 1000, "ease", callback);
|
||||
}, 200);
|
||||
});
|
||||
};
|
||||
|
||||
var transitionLogoToScore = function(callback) {
|
||||
$("#blindsCenter").transition({queue: false, top: "-350px"}, 750, "ease", function () {
|
||||
$("#finalScore").show();
|
||||
$("#finalScore").transition({queue: false, opacity: 1}, 1000, "ease", callback);
|
||||
});
|
||||
};
|
||||
|
||||
var transitionBlankToScore = function(callback) {
|
||||
transitionBlankToLogo(function() {
|
||||
setTimeout(function() { transitionLogoToScore(callback); }, 100);
|
||||
});
|
||||
};
|
||||
|
||||
var transitionScoreToLogo = function(callback) {
|
||||
$("#finalScore").transition({queue: false, opacity: 0}, 500, "linear", function() {
|
||||
$("#finalScore").hide();
|
||||
$("#blindsCenter").transition({queue: false, top: 0}, 750, "ease", callback);
|
||||
});
|
||||
};
|
||||
|
||||
var transitionScoreToBlank = function(callback) {
|
||||
transitionScoreToLogo(function() {
|
||||
transitionLogoToBlank(callback);
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
// Set up the websocket back to the server.
|
||||
websocket = new CheesyWebsocket("/displays/audience/websocket", {
|
||||
setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); },
|
||||
setMatch: function(event) { handleSetMatch(event.data); },
|
||||
matchTiming: function(event) { handleMatchTiming(event.data); },
|
||||
matchTime: function(event) { handleMatchTime(event.data); },
|
||||
realtimeScore: function(event) { handleRealtimeScore(event.data); },
|
||||
setFinalScore: function(event) { handleSetFinalScore(event.data); }
|
||||
});
|
||||
|
||||
// Map how to transition from one screen to another. Missing links between screens indicate that first we
|
||||
// must transition to the blank screen and then to the target screen.
|
||||
transitionMap = {
|
||||
blank: {
|
||||
intro: transitionBlankToIntro,
|
||||
match: transitionBlankToInMatch,
|
||||
score: transitionBlankToScore,
|
||||
logo: transitionBlankToLogo
|
||||
},
|
||||
intro: {
|
||||
blank: transitionIntroToBlank,
|
||||
match: transitionIntroToInMatch
|
||||
},
|
||||
match: {
|
||||
blank: transitionInMatchToBlank,
|
||||
intro: transitionInMatchToIntro
|
||||
},
|
||||
score: {
|
||||
blank: transitionScoreToBlank,
|
||||
logo: transitionScoreToLogo
|
||||
},
|
||||
logo: {
|
||||
blank: transitionLogoToBlank,
|
||||
score: transitionLogoToScore
|
||||
}
|
||||
}
|
||||
});
|
||||
10
static/js/lib/jquery.transit.min.js
vendored
10
static/js/lib/jquery.transit.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -29,6 +29,10 @@ var discardResults = function() {
|
||||
websocket.send("discardResults");
|
||||
};
|
||||
|
||||
var setAudienceDisplay = function() {
|
||||
websocket.send("setAudienceDisplay", $("input[name=audienceDisplay]:checked").val());
|
||||
};
|
||||
|
||||
var handleStatus = function(data) {
|
||||
// Update the team status view.
|
||||
$.each(data.AllianceStations, function(station, stationStatus) {
|
||||
@@ -91,6 +95,10 @@ var handleMatchTime = function(data) {
|
||||
});
|
||||
};
|
||||
|
||||
var handleSetAudienceDisplay = function(data) {
|
||||
$("input[name=audienceDisplay][value=" + data + "]").prop("checked", true);
|
||||
};
|
||||
|
||||
$(function() {
|
||||
// Activate tooltips above the status headers.
|
||||
$("[data-toggle=tooltip]").tooltip({"placement": "top"});
|
||||
@@ -99,6 +107,7 @@ $(function() {
|
||||
websocket = new CheesyWebsocket("/match_play/websocket", {
|
||||
status: function(event) { handleStatus(event.data); },
|
||||
matchTiming: function(event) { handleMatchTiming(event.data); },
|
||||
matchTime: function(event) { handleMatchTime(event.data); }
|
||||
matchTime: function(event) { handleMatchTime(event.data); },
|
||||
setAudienceDisplay: function(event) { handleSetAudienceDisplay(event.data); }
|
||||
});
|
||||
});
|
||||
|
||||
110
templates/audience_display.html
Normal file
110
templates/audience_display.html
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Audience Display - {{.EventSettings.Name}} - Cheesy Arena </title>
|
||||
<link rel="shortcut icon" href="/static/img/favicon32.png">
|
||||
<link rel="stylesheet" href="/static/css/lib/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/static/css/cheesy-arena.css" />
|
||||
<link rel="stylesheet" href="/static/css/audience_display.css" />
|
||||
</head>
|
||||
<body style="background-color: {{.EventSettings.DisplayBackgroundColor}};">
|
||||
<div id="centering">
|
||||
<div id="matchOverlay">
|
||||
<div class="teams" id="redTeams">
|
||||
<span class="valign-cell">
|
||||
<span id="redTeam1"></span><br />
|
||||
<span id="redTeam2"></span><br />
|
||||
<span id="redTeam3"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="score" id="redScore">
|
||||
<div class="score-number" id="redScoreNumber"> </div>
|
||||
<div class="score-fields">
|
||||
<div class="assist" id="redAssist1"></div>
|
||||
<div class="assist" id="redAssist2"></div>
|
||||
<div class="assist" id="redAssist3"></div>
|
||||
<div class="trussCatch" id="redTruss">T</div>
|
||||
<div class="trussCatch" id="redCatch">C</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score" id="blueScore">
|
||||
<div class="score-number pull-right" id="blueScoreNumber"> </div>
|
||||
<div class="score-fields pull-right">
|
||||
<div class="trussCatch" id="blueTruss">T</div>
|
||||
<div class="trussCatch" id="blueCatch">C</div>
|
||||
<div class="assist" id="blueAssist3"></div>
|
||||
<div class="assist" id="blueAssist2"></div>
|
||||
<div class="assist" id="blueAssist1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="teams" id="blueTeams">
|
||||
<span class="valign-cell">
|
||||
<span id="blueTeam1"></span><br />
|
||||
<span id="blueTeam2"></span><br />
|
||||
<span id="blueTeam3"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="eventMatchInfo">
|
||||
<span>{{.EventSettings.Name}} 2014</span>
|
||||
<span class="pull-right" id="matchName"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center" id="matchCircle">
|
||||
<img id="logo" src="/static/img/logo-min.svg"</img>
|
||||
<div id="matchTime"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="blindsContainer">
|
||||
<div class="blinds right background"></div>
|
||||
<div class="blinds right center-blank"></div>
|
||||
<div class="blinds left background"></div>
|
||||
<div id="blindsCenter">
|
||||
<img id="blindsLogo" src="/static/img/logo-min.svg"</img>
|
||||
</div>
|
||||
<div class="blinds left center-blank"></div>
|
||||
<div id="finalScore">
|
||||
<div class="final-score" id="redFinalScore"></div>
|
||||
<div class="final-score" id="blueFinalScore"></div>
|
||||
<div class="final-teams" id="redFinalTeams">
|
||||
<span id="redFinalTeam1"></span>
|
||||
<span id="redFinalTeam2"></span>
|
||||
<span id="redFinalTeam3"></span>
|
||||
</div>
|
||||
<div class="final-teams" id="blueFinalTeams">
|
||||
<span id="blueFinalTeam1"></span>
|
||||
<span id="blueFinalTeam2"></span>
|
||||
<span id="blueFinalTeam3"></span>
|
||||
</div>
|
||||
<div class="final-breakdown" id="redFinalBreakdown">
|
||||
<span class="valign-cell">
|
||||
<span id="redFinalAuto"></span><br />
|
||||
<span id="redFinalTeleop"></span><br />
|
||||
<span id="redFinalFoul"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="final-breakdown" id="centerFinalBreakdown">
|
||||
<span class="valign-cell">Autonomous</br>Teleoperated</br>Foul</span>
|
||||
</div>
|
||||
<div class="final-breakdown" id="blueFinalBreakdown">
|
||||
<span class="valign-cell">
|
||||
<span id="blueFinalAuto"></span><br />
|
||||
<span id="blueFinalTeleop"></span><br />
|
||||
<span id="blueFinalFoul"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="finalEventMatchInfo">
|
||||
<span>{{.EventSettings.Name}} 2014</span>
|
||||
<span class="pull-right" id="finalMatchName"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/lib/handlebars-1.3.0.js"></script>
|
||||
<script src="/static/js/lib/jquery.min.js"></script>
|
||||
<script src="/static/js/lib/jquery.json-2.4.min.js"></script>
|
||||
<script src="/static/js/lib/jquery.websocket-0.0.1.js"></script>
|
||||
<script src="/static/js/lib/jquery.transit.min.js"></script>
|
||||
<script src="/static/js/cheesy-websocket.js"></script>
|
||||
<script src="/static/js/match_timing.js"></script>
|
||||
<script src="/static/js/audience_display.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -55,8 +55,8 @@
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Display</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="disabled"><a href="#">Audience</a></li>
|
||||
<li><a target="_blank" href="/displays/pit">Pit</a></li>
|
||||
<li><a href="/displays/audience">Audience</a></li>
|
||||
<li><a href="/displays/pit">Pit</a></li>
|
||||
<li><a href="/displays/announcer">Announcer</a></li>
|
||||
<li><a href="/displays/referee">Referee</a></li>
|
||||
<li><a href="/displays/scoring/red">Scoring – Red</a></li>
|
||||
|
||||
@@ -46,12 +46,12 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8 text-center">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="row text-center">
|
||||
<div id="matchState" class="col-lg-2 col-lg-offset-4 well well-sm"> </div>
|
||||
<div id="matchTime" class="col-lg-2 well well-sm"> </div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row text-center">
|
||||
<div class="col-lg-6 well well-darkblue">
|
||||
<div class="row form-group">
|
||||
<div class="col-lg-4">Blue Teams</div>
|
||||
@@ -77,7 +77,7 @@
|
||||
{{template "matchPlayTeam" dict "team" .Match.Red1 "color" "R" "position" 1 "data" .}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="row text-center">
|
||||
<button type="button" id="startMatch" class="btn btn-success btn-lg btn-match-play"
|
||||
onclick="startMatch();" disabled>
|
||||
Start Match
|
||||
@@ -96,6 +96,39 @@
|
||||
Discard Results
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-lg-3 well">
|
||||
Audience Display
|
||||
<div class="form-group">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="audienceDisplay" value="blank" onclick="setAudienceDisplay();">Blank
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="audienceDisplay" value="intro" onclick="setAudienceDisplay();">Intro
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="audienceDisplay" value="match" onclick="setAudienceDisplay();">Match
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="audienceDisplay" value="score" onclick="setAudienceDisplay();">Final Score
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="audienceDisplay" value="logo" onclick="setAudienceDisplay();">Logo
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="confirmCommitResults" class="modal" style="top: 20%;">
|
||||
|
||||
2
web.go
2
web.go
@@ -133,6 +133,8 @@ func newHandler() http.Handler {
|
||||
router.HandleFunc("/reports/pdf/schedule/{type}", SchedulePdfReportHandler).Methods("GET")
|
||||
router.HandleFunc("/reports/csv/teams", TeamsCsvReportHandler).Methods("GET")
|
||||
router.HandleFunc("/reports/pdf/teams", TeamsPdfReportHandler).Methods("GET")
|
||||
router.HandleFunc("/displays/audience", AudienceDisplayHandler).Methods("GET")
|
||||
router.HandleFunc("/displays/audience/websocket", AudienceDisplayWebsocketHandler).Methods("GET")
|
||||
router.HandleFunc("/displays/pit", PitDisplayHandler).Methods("GET")
|
||||
router.HandleFunc("/displays/announcer", AnnouncerDisplayHandler).Methods("GET")
|
||||
router.HandleFunc("/displays/announcer/websocket", AnnouncerDisplayWebsocketHandler).Methods("GET")
|
||||
|
||||
11
web_test.go
11
web_test.go
@@ -63,3 +63,14 @@ func readWebsocketType(t *testing.T, ws *Websocket, expectedMessageType string)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func readWebsocketMultiple(t *testing.T, ws *Websocket, count int) map[string]interface{} {
|
||||
messages := make(map[string]interface{})
|
||||
for i := 0; i < count; i++ {
|
||||
messageType, message, err := ws.Read()
|
||||
if assert.Nil(t, err) {
|
||||
messages[messageType] = message
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user