From d964caaa1ca5941ab5e92647b6445c5f38f1d90b Mon Sep 17 00:00:00 2001 From: Nicolai Ommer Date: Sun, 2 Jun 2019 16:56:34 +0200 Subject: [PATCH] Add server part based on old status-board-server --- .gitignore | 3 ++ cmd/ssl-status-board/main.go | 25 +++++++++- pkg/board/multicast.go | 51 ++++++++++++++++++++ pkg/board/refereeConnection.go | 60 +++++++++++++++++++++++ pkg/board/serverConfig.go | 87 ++++++++++++++++++++++++++++++++++ pkg/board/websocket.go | 31 ++++++++++++ src/main.js | 4 +- 7 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 pkg/board/multicast.go create mode 100644 pkg/board/refereeConnection.go create mode 100644 pkg/board/serverConfig.go create mode 100644 pkg/board/websocket.go diff --git a/.gitignore b/.gitignore index a0dddc6..47766b4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ yarn-error.log* *.njsproj *.sln *.sw? + +# application specific +board-config.yaml diff --git a/cmd/ssl-status-board/main.go b/cmd/ssl-status-board/main.go index 8d2660e..260d971 100644 --- a/cmd/ssl-status-board/main.go +++ b/cmd/ssl-status-board/main.go @@ -2,19 +2,27 @@ package main import ( "flag" + "github.com/RoboCup-SSL/ssl-status-board/pkg/board" "github.com/gobuffalo/packr" "log" "net/http" ) var address = flag.String("address", "localhost:8082", "The address on which the UI and API is served") +var configFile = flag.String("c", "board-config.yaml", "The config file to use") func main() { flag.Parse() + config := loadConfig(*configFile) + + refereeBoard := board.NewBoard(config.RefereeConnection) + go refereeBoard.HandleIncomingMessages() + http.HandleFunc(config.RefereeConnection.SubscribePath, refereeBoard.WsHandler) + setupUi() - err := http.ListenAndServe(*address, nil) + err := http.ListenAndServe(config.ListenAddress, nil) if err != nil { log.Fatal(err) } @@ -29,3 +37,18 @@ func setupUi() { log.Print("Backend-only version started. Run the UI separately or get a binary that has the UI included") } } + +// loadConfig loads the config +func loadConfig(configFileName string) board.Config { + cfg, err := board.ReadConfig(configFileName) + if err != nil { + log.Printf("Could not load config: %v", err) + err = cfg.WriteTo(configFileName) + if err != nil { + log.Printf("Failed to write a default config file to %v: %v", configFileName, err) + } else { + log.Println("New default config has been written to", configFileName) + } + } + return cfg +} diff --git a/pkg/board/multicast.go b/pkg/board/multicast.go new file mode 100644 index 0000000..f6aeeec --- /dev/null +++ b/pkg/board/multicast.go @@ -0,0 +1,51 @@ +package board + +import ( + "log" + "net" +) + +// MaxDatagramSize is the maximum read buffer size for network communication +const MaxDatagramSize = 8192 + +// OpenMulticastUdpConnection opens a UDP multicast connection for read and returns it +func OpenMulticastUdpConnection(address string) (err error, listener *net.UDPConn) { + addr, err := net.ResolveUDPAddr("udp", address) + if err != nil { + log.Fatal(err) + } + listener, err = net.ListenMulticastUDP("udp", nil, addr) + if err != nil { + log.Fatal("could not connect to ", address) + } + err = listener.SetReadBuffer(MaxDatagramSize) + if err != nil { + log.Fatalln("could not set read buffer") + } + log.Printf("Listening on %s", address) + return +} + +// HandleIncomingMessages listens for data from a multicast connection and passes data to the consumer +func HandleIncomingMessages(address string, consumer func([]byte)) { + err, listener := OpenMulticastUdpConnection(address) + if err != nil { + log.Println("Could not connect to ", address) + } + + for { + data := make([]byte, MaxDatagramSize) + n, _, err := listener.ReadFromUDP(data) + if err != nil { + log.Println("ReadFromUDP failed: ", err) + break + } + + consumer(data[:n]) + } + + err = listener.Close() + if err != nil { + log.Println("Could not close referee multicast connection") + } +} diff --git a/pkg/board/refereeConnection.go b/pkg/board/refereeConnection.go new file mode 100644 index 0000000..a8ae14c --- /dev/null +++ b/pkg/board/refereeConnection.go @@ -0,0 +1,60 @@ +package board + +import ( + "fmt" + "github.com/RoboCup-SSL/ssl-game-controller/pkg/refproto" + "github.com/golang/protobuf/proto" + "github.com/gorilla/websocket" + "log" + "net/http" + "time" +) + +// Board contains the state of this referee board +type Board struct { + cfg RefereeConfig + referee *refproto.Referee +} + +// NewBoard creates a new referee board +func NewBoard(cfg RefereeConfig) Board { + return Board{cfg: cfg} +} + +// HandleIncomingMessages listens for new messages and stores the latest ones +func (b *Board) HandleIncomingMessages() { + HandleIncomingMessages(b.cfg.ConnectionConfig.MulticastAddress, b.handlingMessage) +} + +func (b *Board) handlingMessage(data []byte) { + message := new(refproto.Referee) + err := proto.Unmarshal(data, message) + if err != nil { + log.Print("Could not parse referee message: ", err) + } else { + b.referee = message + } +} + +// SendToWebSocket sends latest data to the given websocket +func (b *Board) SendToWebSocket(conn *websocket.Conn) { + for { + if b.referee != nil { + data, err := proto.Marshal(b.referee) + if err != nil { + fmt.Println("Marshal error:", err) + } + if err := conn.WriteMessage(websocket.BinaryMessage, data); err != nil { + log.Println("Could not write to referee websocket: ", err) + return + } + } + + time.Sleep(b.cfg.SendingInterval) + } +} + +// WsHandler handles referee websocket connections +func (b *Board) WsHandler(w http.ResponseWriter, r *http.Request) { + WsHandler(w, r, b.SendToWebSocket) +} diff --git a/pkg/board/serverConfig.go b/pkg/board/serverConfig.go new file mode 100644 index 0000000..075afb4 --- /dev/null +++ b/pkg/board/serverConfig.go @@ -0,0 +1,87 @@ +package board + +import ( + "encoding/json" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" + "io/ioutil" + "log" + "os" + "path/filepath" + "time" +) + +// ConnectionConfig contains parameters for multicast -> websocket connections +type ConnectionConfig struct { + SubscribePath string `yaml:"SubscribePath"` + SendingInterval time.Duration `yaml:"SendingInterval"` + MulticastAddress string `yaml:"MulticastAddress"` +} + +// RefereeConfig contains referee specific connection parameters +type RefereeConfig struct { + ConnectionConfig `yaml:"Connection"` +} + +// Config is the root config containing all configs for the server +type Config struct { + ListenAddress string `yaml:"ListenAddress"` + RefereeConnection RefereeConfig `yaml:"RefereeConfig"` +} + +// String converts the config to a string +func (c Config) String() string { + str, err := json.Marshal(c) + if err != nil { + return err.Error() + } + return string(str) +} + +// ReadConfig reads the server config from a yaml file +func ReadConfig(fileName string) (config Config, err error) { + config = DefaultConfig() + f, err := os.Open(fileName) + if err != nil { + return + } + d, err := ioutil.ReadAll(f) + if err != nil { + log.Fatalln("Could not read config file: ", err) + } + err = yaml.Unmarshal(d, &config) + if err != nil { + log.Fatalln("Could not unmarshal config file: ", err) + } + return +} + +// WriteTo writes the config to the specified file +func (c *Config) WriteTo(fileName string) (err error) { + b, err := yaml.Marshal(c) + if err != nil { + err = errors.Wrapf(err, "Could not marshal config %v", c) + return + } + err = os.MkdirAll(filepath.Dir(fileName), 0755) + if err != nil { + err = errors.Wrapf(err, "Could not create directly for config file: %v", fileName) + return + } + err = ioutil.WriteFile(fileName, b, 0600) + return +} + +// DefaultConfig creates a config instance filled with default values +func DefaultConfig() Config { + return Config{ + ListenAddress: ":8082", + RefereeConnection: RefereeConfig{ + ConnectionConfig: ConnectionConfig{ + MulticastAddress: "224.5.23.1:10003", + SendingInterval: time.Millisecond * 100, + SubscribePath: "/api/referee", + }, + }, + } +} diff --git a/pkg/board/websocket.go b/pkg/board/websocket.go new file mode 100644 index 0000000..96e6d50 --- /dev/null +++ b/pkg/board/websocket.go @@ -0,0 +1,31 @@ +package board + +import ( + "github.com/gorilla/websocket" + "log" + "net/http" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(*http.Request) bool { return true }, +} + +// WsHandler converts the request into a websocket connection and passes it to the consumer +func WsHandler(w http.ResponseWriter, r *http.Request, consumer func(conn *websocket.Conn)) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + log.Println("Client connected") + consumer(conn) + log.Println("Client disconnected") + + err = conn.Close() + if err != nil { + log.Println("Could not close connection") + } +} diff --git a/src/main.js b/src/main.js index 13866ea..7860a23 100644 --- a/src/main.js +++ b/src/main.js @@ -56,10 +56,10 @@ const store = new Vuex.Store({ let wsAddress; if (process.env.NODE_ENV === 'development') { // use the default backend port - wsAddress = 'ws://localhost:4201/ssl-status/field-a/subscribe'; + wsAddress = 'ws://localhost:8082/api/referee'; } else { // UI and backend are served on the same host+port on production builds - wsAddress = 'ws://' + window.location.hostname + ':' + window.location.port + '/ssl-status/field-a/subscribe'; + wsAddress = 'ws://' + window.location.hostname + ':' + window.location.port + '/api/referee'; } var ws = new WebSocket(wsAddress);