Skip to content

Commit 5bcebb6

Browse files
Merge pull request #577 from Hyperloop-UPV/develop
Prepare 11.1.4
2 parents a76c062 + 4c10f91 commit 5bcebb6

10 files changed

Lines changed: 225 additions & 135 deletions

File tree

backend/cmd/config.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ keep_alive_ms = 1000 # Keep-alive interval in milliseconds
3535
[udp]
3636
ring_buffer_size = 64 # Size of the ring buffer for incoming packets (number of packets, not bytes)
3737
packet_chan_size = 16 # Size of the channel buffer
38-
keep_alive_check_interval_ms = 5 # How often the keep-alive checks last-received times (ms)
39-
keep_alive_timeout_ms = 100 # Max time without packets from an IP before it is considered disconnected (ms)
38+
# Disabled: UDP keep-alive settings - Javier Ribal del Río (2026-07-15)
39+
# keep_alive_check_interval_ms = 5 # How often the keep-alive checks last-received times (ms)
40+
# keep_alive_timeout_ms = 100 # Max time without packets from an IP before it is considered disconnected (ms)
4041

4142

4243
# Logger Configuration

backend/cmd/dev-config.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ keep_alive_ms = 0 # Keep-alive interval in milliseconds (0 to disab
3737
[udp]
3838
ring_buffer_size = 64 # Size of the ring buffer for incoming packets (number of packets, not bytes)
3939
packet_chan_size = 16 # Size of the channel buffer
40-
keep_alive_check_interval_ms = 5 # How often the keep-alive checks last-received times (ms)
41-
keep_alive_timeout_ms = 100 # Max time without packets from an IP before it is considered disconnected (ms)
40+
# Disabled: UDP keep-alive settings - Javier Ribal del Río (2026-07-15)
41+
# keep_alive_check_interval_ms = 5 # How often the keep-alive checks last-received times (ms)
42+
# keep_alive_timeout_ms = 100 # Max time without packets from an IP before it is considered disconnected (ms)
4243

4344
# Server Configuration
4445
[server.ethernet-view]

