Skip to content

Commit

Permalink
Add server part based on old status-board-server
Browse files Browse the repository at this point in the history
  • Loading branch information
g3force committed Jun 2, 2019
1 parent 60255df commit d964caa
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?

# application specific
board-config.yaml
25 changes: 24 additions & 1 deletion cmd/ssl-status-board/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
}
51 changes: 51 additions & 0 deletions pkg/board/multicast.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
60 changes: 60 additions & 0 deletions pkg/board/refereeConnection.go
Original file line number Diff line number Diff line change
@@ -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)
}
87 changes: 87 additions & 0 deletions pkg/board/serverConfig.go
Original file line number Diff line number Diff line change
@@ -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",
},
},
}
}
31 changes: 31 additions & 0 deletions pkg/board/websocket.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
4 changes: 2 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit d964caa

Please sign in to comment.