From 34f8091eb5d370158e28148a1ce459bc360df268 Mon Sep 17 00:00:00 2001 From: Marc Sanchis Date: Thu, 5 Jun 2025 22:17:33 +0200 Subject: [PATCH 1/6] update BLCU board implementation --- backend/pkg/boards/blcu.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/pkg/boards/blcu.go b/backend/pkg/boards/blcu.go index 7d9cb66c3..39f6a9977 100644 --- a/backend/pkg/boards/blcu.go +++ b/backend/pkg/boards/blcu.go @@ -15,7 +15,9 @@ const ( BlcuName = "BLCU" BlcuId = abstraction.BoardId(1) - AckId = "1" + AckId = abstraction.BoardEvent("ACK") + DownloadEventId = abstraction.BoardEvent("DOWNLOAD") + UploadEventId = abstraction.BoardEvent("UPLOAD") BlcuDownloadOrderId = 1 BlcuUploadOrderId = 2 @@ -39,21 +41,21 @@ func (boards *BLCU) Id() abstraction.BoardId { func (boards *BLCU) Notify(boardNotification abstraction.BoardNotification) { switch notification := boardNotification.(type) { - case AckNotification: + case *AckNotification: boards.ackChan <- struct{}{} - case DownloadEvent: - err := boards.download(notification) + case *DownloadEvent: + err := boards.download(*notification) if err != nil { fmt.Println(ErrDownloadFailure{ Timestamp: time.Now(), Inner: err, }.Error()) } - case UploadEvent: - err := boards.upload(notification) + case *UploadEvent: + err := boards.upload(*notification) if err != nil { - fmt.Println(ErrDownloadFailure{ + fmt.Println(ErrUploadFailure{ Timestamp: time.Now(), Inner: err, }.Error()) From d7f072b986efef7f59ce41ca137874123a3a8ae2 Mon Sep 17 00:00:00 2001 From: Marc Sanchis Date: Thu, 5 Jun 2025 22:17:41 +0200 Subject: [PATCH 2/6] fix BLCU download and upload handlers --- backend/pkg/broker/topics/blcu/download.go | 27 ++++++++--- backend/pkg/broker/topics/blcu/upload.go | 56 +++++++++++++++++----- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/backend/pkg/broker/topics/blcu/download.go b/backend/pkg/broker/topics/blcu/download.go index f98c65225..2910831a3 100644 --- a/backend/pkg/broker/topics/blcu/download.go +++ b/backend/pkg/broker/topics/blcu/download.go @@ -29,19 +29,32 @@ func (request DownloadRequest) Topic() abstraction.BrokerTopic { } func (download *Download) Push(push abstraction.BrokerPush) error { - switch push.Topic() { - case boards.DownloadSuccess{}.Topic(): + switch p := push.(type) { + case *boards.DownloadSuccess: + // Send success response with the downloaded data + response := map[string]interface{}{ + "percentage": 100, + "failure": false, + "file": p.Data, // The downloaded file data + } + payload, _ := json.Marshal(response) err := download.pool.Write(download.client, websocket.Message{ - Topic: push.Topic(), - Payload: nil, + Topic: DownloadName, + Payload: payload, }) if err != nil { return err } - case boards.DownloadFailure{}.Topic(): + case *boards.DownloadFailure: + // Send failure response + response := map[string]interface{}{ + "percentage": 0, + "failure": true, + } + payload, _ := json.Marshal(response) err := download.pool.Write(download.client, websocket.Message{ - Topic: push.Topic(), - Payload: nil, + Topic: DownloadName, + Payload: payload, }) if err != nil { return err diff --git a/backend/pkg/broker/topics/blcu/upload.go b/backend/pkg/broker/topics/blcu/upload.go index 85b803474..82e2b1278 100644 --- a/backend/pkg/broker/topics/blcu/upload.go +++ b/backend/pkg/broker/topics/blcu/upload.go @@ -1,6 +1,7 @@ package blcu import ( + "encoding/base64" "encoding/json" "fmt" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" @@ -22,31 +23,50 @@ func (upload *Upload) Topic() abstraction.BrokerTopic { type UploadRequest struct { Board string `json:"board"` - Data []byte `json:"data"` + File string `json:"file"` // Base64 encoded file data from frontend } func (request UploadRequest) Topic() abstraction.BrokerTopic { return "blcu/uploadRequest" } -func (upload *Upload) Push(push abstraction.BrokerPush) error { - // Switch success/failure - // upload.pool.Write(ID, message) +// UploadRequestInternal is the internal representation with decoded data +type UploadRequestInternal struct { + Board string + Data []byte +} - switch push.Topic() { - case boards.UploadSuccess{}.Topic(): +func (request UploadRequestInternal) Topic() abstraction.BrokerTopic { + return "blcu/uploadRequest" +} + +func (upload *Upload) Push(push abstraction.BrokerPush) error { + switch push.(type) { + case *boards.UploadSuccess: + // Send success response + response := map[string]interface{}{ + "percentage": 100, + "failure": false, + } + payload, _ := json.Marshal(response) err := upload.pool.Write(upload.client, websocket.Message{ - Topic: push.Topic(), - Payload: nil, + Topic: UploadName, + Payload: payload, }) if err != nil { return err } - case boards.UploadFailure{}.Topic(): + case *boards.UploadFailure: + // Send failure response + response := map[string]interface{}{ + "percentage": 0, + "failure": true, + } + payload, _ := json.Marshal(response) err := upload.pool.Write(upload.client, websocket.Message{ - Topic: push.Topic(), - Payload: nil, + Topic: UploadName, + Payload: payload, }) if err != nil { return err @@ -79,7 +99,19 @@ func (upload *Upload) handleUpload(message *websocket.Message) error { return err } - pushErr := upload.api.UserPush(uploadRequest) + // Decode base64 file data + fileData, err := base64.StdEncoding.DecodeString(uploadRequest.File) + if err != nil { + return fmt.Errorf("failed to decode base64 file data: %w", err) + } + + // Create the internal upload event with decoded data + internalRequest := &UploadRequestInternal{ + Board: uploadRequest.Board, + Data: fileData, + } + + pushErr := upload.api.UserPush(internalRequest) return pushErr } From 7d729483e4e38f85fcb404cb6f6ad4a6bcad80eb Mon Sep 17 00:00:00 2001 From: Marc Sanchis Date: Thu, 5 Jun 2025 22:17:53 +0200 Subject: [PATCH 3/6] improve BLCU broker tests --- backend/pkg/broker/topics/blcu/blcu_test.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/pkg/broker/topics/blcu/blcu_test.go b/backend/pkg/broker/topics/blcu/blcu_test.go index 8c17935ee..fd3136224 100644 --- a/backend/pkg/broker/topics/blcu/blcu_test.go +++ b/backend/pkg/broker/topics/blcu/blcu_test.go @@ -36,10 +36,11 @@ func (api MockAPI) UserPush(push abstraction.BrokerPush) error { errorFlag = false log.Printf("Output matches") return nil - case blcu.UploadRequest: - if push.(blcu.UploadRequest).Board != "test" || string(push.(blcu.UploadRequest).Data) != "test" { + case *blcu.UploadRequestInternal: + req := push.(*blcu.UploadRequestInternal) + if req.Board != "test" || string(req.Data) != "test" { errorFlag = true - fmt.Printf("Expected board 'test' and data 'test', got board '%s' and data '%s'\n", push.(blcu.UploadRequest).Board, string(push.(blcu.UploadRequest).Data)) + fmt.Printf("Expected board 'test' and data 'test', got board '%s' and data '%s'\n", req.Board, string(req.Data)) return &OutputNotMatchingError{} } errorFlag = false @@ -151,8 +152,8 @@ func TestBLCUTopic_Upload_Push(t *testing.T) { upload.SetAPI(api) upload.SetPool(pool) - // Simulate sending a download request - request := blcu.UploadRequest{Board: "test", Data: []byte("test")} + // Simulate sending an upload request + request := blcu.UploadRequest{Board: "test", File: "dGVzdA=="} // "test" in base64 err = upload.Push(request) if err != nil { t.Fatal("Error pushing upload request:", err) @@ -188,7 +189,8 @@ func TestBLCUTopic_Upload_ClientMessage(t *testing.T) { upload := blcu.Upload{} upload.SetAPI(&MockAPI{}) - payload := blcu.UploadRequest{Board: "test", Data: []byte("test")} + // Use base64 encoded data as the frontend would send + payload := blcu.UploadRequest{Board: "test", File: "dGVzdA=="} // "test" in base64 payloadBytes, _ := json.Marshal(payload) upload.ClientMessage(websocket.ClientId{0}, &websocket.Message{ From a696492c64157609e2380218944e261f9e2d0813 Mon Sep 17 00:00:00 2001 From: Marc Sanchis Date: Thu, 5 Jun 2025 22:18:00 +0200 Subject: [PATCH 4/6] add BLCU board tests --- backend/pkg/boards/blcu_integration_test.go | 284 ++++++++++++++++++++ backend/pkg/boards/blcu_simple_test.go | 54 ++++ 2 files changed, 338 insertions(+) create mode 100644 backend/pkg/boards/blcu_integration_test.go create mode 100644 backend/pkg/boards/blcu_simple_test.go diff --git a/backend/pkg/boards/blcu_integration_test.go b/backend/pkg/boards/blcu_integration_test.go new file mode 100644 index 000000000..6586c88c2 --- /dev/null +++ b/backend/pkg/boards/blcu_integration_test.go @@ -0,0 +1,284 @@ +package boards_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" + "github.com/HyperloopUPV-H8/h9-backend/pkg/broker" + blcu_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/blcu" + "github.com/HyperloopUPV-H8/h9-backend/pkg/vehicle" + "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" + "github.com/rs/zerolog" +) + +// MockTransport implements abstraction.Transport for testing +type MockTransport struct { + sentMessages []abstraction.TransportMessage +} + +func (m *MockTransport) SendMessage(msg abstraction.TransportMessage) error { + m.sentMessages = append(m.sentMessages, msg) + return nil +} + +func (m *MockTransport) HandleClient(config interface{}, target string) error { + return nil +} + +func (m *MockTransport) HandleServer(config interface{}, addr string) error { + return nil +} + +func (m *MockTransport) HandleSniffer(sniffer interface{}) error { + return nil +} + +func (m *MockTransport) SetAPI(api abstraction.TransportAPI) {} + +func (m *MockTransport) SetIdTarget(id abstraction.PacketId, target abstraction.TransportTarget) {} + +func (m *MockTransport) SetTargetIp(ip string, target abstraction.TransportTarget) {} + +func (m *MockTransport) SetpropagateFault(propagate bool) {} + +func (m *MockTransport) WithDecoder(decoder interface{}) abstraction.Transport { + return m +} + +func (m *MockTransport) WithEncoder(encoder interface{}) abstraction.Transport { + return m +} + +// MockLogger implements abstraction.Logger for testing +type MockLogger struct{} + +func (m *MockLogger) Start() error { + return nil +} + +func (m *MockLogger) Stop() error { + return nil +} + +func (m *MockLogger) PushRecord(record abstraction.LoggerRecord) error { + return nil +} + +func (m *MockLogger) PullRecord(request abstraction.LoggerRequest) (abstraction.LoggerRecord, error) { + return nil, nil +} + +// TestBLCUDownloadOrder tests the BLCU download order flow +func TestBLCUDownloadOrder(t *testing.T) { + // Setup + logger := zerolog.New(nil).Level(zerolog.Disabled) + + // Create vehicle + v := vehicle.New(logger) + + // Create and setup broker + b := broker.New(logger) + connections := make(chan *websocket.Client) + pool := websocket.NewPool(connections, logger) + b.SetPool(pool) + + // Register BLCU topics + blcu_topic.RegisterTopics(b, pool) + + // Set broker and transport + v.SetBroker(b) + mockTransport := &MockTransport{} + v.SetTransport(mockTransport) + mockLogger := &MockLogger{} + v.SetLogger(mockLogger) + + // Create BLCU board + blcuBoard := boards.New("192.168.0.10") // Example IP + + // This is the missing step - register the BLCU board with the vehicle + v.AddBoard(blcuBoard) + + // Note: In a real scenario, we would capture responses through the broker + + // Test download request + t.Run("Download Request", func(t *testing.T) { + downloadRequest := &blcu_topic.DownloadRequest{ + Board: "VCU", + } + + // Send download request through UserPush + err := v.UserPush(downloadRequest) + if err != nil { + t.Fatalf("UserPush failed: %v", err) + } + + // Simulate ACK from board + blcuBoard.Notify(boards.AckNotification{ + ID: boards.AckId, + }) + + // Check if the download order was sent to the board + if len(mockTransport.sentMessages) == 0 { + t.Fatal("No message sent to transport") + } + + // Verify the packet sent contains the correct order ID + // In a real test, we would decode the packet and verify its contents + }) +} + +// TestBLCUUploadOrder tests the BLCU upload order flow +func TestBLCUUploadOrder(t *testing.T) { + // Setup + logger := zerolog.New(nil).Level(zerolog.Disabled) + + // Create vehicle + v := vehicle.New(logger) + + // Create and setup broker + b := broker.New(logger) + connections := make(chan *websocket.Client) + pool := websocket.NewPool(connections, logger) + b.SetPool(pool) + + // Register BLCU topics + blcu_topic.RegisterTopics(b, pool) + + // Set broker and transport + v.SetBroker(b) + mockTransport := &MockTransport{} + v.SetTransport(mockTransport) + mockLogger := &MockLogger{} + v.SetLogger(mockLogger) + + // Create BLCU board + blcuBoard := boards.New("192.168.0.10") // Example IP + + // Register the BLCU board with the vehicle + v.AddBoard(blcuBoard) + + // Test upload request + t.Run("Upload Request", func(t *testing.T) { + // Using the internal request type that has Data field + uploadRequest := &blcu_topic.UploadRequestInternal{ + Board: "VCU", + Data: []byte("test firmware data"), + } + + // Send upload request through UserPush + err := v.UserPush(uploadRequest) + if err != nil { + t.Fatalf("UserPush failed: %v", err) + } + + // Simulate ACK from board + blcuBoard.Notify(boards.AckNotification{ + ID: boards.AckId, + }) + + // Check if the upload order was sent to the board + if len(mockTransport.sentMessages) == 0 { + t.Fatal("No message sent to transport") + } + }) +} + +// TestBLCUWebSocketFlow tests the complete WebSocket flow for BLCU orders +func TestBLCUWebSocketFlow(t *testing.T) { + // Setup + logger := zerolog.New(nil).Level(zerolog.Disabled) + + // Create vehicle + v := vehicle.New(logger) + + // Create and setup broker + b := broker.New(logger) + connections := make(chan *websocket.Client) + pool := websocket.NewPool(connections, logger) + b.SetPool(pool) + + // Register BLCU topics + blcu_topic.RegisterTopics(b, pool) + + // Set broker + v.SetBroker(b) + mockTransport := &MockTransport{} + v.SetTransport(mockTransport) + mockLogger := &MockLogger{} + v.SetLogger(mockLogger) + + // Create BLCU board + blcuBoard := boards.New("192.168.0.10") + v.AddBoard(blcuBoard) + + // Simulate WebSocket client message + t.Run("WebSocket Download Message", func(t *testing.T) { + // Get download topic handler from registered topics + downloadHandler := &blcu_topic.Download{} + downloadHandler.SetAPI(b) + downloadHandler.SetPool(pool) + + // Create WebSocket message + downloadReq := blcu_topic.DownloadRequest{ + Board: "VCU", + } + payload, _ := json.Marshal(downloadReq) + + wsMessage := &websocket.Message{ + Topic: blcu_topic.DownloadName, + Payload: payload, + } + + // Simulate client message + // Create a valid UUID for ClientId + clientUUID := [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + clientId := websocket.ClientId(clientUUID) + downloadHandler.ClientMessage(clientId, wsMessage) + + // Give some time for async operations + time.Sleep(100 * time.Millisecond) + + // Verify order was sent + if len(mockTransport.sentMessages) == 0 { + t.Error("No message sent to transport after WebSocket message") + } + }) +} + +// TestBLCURegistrationIssue demonstrates the issue when BLCU is not registered +func TestBLCURegistrationIssue(t *testing.T) { + // Setup WITHOUT registering BLCU board + logger := zerolog.New(nil).Level(zerolog.Disabled) + + v := vehicle.New(logger) + b := broker.New(logger) + connections := make(chan *websocket.Client) + pool := websocket.NewPool(connections, logger) + b.SetPool(pool) + blcu_topic.RegisterTopics(b, pool) + v.SetBroker(b) + + // Try to send download request without BLCU board registered + t.Run("Download Without Registration", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + // If no panic, check if the request was handled + // In the current implementation, this will fail silently + t.Log("Request handled without BLCU registration - this is the bug!") + } + }() + + downloadRequest := &blcu_topic.DownloadRequest{ + Board: "VCU", + } + + // This will fail because boards[boards.BlcuId] is nil + err := v.UserPush(downloadRequest) + if err == nil { + t.Log("UserPush succeeded but BLCU board notification will fail") + } + }) +} \ No newline at end of file diff --git a/backend/pkg/boards/blcu_simple_test.go b/backend/pkg/boards/blcu_simple_test.go new file mode 100644 index 000000000..2fcf4ddae --- /dev/null +++ b/backend/pkg/boards/blcu_simple_test.go @@ -0,0 +1,54 @@ +package boards_test + +import ( + "testing" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" + blcu_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/blcu" + "github.com/HyperloopUPV-H8/h9-backend/pkg/vehicle" + "github.com/rs/zerolog" +) + +// TestBLCUBoardRegistration tests that BLCU board can be registered +func TestBLCUBoardRegistration(t *testing.T) { + logger := zerolog.New(nil).Level(zerolog.Disabled) + v := vehicle.New(logger) + + // Create and register BLCU board + blcuBoard := boards.New("192.168.0.10") + v.AddBoard(blcuBoard) + + // Verify board is registered + if blcuBoard.Id() != boards.BlcuId { + t.Errorf("Expected board ID %d, got %d", boards.BlcuId, blcuBoard.Id()) + } +} + +// TestBLCURequestStructures tests the request structures +func TestBLCURequestStructures(t *testing.T) { + // Test download request + downloadReq := &blcu_topic.DownloadRequest{ + Board: "VCU", + } + if downloadReq.Topic() != "blcu/downloadRequest" { + t.Errorf("Expected topic 'blcu/downloadRequest', got '%s'", downloadReq.Topic()) + } + + // Test upload request + uploadReq := &blcu_topic.UploadRequest{ + Board: "VCU", + File: "dGVzdCBkYXRh", // base64 for "test data" + } + if uploadReq.Topic() != "blcu/uploadRequest" { + t.Errorf("Expected topic 'blcu/uploadRequest', got '%s'", uploadReq.Topic()) + } + + // Test internal upload request + uploadReqInternal := &blcu_topic.UploadRequestInternal{ + Board: "VCU", + Data: []byte("test data"), + } + if uploadReqInternal.Topic() != "blcu/uploadRequest" { + t.Errorf("Expected topic 'blcu/uploadRequest', got '%s'", uploadReqInternal.Topic()) + } +} \ No newline at end of file From 81e6bf502b5b18587a890d9da5fff41f0536d5fe Mon Sep 17 00:00:00 2001 From: Marc Sanchis Date: Thu, 5 Jun 2025 22:18:11 +0200 Subject: [PATCH 5/6] update vehicle notification handling --- backend/pkg/vehicle/vehicle.go | 58 ++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/backend/pkg/vehicle/vehicle.go b/backend/pkg/vehicle/vehicle.go index 790a212f2..fe1405861 100644 --- a/backend/pkg/vehicle/vehicle.go +++ b/backend/pkg/vehicle/vehicle.go @@ -89,28 +89,46 @@ func (vehicle *Vehicle) UserPush(push abstraction.BrokerPush) error { status.Fulfill(status.Enable()) } - case blcu_topic.DownloadName: + case "blcu/downloadRequest": download := push.(*blcu_topic.DownloadRequest) - vehicle.boards[boards.BlcuId].Notify(abstraction.BoardNotification( - &boards.DownloadEvent{ - BoardEvent: boards.AckId, - BoardID: boards.BlcuId, - Board: download.Board, - }, - )) - - case blcu_topic.UploadName: - upload := push.(*blcu_topic.UploadRequest) - - vehicle.boards[boards.BlcuId].Notify(abstraction.BoardNotification( - &boards.UploadEvent{ - BoardEvent: boards.AckId, - Board: upload.Board, - Data: upload.Data, - Length: len(upload.Data), - }, - )) + if board, exists := vehicle.boards[boards.BlcuId]; exists { + board.Notify(abstraction.BoardNotification( + &boards.DownloadEvent{ + BoardEvent: boards.DownloadEventId, + BoardID: boards.BlcuId, + Board: download.Board, + }, + )) + } else { + fmt.Fprintf(os.Stderr, "BLCU board not registered\n") + } + + case "blcu/uploadRequest": + // Handle both UploadRequest and UploadRequestInternal + var uploadEvent *boards.UploadEvent + switch u := push.(type) { + case *blcu_topic.UploadRequestInternal: + uploadEvent = &boards.UploadEvent{ + BoardEvent: boards.UploadEventId, + Board: u.Board, + Data: u.Data, + Length: len(u.Data), + } + case *blcu_topic.UploadRequest: + // This shouldn't happen as the handler should convert to Internal + fmt.Fprintf(os.Stderr, "received raw UploadRequest, expected UploadRequestInternal\n") + return nil + default: + fmt.Fprintf(os.Stderr, "unknown upload type: %T\n", push) + return nil + } + + if board, exists := vehicle.boards[boards.BlcuId]; exists { + board.Notify(abstraction.BoardNotification(uploadEvent)) + } else { + fmt.Fprintf(os.Stderr, "BLCU board not registered\n") + } default: fmt.Printf("unknow topic %s\n", push.Topic()) From 8cad2d9345bd70a4530ad6ccf79701701975725a Mon Sep 17 00:00:00 2001 From: Marc Sanchis Date: Thu, 5 Jun 2025 22:18:17 +0200 Subject: [PATCH 6/6] update main.go backend initialization --- backend/cmd/main.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index ff279d111..cc046a872 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -32,6 +32,7 @@ import ( "github.com/HyperloopUPV-H8/h9-backend/internal/utils" vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" "github.com/HyperloopUPV-H8/h9-backend/pkg/broker" blcu_topics "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/blcu" connection_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/connection" @@ -219,6 +220,16 @@ func main() { vehicle.SetIdToBoardName(idToBoard) vehicle.SetTransport(transp) + // <--- BLCU Board ---> + // Register BLCU board for handling bootloader operations + if blcuIP, exists := adj.Info.Addresses[BLCU]; exists { + blcuBoard := boards.New(blcuIP) + vehicle.AddBoard(blcuBoard) + trace.Info().Str("ip", blcuIP).Msg("BLCU board registered") + } else { + trace.Warn().Msg("BLCU address not found in ADJ") + } + // <--- transport ---> // Load and set packet decoder and encoder decoder, encoder := getTransportDecEnc(adj.Info, podData)