backend/cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func main() {
9797
// <--- transport --->
9898
transp := transport.NewTransport(trace.Logger)
9999
transp.SetpropagateFault(config.Transport.PropagateFault)
100+
transp.SetADJHash(adj.Commit)
100101

101102
// <--- vehicle --->
102103
err = configureVehicle(

backend/cmd/setup_transport.go

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -141,32 +141,36 @@ func configureUDPServerTransport(
141141
) {
142142
trace.Info().Msg("Starting UDP server")
143143

144-
onKeepAliveTimeout := func(ip string) {
145-
transp.SendFault()
146-
board, ok := transp.TargetFromIp(ip)
147-
if !ok {
148-
board = "unknown"
149-
}
150-
err := fmt.Errorf("UDP keep-alive timeout: no packets received from board %s (%s) for %dms, fault sent", board, ip, config.UDP.KeepAliveTimeoutMs)
151-
transp.ReportError(err)
152-
153-
// Close the board's TCP connection ourselves: the fault we just
154-
// broadcast leaves unacked data on a dead peer, which suppresses the
155-
// TCP keep-alive and would delay disconnect detection by minutes.
156-
if ok {
157-
transp.DisconnectTarget(board, err)
158-
}
159-
}
144+
// Disabled: UDP keep-alive callback — on timeout it broadcast a fault,
145+
// reported the board to the GUI and force-closed its TCP connection
146+
// - Javier Ribal del Río (2026-07-15)
147+
// onKeepAliveTimeout := func(ip string) {
148+
// transp.SendFault()
149+
// board, ok := transp.TargetFromIp(ip)
150+
// if !ok {
151+
// board = "unknown"
152+
// }
153+
// err := fmt.Errorf("UDP keep-alive timeout: no packets received from board %s (%s) for %dms, fault sent", board, ip, config.UDP.KeepAliveTimeoutMs)
154+
// transp.ReportError(err)
155+
//
156+
// // Close the board's TCP connection ourselves: the fault we just
157+
// // broadcast leaves unacked data on a dead peer, which suppresses the
158+
// // TCP keep-alive and would delay disconnect detection by minutes.
159+
// if ok {
160+
// transp.DisconnectTarget(board, err)
161+
// }
162+
// }
160163

161164
udpServer := udp.NewServer(
162165
adj.Info.Addresses[BACKEND],
163166
adj.Info.Ports[UDP],
164167
&trace.Logger,
165168
config.UDP.RingBufferSize,
166169
config.UDP.PacketChanSize,
167-
time.Duration(config.UDP.KeepAliveCheckIntervalMs)*time.Millisecond,
168-
time.Duration(config.UDP.KeepAliveTimeoutMs)*time.Millisecond,
169-
onKeepAliveTimeout,
170+
// Disabled: UDP keep-alive arguments - Javier Ribal del Río (2026-07-15)
171+
// time.Duration(config.UDP.KeepAliveCheckIntervalMs)*time.Millisecond,
172+
// time.Duration(config.UDP.KeepAliveTimeoutMs)*time.Millisecond,
173+
// onKeepAliveTimeout,
170174
)
171175
err := udpServer.Start()
172176
if err != nil {

backend/internal/config/config_types.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ type TCP struct {
2525
}
2626

2727
type UDP struct {
28-
RingBufferSize int `toml:"ring_buffer_size"`
29-
PacketChanSize int `toml:"packet_chan_size"`
30-
KeepAliveCheckIntervalMs int `toml:"keep_alive_check_interval_ms"`
31-
KeepAliveTimeoutMs int `toml:"keep_alive_timeout_ms"`
28+
RingBufferSize int `toml:"ring_buffer_size"`
29+
PacketChanSize int `toml:"packet_chan_size"`
30+
// Disabled: UDP keep-alive settings - Javier Ribal del Río (2026-07-15)
31+
// KeepAliveCheckIntervalMs int `toml:"keep_alive_check_interval_ms"`
32+
// KeepAliveTimeoutMs int `toml:"keep_alive_timeout_ms"`
3233
}
3334

3435
type Logging struct {

backend/pkg/adj/adj.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,14 @@ func downloadADJ(AdjSettings config.Adj) (string, json.RawMessage, json.RawMessa
9696
return "local", nil, nil, err
9797
}
9898

99-
// If not downloading use local ADJ
99+
// If not downloading, recover the commit hash persisted by the last
100+
// successful download. Without it the backend must not start: boards
101+
// receive this hash on connection and could not validate their ADJ.
100102
if commitHash == "" {
101-
commitHash = "local"
103+
commitHash, err = readCommitFile()
104+
if err != nil {
105+
return "", nil, nil, fmt.Errorf("using local ADJ but no stored commit hash was found (run once with an adj branch and internet access): %w", err)
106+
}
102107
}
103108

104109
// After downloading adj apply adj validator

backend/pkg/adj/git.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,40 @@
11
package adj
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
7+
"strings"
68

79
trace "github.com/rs/zerolog/log"
810

911
"github.com/go-git/go-git/v5"
1012
"github.com/go-git/go-git/v5/plumbing"
1113
)
1214

15+
// commitFile persists the commit hash of the last downloaded ADJ. It lives
16+
// next to the repo (not inside it) so wiping the repo before a clone does not
17+
// lose it, and local runs (no branch or no internet) can still report it.
18+
var commitFile = filepath.Join(filepath.Dir(filepath.Clean(RepoPath)), "adj_commit")
19+
20+
// writeCommitFile stores the commit hash of the freshly cloned ADJ
21+
func writeCommitFile(hash string) error {
22+
return os.WriteFile(commitFile, []byte(hash), 0644)
23+
}
24+
25+
// readCommitFile recovers the commit hash persisted by the last download
26+
func readCommitFile() (string, error) {
27+
raw, err := os.ReadFile(commitFile)
28+
if err != nil {
29+
return "", err
30+
}
31+
hash := strings.TrimSpace(string(raw))
32+
if hash == "" {
33+
return "", fmt.Errorf("commit file %s is empty", commitFile)
34+
}
35+
return hash, nil
36+
}
37+
1338
// updateRepo ensures that the local ADJ repository matches the specified remote branch.
1439
// It first performs a test clone to verify remote accessibility (including internet
1540
// connectivity). If the remote branch is accessible, the local repository is completely
@@ -85,5 +110,9 @@ func updateRepo(AdjBranch string) (string, error) {
85110
}
86111
}
87112

113+
if err = writeCommitFile(commitHash); err != nil {
114+
return "", err
115+
}
116+
88117
return commitHash, nil
89118
}

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ func (conn *connWithErr) Close() error {
4444
return conn.Conn.Close()
4545
}
4646

47-
// CloseWithError closes conn, delivering reason to its error channel first so
48-
// the connection handler blocked on that channel wakes up. Closing a
49-
// connection directly would not do this, since Read/Write filter out
50-
// net.ErrClosed.
5147
func CloseWithError(conn net.Conn, reason error) error {
5248
if cwe, ok := conn.(*connWithErr); ok {
5349
select {

backend/pkg/transport/network/udp/server.go

Lines changed: 81 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -35,37 +35,44 @@ type Server struct {
3535
ringMutex sync.Mutex
3636
notEmpty *sync.Cond
3737

38-
lastSeen map[string]time.Time
39-
lastSeenMu sync.Mutex
40-
keepAliveCheckInterval time.Duration
41-
keepAliveTimeout time.Duration
42-
OnDisconnect func(ip string)
38+
// Disabled: state for the UDP keep-alive, which checked that every source
39+
// IP kept sending packets and reported disconnections - Javier Ribal del Río (2026-07-15)
40+
// lastSeen map[string]time.Time
41+
// lastSeenMu sync.Mutex
42+
// keepAliveCheckInterval time.Duration
43+
// keepAliveTimeout time.Duration
44+
// OnDisconnect func(ip string)
4345
}
4446

45-
const (
46-
defaultKeepAliveCheckInterval = 5 * time.Millisecond
47-
defaultKeepAliveTimeout = 100 * time.Millisecond
48-
)
49-
50-
func NewServer(address string, port uint16, logger *zerolog.Logger, ringBufferSize int, packetChanSize int, keepAliveCheckInterval time.Duration, keepAliveTimeout time.Duration, onDisconnect func(ip string)) *Server {
51-
if keepAliveCheckInterval <= 0 {
52-
keepAliveCheckInterval = defaultKeepAliveCheckInterval
53-
}
54-
if keepAliveTimeout <= 0 {
55-
keepAliveTimeout = defaultKeepAliveTimeout
56-
}
47+
// Disabled: default intervals for the UDP keep-alive - Javier Ribal del Río (2026-07-15)
48+
// const (
49+
// defaultKeepAliveCheckInterval = 5 * time.Millisecond
50+
// defaultKeepAliveTimeout = 100 * time.Millisecond
51+
// )
52+
53+
func NewServer(address string, port uint16, logger *zerolog.Logger, ringBufferSize int, packetChanSize int) *Server {
54+
// Disabled: UDP keep-alive parameters and their defaulting; the previous
55+
// signature also took keepAliveCheckInterval, keepAliveTimeout and an
56+
// onDisconnect callback - Javier Ribal del Río (2026-07-15)
57+
// if keepAliveCheckInterval <= 0 {
58+
// keepAliveCheckInterval = defaultKeepAliveCheckInterval
59+
// }
60+
// if keepAliveTimeout <= 0 {
61+
// keepAliveTimeout = defaultKeepAliveTimeout
62+
// }
5763

5864
s := &Server{
59-
address: address,
60-
port: port,
61-
logger: logger,
62-
packetsCh: make(chan Packet, packetChanSize),
63-
errorsCh: make(chan error, 100),
64-
stopCh: make(chan struct{}),
65-
lastSeen: make(map[string]time.Time),
66-
keepAliveCheckInterval: keepAliveCheckInterval,
67-
keepAliveTimeout: keepAliveTimeout,
68-
OnDisconnect: onDisconnect,
65+
address: address,
66+
port: port,
67+
logger: logger,
68+
packetsCh: make(chan Packet, packetChanSize),
69+
errorsCh: make(chan error, 100),
70+
stopCh: make(chan struct{}),
71+
// Disabled: UDP keep-alive state initialization - Javier Ribal del Río (2026-07-15)
72+
// lastSeen: make(map[string]time.Time),
73+
// keepAliveCheckInterval: keepAliveCheckInterval,
74+
// keepAliveTimeout: keepAliveTimeout,
75+
// OnDisconnect: onDisconnect,
6976
}
7077

7178
s.ring = make([]Packet, ringBufferSize)
@@ -93,9 +100,10 @@ func (s *Server) Start() error {
93100
Uint16("port", s.port).
94101
Msg("UDP server started")
95102

96-
go s.readLoop() // Read incoming UDP packets
97-
go s.dispatchLoop() // Dispatch packets from ring buffer to channel
98-
go s.keepAliveLoop() // Monitor keep-alive timeouts
103+
go s.readLoop() // Read incoming UDP packets
104+
go s.dispatchLoop() // Dispatch packets from ring buffer to channel
105+
// Disabled: goroutine that monitored UDP keep-alive timeouts - Javier Ribal del Río (2026-07-15)
106+
// go s.keepAliveLoop()
99107
return nil
100108
}
101109

@@ -144,53 +152,56 @@ func (s *Server) readLoop() {
144152
Int("size", len(payload)).
145153
Msg("received UDP packet")
146154

147-
// Update keep-alive tracking for this source IP
148-
s.lastSeenMu.Lock()
149-
s.lastSeen[addr.IP.String()] = packet.Timestamp
150-
s.lastSeenMu.Unlock()
155+
// Disabled: recorded when each source IP was last seen, feeding
156+
// the UDP keep-alive check - Javier Ribal del Río (2026-07-15)
157+
// s.lastSeenMu.Lock()
158+
// s.lastSeen[addr.IP.String()] = packet.Timestamp
159+
// s.lastSeenMu.Unlock()
151160

152161
// Push packet to ring buffer
153162
s.push(packet)
154163
}
155164
}
156165
}
157166

158-
// keepAliveLoop checks for clients that have not sent any packets within the keepAliveTimeout duration.
159-
func (s *Server) keepAliveLoop() {
160-
ticker := time.NewTicker(s.keepAliveCheckInterval)
161-
defer ticker.Stop()
162-
163-
for {
164-
select {
165-
case <-s.stopCh:
166-
return
167-
case now := <-ticker.C:
168-
var timedOut []string
169-
170-
s.lastSeenMu.Lock()
171-
for ip, last := range s.lastSeen {
172-
if now.Sub(last) > s.keepAliveTimeout {
173-
timedOut = append(timedOut, ip)
174-
delete(s.lastSeen, ip)
175-
}
176-
}
177-
s.lastSeenMu.Unlock()
178-
179-
for _, ip := range timedOut {
180-
s.logger.Error().
181-
Str("ip", ip).
182-
Dur("timeout", s.keepAliveTimeout).
183-
Msg("keep-alive timeout: no UDP packets received")
184-
if s.OnDisconnect != nil {
185-
// Run the callback on its own goroutine so a slow
186-
// handler (e.g. blocking TCP writes) cannot stall
187-
// timeout detection for the remaining IPs
188-
go s.OnDisconnect(ip)
189-
}
190-
}
191-
}
192-
}
193-
}
167+
// Disabled: UDP keep-alive loop — checked every keepAliveCheckInterval whether
168+
// any source IP had gone more than keepAliveTimeout without sending a packet,
169+
// and fired OnDisconnect for it - Javier Ribal del Río (2026-07-15)
170+
// func (s *Server) keepAliveLoop() {
171+
// ticker := time.NewTicker(s.keepAliveCheckInterval)
172+
// defer ticker.Stop()
173+
//
174+
// for {
175+
// select {
176+
// case <-s.stopCh:
177+
// return
178+
// case now := <-ticker.C:
179+
// var timedOut []string
180+
//
181+
// s.lastSeenMu.Lock()
182+
// for ip, last := range s.lastSeen {
183+
// if now.Sub(last) > s.keepAliveTimeout {
184+
// timedOut = append(timedOut, ip)
185+
// delete(s.lastSeen, ip)
186+
// }
187+
// }
188+
// s.lastSeenMu.Unlock()
189+
//
190+
// for _, ip := range timedOut {
191+
// s.logger.Error().
192+
// Str("ip", ip).
193+
// Dur("timeout", s.keepAliveTimeout).
194+
// Msg("keep-alive timeout: no UDP packets received")
195+
// if s.OnDisconnect != nil {
196+
// // Run the callback on its own goroutine so a slow
197+
// // handler (e.g. blocking TCP writes) cannot stall
198+
// // timeout detection for the remaining IPs
199+
// go s.OnDisconnect(ip)
200+
// }
201+
// }
202+
// }
203+
// }
204+
// }
194205

195206
func (s *Server) GetPackets() <-chan Packet {
196207
return s.packetsCh

0 commit comments

Comments
 (0)