Skip to content

Commit d964caa

Browse files
committed
Add server part based on old status-board-server
1 parent 60255df commit d964caa

File tree

7 files changed

+258
-3
lines changed

7 files changed

+258
-3
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ yarn-error.log*
1919
*.njsproj
2020
*.sln
2121
*.sw?
22+
23+
# application specific
24+
board-config.yaml

cmd/ssl-status-board/main.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@ package main
22

33
import (
44
"flag"
5+
"github.com/RoboCup-SSL/ssl-status-board/pkg/board"
56
"github.com/gobuffalo/packr"
67
"log"
78
"net/http"
89
)
910

1011
var address = flag.String("address", "localhost:8082", "The address on which the UI and API is served")
12+
var configFile = flag.String("c", "board-config.yaml", "The config file to use")
1113

1214
func main() {
1315
flag.Parse()
1416

17+
config := loadConfig(*configFile)
18+
19+
refereeBoard := board.NewBoard(config.RefereeConnection)
20+
go refereeBoard.HandleIncomingMessages()
21+
http.HandleFunc(config.RefereeConnection.SubscribePath, refereeBoard.WsHandler)
22+
1523
setupUi()
1624

17-
err := http.ListenAndServe(*address, nil)
25+
err := http.ListenAndServe(config.ListenAddress, nil)
1826
if err != nil {
1927
log.Fatal(err)
2028
}
@@ -29,3 +37,18 @@ func setupUi() {
2937
log.Print("Backend-only version started. Run the UI separately or get a binary that has the UI included")
3038
}
3139
}
40+
41+
// loadConfig loads the config
42+
func loadConfig(configFileName string) board.Config {
43+
cfg, err := board.ReadConfig(configFileName)
44+
if err != nil {
45+
log.Printf("Could not load config: %v", err)
46+
err = cfg.WriteTo(configFileName)
47+
if err != nil {
48+
log.Printf("Failed to write a default config file to %v: %v", configFileName, err)
49+
} else {
50+
log.Println("New default config has been written to", configFileName)
51+
}
52+
}
53+
return cfg
54+
}

pkg/board/multicast.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package board
2+
3+
import (
4+
"log"
5+
"net"
6+
)
7+
8+
// MaxDatagramSize is the maximum read buffer size for network communication
9+
const MaxDatagramSize = 8192
10+
11+
// OpenMulticastUdpConnection opens a UDP multicast connection for read and returns it
12+
func OpenMulticastUdpConnection(address string) (err error, listener *net.UDPConn) {
13+
addr, err := net.ResolveUDPAddr("udp", address)
14+
if err != nil {
15+
log.Fatal(err)
16+
}
17+
listener, err = net.ListenMulticastUDP("udp", nil, addr)
18+
if err != nil {
19+
log.Fatal("could not connect to ", address)
20+
}
21+
err = listener.SetReadBuffer(MaxDatagramSize)
22+
if err != nil {
23+
log.Fatalln("could not set read buffer")
24+
}
25+
log.Printf("Listening on %s", address)
26+
return
27+
}
28+
29+
// HandleIncomingMessages listens for data from a multicast connection and passes data to the consumer
30+
func HandleIncomingMessages(address string, consumer func([]byte)) {
31+
err, listener := OpenMulticastUdpConnection(address)
32+
if err != nil {
33+
log.Println("Could not connect to ", address)
34+
}
35+
36+
for {
37+
data := make([]byte, MaxDatagramSize)
38+
n, _, err := listener.ReadFromUDP(data)
39+
if err != nil {
40+
log.Println("ReadFromUDP failed: ", err)
41+
break
42+
}
43+
44+
consumer(data[:n])
45+
}
46+
47+
err = listener.Close()
48+
if err != nil {
49+
log.Println("Could not close referee multicast connection")
50+
}
51+
}

pkg/board/refereeConnection.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package board
2+
3+
import (
4+
"fmt"
5+
"github.com/RoboCup-SSL/ssl-game-controller/pkg/refproto"
6+
"github.com/golang/protobuf/proto"
7+
"github.com/gorilla/websocket"
8+
"log"
9+
"net/http"
10+
"time"
11+
)
12+
13+
// Board contains the state of this referee board
14+
type Board struct {
15+
cfg RefereeConfig
16+
referee *refproto.Referee
17+
}
18+
19+
// NewBoard creates a new referee board
20+
func NewBoard(cfg RefereeConfig) Board {
21+
return Board{cfg: cfg}
22+
}
23+
24+
// HandleIncomingMessages listens for new messages and stores the latest ones
25+
func (b *Board) HandleIncomingMessages() {
26+
HandleIncomingMessages(b.cfg.ConnectionConfig.MulticastAddress, b.handlingMessage)
27+
}
28+
29+
func (b *Board) handlingMessage(data []byte) {
30+
message := new(refproto.Referee)
31+
err := proto.Unmarshal(data, message)
32+
if err != nil {
33+
log.Print("Could not parse referee message: ", err)
34+
} else {
35+
b.referee = message
36+
}
37+
}
38+
39+
// SendToWebSocket sends latest data to the given websocket
40+
func (b *Board) SendToWebSocket(conn *websocket.Conn) {
41+
for {
42+
if b.referee != nil {
43+
data, err := proto.Marshal(b.referee)
44+
if err != nil {
45+
fmt.Println("Marshal error:", err)
46+
}
47+
if err := conn.WriteMessage(websocket.BinaryMessage, data); err != nil {
48+
log.Println("Could not write to referee websocket: ", err)
49+
return
50+
}
51+
}
52+
53+
time.Sleep(b.cfg.SendingInterval)
54+
}
55+
}
56+
57+
// WsHandler handles referee websocket connections
58+
func (b *Board) WsHandler(w http.ResponseWriter, r *http.Request) {
59+
WsHandler(w, r, b.SendToWebSocket)
60+
}

