Skip to content

Commit 6e3b165

Browse files
Merge pull request #575 from Hyperloop-UPV/features/competition-view-redesign
Competition View Redesign
2 parents 4c10f91 + efcc827 commit 6e3b165

74 files changed

Lines changed: 3416 additions & 1621 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/cmd/config.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ backoff_min_ms = 100 # Minimum backoff duration in milliseconds
2727
backoff_max_ms = 5000 # Maximum backoff duration in milliseconds
2828
backoff_multiplier = 1.5 # Exponential backoff multiplier (e.g., 1.5 means each retry waits 1.5x longer)
2929
max_retries = 0 # Maximum retries before cycling (0 = infinite retries, recommended for persistent reconnection)
30-
connection_timeout_ms = 3000 # Connection timeout in milliseconds
31-
keep_alive_ms = 1000 # Keep-alive interval in milliseconds
30+
keep_alive_interval_ms = 50
31+
3232

3333
# UDP Configuration
3434
# These settings control the UDP server's buffer sizes and performance characteristics

backend/cmd/dev-config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ backoff_max_ms = 999999 # Maximum backoff duration in milliseconds
3030
backoff_multiplier = 1 # Exponential backoff multiplier (e.g., 1.5 means each retry waits 1.5x longer)
3131
max_retries = 0 # Maximum retries before cycling (0 = infinite retries, recommended for persistent reconnection)
3232
connection_timeout_ms = 999999 # Timeout for the initial connection attempt
33-
keep_alive_ms = 0 # Keep-alive interval in milliseconds (0 to disable)
33+
keep_alive_interval_ms = 0 # Keep-alive interval in milliseconds (0 to disable)
3434

3535
# UDP Configuration
3636
# These settings control the UDP server's buffer sizes and performance characteristics

backend/cmd/setup_transport.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package main
22

