From 03f357451a675ae394774bd69c65389a0c263838 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Mon, 3 Sep 2018 14:33:02 -0700 Subject: [PATCH] Set LED test modes via websocket instead of form post. --- field/arena_notifiers.go | 13 +++++++ field/plc.go | 8 +++-- static/js/setup_field.js | 16 +++++++++ templates/setup_field.html | 44 +++++++++++------------ web/setup_field.go | 74 +++++++++++++++++++++----------------- web/setup_field_test.go | 42 +++++++++++++++++++--- web/web.go | 1 - 7 files changed, 134 insertions(+), 64 deletions(-) diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index 1ac61ed..facbeac 100644 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -7,7 +7,9 @@ package field import ( "github.com/Team254/cheesy-arena/game" + "github.com/Team254/cheesy-arena/led" "github.com/Team254/cheesy-arena/model" + "github.com/Team254/cheesy-arena/vaultled" "github.com/Team254/cheesy-arena/websocket" "strconv" "time" @@ -18,6 +20,7 @@ type ArenaNotifiers struct { AllianceStationDisplayModeNotifier *websocket.Notifier ArenaStatusNotifier *websocket.Notifier AudienceDisplayModeNotifier *websocket.Notifier + LedModeNotifier *websocket.Notifier LowerThirdNotifier *websocket.Notifier MatchLoadNotifier *websocket.Notifier MatchTimeNotifier *websocket.Notifier @@ -28,6 +31,11 @@ type ArenaNotifiers struct { ScoringStatusNotifier *websocket.Notifier } +type LedModeMessage struct { + LedMode led.Mode + VaultLedMode vaultled.Mode +} + type MatchTimeMessage struct { MatchState int MatchTimeSec int @@ -50,6 +58,7 @@ func (arena *Arena) configureNotifiers() { arena.ArenaStatusNotifier = websocket.NewNotifier("arenaStatus", arena.generateArenaStatusMessage) arena.AudienceDisplayModeNotifier = websocket.NewNotifier("audienceDisplayMode", arena.generateAudienceDisplayModeMessage) + arena.LedModeNotifier = websocket.NewNotifier("ledMode", arena.generateLedModeMessage) arena.LowerThirdNotifier = websocket.NewNotifier("lowerThird", nil) arena.MatchLoadNotifier = websocket.NewNotifier("matchLoad", arena.generateMatchLoadMessage) arena.MatchTimeNotifier = websocket.NewNotifier("matchTime", arena.generateMatchTimeMessage) @@ -80,6 +89,10 @@ func (arena *Arena) generateAudienceDisplayModeMessage() interface{} { return arena.AudienceDisplayMode } +func (arena *Arena) generateLedModeMessage() interface{} { + return &LedModeMessage{arena.ScaleLeds.GetCurrentMode(), arena.RedVaultLeds.CurrentForceMode} +} + func (arena *Arena) generateMatchLoadMessage() interface{} { teams := make(map[string]*model.Team) for station, allianceStation := range arena.AllianceStations { diff --git a/field/plc.go b/field/plc.go index b759dae..5861266 100644 --- a/field/plc.go +++ b/field/plc.go @@ -109,13 +109,15 @@ const ( func (plc *Plc) SetAddress(address string) { plc.address = address plc.resetConnection() + + if plc.IoChangeNotifier == nil { + // Register a notifier that listeners can subscribe to to get websocket updates about I/O value changes. + plc.IoChangeNotifier = websocket.NewNotifier("plcIoChange", plc.generateIoChangeMessage) + } } // Loops indefinitely to read inputs from and write outputs to PLC. func (plc *Plc) Run() { - // Register a notifier that listeners can subscribe to to get websocket updates about I/O value changes. - plc.IoChangeNotifier = websocket.NewNotifier("plcIoChange", plc.generateIoChangeMessage) - for { if plc.handler == nil { if plc.address == "" { diff --git a/static/js/setup_field.js b/static/js/setup_field.js index e349b3f..2079622 100644 --- a/static/js/setup_field.js +++ b/static/js/setup_field.js @@ -5,6 +5,21 @@ var websocket; +// Sends a websocket message to change the LED display mode. +var setLedMode = function() { + websocket.send("setLedMode", {LedMode: parseInt($("input[name=ledMode]:checked").val()), + VaultLedMode: parseInt($("input[name=vaultLedMode]:checked").val())}); +}; + +// Handles a websocket message to update the LED test mode. +var handleLedMode = function(data) { + $("input[name=ledMode]:checked").prop("checked", false); + $("input[name=ledMode][value=" + data.LedMode + "]").prop("checked", true); + + $("input[name=vaultLedMode]:checked").prop("checked", false); + $("input[name=vaultLedMode][value=" + data.VaultLedMode + "]").prop("checked", true); +}; + // Handles a websocket message to update the PLC IO status. var handlePlcIoChange = function(data) { $.each(data.Inputs, function(index, input) { @@ -23,6 +38,7 @@ var handlePlcIoChange = function(data) { $(function() { // Set up the websocket back to the server. websocket = new CheesyWebsocket("/setup/field/websocket", { + ledMode: function(event) {handleLedMode(event.data); }, plcIoChange: function(event) { handlePlcIoChange(event.data); } }); }); diff --git a/templates/setup_field.html b/templates/setup_field.html index 834283a..a0f8179 100644 --- a/templates/setup_field.html +++ b/templates/setup_field.html @@ -84,30 +84,26 @@
LEDs -
-
- - {{range $i, $name := .LedModeNames}} -
- -
- {{end}} -
-
- - {{range $i, $name := .VaultLedModeNames}} -
- -
- {{end}} -
-
+
+ + {{range $i, $name := .LedModeNames}} +
+ +
+ {{end}} +
+
+ + {{range $i, $name := .VaultLedModeNames}} +
+ +
+ {{end}} +
diff --git a/web/setup_field.go b/web/setup_field.go index a740b5b..ff7a079 100644 --- a/web/setup_field.go +++ b/web/setup_field.go @@ -6,13 +6,16 @@ package web import ( + "fmt" "github.com/Team254/cheesy-arena/field" "github.com/Team254/cheesy-arena/led" "github.com/Team254/cheesy-arena/model" "github.com/Team254/cheesy-arena/vaultled" "github.com/Team254/cheesy-arena/websocket" + "github.com/mitchellh/mapstructure" + "io" + "log" "net/http" - "strconv" ) // Shows the field configuration page. @@ -33,13 +36,10 @@ func (web *Web) fieldGetHandler(w http.ResponseWriter, r *http.Request) { InputNames []string RegisterNames []string CoilNames []string - CurrentLedMode led.Mode LedModeNames map[led.Mode]string - CurrentVaultLedMode vaultled.Mode VaultLedModeNames map[vaultled.Mode]string }{web.arena.EventSettings, web.arena.AllianceStationDisplays, plc.GetInputNames(), plc.GetRegisterNames(), - plc.GetCoilNames(), web.arena.ScaleLeds.GetCurrentMode(), led.ModeNames, - web.arena.RedVaultLeds.CurrentForceMode, vaultled.ModeNames} + plc.GetCoilNames(), led.ModeNames, vaultled.ModeNames} err = template.ExecuteTemplate(w, "base", data) if err != nil { handleWebErr(w, err) @@ -70,31 +70,6 @@ func (web *Web) fieldReloadDisplaysHandler(w http.ResponseWriter, r *http.Reques http.Redirect(w, r, "/setup/field", 303) } -// Controls the field LEDs for testing or effect. -func (web *Web) fieldTestPostHandler(w http.ResponseWriter, r *http.Request) { - if !web.userIsAdmin(w, r) { - return - } - - if web.arena.MatchState != field.PreMatch { - http.Error(w, "Arena must be in pre-match state", 400) - return - } - - mode, _ := strconv.Atoi(r.PostFormValue("mode")) - ledMode := led.Mode(mode) - web.arena.ScaleLeds.SetMode(ledMode, ledMode) - web.arena.RedSwitchLeds.SetMode(ledMode, ledMode) - web.arena.BlueSwitchLeds.SetMode(ledMode, ledMode) - - vaultMode, _ := strconv.Atoi(r.PostFormValue("vaultMode")) - vaultLedMode := vaultled.Mode(vaultMode) - web.arena.RedVaultLeds.SetAllModes(vaultLedMode) - web.arena.BlueVaultLeds.SetAllModes(vaultLedMode) - - http.Redirect(w, r, "/setup/field", 303) -} - // The websocket endpoint for sending realtime updates to the field setup page. func (web *Web) fieldWebsocketHandler(w http.ResponseWriter, r *http.Request) { if !web.userIsAdmin(w, r) { @@ -108,6 +83,41 @@ func (web *Web) fieldWebsocketHandler(w http.ResponseWriter, r *http.Request) { } defer ws.Close() - // Subscribe the websocket to the notifiers whose messages will be passed on to the client. - ws.HandleNotifiers(web.arena.Plc.IoChangeNotifier) + // Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine. + go ws.HandleNotifiers(web.arena.LedModeNotifier, web.arena.Plc.IoChangeNotifier) + // Loop, waiting for commands and responding to them, until the client closes the connection. + for { + messageType, data, err := ws.Read() + if err != nil { + if err == io.EOF { + // Client has closed the connection; nothing to do here. + return + } + log.Println(err) + return + } + + switch messageType { + case "setLedMode": + if web.arena.MatchState != field.PreMatch { + ws.WriteError("Arena must be in pre-match state") + continue + } + var modeMessage field.LedModeMessage + err = mapstructure.Decode(data, &modeMessage) + if err != nil { + ws.WriteError(err.Error()) + continue + } + + web.arena.ScaleLeds.SetMode(modeMessage.LedMode, modeMessage.LedMode) + web.arena.RedSwitchLeds.SetMode(modeMessage.LedMode, modeMessage.LedMode) + web.arena.BlueSwitchLeds.SetMode(modeMessage.LedMode, modeMessage.LedMode) + web.arena.RedVaultLeds.SetAllModes(modeMessage.VaultLedMode) + web.arena.BlueVaultLeds.SetAllModes(modeMessage.VaultLedMode) + web.arena.LedModeNotifier.Notify() + default: + ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) + } + } } diff --git a/web/setup_field_test.go b/web/setup_field_test.go index 5be960b..b933877 100644 --- a/web/setup_field_test.go +++ b/web/setup_field_test.go @@ -4,8 +4,15 @@ package web import ( + "github.com/Team254/cheesy-arena/websocket" + gorillawebsocket "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "testing" + + "github.com/Team254/cheesy-arena/field" + "github.com/Team254/cheesy-arena/led" + "github.com/Team254/cheesy-arena/vaultled" + "github.com/mitchellh/mapstructure" ) func TestSetupField(t *testing.T) { @@ -22,8 +29,35 @@ func TestSetupField(t *testing.T) { recorder = web.getHttpResponse("/setup/field") assert.Contains(t, recorder.Body.String(), "12345") assert.Contains(t, recorder.Body.String(), "selected") - - recorder = web.postHttpResponse("/setup/field/test", "mode=1") - assert.Equal(t, 303, recorder.Code) - assert.Equal(t, 1, int(web.arena.ScaleLeds.GetCurrentMode())) +} + +func TestSetupFieldWebsocket(t *testing.T) { + web := setupTestWeb(t) + + server, wsUrl := web.startTestServer() + defer server.Close() + conn, _, err := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/setup/field/websocket", nil) + assert.Nil(t, err) + defer conn.Close() + ws := websocket.NewTestWebsocket(conn) + + // Should get a few status updates right after connection. + ledModeMessage := readLedModes(t, ws) + assert.Equal(t, led.OffMode, ledModeMessage.LedMode) + assert.Equal(t, vaultled.OffMode, ledModeMessage.VaultLedMode) + readWebsocketType(t, ws, "plcIoChange") + + // Change the LED modes and verify that the new modes are broadcast back. + ws.Write("setLedMode", field.LedModeMessage{LedMode: led.RandomMode, VaultLedMode: vaultled.BluePlayedMode}) + ledModeMessage = readLedModes(t, ws) + assert.Equal(t, led.RandomMode, ledModeMessage.LedMode) + assert.Equal(t, vaultled.BluePlayedMode, ledModeMessage.VaultLedMode) +} + +func readLedModes(t *testing.T, ws *websocket.Websocket) *field.LedModeMessage { + message := readWebsocketType(t, ws, "ledMode") + var ledModeMessage field.LedModeMessage + err := mapstructure.Decode(message, &ledModeMessage) + assert.Nil(t, err) + return &ledModeMessage } diff --git a/web/web.go b/web/web.go index 685316d..6a62521 100644 --- a/web/web.go +++ b/web/web.go @@ -151,7 +151,6 @@ func (web *Web) newHandler() http.Handler { router.HandleFunc("/setup/field", web.fieldGetHandler).Methods("GET") router.HandleFunc("/setup/field", web.fieldPostHandler).Methods("POST") router.HandleFunc("/setup/field/reload_displays", web.fieldReloadDisplaysHandler).Methods("GET") - router.HandleFunc("/setup/field/test", web.fieldTestPostHandler).Methods("POST") router.HandleFunc("/setup/field/websocket", web.fieldWebsocketHandler).Methods("GET") router.HandleFunc("/setup/lower_thirds", web.lowerThirdsGetHandler).Methods("GET") router.HandleFunc("/setup/lower_thirds/websocket", web.lowerThirdsWebsocketHandler).Methods("GET")