pkg/board/serverConfig.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package board
2+
3+
import (
4+
"encoding/json"
5+
"github.com/pkg/errors"
6+
"gopkg.in/yaml.v2"
7+
"io/ioutil"
8+
"log"
9+
"os"
10+
"path/filepath"
11+
"time"
12+
)
13+
14+
// ConnectionConfig contains parameters for multicast -> websocket connections
15+
type ConnectionConfig struct {
16+
SubscribePath string `yaml:"SubscribePath"`
17+
SendingInterval time.Duration `yaml:"SendingInterval"`
18+
MulticastAddress string `yaml:"MulticastAddress"`
19+
}
20+
21+
// RefereeConfig contains referee specific connection parameters
22+
type RefereeConfig struct {
23+
ConnectionConfig `yaml:"Connection"`
24+
}
25+
26+
// Config is the root config containing all configs for the server
27+
type Config struct {
28+
ListenAddress string `yaml:"ListenAddress"`
29+
RefereeConnection RefereeConfig `yaml:"RefereeConfig"`
30+
}
31+
32+
// String converts the config to a string
33+
func (c Config) String() string {
34+
str, err := json.Marshal(c)
35+
if err != nil {
36+
return err.Error()
37+
}
38+
return string(str)
39+
}
40+
41+
// ReadConfig reads the server config from a yaml file
42+
func ReadConfig(fileName string) (config Config, err error) {
43+
config = DefaultConfig()
44+
f, err := os.Open(fileName)
45+
if err != nil {
46+
return
47+
}
48+
d, err := ioutil.ReadAll(f)
49+
if err != nil {
50+
log.Fatalln("Could not read config file: ", err)
51+
}
52+
err = yaml.Unmarshal(d, &config)
53+
if err != nil {
54+
log.Fatalln("Could not unmarshal config file: ", err)
55+
}
56+
return
57+
}
58+
59+
// WriteTo writes the config to the specified file
60+
func (c *Config) WriteTo(fileName string) (err error) {
61+
b, err := yaml.Marshal(c)
62+
if err != nil {
63+
err = errors.Wrapf(err, "Could not marshal config %v", c)
64+
return
65+
}
66+
err = os.MkdirAll(filepath.Dir(fileName), 0755)
67+
if err != nil {
68+
err = errors.Wrapf(err, "Could not create directly for config file: %v", fileName)
69+
return
70+
}
71+
err = ioutil.WriteFile(fileName, b, 0600)
72+
return
73+
}
74+
75+
// DefaultConfig creates a config instance filled with default values
76+
func DefaultConfig() Config {
77+
return Config{
78+
ListenAddress: ":8082",
79+
RefereeConnection: RefereeConfig{
80+
ConnectionConfig: ConnectionConfig{
81+
MulticastAddress: "224.5.23.1:10003",
82+
SendingInterval: time.Millisecond * 100,
83+
SubscribePath: "/api/referee",
84+
},
85+
},
86+
}
87+
}

pkg/board/websocket.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package board
2+
3+
import (
4+
"github.com/gorilla/websocket"
5+
"log"
6+
"net/http"
7+
)
8+
9+
var upgrader = websocket.Upgrader{
10+
ReadBufferSize: 1024,
11+
WriteBufferSize: 1024,
12+
CheckOrigin: func(*http.Request) bool { return true },
13+
}
14+
15+
// WsHandler converts the request into a websocket connection and passes it to the consumer
16+
func WsHandler(w http.ResponseWriter, r *http.Request, consumer func(conn *websocket.Conn)) {
17+
conn, err := upgrader.Upgrade(w, r, nil)
18+
if err != nil {
19+
log.Println(err)
20+
return
21+
}
22+
23+
log.Println("Client connected")
24+
consumer(conn)
25+
log.Println("Client disconnected")
26+
27+
err = conn.Close()
28+
if err != nil {
29+
log.Println("Could not close connection")
30+
}
31+
}

src/main.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ const store = new Vuex.Store({
5656
let wsAddress;
5757
if (process.env.NODE_ENV === 'development') {
5858
// use the default backend port
59-
wsAddress = 'ws://localhost:4201/ssl-status/field-a/subscribe';
59+
wsAddress = 'ws://localhost:8082/api/referee';
6060
} else {
6161
// UI and backend are served on the same host+port on production builds
62-
wsAddress = 'ws://' + window.location.hostname + ':' + window.location.port + '/ssl-status/field-a/subscribe';
62+
wsAddress = 'ws://' + window.location.hostname + ':' + window.location.port + '/api/referee';
6363
}
6464

6565
var ws = new WebSocket(wsAddress);

0 commit comments

Comments
 (0)