Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions backend/cmd/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ keep_alive_ms = 1000 # Keep-alive interval in milliseconds
[udp]
ring_buffer_size = 64 # Size of the ring buffer for incoming packets (number of packets, not bytes)
packet_chan_size = 16 # Size of the channel buffer
keep_alive_check_interval_ms = 5 # How often the keep-alive checks last-received times (ms)
keep_alive_timeout_ms = 100 # Max time without packets from an IP before it is considered disconnected (ms)
# Disabled: UDP keep-alive settings - Javier Ribal del Río (2026-07-15)
# keep_alive_check_interval_ms = 5 # How often the keep-alive checks last-received times (ms)
# keep_alive_timeout_ms = 100 # Max time without packets from an IP before it is considered disconnected (ms)


# Logger Configuration
Expand Down
5 changes: 3 additions & 2 deletions backend/cmd/dev-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ keep_alive_ms = 0 # Keep-alive interval in milliseconds (0 to disab
[udp]
ring_buffer_size = 64 # Size of the ring buffer for incoming packets (number of packets, not bytes)
packet_chan_size = 16 # Size of the channel buffer
keep_alive_check_interval_ms = 5 # How often the keep-alive checks last-received times (ms)
keep_alive_timeout_ms = 100 # Max time without packets from an IP before it is considered disconnected (ms)
# Disabled: UDP keep-alive settings - Javier Ribal del Río (2026-07-15)
# keep_alive_check_interval_ms = 5 # How often the keep-alive checks last-received times (ms)
# keep_alive_timeout_ms = 100 # Max time without packets from an IP before it is considered disconnected (ms)

