Make PLC inputs/outputs on field setup page update in realtime.

This commit is contained in:
Patrick Fairbank
2018-08-18 21:01:42 -07:00
parent 11b1b6a943
commit 347ad04885
5 changed files with 115 additions and 23 deletions

View File

@@ -13,14 +13,18 @@ import (
)
type Plc struct {
IsHealthy bool
address string
handler *modbus.TCPClientHandler
client modbus.Client
Inputs [inputCount]bool
Registers [registerCount]uint16
Coils [coilCount]bool
cycleCounter int
IsHealthy bool
address string
handler *modbus.TCPClientHandler
client modbus.Client
Inputs [inputCount]bool
Registers [registerCount]uint16
Coils [coilCount]bool
oldInputs [inputCount]bool
oldRegisters [registerCount]uint16
oldCoils [coilCount]bool
IoChangeNotifier *Notifier
cycleCounter int
}
const (
@@ -100,6 +104,8 @@ func (plc *Plc) SetAddress(address string) {
// Loops indefinitely to read inputs from and write outputs to PLC.
func (plc *Plc) Run() {
plc.IoChangeNotifier = NewNotifier()
for {
if plc.handler == nil {
if plc.address == "" {
@@ -131,6 +137,14 @@ func (plc *Plc) Run() {
plc.cycleCounter = 0
}
// Detect any changes in input or output and notify listeners if so.
if plc.Inputs != plc.oldInputs || plc.Registers != plc.oldRegisters || plc.Coils != plc.oldCoils {
plc.IoChangeNotifier.Notify(nil)
plc.oldInputs = plc.Inputs
plc.oldRegisters = plc.Registers
plc.oldCoils = plc.Coils
}
time.Sleep(time.Until(startTime.Add(time.Millisecond * plcLoopPeriodMs)))
}
}

28
static/js/setup_field.js Normal file
View File

@@ -0,0 +1,28 @@
// Copyright 2018 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Client-side logic for the field setup page.
var websocket;
// Handles a websocket message to update the PLC IO status.
var handlePlcIoChange = function(data) {
$.each(data.Inputs, function(index, input) {
$("#input" + index).text(input)
});
$.each(data.Registers, function(index, register) {
$("#register" + index).text(register)
});
$.each(data.Coils, function(index, coil) {
$("#coil" + index).text(coil)
});
};
$(function() {
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/setup/field/websocket", {
plcIoChange: function(event) { handlePlcIoChange(event.data); }
});
});

View File

@@ -44,10 +44,10 @@
<tr>
<th colspan="2">Inputs</th>
</tr>
{{range $i, $value := .Inputs}}
{{range $i, $name := .InputNames}}
<tr>
<td>{{index $.InputNames $i}}</td>
<td>{{$value}}</td>
<td>{{$name}}</td>
<td id="input{{$i}}"></td>
</tr>
{{end}}
</table>
@@ -57,10 +57,10 @@
<tr>
<th colspan="2">Registers</th>
</tr>
{{range $i, $value := .Registers}}
{{range $i, $name := .RegisterNames}}
<tr>
<td>{{index $.RegisterNames $i}}</td>
<td>{{$value}}</td>
<td>{{$name}}</td>
<td id="register{{$i}}"></td>
</tr>
{{end}}
</table>
@@ -70,10 +70,10 @@
<tr>
<th colspan="2">Coils</th>
</tr>
{{range $i, $value := .Coils}}
{{range $i, $name := .CoilNames}}
<tr>
<td>{{index $.CoilNames $i}}</td>
<td>{{$value}}</td>
<td>{{$name}}</td>
<td id="coil{{$i}}"></td>
</tr>
{{end}}
</table>
@@ -101,4 +101,5 @@
</div>
{{end}}
{{define "script"}}
<script src="/static/js/setup_field.js"></script>
{{end}}

View File

@@ -9,6 +9,7 @@ import (
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/led"
"github.com/Team254/cheesy-arena/model"
"log"
"net/http"
"strconv"
)
@@ -28,17 +29,13 @@ func (web *Web) fieldGetHandler(w http.ResponseWriter, r *http.Request) {
data := struct {
*model.EventSettings
AllianceStationDisplays map[string]string
Inputs []bool
InputNames []string
Registers []uint16
RegisterNames []string
Coils []bool
CoilNames []string
CurrentLedMode led.Mode
LedModeNames map[led.Mode]string
}{web.arena.EventSettings, web.arena.AllianceStationDisplays, plc.Inputs[:], plc.GetInputNames(), plc.Registers[:],
plc.GetRegisterNames(), plc.Coils[:], plc.GetCoilNames(), web.arena.ScaleLeds.GetCurrentMode(),
led.ModeNames}
}{web.arena.EventSettings, web.arena.AllianceStationDisplays, plc.GetInputNames(), plc.GetRegisterNames(),
plc.GetCoilNames(), web.arena.ScaleLeds.GetCurrentMode(), led.ModeNames}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
@@ -88,3 +85,54 @@ func (web *Web) fieldTestPostHandler(w http.ResponseWriter, r *http.Request) {
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) {
return
}
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer websocket.Close()
plcIoChangeListener := web.arena.Plc.IoChangeNotifier.Listen()
defer close(plcIoChangeListener)
// Send the PLC status immediately upon connection.
data := struct {
Inputs []bool
Registers []uint16
Coils []bool
}{web.arena.Plc.Inputs[:], web.arena.Plc.Registers[:], web.arena.Plc.Coils[:]}
err = websocket.Write("plcIoChange", data)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
for {
var messageType string
var message interface{}
select {
case _, ok := <-plcIoChangeListener:
if !ok {
return
}
messageType = "plcIoChange"
message = struct {
Inputs []bool
Registers []uint16
Coils []bool
}{web.arena.Plc.Inputs[:], web.arena.Plc.Registers[:], web.arena.Plc.Coils[:]}
}
err = websocket.Write(messageType, message)
if err != nil {
// The client has probably closed the connection; nothing to do here.
return
}
}
}

View File

@@ -152,6 +152,7 @@ func (web *Web) newHandler() http.Handler {
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")
router.HandleFunc("/setup/sponsor_slides", web.sponsorSlidesGetHandler).Methods("GET")