Cleaned up the code and added comments.

This commit is contained in:
Patrick Fairbank
2014-09-06 22:25:12 -07:00
parent 247d5d1fee
commit d86a46f12e
35 changed files with 143 additions and 27 deletions

View File

@@ -75,6 +75,7 @@ func ConfigureTeamWifi(red1, red2, red3, blue1, blue2, blue3 *Team) error {
removeSsidsCommand += fmt.Sprintf("no dot11 ssid %s\n", ssid)
}
// Build and run the overall command to do everything in a single telnet session.
command := removeSsidsCommand + addSsidsCommand + associateSsidsCommand
if len(command) > 0 {
_, err = runAironetConfigCommand(removeSsidsCommand + addSsidsCommand + associateSsidsCommand)

View File

@@ -24,8 +24,10 @@ func AllianceStationDisplayHandler(w http.ResponseWriter, r *http.Request) {
displayId := ""
if _, ok := r.URL.Query()["displayId"]; ok {
// Register the display in memory by its ID so that it can be configured to a certain station.
displayId = r.URL.Query()["displayId"][0]
}
data := struct {
*EventSettings
DisplayId string
@@ -202,6 +204,7 @@ func AllianceStationDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reque
switch messageType {
case "setAllianceStation":
// The client knows what station it is (e.g. across a server restart) and is informing the server.
station, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))

View File

@@ -184,6 +184,7 @@ func AnnouncerDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
switch messageType {
case "setAudienceDisplay":
// The announcer can make the final score screen show when they are ready to announce the score.
screen, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))

View File

@@ -1,3 +1,5 @@
! Baseline configuration for the Cisco Aironet AP1252AG access point. Load this into the AP prior to
! configuring Cheesy Arena to connect to it. Default user/pass is cheesyarena/1234Five.
!
version 15.2
no service pad

3
api.go
View File

@@ -55,6 +55,7 @@ func MatchesApiHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Generates a JSON dump of the sponsor slides for use by the audience display.
func SponsorSlidesApiHandler(w http.ResponseWriter, r *http.Request) {
sponsors, err := db.GetAllSponsorSlides()
if err != nil {
@@ -76,7 +77,7 @@ func SponsorSlidesApiHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Generates a JSON dump of the qualification rankings.
// Generates a JSON dump of the qualification rankings, primarily for use by the pit display.
func RankingsApiHandler(w http.ResponseWriter, r *http.Request) {
rankings, err := db.GetAllRankings()
if err != nil {

View File

@@ -20,13 +20,13 @@ const (
// Progression of match states.
const (
PRE_MATCH = iota
START_MATCH
AUTO_PERIOD
PAUSE_PERIOD
TELEOP_PERIOD
ENDGAME_PERIOD
POST_MATCH
PRE_MATCH = 0
START_MATCH = 1
AUTO_PERIOD = 2
PAUSE_PERIOD = 3
TELEOP_PERIOD = 4
ENDGAME_PERIOD = 5
POST_MATCH = 6
)
type AllianceStation struct {
@@ -229,16 +229,16 @@ func (arena *Arena) LoadMatch(match *Match) error {
arena.redRealtimeScore = NewRealtimeScore()
arena.blueRealtimeScore = NewRealtimeScore()
// Notify any listeners about the new match.
arena.matchLoadTeamsNotifier.Notify(nil)
arena.realtimeScoreNotifier.Notify(nil)
arena.allianceStationDisplayScreen = "match"
arena.allianceStationDisplayNotifier.Notify(nil)
return nil
}
// Sets a new test match as the current match.
// Sets a new test match containing no teams as the current match.
func (arena *Arena) LoadTestMatch() error {
return arena.LoadMatch(&Match{Type: "test"})
}
@@ -488,8 +488,6 @@ func (arena *Arena) Update() {
// Send a packet if at a period transition point or if it's been long enough since the last one.
if sendDsPacket || time.Since(arena.lastDsPacketTime).Seconds()*1000 >= dsPacketPeriodMs {
arena.sendDsPacket(auto, enabled)
// TODO(pat): Come up with better criteria for sending robot status updates.
arena.robotStatusNotifier.Notify(nil)
}
@@ -519,6 +517,7 @@ func (arena *Arena) sendDsPacket(auto bool, enabled bool) {
arena.lastDsPacketTime = time.Now()
}
// Calculates the integer score value for the given realtime snapshot.
func (realtimeScore *RealtimeScore) Score(opponentFouls []Foul) int {
score := scoreSummary(&realtimeScore.CurrentScore, opponentFouls).Score
if realtimeScore.CurrentCycle.Truss {

View File

@@ -58,6 +58,7 @@ func ConfigureTeamEthernet(red1, red2, red3, blue1, blue2, blue3 *Team) error {
removeTeamVlansCommand += fmt.Sprintf("interface Vlan%d\nno ip address\nno access-list 1%d\n", vlan, vlan)
}
// Build and run the overall command to do everything in a single telnet session.
command := removeTeamVlansCommand + addTeamVlansCommand
if len(command) > 0 {
_, err = runCatalystConfigCommand(removeTeamVlansCommand + addTeamVlansCommand)

View File

@@ -123,7 +123,9 @@ func ListenForDsPackets(listener *net.UDPConn) {
}
}
// Called at the start of the match to allow for driver station initialization.
func (dsConn *DriverStationConnection) signalMatchStart(match *Match) error {
// Zero out missed packet count and begin logging.
dsConn.missedPacketOffset = dsConn.DriverStationStatus.MissedPacketCount
var err error
dsConn.log, err = NewTeamMatchLog(dsConn.TeamId, match)

View File

@@ -210,6 +210,7 @@ func (database *Database) buildEliminationMatchSet(round int, group int, numAlli
return []AllianceTeam{}, nil
}
// Creates a match at the given point in the elimination bracket and populates the teams.
func createMatch(roundName string, round int, group int, instance int, redAlliance []AllianceTeam, blueAlliance []AllianceTeam) *Match {
match := Match{Type: "elimination", DisplayName: fmt.Sprintf("%s-%d", roundName, instance),
ElimRound: round, ElimGroup: group, ElimInstance: instance}
@@ -218,6 +219,7 @@ func createMatch(roundName string, round int, group int, instance int, redAllian
return &match
}
// Assigns the first three teams from the alliance randomly into the red team slots for the match.
func shuffleRedTeams(match *Match, alliance []AllianceTeam) {
shuffle := rand.Perm(3)
match.Red1 = alliance[shuffle[0]].TeamId
@@ -225,6 +227,7 @@ func shuffleRedTeams(match *Match, alliance []AllianceTeam) {
match.Red3 = alliance[shuffle[2]].TeamId
}
// Assigns the first three teams from the alliance randomly into the blue team slots for the match.
func shuffleBlueTeams(match *Match, alliance []AllianceTeam) {
shuffle := rand.Perm(3)
match.Blue1 = alliance[shuffle[0]].TeamId
@@ -232,6 +235,7 @@ func shuffleBlueTeams(match *Match, alliance []AllianceTeam) {
match.Blue3 = alliance[shuffle[2]].TeamId
}
// Returns true if the given team is part of the given alliance.
func teamInAlliance(teamId int, alliance []AllianceTeam) bool {
for _, allianceTeam := range alliance {
if teamId == allianceTeam.TeamId {

View File

@@ -23,6 +23,7 @@ type Lights struct {
animationCount int
}
// Sets the color by name and transition time for the given LED channel.
func (lightPacket *LightPacket) setColorFade(channel int, color string, fade byte) {
switch color {
case "off":
@@ -44,10 +45,12 @@ func (lightPacket *LightPacket) setColorFade(channel int, color string, fade byt
}
}
// Sets the color by name with instant transition for the given LED channel.
func (lightPacket *LightPacket) setColor(channel int, color string) {
lightPacket.setColorFade(channel, color, 0)
}
// Sets the color by RGB values and transition time for the given LED channel.
func (lightPacket *LightPacket) setRgbFade(channel int, red byte, green byte, blue byte, fade byte) {
lightPacket[channel*4] = red
lightPacket[channel*4+1] = green
@@ -55,10 +58,12 @@ func (lightPacket *LightPacket) setRgbFade(channel int, red byte, green byte, bl
lightPacket[channel*4+3] = fade
}
// Sets the color by name with instant transition for all LED channels.
func (lightPacket *LightPacket) setAllColor(color string) {
lightPacket.setAllColorFade(color, 0)
}
// Sets the color by name and transition time for all LED channels.
func (lightPacket *LightPacket) setAllColorFade(color string, fade byte) {
for i := 0; i < 8; i++ {
lightPacket.setColorFade(i, color, fade)
@@ -118,6 +123,7 @@ func (lights *Lights) SetupConnections() error {
return nil
}
// Makes a goal for the given alliance hot.
func (lights *Lights) SetHotGoal(alliance string, leftSide bool) {
if leftSide {
lights.packets[alliance].setColor(0, "off")
@@ -139,6 +145,7 @@ func (lights *Lights) SetHotGoal(alliance string, leftSide bool) {
lights.sendLights()
}
// Lights up the given alliance's goal for the given number of assists.
func (lights *Lights) SetAssistGoal(alliance string, numAssists int) {
lights.packets[alliance].setColor(0, "off")
lights.packets[alliance].setColor(1, "off")
@@ -162,6 +169,7 @@ func (lights *Lights) SetAssistGoal(alliance string, numAssists int) {
lights.sendLights()
}
// Turns off all lights for the given alliance's goal.
func (lights *Lights) ClearGoal(alliance string) {
lights.packets[alliance].setColorFade(0, "off", 10)
lights.packets[alliance].setColorFade(1, "off", 10)
@@ -171,6 +179,7 @@ func (lights *Lights) ClearGoal(alliance string) {
lights.packets[alliance].setColorFade(5, "off", 10)
}
// Turns on the given alliance's pedestal.
func (lights *Lights) SetPedestal(alliance string) {
if alliance == "red" {
lights.packets["blue"].setColor(6, alliance)
@@ -180,6 +189,7 @@ func (lights *Lights) SetPedestal(alliance string) {
lights.sendLights()
}
// Turns off the given alliance's pedestal.
func (lights *Lights) ClearPedestal(alliance string) {
if alliance == "red" {
lights.packets["blue"].setColorFade(6, "off", 10)
@@ -189,12 +199,14 @@ func (lights *Lights) ClearPedestal(alliance string) {
lights.sendLights()
}
// Turns all lights green to signal that the field is safe to enter.
func (lights *Lights) SetFieldReset() {
lights.packets["red"].setAllColor("green")
lights.packets["blue"].setAllColor("green")
lights.sendLights()
}
// Sets the lights to the given non-match mode for show or testing.
func (lights *Lights) SetMode(mode string) {
lights.currentMode = mode
lights.animationCount = 0
@@ -219,6 +231,7 @@ func (lights *Lights) SetMode(mode string) {
lights.sendLights()
}
// Sends a control packet to the LED controllers only if their state needs to be updated.
func (lights *Lights) sendLights() {
for alliance, connection := range lights.connections {
if lights.newConnections || *lights.packets[alliance] != *lights.oldPackets[alliance] {
@@ -235,6 +248,7 @@ func (lights *Lights) sendLights() {
lights.newConnections = false
}
// State machine for controlling light sequences in the non-match modes.
func (lights *Lights) animate() {
lights.animationCount += 1

View File

@@ -14,10 +14,12 @@ const eventDbPath = "./event.db"
var db *Database
var eventSettings *EventSettings
// Main entry point for the application.
func main() {
rand.Seed(time.Now().UnixNano())
initDb()
// Run the webserver and DS packet listener in goroutines and use the main one for the arena state machine.
go ServeWebInterface()
listener, err := DsPacketListener()
checkErr(err)
@@ -26,6 +28,7 @@ func main() {
mainArena.Run()
}
// Opens the database and stores a handle to it in a global variable.
func initDb() {
var err error
db, err = OpenDatabase(eventDbPath)
@@ -34,6 +37,7 @@ func initDb() {
checkErr(err)
}
// Logs and exits the application if the given error is not nil.
func checkErr(err error) {
if err != nil {
log.Fatalln("Error: ", err)

View File

@@ -87,6 +87,7 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Loads the given match onto the arena in preparation for playing it.
func MatchPlayLoadHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
matchId, _ := strconv.Atoi(vars["matchId"])
@@ -435,6 +436,7 @@ func CommitMatchScore(match *Match, matchResult *MatchResult) error {
}
if match.Type != "practice" {
// Regenerate the residual yellow cards that teams may carry.
db.CalculateTeamCards(match.Type)
}
@@ -488,18 +490,22 @@ func CommitCurrentMatchScore() error {
return CommitMatchScore(mainArena.currentMatch, &matchResult)
}
// Helper function to implement the required interface for Sort.
func (list MatchPlayList) Len() int {
return len(list)
}
// Helper function to implement the required interface for Sort.
func (list MatchPlayList) Less(i, j int) bool {
return list[i].Status != "complete" && list[j].Status == "complete"
}
// Helper function to implement the required interface for Sort.
func (list MatchPlayList) Swap(i, j int) {
list[i], list[j] = list[j], list[i]
}
// Constructs the list of matches to display on the side of the match play interface.
func buildMatchPlayList(matchType string) (MatchPlayList, error) {
matches, err := db.GetMatchesByType(matchType)
if err != nil {

View File

@@ -124,6 +124,7 @@ func MatchReviewEditPostHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/match_review", 302)
}
// Load the match result for the match referenced in the HTTP query string.
func getMatchResultFromRequest(r *http.Request) (*Match, *MatchResult, error) {
vars := mux.Vars(r)
matchId, _ := strconv.Atoi(vars["matchId"])
@@ -146,6 +147,7 @@ func getMatchResultFromRequest(r *http.Request) (*Match, *MatchResult, error) {
return match, matchResult, nil
}
// Constructs the list of matches to display in the match review interface.
func buildMatchReviewList(matchType string) ([]MatchReviewListItem, error) {
matches, err := db.GetMatchesByType(matchType)
if err != nil {

View File

@@ -47,7 +47,7 @@ func (notifier *Notifier) notifyListener(listener chan interface{}, message inte
}()
// Do a non-blocking send. This guarantees that sending notifications won't interrupt the main event loop,
// at the risk of clients missing some messages.
// at the risk of clients missing some messages if they don't read them all promptly.
select {
case listener <- message:
// The notification was sent and received successfully.

View File

@@ -99,7 +99,7 @@ func (database *Database) CalculateRankings() error {
}
sortedRankings := sortRankings(rankings)
// Stuff the match results into the database in an atomic operation.
// Stuff the rankings into the database in an atomic operation to prevent messing them up halfway.
transaction, err := database.rankingMap.Begin()
if err != nil {
return err
@@ -174,6 +174,7 @@ func (database *Database) CalculateTeamCards(matchType string) error {
return nil
}
// Incrementally accounts for the given match result in the set of rankings that are being built.
func addMatchResultToRankings(rankings map[int]*Ranking, teamId int, matchResult *MatchResult, isRed bool) {
ranking := rankings[teamId]
if ranking == nil {
@@ -233,10 +234,12 @@ func sortRankings(rankings map[int]*Ranking) Rankings {
return sortedRankings
}
// Helper function to implement the required interface for Sort.
func (rankings Rankings) Len() int {
return len(rankings)
}
// Helper function to implement the required interface for Sort.
func (rankings Rankings) Less(i, j int) bool {
a := rankings[i]
b := rankings[j]
@@ -259,6 +262,7 @@ func (rankings Rankings) Less(i, j int) bool {
return a.QualificationScore*b.Played > b.QualificationScore*a.Played
}
// Helper function to implement the required interface for Sort.
func (rankings Rankings) Swap(i, j int) {
rankings[i], rankings[j] = rankings[j], rankings[i]
}

View File

@@ -91,6 +91,7 @@ func BuildRandomSchedule(teams []Team, scheduleBlocks []ScheduleBlock, matchType
return matches, nil
}
// Returns the total number of matches that can be run within the given schedule blocks.
func countMatches(scheduleBlocks []ScheduleBlock) int {
numMatches := 0
for _, block := range scheduleBlocks {

View File

@@ -249,6 +249,7 @@ func getOfficialTeamInfo(teamId int) (*Team, error) {
re := regexp.MustCompile("(?s).*<PRE>(.*)</PRE>.*")
teamsCsv := re.FindStringSubmatch(string(body))[1]
// Parse the tab-separated data.
reader := csv.NewReader(strings.NewReader(teamsCsv))
reader.Comma = '\t'
reader.FieldsPerRecord = -1

View File

@@ -1,3 +1,8 @@
/*
Copyright 2014 Team 254. All Rights Reserved.
Author: nick@team254.com (Nick Eyre)
*/
html {
-webkit-user-select: none;
-moz-user-select: none;

View File

@@ -1,3 +1,8 @@
/*
Copyright 2014 Team 254. All Rights Reserved.
Author: pat@patfairbank.com (Patrick Fairbank)
*/
html {
cursor: none;
-webkit-user-select: none;

View File

@@ -1,3 +1,8 @@
/*
Copyright 2014 Team 254. All Rights Reserved.
Author: pat@patfairbank.com (Patrick Fairbank)
*/
/* Bootstrap overrides. */
.form-control[disabled] {
cursor: default;

View File

@@ -1,6 +1,7 @@
/*
Copyright 2014 Team 254. All Rights Reserved.
Author: nick@team254.com (Nick Eyre)
Copyright 2014 Team 254. All Rights Reserved.
Author: nick@team254.com (Nick Eyre)
Author: pat@patfairbank.com (Patrick Fairbank)
*/
html {

View File

@@ -1,3 +1,8 @@
/*
Copyright 2014 Team 254. All Rights Reserved.
Author: pat@patfairbank.com (Patrick Fairbank)
*/
html {
cursor: default;
-webkit-user-select: none;

View File

@@ -8,6 +8,7 @@ var blinkInterval;
var currentScreen = "blank";
var websocket;
// Handles a websocket message to change which screen is displayed.
var handleSetAllianceStationDisplay = function(targetScreen) {
currentScreen = targetScreen;
if (allianceStation == "") {
@@ -28,6 +29,7 @@ var handleSetAllianceStationDisplay = function(targetScreen) {
}
};
// Handles a websocket message to update the team to display.
var handleSetMatch = function(data) {
if (allianceStation != "" && data.AllianceStation == "") {
// The client knows better what display this should be; let the server know.
@@ -52,6 +54,7 @@ var handleSetMatch = function(data) {
}
};
// Handles a websocket message to update the team connection status.
var handleStatus = function(data) {
stationStatus = data.AllianceStations[allianceStation];
var blink = false;
@@ -79,6 +82,7 @@ var handleStatus = function(data) {
}
};
// Handles a websocket message to update the match time countdown.
var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
var countdownString = String(countdownSec % 60);
@@ -91,11 +95,13 @@ var handleMatchTime = function(data) {
});
};
// Handles a websocket message to update the match score.
var handleRealtimeScore = function(data) {
$("#redScore").text(data.RedScore);
$("#blueScore").text(data.BlueScore);
};
// Handles a websocket message to show or hide the hot goal indication.
var handleHotGoalLight = function(side) {
if (allianceStation != "" && (side == "left" && allianceStation[1] == "3" ||
side == "right" && allianceStation[1] == "1")) {

View File

@@ -7,6 +7,7 @@ var websocket;
var teamTemplate = Handlebars.compile($("#teamTemplate").html());
var matchResultTemplate = Handlebars.compile($("#matchResultTemplate").html());
// Handles a websocket message to hide the score dialog once the next match is being introduced.
var handleSetAudienceDisplay = function(targetScreen) {
// Hide the final results so that they aren't blocking the current teams when the announcer needs them most.
if (targetScreen == "intro" || targetScreen == "match") {
@@ -14,6 +15,7 @@ var handleSetAudienceDisplay = function(targetScreen) {
}
};
// Handles a websocket message to update the teams for the current match.
var handleSetMatch = function(data) {
$("#matchName").text(data.MatchType + " Match " + data.MatchDisplayName);
$("#red1").html(teamTemplate(formatTeam(data.Red1)));
@@ -24,6 +26,7 @@ var handleSetMatch = function(data) {
$("#blue3").html(teamTemplate(formatTeam(data.Blue3)));
};
// Handles a websocket message to update the match time countdown.
var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
$("#matchState").text(matchStateText);
@@ -31,11 +34,13 @@ var handleMatchTime = function(data) {
});
};
// Handles a websocket message to update the match score.
var handleRealtimeScore = function(data) {
$("#redScore").text(data.RedScore);
$("#blueScore").text(data.BlueScore);
};
// Handles a websocket message to populate the final score data.
var handleSetFinalScore = function(data) {
console.log(data);
$("#scoreMatchName").text(data.MatchType + " Match " + data.MatchDisplayName);

View File

@@ -1,6 +1,6 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Authors: pat@patfairbank.com (Patrick Fairbank)
// nick@team254.com (Nick Eyre)
// Author: pat@patfairbank.com (Patrick Fairbank)
// Author: nick@team254.com (Nick Eyre)
//
// Client-side methods for the audience display.
@@ -9,6 +9,7 @@ var transitionMap;
var currentScreen = "blank";
var allianceSelectionTemplate = Handlebars.compile($("#allianceSelectionTemplate").html());
// Handles a websocket message to change which screen is displayed.
var handleSetAudienceDisplay = function(targetScreen) {
if (targetScreen == currentScreen) {
return;
@@ -26,6 +27,7 @@ var handleSetAudienceDisplay = function(targetScreen) {
currentScreen = targetScreen;
};
// Handles a websocket message to update the teams for the current match.
var handleSetMatch = function(data) {
$("#redTeam1").text(data.Match.Red1)
$("#redTeam2").text(data.Match.Red2)
@@ -36,6 +38,7 @@ var handleSetMatch = function(data) {
$("#matchName").text(data.MatchName + " " + data.Match.DisplayName);
};
// Handles a websocket message to update the match time countdown.
var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
var countdownString = String(countdownSec % 60);
@@ -47,6 +50,7 @@ var handleMatchTime = function(data) {
});
};
// Handles a websocket message to update the match score.
var handleRealtimeScore = function(data) {
$("#redScoreNumber").text(data.RedScore);
$("#redAssist1").attr("data-on", data.RedCycle.Assists >= 1);
@@ -62,6 +66,7 @@ var handleRealtimeScore = function(data) {
$("#blueCatch").attr("data-on", data.BlueCycle.Catch);
};
// Handles a websocket message to populate the final score data.
var handleSetFinalScore = function(data) {
$("#redFinalScore").text(data.RedScore.Score);
$("#redFinalTeam1").text(data.Match.Red1);
@@ -80,6 +85,7 @@ var handleSetFinalScore = function(data) {
$("#finalMatchName").text(data.MatchName + " " + data.Match.DisplayName);
};
// Handles a websocket message to play a sound to signal match start/stop/etc.
var handlePlaySound = function(sound) {
$("audio").each(function(k, v) {
// Stop and reset any sounds that are still playing.
@@ -89,6 +95,7 @@ var handlePlaySound = function(sound) {
$("#" + sound)[0].play();
};
// Handles a websocket message to update the alliance selection screen.
var handleAllianceSelection = function(alliances) {
if (alliances) {
$.each(alliances, function(k, v) {
@@ -98,6 +105,7 @@ var handleAllianceSelection = function(alliances) {
}
};
// Handles a websocket message to populate and/or show/hide a lower third.
var handleLowerThird = function(data) {
if (data.BottomText == "") {
$("#lowerThirdTop").hide();
@@ -314,7 +322,7 @@ var transitionSponsorToScore = function(callback) {
});
};
// Load and Prioritize Sponsor Data
// Loads sponsor slide data and builds the slideshow HTML.
var initializeSponsorDisplay = function() {
$.getJSON("/api/sponsor_slides", function(sponsors) {
@@ -355,7 +363,6 @@ var initializeSponsorDisplay = function() {
});
}
$(function() {
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/displays/audience/websocket", {

View File

@@ -20,7 +20,7 @@ var CheesyWebsocket = function(path, events) {
}
}
// Insert an event to allow the server to force-reload the client.
// Insert an event to allow the server to force-reload the client for any display.
events.reload = function(event) {
location.reload();
};
@@ -43,4 +43,4 @@ var CheesyWebsocket = function(path, events) {
};
this.connect();
}
};

View File

@@ -6,6 +6,7 @@
var websocket;
// Handles a websocket message to update the team connection status.
var handleStatus = function(data) {
// Update the team status view.
$.each(data.AllianceStations, function(station, stationStatus) {

View File

@@ -6,34 +6,42 @@
var websocket;
var scoreIsReady;
// Sends a websocket message to load a team into an alliance station.
var substituteTeam = function(team, position) {
websocket.send("substituteTeam", { team: parseInt(team), position: position })
};
// Sends a websocket message to toggle the bypass status for an alliance station.
var toggleBypass = function(station) {
websocket.send("toggleBypass", station);
};
// Sends a websocket message to start the match.
var startMatch = function() {
websocket.send("startMatch");
};
// Sends a websocket message to abort the match.
var abortMatch = function() {
websocket.send("abortMatch");
};
// Sends a websocket message to commit the match score and load the next match.
var commitResults = function() {
websocket.send("commitResults");
};
// Sends a websocket message to discard the match score and load the next match.
var discardResults = function() {
websocket.send("discardResults");
};
// Sends a websocket message to change what the audience display is showing.
var setAudienceDisplay = function() {
websocket.send("setAudienceDisplay", $("input[name=audienceDisplay]:checked").val());
};
// Sends a websocket message to change what the alliance station display is showing.
var setAllianceStationDisplay = function() {
websocket.send("setAllianceStationDisplay", $("input[name=allianceStationDisplay]:checked").val());
};
@@ -49,6 +57,7 @@ var confirmCommit = function(isReplay) {
}
};
// Handles a websocket message to update the team connection status.
var handleStatus = function(data) {
// Update the team status view.
$.each(data.AllianceStations, function(station, stationStatus) {
@@ -115,6 +124,7 @@ var handleStatus = function(data) {
}
};
// Handles a websocket message to update the match time countdown.
var handleMatchTime = function(data) {
translateMatchTime(data, function(matchState, matchStateText, countdownSec) {
$("#matchState").text(matchStateText);
@@ -122,11 +132,13 @@ var handleMatchTime = function(data) {
});
};
// Handles a websocket message to update the audience display screen selector.
var handleSetAudienceDisplay = function(data) {
$("input[name=audienceDisplay]:checked").prop("checked", false);
$("input[name=audienceDisplay][value=" + data + "]").prop("checked", true);
};
// Handles a websocket message to signal whether the referee and scorers have committed after the match.
var handleScoringStatus = function(data) {
scoreIsReady = data.RefereeScoreReady && data.RedScoreReady && data.BlueScoreReady;
$("#refereeScoreStatus").attr("data-ready", data.RefereeScoreReady);
@@ -134,6 +146,7 @@ var handleScoringStatus = function(data) {
$("#blueScoreStatus").attr("data-ready", data.BlueScoreReady);
};
// Handles a websocket message to update the alliance station display screen selector.
var handleSetAllianceStationDisplay = function(data) {
$("input[name=allianceStationDisplay]:checked").prop("checked", false);
$("input[name=allianceStationDisplay][value=" + data + "]").prop("checked", true);

View File

@@ -14,10 +14,13 @@ var matchStates = {
};
var matchTiming;
// Handles a websocket message containing the length of each period in the match.
var handleMatchTiming = function(data) {
matchTiming = data;
};
// Converts the raw match state and time into a human-readable state and per-period time. Calls the provided
// callback with the result.
var translateMatchTime = function(data, callback) {
var matchStateText;
switch (matchStates[data.MatchState]) {
@@ -42,6 +45,7 @@ var translateMatchTime = function(data, callback) {
callback(matchStates[data.MatchState], matchStateText, getCountdown(data.MatchState, data.MatchTimeSec));
};
// Returns the per-period countdown for the given match state and overall time into the match.
var getCountdown = function(matchState, matchTimeSec) {
switch (matchStates[matchState]) {
case "PRE_MATCH":

View File

@@ -5,6 +5,7 @@
var websocket;
// Handles a websocket message to update the realtime scoring fields.
var handleScore = function(data) {
// Update autonomous period values.
var score = data.CurrentScore;
@@ -59,6 +60,7 @@ var handleScore = function(data) {
}
};
// Handles a keyboard event and sends the appropriate websocket message.
var handleKeyPress = function(event) {
var key = String.fromCharCode(event.keyCode);
switch(key) {
@@ -110,6 +112,7 @@ var handleKeyPress = function(event) {
}
};
// Sends a websocket message to indicate that the score for this alliance is ready.
var commitMatchScore = function() {
websocket.send("commitMatch");
};

View File

@@ -1,3 +1,5 @@
! Baseline configuration for the Catalyst 3500-series switch. Load this into the switch prior to configuring
! Cheesy Arena to connect to it. Default password is 1234Five.
!
version 12.1
no service pad

View File

@@ -19,6 +19,7 @@ type TeamMatchLog struct {
logFile *os.File
}
// Creates a file to log to for the given match and team.
func NewTeamMatchLog(teamId int, match *Match) (*TeamMatchLog, error) {
err := os.MkdirAll(logsDir, 0755)
if err != nil {
@@ -39,6 +40,7 @@ func NewTeamMatchLog(teamId int, match *Match) (*TeamMatchLog, error) {
return &log, nil
}
// Adds a line to the log when a packet is received.
func (log *TeamMatchLog) LogDsStatus(matchTimeSec float64, dsStatus *DriverStationStatus) {
log.logger.Printf("%f,%d,%s,%v,%v,%v,%v,%f,%s,%d,%d,%d", matchTimeSec, dsStatus.TeamId,
dsStatus.AllianceStation, dsStatus.RobotLinked, dsStatus.Auto, dsStatus.Enabled,

View File

@@ -116,7 +116,8 @@
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Unscored balls</div>
<div class="col-lg-2 scoring-comment" id="autoUnscoredBalls">0</div>
</div>
<h3 class="text-center scoring-message">Press Enter to commit autonomous score.<br />This cannot be undone.</h3>
<h3 class="text-center scoring-message">Press Enter to commit autonomous score.<br />
This cannot be undone.</h3>
</div>
<div id="teleopScore" style="display: none;">
<h2>Current Cycle</h2>

View File

@@ -109,14 +109,14 @@
{{end}}
{{define "script"}}
<script type="text/javascript">
$(function(){
$("form.existing").each(function(index){
$(function() {
// Set up the toggling between specifying an image and specifying two lines of text.
$("form.existing").each(function(index) {
if (!$(this).find("input[name=image]").val().length && ($(this).find("input[name=line2]").val().length
|| $(this).find("input[name=line1]").val().length)) {
$(this).find(".imagetoggle").toggleClass("hidden");
}
});
$("button[name=toggleImage]").click(function(event) {
event.preventDefault();
$(this).parents("form").find(".imagetoggle").toggleClass("hidden");

7
web.go
View File

@@ -37,7 +37,7 @@ var templateHelpers = template.FuncMap{
},
}
// Wraps the Gorilla Websocket module for convenience.
// Wraps the Gorilla Websocket module so that we can define additional functions on it.
type Websocket struct {
conn *websocket.Conn
}
@@ -47,6 +47,7 @@ type WebsocketMessage struct {
Data interface{} `json:"data"`
}
// Upgrades the given HTTP request to a websocket connection.
func NewWebsocket(w http.ResponseWriter, r *http.Request) (*Websocket, error) {
conn, err := websocketUpgrader.Upgrade(w, r, nil)
if err != nil {
@@ -73,6 +74,7 @@ func (websocket *Websocket) WriteError(errorMessage string) error {
return websocket.conn.WriteJSON(WebsocketMessage{"error", errorMessage})
}
// Serves the root page of Cheesy Arena.
func IndexHandler(w http.ResponseWriter, r *http.Request) {
template, err := template.ParseFiles("templates/index.html", "templates/base.html")
if err != nil {
@@ -89,6 +91,7 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Starts the webserver and blocks, waiting on requests. Does not return until the application exits.
func ServeWebInterface() {
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static/"))))
http.Handle("/", newHandler())
@@ -98,6 +101,7 @@ func ServeWebInterface() {
http.ListenAndServe(fmt.Sprintf(":%d", httpPort), nil)
}
// Sets up the mapping between URLs and handlers.
func newHandler() http.Handler {
router := mux.NewRouter()
router.HandleFunc("/setup/settings", SettingsGetHandler).Methods("GET")
@@ -163,6 +167,7 @@ func newHandler() http.Handler {
return router
}
// Writes the given error out as plain text with a status code of 500.
func handleWebErr(w http.ResponseWriter, err error) {
http.Error(w, "Internal server error: "+err.Error(), 500)
}