33
import (
4-
"context"
54
"encoding/binary"
65
"fmt"
76
"net"
@@ -52,6 +51,8 @@ func configureTransport(
5251
// Start handling network packets using UDP server
5352
configureUDPServerTransport(adj, transp, config)
5453

54+
// Start the application-level TCP keep-alive (empty order with id 1)
55+
go transp.HandleKeepAlive(time.Duration(config.TCP.KeepAliveIntervalMs) * time.Millisecond)
5556
}
5657

5758
func configureTCPClientTransport(
@@ -86,11 +87,6 @@ func configureTCPClientTransport(
8687
clientConfig.Timeout = time.Duration(config.TCP.ConnectionTimeout) * time.Millisecond
8788
}
8889

89-
// Apply custom keep-alive if specified
90-
if config.TCP.KeepAlive > 0 {
91-
clientConfig.KeepAlive = time.Duration(config.TCP.KeepAlive) * time.Millisecond
92-
}
93-
9490
// Apply custom backoff parameters
9591
if config.TCP.BackoffMinMs > 0 || config.TCP.BackoffMaxMs > 0 || config.TCP.BackoffMultiplier > 0 {
9692
minBackoff := 100 * time.Millisecond // default
@@ -118,18 +114,12 @@ func configureTCPClientTransport(
118114
}
119115
}
120116

121-
// configureTCPServerTransport starts the TCP server handler using a ListenConfig with KeepAlive.
117+
// configureTCPServerTransport starts the TCP server handler.
122118
func configureTCPServerTransport(
123119
adj adj_module.ADJ,
124120
transp *transport.Transport,
125121
) {
126-
go transp.HandleServer(tcp.ServerConfig{
127-
ListenConfig: net.ListenConfig{
128-
KeepAlive: time.Second,
129-
},
130-
Context: context.TODO(),
131-
}, fmt.Sprintf("%s:%d", adj.Info.Addresses[BACKEND], adj.Info.Ports[TcpServer]))
132-
122+
go transp.HandleServer(tcp.NewServerConfig(), fmt.Sprintf("%s:%d", adj.Info.Addresses[BACKEND], adj.Info.Ports[TcpServer]))
133123
}
134124

135125
// configureUDPServerTransport creates and starts the UDP server then delegates handling to transport.
@@ -246,6 +236,15 @@ func getTransportDecEnc(info adj_module.Info, podData pod_data.PodData) (*presen
246236
encoder.SetPacketEncoder(id, dataEncoder)
247237
}
248238

239+
// The TCP keep-alive order is an empty packet, so give it an empty
240+
// descriptor unless the ADJ already defines packet id 1
241+
if !common.Contains(ids, transport.KeepAliveId) {
242+
dataDecoder.SetDescriptor(transport.KeepAliveId, data.Descriptor{})
243+
dataEncoder.SetDescriptor(transport.KeepAliveId, data.Descriptor{})
244+
decoder.SetPacketDecoder(transport.KeepAliveId, dataDecoder)
245+
encoder.SetPacketEncoder(transport.KeepAliveId, dataEncoder)
246+
}
247+
249248
// TODO Solve this foking mess, I have tried...
250249
stateOrdersDecoder := order.NewDecoder(binary.LittleEndian)
251250
stateOrdersDecoder.SetActionId(abstraction.PacketId(info.MessageIds[AddStateOrder]), stateOrdersDecoder.DecodeAdd)

backend/cmd/setup_vehicle.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ func configureBroker(subloggers abstraction.SubloggersMap, loggerHandler *logger
6464
pool := websocket.NewPool(connections, trace.Logger)
6565
pool.SetOnDisconnect(func(count int) {
6666
if count == 0 {
67+
// Losing the last control-station GUI (closed, crashed or hung:
68+
// heartbeats stop and the read deadline expires) must put the
69+
// vehicle in a safe state, as required by competition rules.
70+
trace.Warn().Msg("no clients connected, sending FAULT order")
71+
faultOrder := &order_topic.Order{Id: 0, Fields: map[string]order_topic.Field{}}
72+
if err := broker.UserPush(faultOrder); err != nil {
73+
trace.Error().Err(err).Msg("failed to send fault order on client disconnect")
74+
}
75+
6776
trace.Info().Msg("no clients connected, stopping logger")
6877
loggerHandler.Stop()
6978
if err := loggerTopic.NotifyStopped(); err != nil {

backend/internal/config/config_types.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ type Transport struct {
1616
}
1717

1818
type TCP struct {
19-
BackoffMinMs int `toml:"backoff_min_ms"`
20-
BackoffMaxMs int `toml:"backoff_max_ms"`
21-
BackoffMultiplier float64 `toml:"backoff_multiplier"`
22-
MaxRetries int `toml:"max_retries"`
23-
ConnectionTimeout int `toml:"connection_timeout_ms"`
24-
KeepAlive int `toml:"keep_alive_ms"`
19+
BackoffMinMs int `toml:"backoff_min_ms"`
20+
BackoffMaxMs int `toml:"backoff_max_ms"`
21+
BackoffMultiplier float64 `toml:"backoff_multiplier"`
22+
MaxRetries int `toml:"max_retries"`
23+
ConnectionTimeout int `toml:"connection_timeout_ms"`
24+
KeepAliveIntervalMs int `toml:"keep_alive_interval_ms"`
2525
}
2626

2727
type UDP struct {

backend/pkg/transport/keepalive.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package transport
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"time"
8+
9+
"github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction"
10+
"github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data"
11+
)
12+
13+
// KeepAliveId is the packet id of the empty order used as the
14+
// application-level TCP keep-alive.
15+
const KeepAliveId abstraction.PacketId = 1
16+
17+
// DefaultKeepAliveInterval is how often the keep-alive order is sent to each
18+
// board when no interval is configured.
19+
const DefaultKeepAliveInterval = 50 * time.Millisecond
20+
21+
// HandleKeepAlive broadcasts an empty order with id KeepAliveId to every
22+
// connected board every interval. Write errors are forwarded to the
23+
// connection handler by the connection wrapper, which tears the connection
24+
// down. This method blocks.
25+
func (transport *Transport) HandleKeepAlive(interval time.Duration) {
26+
if interval <= 0 {
27+
interval = DefaultKeepAliveInterval
28+
}
29+
// The keep-alive frame never changes, so encode it once up front
30+
buf, err := transport.encoder.Encode(data.NewPacket(KeepAliveId))
31+
if err != nil {
32+
transport.logger.Error().Stack().Err(err).Msg("encode keep-alive")
33+
transport.errChan <- err
34+
return
35+
}
36+
frame := make([]byte, buf.Len())
37+
copy(frame, buf.Bytes())
38+
transport.encoder.ReleaseBuffer(buf)
39+
40+
ticker := time.NewTicker(interval)
41+
defer ticker.Stop()
42+
for range ticker.C {
43+
transport.broadcastKeepAlive(frame, interval)
44+
}
45+
}
46+
47+
// broadcastKeepAlive writes the keep-alive frame to all active connections.
48+
func (transport *Transport) broadcastKeepAlive(frame []byte, interval time.Duration) {
49+
transport.connectionsMx.RLock()
50+
defer transport.connectionsMx.RUnlock()
51+
52+
for target, conn := range transport.connections {
53+
// Bound the write so a dead peer with a full send buffer cannot hold
54+
// the connections lock past the next tick
55+
conn.SetWriteDeadline(time.Now().Add(interval))
56+
totalWritten := 0
57+
for totalWritten < len(frame) {
58+
n, err := conn.Write(frame[totalWritten:])
59+
totalWritten += n
60+
if err != nil {
61+
// net.ErrClosed means the connection was closed on purpose
62+
// and its handler already reported the reason
63+
if !errors.Is(err, net.ErrClosed) {
64+
transport.logger.Error().Stack().Err(err).Str("target", string(target)).Msg("keep-alive write")
65+
transport.errChan <- fmt.Errorf("TCP keep-alive to board %s failed: %w", target, err)
66+
}
67+
break
68+
}
69+
}
70+
conn.SetWriteDeadline(time.Time{})
71+
}
72+
}

backend/pkg/transport/network/tcp/config.go

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,9 @@ func NewClientConfig(laddr net.Addr) ClientConfig {
2323
Dialer: net.Dialer{
2424
Timeout: time.Second,
2525
LocalAddr: laddr,
26-
KeepAlive: -1, // managed via KeepAliveConfig
27-
KeepAliveConfig: net.KeepAliveConfig{
28-
Enable: true,
29-
Idle: time.Second,
30-
Interval: time.Second,
31-
Count: 3,
32-
},
26+
// Kernel keep-alive disabled: liveness is handled by the
27+
// application-level keep-alive order (transport.HandleKeepAlive)
28+
KeepAlive: -1,
3329
},
3430

3531
Context: context.TODO(),
@@ -73,13 +69,9 @@ type ServerConfig struct {
7369
func NewServerConfig() ServerConfig {
7470
return ServerConfig{
7571
ListenConfig: net.ListenConfig{
76-
KeepAlive: -1, // managed via KeepAliveConfig
77-
KeepAliveConfig: net.KeepAliveConfig{
78-
Enable: true,
79-
Idle: time.Second,
80-
Interval: time.Second,
81-
Count: 3,
82-
},
72+
// Kernel keep-alive disabled: liveness is handled by the
73+
// application-level keep-alive order (transport.HandleKeepAlive)
74+
KeepAlive: -1,
8375
},
8476
Context: context.TODO(),
8577
}

backend/pkg/websocket/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ import (
99
ws "github.com/gorilla/websocket"
1010
)
1111

12+
// HeartbeatTopic is the liveness message every frontend sends once per
13+
// second. It is consumed by the websocket layer and never reaches the broker.
14+
const HeartbeatTopic = "connection/heartbeat"
15+
16+
// heartbeatTimeout bounds how long a dead or frozen frontend goes undetected:
17+
// if no message (the heartbeat guarantees one per second) arrives within this
18+
// window, Read fails and the client is treated as disconnected.
19+
const heartbeatTimeout = 3 * time.Second
20+
1221
type Client struct {
1322
readMx *sync.Mutex
1423
writeMx *sync.Mutex
@@ -26,6 +35,8 @@ func NewClient(conn *ws.Conn) *Client {
2635
onClose: func() {},
2736
}
2837

38+
conn.SetReadDeadline(time.Now().Add(heartbeatTimeout))
39+
2940
return client
3041
}
3142

@@ -46,6 +57,9 @@ func (client *Client) Read() (Message, error) {
4657

4758
var message Message
4859
err := client.conn.ReadJSON(&message)
60+
if err == nil {
61+
client.conn.SetReadDeadline(time.Now().Add(heartbeatTimeout))
62+
}
4963
return message, err
5064
}
5165

backend/pkg/websocket/pool.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ func (pool *Pool) handle(id ClientId, client *Client) {
8484
}
8585

8686
clientLogger.Trace().Msg("read")
87+
88+
// Heartbeats only refresh the read deadline; they carry no payload
89+
// for the broker.
90+
if string(message.Topic) == HeartbeatTopic {
91+
continue
92+
}
93+
8794
pool.onMessage(id, &message)
8895
}
8996
}

0 commit comments

Comments
 (0)