# Server Configuration
[server.ethernet-view]
Expand Down
1 change: 1 addition & 0 deletions backend/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func main() {
// <--- transport --->
transp := transport.NewTransport(trace.Logger)
transp.SetpropagateFault(config.Transport.PropagateFault)
transp.SetADJHash(adj.Commit)

// <--- vehicle --->
err = configureVehicle(
Expand Down
42 changes: 23 additions & 19 deletions backend/cmd/setup_transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,32 +141,36 @@ func configureUDPServerTransport(
) {
trace.Info().Msg("Starting UDP server")

onKeepAliveTimeout := func(ip string) {
transp.SendFault()
board, ok := transp.TargetFromIp(ip)
if !ok {
board = "unknown"
}
err := fmt.Errorf("UDP keep-alive timeout: no packets received from board %s (%s) for %dms, fault sent", board, ip, config.UDP.KeepAliveTimeoutMs)
transp.ReportError(err)

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

udpServer := udp.NewServer(
adj.Info.Addresses[BACKEND],
adj.Info.Ports[UDP],
&trace.Logger,
config.UDP.RingBufferSize,
config.UDP.PacketChanSize,
time.Duration(config.UDP.KeepAliveCheckIntervalMs)*time.Millisecond,
time.Duration(config.UDP.KeepAliveTimeoutMs)*time.Millisecond,
onKeepAliveTimeout,
// Disabled: UDP keep-alive arguments - Javier Ribal del Río (2026-07-15)
// time.Duration(config.UDP.KeepAliveCheckIntervalMs)*time.Millisecond,
// time.Duration(config.UDP.KeepAliveTimeoutMs)*time.Millisecond,
// onKeepAliveTimeout,
)
err := udpServer.Start()
if err != nil {
Expand Down
9 changes: 5 additions & 4 deletions backend/internal/config/config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ type TCP struct {
}

type UDP struct {
RingBufferSize int `toml:"ring_buffer_size"`
PacketChanSize int `toml:"packet_chan_size"`
KeepAliveCheckIntervalMs int `toml:"keep_alive_check_interval_ms"`
KeepAliveTimeoutMs int `toml:"keep_alive_timeout_ms"`
RingBufferSize int `toml:"ring_buffer_size"`
PacketChanSize int `toml:"packet_chan_size"`
// Disabled: UDP keep-alive settings - Javier Ribal del Río (2026-07-15)
// KeepAliveCheckIntervalMs int `toml:"keep_alive_check_interval_ms"`
// KeepAliveTimeoutMs int `toml:"keep_alive_timeout_ms"`
}

type Logging struct {
Expand Down
9 changes: 7 additions & 2 deletions backend/pkg/adj/adj.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,14 @@ func downloadADJ(AdjSettings config.Adj) (string, json.RawMessage, json.RawMessa
return "local", nil, nil, err
}

// If not downloading use local ADJ
// If not downloading, recover the commit hash persisted by the last
// successful download. Without it the backend must not start: boards
// receive this hash on connection and could not validate their ADJ.
if commitHash == "" {
commitHash = "local"
commitHash, err = readCommitFile()
if err != nil {
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)
}
}

// After downloading adj apply adj validator
Expand Down
29 changes: 29 additions & 0 deletions backend/pkg/adj/git.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
package adj

import (
"fmt"
"os"
"path/filepath"
"strings"

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

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)

// commitFile persists the commit hash of the last downloaded ADJ. It lives
// next to the repo (not inside it) so wiping the repo before a clone does not
// lose it, and local runs (no branch or no internet) can still report it.
var commitFile = filepath.Join(filepath.Dir(filepath.Clean(RepoPath)), "adj_commit")

// writeCommitFile stores the commit hash of the freshly cloned ADJ
func writeCommitFile(hash string) error {
return os.WriteFile(commitFile, []byte(hash), 0644)
}

// readCommitFile recovers the commit hash persisted by the last download
func readCommitFile() (string, error) {
raw, err := os.ReadFile(commitFile)
if err != nil {
return "", err
}
hash := strings.TrimSpace(string(raw))
if hash == "" {
return "", fmt.Errorf("commit file %s is empty", commitFile)
}
return hash, nil
}

// updateRepo ensures that the local ADJ repository matches the specified remote branch.
// It first performs a test clone to verify remote accessibility (including internet
// connectivity). If the remote branch is accessible, the local repository is completely
Expand Down Expand Up @@ -85,5 +110,9 @@ func updateRepo(AdjBranch string) (string, error) {
}
}

if err = writeCommitFile(commitHash); err != nil {
return "", err
}

return commitHash, nil
}
4 changes: 0 additions & 4 deletions backend/pkg/transport/network/tcp/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ func (conn *connWithErr) Close() error {
return conn.Conn.Close()
}

// CloseWithError closes conn, delivering reason to its error channel first so
// the connection handler blocked on that channel wakes up. Closing a
// connection directly would not do this, since Read/Write filter out
// net.ErrClosed.
func CloseWithError(conn net.Conn, reason error) error {
if cwe, ok := conn.(*connWithErr); ok {
select {
Expand Down
151 changes: 81 additions & 70 deletions backend/pkg/transport/network/udp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,37 +35,44 @@ type Server struct {
ringMutex sync.Mutex
notEmpty *sync.Cond

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

const (
defaultKeepAliveCheckInterval = 5 * time.Millisecond
defaultKeepAliveTimeout = 100 * time.Millisecond
)

func NewServer(address string, port uint16, logger *zerolog.Logger, ringBufferSize int, packetChanSize int, keepAliveCheckInterval time.Duration, keepAliveTimeout time.Duration, onDisconnect func(ip string)) *Server {
if keepAliveCheckInterval <= 0 {
keepAliveCheckInterval = defaultKeepAliveCheckInterval
}
if keepAliveTimeout <= 0 {
keepAliveTimeout = defaultKeepAliveTimeout
}
// Disabled: default intervals for the UDP keep-alive - Javier Ribal del Río (2026-07-15)
// const (
// defaultKeepAliveCheckInterval = 5 * time.Millisecond
// defaultKeepAliveTimeout = 100 * time.Millisecond
// )

func NewServer(address string, port uint16, logger *zerolog.Logger, ringBufferSize int, packetChanSize int) *Server {
// Disabled: UDP keep-alive parameters and their defaulting; the previous
// signature also took keepAliveCheckInterval, keepAliveTimeout and an
// onDisconnect callback - Javier Ribal del Río (2026-07-15)
// if keepAliveCheckInterval <= 0 {
// keepAliveCheckInterval = defaultKeepAliveCheckInterval
// }
// if keepAliveTimeout <= 0 {
// keepAliveTimeout = defaultKeepAliveTimeout
// }

s := &Server{
address: address,
port: port,
logger: logger,
packetsCh: make(chan Packet, packetChanSize),
errorsCh: make(chan error, 100),
stopCh: make(chan struct{}),
lastSeen: make(map[string]time.Time),
keepAliveCheckInterval: keepAliveCheckInterval,
keepAliveTimeout: keepAliveTimeout,
OnDisconnect: onDisconnect,
address: address,
port: port,
logger: logger,
packetsCh: make(chan Packet, packetChanSize),
errorsCh: make(chan error, 100),
stopCh: make(chan struct{}),
// Disabled: UDP keep-alive state initialization - Javier Ribal del Río (2026-07-15)
// lastSeen: make(map[string]time.Time),
// keepAliveCheckInterval: keepAliveCheckInterval,
// keepAliveTimeout: keepAliveTimeout,
// OnDisconnect: onDisconnect,
}

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

go s.readLoop() // Read incoming UDP packets
go s.dispatchLoop() // Dispatch packets from ring buffer to channel
go s.keepAliveLoop() // Monitor keep-alive timeouts
go s.readLoop() // Read incoming UDP packets
go s.dispatchLoop() // Dispatch packets from ring buffer to channel
// Disabled: goroutine that monitored UDP keep-alive timeouts - Javier Ribal del Río (2026-07-15)
// go s.keepAliveLoop()
return nil
}

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

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

// Push packet to ring buffer
s.push(packet)
}
}
}

// keepAliveLoop checks for clients that have not sent any packets within the keepAliveTimeout duration.
func (s *Server) keepAliveLoop() {
ticker := time.NewTicker(s.keepAliveCheckInterval)
defer ticker.Stop()

for {
select {
case <-s.stopCh:
return
case now := <-ticker.C:
var timedOut []string

s.lastSeenMu.Lock()
for ip, last := range s.lastSeen {
if now.Sub(last) > s.keepAliveTimeout {
timedOut = append(timedOut, ip)
delete(s.lastSeen, ip)
}
}
s.lastSeenMu.Unlock()

for _, ip := range timedOut {
s.logger.Error().
Str("ip", ip).
Dur("timeout", s.keepAliveTimeout).
Msg("keep-alive timeout: no UDP packets received")
if s.OnDisconnect != nil {
// Run the callback on its own goroutine so a slow
// handler (e.g. blocking TCP writes) cannot stall
// timeout detection for the remaining IPs
go s.OnDisconnect(ip)
}
}
}
}
}
// Disabled: UDP keep-alive loop — checked every keepAliveCheckInterval whether
// any source IP had gone more than keepAliveTimeout without sending a packet,
// and fired OnDisconnect for it - Javier Ribal del Río (2026-07-15)
// func (s *Server) keepAliveLoop() {
// ticker := time.NewTicker(s.keepAliveCheckInterval)
// defer ticker.Stop()
//
// for {
// select {
// case <-s.stopCh:
// return
// case now := <-ticker.C:
// var timedOut []string
//
// s.lastSeenMu.Lock()
// for ip, last := range s.lastSeen {
// if now.Sub(last) > s.keepAliveTimeout {
// timedOut = append(timedOut, ip)
// delete(s.lastSeen, ip)
// }
// }
// s.lastSeenMu.Unlock()
//
// for _, ip := range timedOut {
// s.logger.Error().
// Str("ip", ip).
// Dur("timeout", s.keepAliveTimeout).
// Msg("keep-alive timeout: no UDP packets received")
// if s.OnDisconnect != nil {
// // Run the callback on its own goroutine so a slow
// // handler (e.g. blocking TCP writes) cannot stall
// // timeout detection for the remaining IPs
// go s.OnDisconnect(ip)
// }
// }
// }
// }
// }

func (s *Server) GetPackets() <-chan Packet {
return s.packetsCh
Expand Down
Loading
Loading