From 470d54383ac30bad1a0023383e31d86d2506ec17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Ribal=20del=20R=C3=ADo?= Date: Mon, 16 Feb 2026 15:37:44 +0100 Subject: [PATCH 01/37] feat(backend): remove blcu --- backend/cmd/config.toml | 6 - backend/cmd/dev-config.toml | 6 +- backend/cmd/main.go | 3 +- backend/cmd/setup_transport.go | 40 --- backend/cmd/setup_vehicle.go | 42 --- backend/internal/config/config_types.go | 7 - backend/internal/vehicle/models/order_data.go | 2 +- backend/pkg/boards/blcu.go | 275 ----------------- backend/pkg/boards/blcu_integration_test.go | 284 ------------------ backend/pkg/boards/blcu_simple_test.go | 106 ------- backend/pkg/boards/events.go | 28 -- backend/pkg/broker/topics/blcu/blcu_test.go | 204 ------------- backend/pkg/broker/topics/blcu/download.go | 100 ------ backend/pkg/broker/topics/blcu/register.go | 22 -- backend/pkg/broker/topics/blcu/upload.go | 124 -------- backend/pkg/transport/packet/blcu/decoder.go | 20 -- backend/pkg/transport/packet/blcu/packet.go | 22 -- backend/pkg/transport/presentation/decoder.go | 2 - .../transport/presentation/decoder_test.go | 21 +- backend/pkg/transport/transport.go | 35 +-- backend/pkg/vehicle/constructor.go | 5 - backend/pkg/vehicle/notification.go | 10 +- backend/pkg/vehicle/vehicle.go | 43 --- 23 files changed, 14 insertions(+), 1393 deletions(-) delete mode 100644 backend/pkg/boards/blcu.go delete mode 100644 backend/pkg/boards/blcu_integration_test.go delete mode 100644 backend/pkg/boards/blcu_simple_test.go delete mode 100644 backend/pkg/broker/topics/blcu/blcu_test.go delete mode 100644 backend/pkg/broker/topics/blcu/download.go delete mode 100644 backend/pkg/broker/topics/blcu/register.go delete mode 100644 backend/pkg/broker/topics/blcu/upload.go delete mode 100644 backend/pkg/transport/packet/blcu/decoder.go delete mode 100644 backend/pkg/transport/packet/blcu/packet.go diff --git a/backend/cmd/config.toml b/backend/cmd/config.toml index f75149e79..90f742c3b 100644 --- a/backend/cmd/config.toml +++ b/backend/cmd/config.toml @@ -33,12 +33,6 @@ max_retries = 0 # Maximum retries before cycling (0 = infinite retr connection_timeout_ms = 1000 # Connection timeout in milliseconds keep_alive_ms = 1000 # Keep-alive interval in milliseconds -# BLCU (Boot Loader Control Unit) Configuration -[blcu] -ip = "127.0.0.1" # TFTP server IP address -download_order_id = 0 # Packet ID for download orders (0 = use default) -upload_order_id = 0 # Packet ID for upload orders (0 = use default) - # TFTP Configuration [tftp] block_size = 131072 # TFTP block size in bytes (128kB) diff --git a/backend/cmd/dev-config.toml b/backend/cmd/dev-config.toml index 876049230..0ea168ec3 100644 --- a/backend/cmd/dev-config.toml +++ b/backend/cmd/dev-config.toml @@ -66,11 +66,7 @@ timeout_ms = 5000 # Timeout for TFTP operations in milliseconds backoff_factor = 2 # Backoff factor for retries enable_progress = true # Enable progress updates during transfers -# BLCU Configuration -[blcu] -ip = "10.10.10.5" # BLCU IP address -download_order_id = 0 # Order ID for download operations (0 = use default) -upload_order_id = 0 # Order ID for upload operations (0 = use default) + # Logging Configuration [logging] diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 637bfd8e7..addc5c879 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -23,7 +23,6 @@ const ( TcpServer = "TCP_SERVER" UDP = "UDP" SNTP = "SNTP" - BlcuAck = "blcu_ack" AddStateOrder = "add_state_order" RemoveStateOrder = "remove_state_order" ) @@ -62,7 +61,7 @@ func main() { } // <--- vehicle orders ---> - vehicleOrders, err := vehicle_models.NewVehicleOrders(podData.Boards, adj.Info.Addresses[BLCU]) + vehicleOrders, err := vehicle_models.NewVehicleOrders(podData.Boards) if err != nil { trace.Fatal().Err(err).Msg("creating vehicleOrders") } diff --git a/backend/cmd/setup_transport.go b/backend/cmd/setup_transport.go index 796ba29af..c86f9270e 100644 --- a/backend/cmd/setup_transport.go +++ b/backend/cmd/setup_transport.go @@ -13,11 +13,9 @@ import ( "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/utils" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/tcp" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/udp" - blcu_packet "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/blcu" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/order" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" @@ -45,11 +43,6 @@ func configureTransport( transp.SetTargetIp(adj.Info.Addresses[board.Name], abstraction.TransportTarget(board.Name)) } - // If BLCU is configured set BLCU packet ID mappings - if common.Contains(config.Vehicle.Boards, "BLCU") { - configureBLCUTransport(adj, transp, config) - } - // Start handling TCP CLIENT connections configureTCPClientTransport(adj, podData, transp, config) @@ -61,36 +54,6 @@ func configureTransport( } -// configureBLCUTransport sets the packet IDs and target IP for the BLCU board. -// It prefers values from config, falls back to ADJ and finally to a loopback default. -func configureBLCUTransport(adj adj_module.ADJ, - transp *transport.Transport, - config config.Config) { - // Use configurable packet IDs or defaults - downloadOrderID := config.Blcu.DownloadOrderId - uploadOrderID := config.Blcu.UploadOrderId - if downloadOrderID == 0 { - downloadOrderID = boards.DefaultBlcuDownloadOrderId - } - if uploadOrderID == 0 { - uploadOrderID = boards.DefaultBlcuUploadOrderId - } - - transp.SetIdTarget(abstraction.PacketId(downloadOrderID), abstraction.TransportTarget("BLCU")) - transp.SetIdTarget(abstraction.PacketId(uploadOrderID), abstraction.TransportTarget("BLCU")) - - // Use BLCU address from config, ADJ, or default - blcuIP := config.Blcu.IP - if blcuIP == "" { - if adjBlcuIP, exists := adj.Info.Addresses[BLCU]; exists { - blcuIP = adjBlcuIP - } else { - blcuIP = "127.0.0.1" - } - } - transp.SetTargetIp(blcuIP, abstraction.TransportTarget("BLCU")) -} - func configureTCPClientTransport( adj adj_module.ADJ, podData pod_data.PodData, @@ -247,9 +210,6 @@ func getTransportDecEnc(info adj_module.Info, podData pod_data.PodData) (*presen encoder.SetPacketEncoder(id, dataEncoder) } - // Register BLCU ack decoder - decoder.SetPacketDecoder(abstraction.PacketId(info.MessageIds[BlcuAck]), blcu_packet.NewDecoder()) - // TODO Solve this foking mess, I have tried... stateOrdersDecoder := order.NewDecoder(binary.LittleEndian) stateOrdersDecoder.SetActionId(abstraction.PacketId(info.MessageIds[AddStateOrder]), stateOrdersDecoder.DecodeAdd) diff --git a/backend/cmd/setup_vehicle.go b/backend/cmd/setup_vehicle.go index 21423de33..b8a7b62e0 100644 --- a/backend/cmd/setup_vehicle.go +++ b/backend/cmd/setup_vehicle.go @@ -18,9 +18,7 @@ import ( "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory" "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" data_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/data" logger_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/logger" @@ -75,7 +73,6 @@ func configureBroker(subloggers abstraction.SubloggersMap, loggerHandler *logger }) broker.SetPool(pool) - blcu_topics.RegisterTopics(broker, pool) return broker, cleanup } @@ -101,45 +98,6 @@ func configureVehicle( vehicle.SetIdToBoardName(idToBoard) vehicle.SetTransport(transp) - // Register BLCU board for handling bootloader operations - if blcuIP, exists := adj.Info.Addresses[BLCU]; exists { - blcuId, idExists := adj.Info.BoardIds["BLCU"] - if !idExists { - return fmt.Errorf("BLCU IP found in ADJ but board ID missing") - } else { - // Get configurable order IDs or use defaults - downloadOrderId := config.Blcu.DownloadOrderId - uploadOrderId := config.Blcu.UploadOrderId - if downloadOrderId == 0 { - downloadOrderId = boards.DefaultBlcuDownloadOrderId - } - if uploadOrderId == 0 { - uploadOrderId = boards.DefaultBlcuUploadOrderId - } - - tftpConfig := boards.TFTPConfig{ - BlockSize: config.TFTP.BlockSize, - Retries: config.TFTP.Retries, - TimeoutMs: config.TFTP.TimeoutMs, - BackoffFactor: config.TFTP.BackoffFactor, - EnableProgress: config.TFTP.EnableProgress, - } - blcuBoard := boards.NewWithConfig(blcuIP, tftpConfig, abstraction.BoardId(blcuId), downloadOrderId, uploadOrderId) - vehicle.AddBoard(blcuBoard) - vehicle.SetBlcuId(abstraction.BoardId(blcuId)) - - trace. - Info(). - Str("ip", blcuIP). - Int("id", int(blcuId)). - Uint16("download_order_id", downloadOrderId). - Uint16("upload_order_id", uploadOrderId). - Msg("BLCU board registered") - } - } else { - trace.Warn().Msg("BLCU not found in ADJ configuration - bootloader operations unavailable") - } - return nil } diff --git a/backend/internal/config/config_types.go b/backend/internal/config/config_types.go index 256ece881..1c0a3a559 100644 --- a/backend/internal/config/config_types.go +++ b/backend/internal/config/config_types.go @@ -26,12 +26,6 @@ type TFTP struct { EnableProgress bool `toml:"enable_progress"` } -type Blcu struct { - IP string `toml:"ip"` - DownloadOrderId uint16 `toml:"download_order_id"` - UploadOrderId uint16 `toml:"upload_order_id"` -} - type TCP struct { BackoffMinMs int `toml:"backoff_min_ms"` BackoffMaxMs int `toml:"backoff_max_ms"` @@ -54,6 +48,5 @@ type Config struct { Transport Transport TFTP TFTP TCP TCP - Blcu Blcu Logging Logging } diff --git a/backend/internal/vehicle/models/order_data.go b/backend/internal/vehicle/models/order_data.go index ac4bc044b..e19ff59de 100644 --- a/backend/internal/vehicle/models/order_data.go +++ b/backend/internal/vehicle/models/order_data.go @@ -36,7 +36,7 @@ type StateOrderDescription struct { Enabled bool `json:"enabled"` } -func NewVehicleOrders(boards []pod_data.Board, blcuName string) (VehicleOrders, error) { +func NewVehicleOrders(boards []pod_data.Board) (VehicleOrders, error) { vehicleOrders := VehicleOrders{ Boards: make([]BoardOrders, 0), } diff --git a/backend/pkg/boards/blcu.go b/backend/pkg/boards/blcu.go deleted file mode 100644 index 9c4534404..000000000 --- a/backend/pkg/boards/blcu.go +++ /dev/null @@ -1,275 +0,0 @@ -package boards - -import ( - "bytes" - "fmt" - "time" - - "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/transport" - "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/tftp" - dataPacket "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" -) - -const ( - BlcuName = "BLCU" - - AckId = abstraction.BoardEvent("ACK") - DownloadEventId = abstraction.BoardEvent("DOWNLOAD") - UploadEventId = abstraction.BoardEvent("UPLOAD") - - // Default order IDs - can be overridden via config.toml - DefaultBlcuDownloadOrderId = 701 - DefaultBlcuUploadOrderId = 700 -) - -type TFTPConfig struct { - BlockSize int - Retries int - TimeoutMs int - BackoffFactor int - EnableProgress bool -} - -type BLCU struct { - api abstraction.BoardAPI - ackChan chan struct{} - ip string - tftpConfig TFTPConfig - id abstraction.BoardId - downloadOrderId uint16 - uploadOrderId uint16 -} - -// Deprecated: Use NewWithConfig with proper board ID and order IDs from configuration -func New(ip string) *BLCU { - return NewWithTFTPConfig(ip, TFTPConfig{ - BlockSize: 131072, // 128kB - Retries: 3, - TimeoutMs: 5000, - BackoffFactor: 2, - EnableProgress: true, - }, 0) // Board ID 0 indicates missing configuration -} - -// Deprecated: Use NewWithConfig for proper order ID configuration -func NewWithTFTPConfig(ip string, tftpConfig TFTPConfig, id abstraction.BoardId) *BLCU { - return &BLCU{ - ackChan: make(chan struct{}), - ip: ip, - tftpConfig: tftpConfig, - id: id, - downloadOrderId: DefaultBlcuDownloadOrderId, - uploadOrderId: DefaultBlcuUploadOrderId, - } -} - -func NewWithConfig(ip string, tftpConfig TFTPConfig, id abstraction.BoardId, downloadOrderId, uploadOrderId uint16) *BLCU { - return &BLCU{ - ackChan: make(chan struct{}), - ip: ip, - tftpConfig: tftpConfig, - id: id, - downloadOrderId: downloadOrderId, - uploadOrderId: uploadOrderId, - } -} -func (board *BLCU) Id() abstraction.BoardId { - return board.id -} - -func (boards *BLCU) Notify(boardNotification abstraction.BoardNotification) { - switch notification := boardNotification.(type) { - case *AckNotification: - boards.ackChan <- struct{}{} - - case *DownloadEvent: - err := boards.download(*notification) - if err != nil { - fmt.Println(ErrDownloadFailure{ - Timestamp: time.Now(), - Inner: err, - }.Error()) - } - case *UploadEvent: - err := boards.upload(*notification) - if err != nil { - fmt.Println(ErrUploadFailure{ - Timestamp: time.Now(), - Inner: err, - }.Error()) - } - default: - fmt.Println(ErrInvalidBoardEvent{ - Event: notification.Event(), - Timestamp: time.Now(), - }.Error()) - } -} - -func (boards *BLCU) SetAPI(api abstraction.BoardAPI) { - boards.api = api -} - -func (boards *BLCU) download(notification DownloadEvent) error { - // Notify the BLCU - ping := dataPacket.NewPacketWithValues( - abstraction.PacketId(boards.downloadOrderId), - map[dataPacket.ValueName]dataPacket.Value{ - BlcuName: dataPacket.NewEnumValue(dataPacket.EnumVariant(notification.Board)), - }, - map[dataPacket.ValueName]bool{ - BlcuName: true, - }) - - err := boards.api.SendMessage(transport.NewPacketMessage(ping)) - if err != nil { - return ErrSendMessageFailed{ - Timestamp: time.Now(), - Inner: err, - } - } - - // Wait for the ACK - <-boards.ackChan - - // TODO! Notify on progress - - client, err := tftp.NewClient(boards.ip, - tftp.WithBlockSize(boards.tftpConfig.BlockSize), - tftp.WithRetries(boards.tftpConfig.Retries), - tftp.WithTimeout(time.Duration(boards.tftpConfig.TimeoutMs)*time.Millisecond), - ) - if err != nil { - return ErrNewClientFailed{ - Addr: boards.ip, - Timestamp: time.Now(), - Inner: err, - } - } - - buffer := &bytes.Buffer{} - - _, err = client.ReadFile(BlcuName, tftp.BinaryMode, buffer) - if err != nil { - pushErr := boards.api.SendPush(abstraction.BrokerPush( - &DownloadFailure{ - Error: err, - }, - )) - if pushErr != nil { - return ErrSendMessageFailed{ - Timestamp: time.Now(), - Inner: pushErr, - } - } - - return ErrReadingFileFailed{ - Filename: string(notification.Event()), - Timestamp: time.Now(), - Inner: err, - } - } - - pushErr := boards.api.SendPush(abstraction.BrokerPush( - &DownloadSuccess{ - Data: buffer.Bytes(), - }, - )) - if pushErr != nil { - return ErrSendMessageFailed{ - Timestamp: time.Now(), - Inner: err, - } - } - - return nil -} - -func (boards *BLCU) upload(notification UploadEvent) error { - ping := dataPacket.NewPacketWithValues(abstraction.PacketId(boards.uploadOrderId), - map[dataPacket.ValueName]dataPacket.Value{ - BlcuName: dataPacket.NewEnumValue(dataPacket.EnumVariant(notification.Board)), - }, - map[dataPacket.ValueName]bool{ - BlcuName: true, - }) - - err := boards.api.SendMessage(transport.NewPacketMessage(ping)) - if err != nil { - return ErrSendMessageFailed{ - Timestamp: time.Now(), - Inner: err, - } - } - - <-boards.ackChan - - // TODO! Notify on progress - - client, err := tftp.NewClient(boards.ip, - tftp.WithBlockSize(boards.tftpConfig.BlockSize), - tftp.WithRetries(boards.tftpConfig.Retries), - tftp.WithTimeout(time.Duration(boards.tftpConfig.TimeoutMs)*time.Millisecond), - ) - if err != nil { - return ErrNewClientFailed{ - Addr: boards.ip, - Timestamp: time.Now(), - Inner: err, - } - } - - data := notification.Data - buffer := bytes.NewBuffer(data) - - read, err := client.WriteFile(BlcuName, tftp.BinaryMode, buffer) - if err != nil { - pushErr := boards.api.SendPush(abstraction.BrokerPush( - &UploadFailure{ - Error: err, - })) - if pushErr != nil { - return ErrSendMessageFailed{ - Timestamp: time.Now(), - Inner: pushErr, - } - } - - return ErrReadingFileFailed{ - Filename: string(notification.Event()), - Timestamp: time.Now(), - Inner: err, - } - } - - // Check if all bytes written - if int(read) != len(data) { - err = ErrNotAllBytesWritten{ - Timestamp: time.Now(), - } - - pushErr := boards.api.SendPush(abstraction.BrokerPush( - &UploadFailure{ - Error: err, - })) - if pushErr != nil { - return ErrSendMessageFailed{ - Timestamp: time.Now(), - Inner: pushErr, - } - } - - return err - } - - pushErr := boards.api.SendPush(abstraction.BrokerPush( - &UploadSuccess{})) - if pushErr != nil { - return ErrSendMessageFailed{ - Timestamp: time.Now(), - Inner: pushErr, - } - } - return nil -} diff --git a/backend/pkg/boards/blcu_integration_test.go b/backend/pkg/boards/blcu_integration_test.go deleted file mode 100644 index 6586c88c2..000000000 --- a/backend/pkg/boards/blcu_integration_test.go +++ /dev/null @@ -1,284 +0,0 @@ -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 deleted file mode 100644 index 7ff5633e8..000000000 --- a/backend/pkg/boards/blcu_simple_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package boards_test - -import ( - "testing" - - "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "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 with different configurations -func TestBLCUBoardRegistration(t *testing.T) { - logger := zerolog.New(nil).Level(zerolog.Disabled) - v := vehicle.New(logger) - - // Test deprecated constructor (should use board ID 0) - blcuBoard := boards.New("192.168.0.10") - v.AddBoard(blcuBoard) - - // Verify board is registered with ID 0 (missing configuration) - if blcuBoard.Id() != 0 { - t.Errorf("Expected board ID 0 for deprecated constructor, got %d", blcuBoard.Id()) - } -} - -// TestBLCUWithCustomConfiguration tests BLCU with custom board ID and order IDs -func TestBLCUWithCustomConfiguration(t *testing.T) { - logger := zerolog.New(nil).Level(zerolog.Disabled) - v := vehicle.New(logger) - - // Test new constructor with custom configuration - tftpConfig := boards.TFTPConfig{ - BlockSize: 131072, - Retries: 3, - TimeoutMs: 5000, - BackoffFactor: 2, - EnableProgress: true, - } - - customBoardId := abstraction.BoardId(7) - customDownloadOrderId := uint16(801) - customUploadOrderId := uint16(802) - - blcuBoard := boards.NewWithConfig("192.168.0.10", tftpConfig, customBoardId, customDownloadOrderId, customUploadOrderId) - v.AddBoard(blcuBoard) - - // Verify board is registered with custom ID - if blcuBoard.Id() != customBoardId { - t.Errorf("Expected board ID %d, got %d", customBoardId, blcuBoard.Id()) - } -} - -// TestBLCUWithDefaultConfiguration tests BLCU with default order IDs -func TestBLCUWithDefaultConfiguration(t *testing.T) { - logger := zerolog.New(nil).Level(zerolog.Disabled) - v := vehicle.New(logger) - - // Test deprecated constructor (should use default order IDs) - tftpConfig := boards.TFTPConfig{ - BlockSize: 131072, - Retries: 3, - TimeoutMs: 5000, - BackoffFactor: 2, - EnableProgress: true, - } - - boardId := abstraction.BoardId(7) - blcuBoard := boards.NewWithTFTPConfig("192.168.0.10", tftpConfig, boardId) - v.AddBoard(blcuBoard) - - // Verify board is registered - if blcuBoard.Id() != boardId { - t.Errorf("Expected board ID %d, got %d", boardId, 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 diff --git a/backend/pkg/boards/events.go b/backend/pkg/boards/events.go index 045195a83..77a3cbef1 100644 --- a/backend/pkg/boards/events.go +++ b/backend/pkg/boards/events.go @@ -18,10 +18,6 @@ type DownloadEvent struct { Board string } -func (download DownloadEvent) Topic() abstraction.BrokerTopic { - return "blcu/download" -} - func (download DownloadEvent) Event() abstraction.BoardEvent { return download.BoardEvent } @@ -33,10 +29,6 @@ type UploadEvent struct { Length int } -func (upload UploadEvent) Topic() abstraction.BrokerTopic { - return "blcu/upload" -} - func (upload UploadEvent) Event() abstraction.BoardEvent { return upload.BoardEvent } @@ -45,40 +37,20 @@ type BoardPush struct { Data int64 } -func (boardPush BoardPush) Topic() abstraction.BrokerTopic { - return "blcu/boardPush" -} - type DownloadSuccess struct { Data []byte } -func (downloadSuccess DownloadSuccess) Topic() abstraction.BrokerTopic { - return "blcu/download/success" -} - type UploadSuccess struct{} -func (uploadSuccess UploadSuccess) Topic() abstraction.BrokerTopic { - return "blcu/upload/success" -} - type DownloadFailure struct { Error error } -func (downloadFailure DownloadFailure) Topic() abstraction.BrokerTopic { - return "blcu/download/failure" -} - type UploadFailure struct { Error error } -func (uploadFailure UploadFailure) Topic() abstraction.BrokerTopic { - return "blcu/upload/failure" -} - type BoardMessage struct { ID abstraction.TransportEvent // UploadName } diff --git a/backend/pkg/broker/topics/blcu/blcu_test.go b/backend/pkg/broker/topics/blcu/blcu_test.go deleted file mode 100644 index fd3136224..000000000 --- a/backend/pkg/broker/topics/blcu/blcu_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package blcu_test - -import ( - "encoding/json" - "fmt" - "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/broker" - "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/blcu" - "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/tests_functions" - "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" - ws "github.com/gorilla/websocket" - "github.com/rs/zerolog" - "log" - "os" - "testing" - "time" -) - -var errorFlag bool - -type OutputNotMatchingError struct{} - -func (e *OutputNotMatchingError) Error() string { - return "Output does not match" -} - -type MockAPI struct{} - -func (api MockAPI) UserPush(push abstraction.BrokerPush) error { - switch push.(type) { - case blcu.DownloadRequest: - if push.(blcu.DownloadRequest).Board != "test" { - errorFlag = true - return &OutputNotMatchingError{} - } - errorFlag = false - log.Printf("Output matches") - return nil - 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", req.Board, string(req.Data)) - return &OutputNotMatchingError{} - } - errorFlag = false - log.Printf("Output matches") - return nil - } - return nil -} - -func (api MockAPI) UserPull(request abstraction.BrokerRequest) (abstraction.BrokerResponse, error) { - return nil, nil -} - -func TestBLCUTopic_Download_Push(t *testing.T) { - logger := zerolog.New(os.Stdout).With().Timestamp().Logger() - clientChan := make(chan *websocket.Client) - u := tests_functions.StartServer(logger, "download") - - // Mock first client as it always fails - c, _, err := ws.DefaultDialer.Dial(u.String(), nil) - if err != nil { - log.Printf("Expected dial error") - } - c.Close() - - // Set up the client - c, _, err = ws.DefaultDialer.Dial(u.String(), nil) - if err != nil { - logger.Fatal().Err(err).Msg("Error dialing") - } - defer c.Close() - defer logger.Info().Str("id", "client").Msg("Client connection closed") - - api := broker.New(logger) - pool := websocket.NewPool(clientChan, logger) - client := websocket.NewClient(c) - clientChan <- client - - download := blcu.Download{} - download.SetAPI(api) - download.SetPool(pool) - - // Simulate sending a download request - request := blcu.DownloadRequest{Board: "test"} - err = download.Push(request) - if err != nil { - t.Fatal("Error pushing download request:", err) - } - - // Use a timeout for client read - done := make(chan struct{}) - go func() { - output, readErr := client.Read() - if readErr != nil { - logger.Error().Err(readErr).Msg("Client read failed") - done <- struct{}{} - return - } - if output.Topic != blcu.DownloadName { - t.Errorf("Expected topic %s, got %s", blcu.DownloadName, output.Topic) - } - if string(output.Payload) != "test" { - t.Error("Expected payload 'test', got", string(output.Payload)) - } - done <- struct{}{} - }() - - select { - case <-done: - logger.Info().Msg("Test completed successfully") - case <-time.After(3 * time.Second): - t.Error("Test timed out") - } -} - -func TestBLCUTopic_Download_ClientMessage(t *testing.T) { - download := blcu.Download{} - download.SetAPI(&MockAPI{}) - - download.ClientMessage(websocket.ClientId{0}, &websocket.Message{ - Topic: blcu.DownloadName, - Payload: []byte(`{"board":"test"}`), - }) - - if errorFlag { - t.Fatal("Output does not match") - } -} - -func TestBLCUTopic_Upload_Push(t *testing.T) { - logger := zerolog.New(os.Stdout).With().Timestamp().Logger() - clientChan := make(chan *websocket.Client) - u := tests_functions.StartServer(logger, "upload") - - // Set up the client - c, _, err := ws.DefaultDialer.Dial(u.String(), nil) - if err != nil { - logger.Fatal().Err(err).Msg("Error dialing") - } - defer c.Close() - defer logger.Info().Str("id", "client").Msg("Client connection closed") - - api := broker.New(logger) - pool := websocket.NewPool(clientChan, logger) - client := websocket.NewClient(c) - clientChan <- client - - upload := blcu.Upload{} - upload.SetAPI(api) - upload.SetPool(pool) - - // 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) - } - - // Use a timeout for client read - done := make(chan struct{}) - go func() { - output, err := client.Read() - if err != nil { - logger.Error().Err(err).Msg("Client read failed") - done <- struct{}{} - return - } - if output.Topic != blcu.UploadName { - t.Errorf("Expected topic %s, got %s", blcu.UploadName, output.Topic) - } - if string(output.Payload) != "test" { - t.Error("Expected payload 'test', got", string(output.Payload)) - } - done <- struct{}{} - }() - - select { - case <-done: - logger.Info().Msg("Test completed successfully") - case <-time.After(3 * time.Second): - t.Error("Test timed out") - } -} - -func TestBLCUTopic_Upload_ClientMessage(t *testing.T) { - upload := blcu.Upload{} - upload.SetAPI(&MockAPI{}) - - // 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{ - Topic: blcu.UploadName, - Payload: payloadBytes, - }) - - if errorFlag { - t.Fatal("Output does not match") - } -} diff --git a/backend/pkg/broker/topics/blcu/download.go b/backend/pkg/broker/topics/blcu/download.go deleted file mode 100644 index 8ee022956..000000000 --- a/backend/pkg/broker/topics/blcu/download.go +++ /dev/null @@ -1,100 +0,0 @@ -package blcu - -import ( - "encoding/json" - "fmt" - "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" - "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" -) - -const DownloadName abstraction.BrokerTopic = "blcu/download" - -type Download struct { - pool *websocket.Pool - api abstraction.BrokerAPI - client websocket.ClientId -} - -func (download *Download) Topic() abstraction.BrokerTopic { - return DownloadName -} - -type DownloadRequest struct { - Board string `json:"board"` -} - -func (request DownloadRequest) Topic() abstraction.BrokerTopic { - return "blcu/downloadRequest" -} - -func (download *Download) Push(push abstraction.BrokerPush) error { - 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: DownloadName, - Payload: payload, - }) - if err != nil { - return err - } - 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: DownloadName, - Payload: payload, - }) - if err != nil { - return err - } - } - - return nil -} - -func (download *Download) Pull(request abstraction.BrokerRequest) (abstraction.BrokerResponse, error) { - return nil, nil -} - -func (download *Download) ClientMessage(id websocket.ClientId, message *websocket.Message) { - download.client = id - - switch message.Topic { - case DownloadName: - err := download.handleDownload(message) - if err != nil { - fmt.Printf("error handling download: %v\n", err) - } - } -} - -func (download *Download) handleDownload(message *websocket.Message) error { - var downloadRequest DownloadRequest - err := json.Unmarshal(message.Payload, &downloadRequest) - if err != nil { - return err - } - - pushErr := download.api.UserPush(&downloadRequest) - return pushErr -} - -func (download *Download) SetPool(pool *websocket.Pool) { - download.pool = pool -} - -func (download *Download) SetAPI(api abstraction.BrokerAPI) { - download.api = api -} diff --git a/backend/pkg/broker/topics/blcu/register.go b/backend/pkg/broker/topics/blcu/register.go deleted file mode 100644 index ee735b716..000000000 --- a/backend/pkg/broker/topics/blcu/register.go +++ /dev/null @@ -1,22 +0,0 @@ -package blcu - -import ( - "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" - "github.com/HyperloopUPV-H8/h9-backend/pkg/broker" - "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" -) - -func RegisterTopics(b *broker.Broker, pool *websocket.Pool) { - upload := &Upload{} - upload.SetAPI(b) - upload.SetPool(pool) - download := &Download{} - download.SetAPI(b) - download.SetPool(pool) - b.AddTopic(UploadName, upload) - b.AddTopic(DownloadName, download) - b.AddTopic(boards.UploadSuccess{}.Topic(), upload) - b.AddTopic(boards.UploadFailure{}.Topic(), upload) - b.AddTopic(boards.DownloadSuccess{}.Topic(), download) - b.AddTopic(boards.DownloadFailure{}.Topic(), download) -} diff --git a/backend/pkg/broker/topics/blcu/upload.go b/backend/pkg/broker/topics/blcu/upload.go deleted file mode 100644 index 82e2b1278..000000000 --- a/backend/pkg/broker/topics/blcu/upload.go +++ /dev/null @@ -1,124 +0,0 @@ -package blcu - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" - "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" -) - -const UploadName abstraction.BrokerTopic = "blcu/upload" - -type Upload struct { - pool *websocket.Pool - api abstraction.BrokerAPI - client websocket.ClientId -} - -func (upload *Upload) Topic() abstraction.BrokerTopic { - return UploadName -} - -type UploadRequest struct { - Board string `json:"board"` - File string `json:"file"` // Base64 encoded file data from frontend -} - -func (request UploadRequest) Topic() abstraction.BrokerTopic { - return "blcu/uploadRequest" -} - -// UploadRequestInternal is the internal representation with decoded data -type UploadRequestInternal struct { - Board string - Data []byte -} - -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: UploadName, - Payload: payload, - }) - if err != nil { - return err - } - - 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: UploadName, - Payload: payload, - }) - if err != nil { - return err - } - } - - return nil -} - -func (upload *Upload) Pull(request abstraction.BrokerRequest) (abstraction.BrokerResponse, error) { - return nil, nil -} - -func (upload *Upload) ClientMessage(id websocket.ClientId, message *websocket.Message) { - upload.client = id - - switch message.Topic { - case UploadName: - err := upload.handleUpload(message) - if err != nil { - fmt.Printf("error handling download: %v\n", err) - } - } -} - -func (upload *Upload) handleUpload(message *websocket.Message) error { - var uploadRequest UploadRequest - err := json.Unmarshal(message.Payload, &uploadRequest) - if err != nil { - return err - } - - // 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 -} - -func (upload *Upload) SetPool(pool *websocket.Pool) { - upload.pool = pool -} - -func (upload *Upload) SetAPI(api abstraction.BrokerAPI) { - upload.api = api -} diff --git a/backend/pkg/transport/packet/blcu/decoder.go b/backend/pkg/transport/packet/blcu/decoder.go deleted file mode 100644 index 273bf48c8..000000000 --- a/backend/pkg/transport/packet/blcu/decoder.go +++ /dev/null @@ -1,20 +0,0 @@ -package blcu - -import ( - "io" - - "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" -) - -// Decoder is a decoder for the blcu Ack packet -type Decoder struct{} - -// NewDecoder creates a new Decoder -func NewDecoder() *Decoder { - return &Decoder{} -} - -// Decode decodes the next packet on reader and returns the corresponding blcuAck. -func (decoder *Decoder) Decode(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) { - return NewAck(id), nil -} diff --git a/backend/pkg/transport/packet/blcu/packet.go b/backend/pkg/transport/packet/blcu/packet.go deleted file mode 100644 index ebe5c57c9..000000000 --- a/backend/pkg/transport/packet/blcu/packet.go +++ /dev/null @@ -1,22 +0,0 @@ -package blcu - -import "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - -// Ack is the blcu Ack message sent when the blcu is ready to create a tftp connection. -// -// the packet just has the ID. -type Ack struct { - id abstraction.PacketId -} - -// NewAck creates a new blcu ack packet with the given id -func NewAck(id abstraction.PacketId) *Ack { - return &Ack{ - id: id, - } -} - -// Id returns the ID of the packet -func (packet *Ack) Id() abstraction.PacketId { - return packet.id -} diff --git a/backend/pkg/transport/presentation/decoder.go b/backend/pkg/transport/presentation/decoder.go index 14e2924ec..bc30a1272 100644 --- a/backend/pkg/transport/presentation/decoder.go +++ b/backend/pkg/transport/presentation/decoder.go @@ -5,7 +5,6 @@ import ( "io" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/blcu" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/order" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" @@ -20,7 +19,6 @@ type PacketDecoder interface { // Type assertions to check packet decoders follows the Decoder interface var _ PacketDecoder = &data.Decoder{} -var _ PacketDecoder = &blcu.Decoder{} var _ PacketDecoder = &order.Decoder{} var _ PacketDecoder = &protection.Decoder{} var _ PacketDecoder = &state.Decoder{} diff --git a/backend/pkg/transport/presentation/decoder_test.go b/backend/pkg/transport/presentation/decoder_test.go index 8df16058d..829f85b89 100644 --- a/backend/pkg/transport/presentation/decoder_test.go +++ b/backend/pkg/transport/presentation/decoder_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/blcu" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/order" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" @@ -29,22 +28,7 @@ func TestDecoder(t *testing.T) { endianness := binary.LittleEndian testcases := []testcase{ - { - name: "blcu ack", - input: bytes.NewReader([]byte{0x01, 0x00}), - output: []abstraction.Packet{ - blcu.NewAck(1), - }, - }, - { - name: "multiple blcu ack", - input: bytes.NewReader([]byte{0x01, 0x00, 0x01, 0x00, 0x01, 0x00}), - output: []abstraction.Packet{ - blcu.NewAck(1), - blcu.NewAck(1), - blcu.NewAck(1), - }, - }, + { name: "state orders add", input: bytes.NewReader([]byte{0x03, 0x00, 0x01, 0x00, 0xFF, 0xFF}), @@ -648,7 +632,6 @@ func TestDecoder(t *testing.T) { } // getDecoder generates a mock Decoder with the following packet IDs: -// 1 - blcuAck // 3 - add state order // 4 - remove state order // 5 - protection warning @@ -659,8 +642,6 @@ func getDecoder(endianness binary.ByteOrder) *presentation.Decoder { nullLogger := zerolog.New(io.Discard) decoder := presentation.NewDecoder(endianness, nullLogger) - decoder.SetPacketDecoder(1, blcu.NewDecoder()) - ordersDecoder := order.NewDecoder(endianness) ordersDecoder.SetActionId(3, ordersDecoder.DecodeAdd) ordersDecoder.SetActionId(4, ordersDecoder.DecodeRemove) diff --git a/backend/pkg/transport/transport.go b/backend/pkg/transport/transport.go index 2fb9ea39f..cca4b0e7a 100644 --- a/backend/pkg/transport/transport.go +++ b/backend/pkg/transport/transport.go @@ -58,7 +58,7 @@ var zeroTime time.Time // applying exponential backoff between attempts. func (transport *Transport) HandleClient(config tcp.ClientConfig, remote string) error { client := tcp.NewClient(remote, config, transport.logger) - clientLogger := transport.logger.With().Str("remoteAddress", remote).Logger() + clientLogger := transport.logger.With().Str("remoteAddress", remote).Logger() defer clientLogger.Warn().Msg("abort connection") var hasConnected = false @@ -205,7 +205,7 @@ func (transport *Transport) targetFromTCPConn(conn net.Conn) (abstraction.Transp } // rejectIfConnectedTCPConn closes and rejects conn if target already has an active connection. -func (transport *Transport) rejectIfConnectedTCPConn(target abstraction.TransportTarget, conn net.Conn, logger zerolog.Logger,) error { +func (transport *Transport) rejectIfConnectedTCPConn(target abstraction.TransportTarget, conn net.Conn, logger zerolog.Logger) error { transport.connectionsMx.Lock() defer transport.connectionsMx.Unlock() @@ -238,7 +238,7 @@ func (transport *Transport) registerTCPConn(target abstraction.TransportTarget, func (transport *Transport) readLoopTCPConn(conn net.Conn, logger zerolog.Logger) { from := conn.RemoteAddr().String() to := conn.LocalAddr().String() - + go func() { for { packet, err := transport.decoder.DecodeNext(conn) @@ -267,7 +267,6 @@ func (transport *Transport) readLoopTCPConn(conn net.Conn, logger zerolog.Logger }() } - // SendMessage triggers an event to send something to the vehicle. Some messages // might additional means to pass information around (e.g. file read and write) func (transport *Transport) SendMessage(message abstraction.TransportMessage) error { @@ -342,7 +341,7 @@ func (transport *Transport) handlePacketEvent(message PacketMessage) error { defer transport.connectionsMx.RUnlock() conn, ok := transport.connections[target] if !ok { - eventLogger.Warn().Msg("target not connected") + eventLogger.Warn().Msg("target not connected") err := ErrConnClosed{Target: target} return nil, err @@ -379,24 +378,6 @@ func (transport *Transport) handlePacketEvent(message PacketMessage) error { return nil } -// handleFileWrite writes a file through tftp to the blcu -func (transport *Transport) handleFileWrite(message FileWriteMessage) error { - _, err := transport.tftp.WriteFile(message.Filename(), tftp.BinaryMode, message) - if err != nil { - transport.errChan <- err - } - return err -} - -// handleFileRead reads a file through tftp from the blcu -func (transport *Transport) handleFileRead(message FileReadMessage) error { - _, err := transport.tftp.ReadFile(message.Filename(), tftp.BinaryMode, message) - if err != nil { - transport.errChan <- err - } - return err -} - // HandleSniffer starts listening for packets on the provided sniffer and handles them. // // This function will block until the sniffer is closed @@ -414,7 +395,7 @@ func (transport *Transport) HandleSniffer(sniffer *sniffer.Sniffer) { func (transport *Transport) HandleUDPServer(server *udp.Server) { packetsCh := server.GetPackets() errorsCh := server.GetErrors() - + for { select { case packet := <-packetsCh: @@ -437,7 +418,7 @@ func (transport *Transport) replicateFault(packet abstraction.Packet, logger zer func (transport *Transport) handleUDPPacket(udpPacket udp.Packet) { srcAddr := fmt.Sprintf("%s:%d", udpPacket.SourceIP, udpPacket.SourcePort) dstAddr := fmt.Sprintf("%s:%d", udpPacket.DestIP, udpPacket.DestPort) - + // Create a reader from the payload readerAny := transport.byteReaderPool.Get() var reader *bytes.Reader @@ -460,12 +441,12 @@ func (transport *Transport) handleUDPPacket(udpPacket udp.Packet) { transport.errChan <- err return } - + // Intercept packets with id == 0 and replicate if transport.propagateFault && packet.Id() == 0 { transport.replicateFault(packet, transport.logger) } - + // Send notification transport.api.Notification(NewPacketNotification(packet, srcAddr, dstAddr, udpPacket.Timestamp)) diff --git a/backend/pkg/vehicle/constructor.go b/backend/pkg/vehicle/constructor.go index 6dab58618..8468195f9 100644 --- a/backend/pkg/vehicle/constructor.go +++ b/backend/pkg/vehicle/constructor.go @@ -75,8 +75,3 @@ func (vehicle *Vehicle) SetIpToBoardId(ipToBoardId map[string]abstraction.BoardI vehicle.ipToBoardId = ipToBoardId vehicle.trace.Info().Msg("set ip to board id") } - -func (vehicle *Vehicle) SetBlcuId(id abstraction.BoardId) { - vehicle.BlcuId = id - vehicle.trace.Info().Uint16("blcu_id", uint16(id)).Msg("set blcu id") -} diff --git a/backend/pkg/vehicle/notification.go b/backend/pkg/vehicle/notification.go index 8a049e606..4bf0d9d05 100644 --- a/backend/pkg/vehicle/notification.go +++ b/backend/pkg/vehicle/notification.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" data_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/data" message_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/message" order_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/order" @@ -15,7 +14,7 @@ import ( protection_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/protection" state_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/state" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport" - blcu_packet "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/blcu" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/order" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" @@ -130,12 +129,7 @@ func (vehicle *Vehicle) handlePacketNotification(notification transport.PacketNo vehicle.trace.Error().Stack().Err(err).Msg("remove state orders") return errors.Join(fmt.Errorf("remove state orders (state orders from %s to %s)", notification.From, notification.To), err) } - case *blcu_packet.Ack: - vehicle.boards[vehicle.BlcuId].Notify(abstraction.BoardNotification( - &boards.AckNotification{ - ID: boards.AckId, - }, - )) + } return nil } diff --git a/backend/pkg/vehicle/vehicle.go b/backend/pkg/vehicle/vehicle.go index e682f29f4..6e51cbb24 100644 --- a/backend/pkg/vehicle/vehicle.go +++ b/backend/pkg/vehicle/vehicle.go @@ -5,12 +5,10 @@ import ( "fmt" "os" - "github.com/HyperloopUPV-H8/h9-backend/pkg/boards" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - blcu_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/blcu" connection_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/connection" logger_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/logger" message_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/message" @@ -95,47 +93,6 @@ func (vehicle *Vehicle) UserPush(push abstraction.BrokerPush) error { status.Fulfill(status.Enable()) } - case "blcu/downloadRequest": - download := push.(*blcu_topic.DownloadRequest) - - if board, exists := vehicle.boards[vehicle.BlcuId]; exists { - board.Notify(abstraction.BoardNotification( - &boards.DownloadEvent{ - BoardEvent: boards.DownloadEventId, - BoardID: vehicle.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[vehicle.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 76ed8879324ff4c8c5e9a5200db9bf6bc4d57018 Mon Sep 17 00:00:00 2001 From: Alex <80858832+Humanoidear@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:11:44 +0100 Subject: [PATCH 02/37] Add testing view image to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 21109ad84..3ce862d20 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Hyperloop Control Station H11 + ![Testing View](https://hyperloopupv.com/assets/testing-view.png) ## Monorepo usage From 5a850e94e690f7ebfa72d358a7fcdae6b0bfed4a Mon Sep 17 00:00:00 2001 From: Alex <80858832+Humanoidear@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:14:56 +0100 Subject: [PATCH 03/37] Fix Testing View image URL in README Updated image link for Testing View in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ce862d20..c7bff8ea9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Hyperloop Control Station H11 - ![Testing View](https://hyperloopupv.com/assets/testing-view.png) + ![Testing View](https://raw.githubusercontent.com/Hyperloop-UPV/webpage/5c1c827d82d380689856ee61af43da30da22e0fc/src/assets/backgrounds/testing-view.png) ## Monorepo usage From 7e4adad60b1b560767b819d4ffbab23614e15cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Ribal=20del=20R=C3=ADo?= Date: Mon, 16 Feb 2026 19:22:46 +0100 Subject: [PATCH 04/37] feat(backend): remove blcu --- backend/pkg/transport/transport.go | 4 - backend/pkg/transport/transport_test.go | 234 ++++++++---------------- 2 files changed, 81 insertions(+), 157 deletions(-) diff --git a/backend/pkg/transport/transport.go b/backend/pkg/transport/transport.go index cca4b0e7a..d4020a5d6 100644 --- a/backend/pkg/transport/transport.go +++ b/backend/pkg/transport/transport.go @@ -275,10 +275,6 @@ func (transport *Transport) SendMessage(message abstraction.TransportMessage) er switch msg := message.(type) { case PacketMessage: err = transport.handlePacketEvent(msg) - case FileWriteMessage: - err = transport.handleFileWrite(msg) - case FileReadMessage: - err = transport.handleFileRead(msg) default: err = ErrUnrecognizedEvent{message.Event()} } diff --git a/backend/pkg/transport/transport_test.go b/backend/pkg/transport/transport_test.go index 428dc4f13..8cde13a44 100644 --- a/backend/pkg/transport/transport_test.go +++ b/backend/pkg/transport/transport_test.go @@ -24,7 +24,6 @@ import ( "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "github.com/google/gopacket/pcapgo" - tftpv3 "github.com/pin/tftp/v3" "github.com/rs/zerolog" ) @@ -123,8 +122,8 @@ func NewMockBoardServer(address string) *MockBoardServer { logger := zerolog.Nop() enc := presentation.NewEncoder(binary.BigEndian, logger) - dec := presentation.NewDecoder(binary.BigEndian, logger) - wireTestPacketCodec(enc, dec, abstraction.PacketId(100)) + dec := presentation.NewDecoder(binary.BigEndian, logger) + wireTestPacketCodec(enc, dec, abstraction.PacketId(100)) return &MockBoardServer{ address: address, @@ -138,47 +137,47 @@ func NewMockBoardServer(address string) *MockBoardServer { func (s *MockBoardServer) Start() error { s.mu.Lock() defer s.mu.Unlock() - + if s.running { return fmt.Errorf("server already running") } - + listener, err := net.Listen("tcp", s.address) if err != nil { return fmt.Errorf("failed to listen on %s: %w", s.address, err) } - + s.listener = listener s.running = true - + go s.acceptLoop() - + return nil } func (s *MockBoardServer) Stop() error { s.mu.Lock() defer s.mu.Unlock() - + if !s.running { return nil } - + s.running = false - + // Close all connections for _, conn := range s.connections { conn.Close() } s.connections = s.connections[:0] - + // Close listener if s.listener != nil { err := s.listener.Close() s.listener = nil return err } - + return nil } @@ -194,11 +193,11 @@ func (s *MockBoardServer) acceptLoop() { } continue } - + s.mu.Lock() s.connections = append(s.connections, conn) s.mu.Unlock() - + go s.handleConnection(conn) } } @@ -216,19 +215,19 @@ func (s *MockBoardServer) handleConnection(conn net.Conn) { } s.mu.Unlock() }() - + for { s.mu.RLock() running := s.running s.mu.RUnlock() - + if !running { return } - + // Set read timeout to avoid blocking forever conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) - + packet, err := s.decoder.DecodeNext(conn) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { @@ -236,7 +235,7 @@ func (s *MockBoardServer) handleConnection(conn net.Conn) { } return } - + s.mu.Lock() s.packetsRecv = append(s.packetsRecv, packet) s.mu.Unlock() @@ -264,18 +263,17 @@ func createTestTransport(t *testing.T) (*Transport, *TestTransportAPI) { logger := zerolog.New(zerolog.Nop()).With().Timestamp().Logger() enc := presentation.NewEncoder(binary.BigEndian, logger) - dec := presentation.NewDecoder(binary.BigEndian, logger) - wireTestPacketCodec(enc, dec, abstraction.PacketId(100)) + dec := presentation.NewDecoder(binary.BigEndian, logger) + wireTestPacketCodec(enc, dec, abstraction.PacketId(100)) wireTestPacketCodec(enc, dec, abstraction.PacketId(0)) - transport := NewTransport(logger). WithEncoder(enc). WithDecoder(dec) - + api := NewTestTransportAPI() transport.SetAPI(api) - + return transport, api } @@ -315,23 +313,23 @@ func waitForCondition(condition func() bool, timeout time.Duration, message stri // test wiring: register a trivial codec for a data packet id. func wireTestPacketCodec(enc *presentation.Encoder, dec *presentation.Decoder, id abstraction.PacketId) { - dataEnc := data.NewEncoder(binary.BigEndian) - dataDec := data.NewDecoder(binary.BigEndian) + dataEnc := data.NewEncoder(binary.BigEndian) + dataDec := data.NewDecoder(binary.BigEndian) - // Empty descriptor = no payload values, just the id header. - var desc data.Descriptor - dataEnc.SetDescriptor(id, desc) - dataDec.SetDescriptor(id, desc) + // Empty descriptor = no payload values, just the id header. + var desc data.Descriptor + dataEnc.SetDescriptor(id, desc) + dataDec.SetDescriptor(id, desc) - enc.SetPacketEncoder(id, dataEnc) - dec.SetPacketDecoder(id, dataDec) + enc.SetPacketEncoder(id, dataEnc) + dec.SetPacketDecoder(id, dataDec) } // Unit Tests func TestTransport_Creation(t *testing.T) { logger := zerolog.Nop() transport := NewTransport(logger) - + if transport == nil { t.Fatal("Transport should not be nil") } @@ -351,10 +349,10 @@ func TestTransport_Creation(t *testing.T) { func TestTransport_SetIdTarget(t *testing.T) { transport, _ := createTestTransport(t) - + transport.SetIdTarget(100, "TEST_BOARD") transport.SetIdTarget(200, "ANOTHER_BOARD") - + // Access the internal map to verify if target := transport.idToTarget[100]; target != abstraction.TransportTarget("TEST_BOARD") { t.Errorf("Expected TEST_BOARD, got %s", target) @@ -366,10 +364,10 @@ func TestTransport_SetIdTarget(t *testing.T) { func TestTransport_SetTargetIp(t *testing.T) { transport, _ := createTestTransport(t) - + transport.SetTargetIp("192.168.1.100", "TEST_BOARD") transport.SetTargetIp("192.168.1.101", "ANOTHER_BOARD") - + // Access the internal map to verify if target := transport.ipToTarget["192.168.1.100"]; target != abstraction.TransportTarget("TEST_BOARD") { t.Errorf("Expected TEST_BOARD, got %s", target) @@ -585,15 +583,15 @@ func TestHandleUDPPacket_Success(t *testing.T) { // Integration Tests func TestTransport_ClientServerConnection(t *testing.T) { transport, api := createTestTransport(t) - + // Setup board configuration boardIP := "127.0.0.1" boardPort := getAvailablePort(t) target := abstraction.TransportTarget("TEST_BOARD") - + transport.SetTargetIp(boardIP, target) transport.SetIdTarget(100, target) - + // Create and start mock board server mockBoard := NewMockBoardServer(boardPort) err := mockBoard.Start() @@ -601,23 +599,23 @@ func TestTransport_ClientServerConnection(t *testing.T) { t.Fatalf("Failed to start mock board: %v", err) } defer mockBoard.Stop() - + // Configure client clientAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Failed to resolve client address: %v", err) } - + clientConfig := tcp.NewClientConfig(clientAddr) clientConfig.TryReconnect = false // Don't retry for this test - + // Start client connection in goroutine clientDone := make(chan error, 1) go func() { err := transport.HandleClient(clientConfig, boardPort) clientDone <- err }() - + // Ensure cleanup defer func() { mockBoard.Stop() @@ -628,7 +626,7 @@ func TestTransport_ClientServerConnection(t *testing.T) { // Client should exit when board stops } }() - + // Wait for connection err = waitForCondition(func() bool { return mockBoard.GetConnectionCount() > 0 @@ -636,7 +634,7 @@ func TestTransport_ClientServerConnection(t *testing.T) { if err != nil { t.Fatal(err) } - + // Verify connection update was sent err = waitForCondition(func() bool { updates := api.GetConnectionUpdates() @@ -645,10 +643,10 @@ func TestTransport_ClientServerConnection(t *testing.T) { if err != nil { t.Fatal(err) } - + // Stop the board to trigger disconnection mockBoard.Stop() - + // Wait for client to detect disconnection select { case err := <-clientDone: @@ -659,7 +657,7 @@ func TestTransport_ClientServerConnection(t *testing.T) { case <-time.After(2 * time.Second): t.Fatal("Client should have detected disconnection") } - + // Verify disconnection update err = waitForCondition(func() bool { updates := api.GetConnectionUpdates() @@ -672,16 +670,16 @@ func TestTransport_ClientServerConnection(t *testing.T) { func TestTransport_PacketSending(t *testing.T) { transport, api := createTestTransport(t) - + // Setup boardIP := "127.0.0.1" boardPort := getAvailablePort(t) target := abstraction.TransportTarget("TEST_BOARD") packetID := abstraction.PacketId(100) - + transport.SetTargetIp(boardIP, target) transport.SetIdTarget(packetID, target) - + // Create mock board mockBoard := NewMockBoardServer(boardPort) err := mockBoard.Start() @@ -689,18 +687,18 @@ func TestTransport_PacketSending(t *testing.T) { t.Fatalf("Failed to start mock board: %v", err) } defer mockBoard.Stop() - + // Start client clientAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:0") clientConfig := tcp.NewClientConfig(clientAddr) clientConfig.TryReconnect = false - + clientDone := make(chan struct{}) go func() { defer close(clientDone) transport.HandleClient(clientConfig, boardPort) }() - + // Ensure cleanup defer func() { mockBoard.Stop() @@ -709,25 +707,25 @@ func TestTransport_PacketSending(t *testing.T) { case <-time.After(1 * time.Second): } }() - + // Wait for connection err = waitForCondition(func() bool { - updates := api.GetConnectionUpdates() - return len(updates) > 0 && updates[len(updates)-1].Target == target && updates[len(updates)-1].IsConnected + updates := api.GetConnectionUpdates() + return len(updates) > 0 && updates[len(updates)-1].Target == target && updates[len(updates)-1].IsConnected }, 2*time.Second, "Should establish connection") if err != nil { t.Fatal(err) } - + // Create and send packet testPacket := data.NewPacket(packetID) testPacket.SetTimestamp(time.Now()) - + err = transport.SendMessage(NewPacketMessage(testPacket)) if err != nil { t.Fatalf("Failed to send packet: %v", err) } - + // Verify packet was received by board err = waitForCondition(func() bool { packets := mockBoard.GetReceivedPackets() @@ -736,7 +734,7 @@ func TestTransport_PacketSending(t *testing.T) { if err != nil { t.Fatal(err) } - + // Verify no error notifications notifications := api.GetNotifications() for _, notification := range notifications { @@ -748,16 +746,16 @@ func TestTransport_PacketSending(t *testing.T) { func TestTransport_UnknownTarget(t *testing.T) { transport, api := createTestTransport(t) - + // Try to send packet to unknown target unknownPacket := data.NewPacket(999) // Unknown packet ID unknownPacket.SetTimestamp(time.Now()) - + err := transport.SendMessage(NewPacketMessage(unknownPacket)) if err == nil { t.Fatal("Expected error when sending to unknown target") } - + // Should be ErrUnrecognizedId var unrecognizedErr ErrUnrecognizedId if !ErrorAs(err, &unrecognizedErr) { @@ -765,7 +763,7 @@ func TestTransport_UnknownTarget(t *testing.T) { } else if unrecognizedErr.Id != abstraction.PacketId(999) { t.Errorf("Expected packet ID 999, got %d", unrecognizedErr.Id) } - + // Verify error notification err = waitForCondition(func() bool { notifications := api.GetNotifications() @@ -782,43 +780,43 @@ func TestTransport_UnknownTarget(t *testing.T) { func TestTransport_ReconnectionBehavior(t *testing.T) { transport, api := createTestTransport(t) - + // Setup boardIP := "127.0.0.1" boardPort := getAvailablePort(t) target := abstraction.TransportTarget("RECONNECT_BOARD") - + transport.SetTargetIp(boardIP, target) transport.SetIdTarget(100, target) - + // Create mock board mockBoard := NewMockBoardServer(boardPort) err := mockBoard.Start() if err != nil { t.Fatalf("Failed to start mock board: %v", err) } - + // Configure client with fast reconnection for testing clientAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:0") clientConfig := tcp.NewClientConfig(clientAddr) clientConfig.TryReconnect = true clientConfig.MaxConnectionRetries = 0 // Infinite retries clientConfig.ConnectionBackoffFunction = tcp.NewExponentialBackoff( - 10*time.Millisecond, // Fast for testing + 10*time.Millisecond, // Fast for testing 1.5, 100*time.Millisecond, ) - + // Start client with proper cleanup ctx, cancel := context.WithCancel(context.Background()) clientConfig.Context = ctx - + clientDone := make(chan struct{}) go func() { defer close(clientDone) transport.HandleClient(clientConfig, boardPort) }() - + // Ensure cleanup happens defer func() { cancel() @@ -830,7 +828,7 @@ func TestTransport_ReconnectionBehavior(t *testing.T) { t.Log("Warning: client goroutine did not finish within timeout") } }() - + // Wait for initial connection err = waitForCondition(func() bool { return mockBoard.GetConnectionCount() > 0 @@ -838,7 +836,7 @@ func TestTransport_ReconnectionBehavior(t *testing.T) { if err != nil { t.Fatal(err) } - + // Verify connection update err = waitForCondition(func() bool { updates := api.GetConnectionUpdates() @@ -847,10 +845,10 @@ func TestTransport_ReconnectionBehavior(t *testing.T) { if err != nil { t.Fatal(err) } - + // Simulate board restart mockBoard.Stop() - + // Wait for disconnection detection err = waitForCondition(func() bool { updates := api.GetConnectionUpdates() @@ -864,14 +862,14 @@ func TestTransport_ReconnectionBehavior(t *testing.T) { if err != nil { t.Fatal(err) } - + // Restart board mockBoard = NewMockBoardServer(boardPort) err = mockBoard.Start() if err != nil { t.Fatalf("Failed to restart mock board: %v", err) } - + // Wait for reconnection err = waitForCondition(func() bool { return mockBoard.GetConnectionCount() > 0 @@ -879,7 +877,7 @@ func TestTransport_ReconnectionBehavior(t *testing.T) { if err != nil { t.Fatal(err) } - + // Verify reconnection update err = waitForCondition(func() bool { updates := api.GetConnectionUpdates() @@ -1001,76 +999,6 @@ func TestHandleUDPServer_Dispatches(t *testing.T) { } } -func TestHandleFileWriteRead_WithRealTFTP(t *testing.T) { - readHandler := func(filename string, rf io.ReaderFrom) error { - _, err := rf.ReadFrom(bytes.NewBufferString("from-server")) - return err - } - writeBuf := &bytes.Buffer{} - writeHandler := func(filename string, wt io.WriterTo) error { - _, err := wt.WriteTo(writeBuf) - return err - } - server := tftpv3.NewServer(readHandler, writeHandler) - addr := fmt.Sprintf("127.0.0.1:%d", getAvailableUDPPort(t)) - go func() { - _ = server.ListenAndServe(addr) - }() - defer server.Shutdown() - time.Sleep(20 * time.Millisecond) - - client, err := tftp.NewClient(addr) - if err != nil { - t.Fatalf("failed to create tftp client: %v", err) - } - - tr := NewTransport(defaultLogger()).WithTFTP(client) - tr.SetAPI(NewTestTransportAPI()) - - if err := tr.handleFileWrite(NewFileWriteMessage("file.bin", bytes.NewBufferString("hello"))); err != nil { - t.Fatalf("handleFileWrite error: %v", err) - } - if writeBuf.String() != "hello" { - t.Fatalf("expected written data 'hello', got %q", writeBuf.String()) - } - - out := &bytes.Buffer{} - if err := tr.handleFileRead(NewFileReadMessage("file.bin", out)); err != nil { - t.Fatalf("handleFileRead error: %v", err) - } - if out.String() != "from-server" { - t.Fatalf("expected read data 'from-server', got %q", out.String()) - } -} - -func TestHandleFileWriteRead_ErrorPath(t *testing.T) { - // Point to an unused UDP port to force WriteFile/ReadFile errors. - addr := fmt.Sprintf("127.0.0.1:%d", getAvailableUDPPort(t)) - client, err := tftp.NewClient(addr, tftp.WithTimeout(50*time.Millisecond), tftp.WithRetries(1)) - if err != nil { - t.Fatalf("failed to create tftp client: %v", err) - } - - tr := NewTransport(defaultLogger()).WithTFTP(client) - api := NewTestTransportAPI() - tr.SetAPI(api) - - if err := tr.handleFileWrite(NewFileWriteMessage("file.bin", bytes.NewBufferString("hello"))); err == nil { - t.Fatalf("expected error writing to unreachable TFTP server") - } - if err := waitForCondition(func() bool { return len(api.GetNotifications()) > 0 }, time.Second, "error notification"); err != nil { - t.Fatalf("expected error notification") - } - - api.Reset() - if err := tr.handleFileRead(NewFileReadMessage("file.bin", &bytes.Buffer{})); err == nil { - t.Fatalf("expected error reading from unreachable TFTP server") - } - if err := waitForCondition(func() bool { return len(api.GetNotifications()) > 0 }, time.Second, "error notification"); err != nil { - t.Fatalf("expected error notification") - } -} - func TestHandleSniffer_Dispatches(t *testing.T) { tr, api := createTestTransport(t) @@ -1152,4 +1080,4 @@ func ErrorAs(err error, target interface{}) bool { } } return false -} \ No newline at end of file +} From cc87f1e1ff3d461c938aeaab4105d668141b5f71 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:22:37 +0100 Subject: [PATCH 05/37] feat(testing-view): add mode switcher toggle in dev mode --- .../ui/src/icons/accessibility.ts | 2 +- .../src/components/devTools/ModeSwitcher.tsx | 9 +++-- .../src/components/leftSidebar/AppSidebar.tsx | 7 ++++ .../components/leftSidebar/DevToolsItem.tsx | 35 +++++++++++++++++++ .../testing-view/src/store/slices/appSlice.ts | 9 +++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 frontend/testing-view/src/components/leftSidebar/DevToolsItem.tsx diff --git a/frontend/frontend-kit/ui/src/icons/accessibility.ts b/frontend/frontend-kit/ui/src/icons/accessibility.ts index 8740ecc74..2a80f32ba 100644 --- a/frontend/frontend-kit/ui/src/icons/accessibility.ts +++ b/frontend/frontend-kit/ui/src/icons/accessibility.ts @@ -1 +1 @@ -export { SunMoon } from "lucide-react"; +export { Eye, EyeOff, SunMoon } from "lucide-react"; diff --git a/frontend/testing-view/src/components/devTools/ModeSwitcher.tsx b/frontend/testing-view/src/components/devTools/ModeSwitcher.tsx index 04745b2d7..c25c69428 100644 --- a/frontend/testing-view/src/components/devTools/ModeSwitcher.tsx +++ b/frontend/testing-view/src/components/devTools/ModeSwitcher.tsx @@ -14,12 +14,17 @@ export const ModeSwitcher = () => { const modeOverride = useStore((s) => s.modeOverride); const setModeOverride = useStore((s) => s.setModeOverride); const currentMode = useStore((s) => s.appMode); + const isDevToolsVisible = useStore((s) => s.isDevToolsVisible); // Only show in development - if (!import.meta.env.DEV && !import.meta.env.VITE_FORCE_DEV) return null; + if ( + (!import.meta.env.DEV && !import.meta.env.VITE_FORCE_DEV) || + !isDevToolsVisible + ) + return null; return ( -
+
Dev Mode Switcher
diff --git a/frontend/testing-view/src/components/leftSidebar/AppSidebar.tsx b/frontend/testing-view/src/components/leftSidebar/AppSidebar.tsx index 4df0e11e6..35940ea2e 100644 --- a/frontend/testing-view/src/components/leftSidebar/AppSidebar.tsx +++ b/frontend/testing-view/src/components/leftSidebar/AppSidebar.tsx @@ -8,6 +8,7 @@ import { import { PAGES_ARRAY } from "../../constants/pages"; import ColorSchemeToggle from "./ColorSchemeToggle"; import ConnectionStatusGroup from "./ConnectionStatusGroup"; +import DevToolsItem from "./DevToolsItem"; import Logo from "./Logo"; import NavigationGroup from "./NavigationGroup"; import SettingsItem from "./SettingsItem"; @@ -27,8 +28,14 @@ const AppSidebar = ({ backendConnected, ...props }: AppSidebarProps) => { + {/* Only visible in dev mode */} + + +
+
+ diff --git a/frontend/testing-view/src/components/leftSidebar/DevToolsItem.tsx b/frontend/testing-view/src/components/leftSidebar/DevToolsItem.tsx new file mode 100644 index 000000000..95e2dff3d --- /dev/null +++ b/frontend/testing-view/src/components/leftSidebar/DevToolsItem.tsx @@ -0,0 +1,35 @@ +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenuButton, +} from "@workspace/ui"; +import { Eye, EyeOff } from "@workspace/ui/icons"; +import { useStore } from "../../store/store"; + +const DevToolsItem = () => { + const toggleDevToolsVisible = useStore((s) => s.toggleDevToolsVisible); + const isVisible = useStore((s) => s.isDevToolsVisible); + + const isDev = import.meta.env.DEV || import.meta.env.VITE_FORCE_DEV; + + if (!isDev) return null; + + return ( + + Dev mode + toggleDevToolsVisible()} + > + {isVisible ? ( + + ) : ( + + )} + Toggle mode switcher + + + ); +}; + +export default DevToolsItem; diff --git a/frontend/testing-view/src/store/slices/appSlice.ts b/frontend/testing-view/src/store/slices/appSlice.ts index 7afc5a42f..8d9251000 100644 --- a/frontend/testing-view/src/store/slices/appSlice.ts +++ b/frontend/testing-view/src/store/slices/appSlice.ts @@ -45,6 +45,10 @@ export interface AppSlice { setConfig: (config: ConfigData | null) => void; isLoadingConfig: boolean; setIsLoadingConfig: (loading: boolean) => void; + + // Dev mode + isDevToolsVisible: boolean; + toggleDevToolsVisible: () => void; } export const createAppSlice: StateCreator = (set) => ({ @@ -96,4 +100,9 @@ export const createAppSlice: StateCreator = (set) => ({ setConfig: (config: ConfigData | null) => set({ config }), isLoadingConfig: false, setIsLoadingConfig: (loading: boolean) => set({ isLoadingConfig: loading }), + + // Dev mode + isDevToolsVisible: false, + toggleDevToolsVisible: () => + set((state) => ({ isDevToolsVisible: !state.isDevToolsVisible })), }); From b5335262fc5f049da51a44263f6377fca1ec085b Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:33:56 +0100 Subject: [PATCH 06/37] docs: replace npm mentions with pnpm --- electron-app/README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/electron-app/README.md b/electron-app/README.md index 146d8d643..854930745 100644 --- a/electron-app/README.md +++ b/electron-app/README.md @@ -22,11 +22,11 @@ When running in development mode (unpackaged), the application creates temporary - `config.toml.backup-{timestamp}` - Automatic backup files created when importing a configuration. These timestamped backups help recover previous configurations if needed. -- `binaries/` - Directory containing compiled backend executables for your platform. These are generated during the build process, when running `npm run build`. +- `binaries/` - Directory containing compiled backend executables for your platform. These are generated during the build process, when running `pnpm run build`. -- `renderer/` - Directory containing built frontend views (control-station, ethernet-view). These are generated during the build process, when running `npm run build`. +- `renderer/` - Directory containing built frontend views (control-station, ethernet-view). These are generated during the build process, when running `pnpm run build`. -- `dist/` - Build output directory containing compiled and packaged application files. Generated during build and distribution processes, when running `npm run dist`. +- `dist/` - Build output directory containing compiled and packaged application files. Generated during build and distribution processes, when running `pnpm run dist`. **Note**: These files and directories are created in the `electron-app/` directory root during development. In production (packaged) mode: @@ -54,24 +54,24 @@ Typical locations: ``` # Install dependencies -npm install +pnpm install # Build backend and frontends -npm run build +pnpm run build -# Run in development mode (you MUST run `npm run build` BEFORE!) -npm start +# Run in development mode (you MUST run `pnpm run build` BEFORE!) +pnpm start ``` ## Build for production This script creates distributables and executables. -**Note**: You must run `npm run build` for this script to work correctly. +**Note**: You must run `pnpm run build` for this script to work correctly. ``` -npm run dist:win # Windows -npm run dist:mac # macOS -npm run dist:linux # Linux +pnpm run dist:win # Windows +pnpm run dist:mac # macOS +pnpm run dist:linux # Linux ``` ### macOS Requirements @@ -85,14 +85,14 @@ sudo ipconfig set en0 INFORM 127.0.0.9 ## Available Scripts ``` -- `npm run build` - Build all frontend views and backend -- `npm start` - Run application in development mode -- `npm run dist` - Build production executable -- `npm test` - Run tests +- `pnpm run build` - Build all frontend views and backend +- `pnpm start` - Run application in development mode +- `pnpm run dist` - Build production executable +- `pnpm test` - Run tests ...and many custom variations (see package.json) -# Only works and makes sense after running `npm run dist` -- `npm run asar:{platform}` - Shows .asar application package content for [win, linux, mac] platforms +# Only works and makes sense after running `pnpm run dist` +- `pnpm run asar:{platform}` - Shows .asar application package content for [win, linux, mac] platforms ``` ## Architecture From bab43a49a5e8ef79299758914a6928cd2567c2e0 Mon Sep 17 00:00:00 2001 From: Alex <80858832+Humanoidear@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:28:52 +0100 Subject: [PATCH 07/37] Chore: Merge packet sender refactor to develop. --- packet-sender/.gitignore | 2 - packet-sender/Cargo.lock | 1734 --------------------- packet-sender/Cargo.toml | 51 - packet-sender/RANDOM_FEATURE.md | 41 - packet-sender/README.md | 232 --- packet-sender/go.mod | 34 + packet-sender/go.sum | 145 ++ packet-sender/main.go | 101 ++ packet-sender/main_test.go | 206 +++ packet-sender/package.json | 12 - packet-sender/pkg/board/board.go | 36 + packet-sender/pkg/listener/listener.go | 108 ++ packet-sender/pkg/sender/customSender.go | 105 ++ packet-sender/pkg/sender/randomSender.go | 179 +++ packet-sender/pkg/sender/sender.go | 26 + packet-sender/src/adj/board.rs | 48 - packet-sender/src/adj/mod.rs | 239 --- packet-sender/src/adj/packet.rs | 159 -- packet-sender/src/cli/manual.rs | 203 --- packet-sender/src/cli/mod.rs | 178 --- packet-sender/src/generator/data.rs | 113 -- packet-sender/src/generator/mod.rs | 113 -- packet-sender/src/generator/protection.rs | 28 - packet-sender/src/generator/random.rs | 118 -- packet-sender/src/logger.rs | 27 - packet-sender/src/main.rs | 178 --- packet-sender/src/network/macos.rs | 90 -- packet-sender/src/network/mod.rs | 195 --- packet-sender/src/network/sender.rs | 179 --- packet-sender/src/test_listener.rs | 53 - packet-sender/test_manual.sh | 18 - packet-sender/test_ranges.py | 125 -- packet-sender/testadj.py | 121 ++ 33 files changed, 1061 insertions(+), 4136 deletions(-) delete mode 100644 packet-sender/.gitignore delete mode 100644 packet-sender/Cargo.lock delete mode 100644 packet-sender/Cargo.toml delete mode 100644 packet-sender/RANDOM_FEATURE.md delete mode 100644 packet-sender/README.md create mode 100644 packet-sender/go.mod create mode 100644 packet-sender/go.sum create mode 100644 packet-sender/main.go create mode 100644 packet-sender/main_test.go delete mode 100644 packet-sender/package.json create mode 100644 packet-sender/pkg/board/board.go create mode 100644 packet-sender/pkg/listener/listener.go create mode 100644 packet-sender/pkg/sender/customSender.go create mode 100644 packet-sender/pkg/sender/randomSender.go create mode 100644 packet-sender/pkg/sender/sender.go delete mode 100644 packet-sender/src/adj/board.rs delete mode 100644 packet-sender/src/adj/mod.rs delete mode 100644 packet-sender/src/adj/packet.rs delete mode 100644 packet-sender/src/cli/manual.rs delete mode 100644 packet-sender/src/cli/mod.rs delete mode 100644 packet-sender/src/generator/data.rs delete mode 100644 packet-sender/src/generator/mod.rs delete mode 100644 packet-sender/src/generator/protection.rs delete mode 100644 packet-sender/src/generator/random.rs delete mode 100644 packet-sender/src/logger.rs delete mode 100644 packet-sender/src/main.rs delete mode 100644 packet-sender/src/network/macos.rs delete mode 100644 packet-sender/src/network/mod.rs delete mode 100644 packet-sender/src/network/sender.rs delete mode 100644 packet-sender/src/test_listener.rs delete mode 100755 packet-sender/test_manual.sh delete mode 100755 packet-sender/test_ranges.py create mode 100644 packet-sender/testadj.py diff --git a/packet-sender/.gitignore b/packet-sender/.gitignore deleted file mode 100644 index 6d23150b2..000000000 --- a/packet-sender/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Rust target directory -/target/ \ No newline at end of file diff --git a/packet-sender/Cargo.lock b/packet-sender/Cargo.lock deleted file mode 100644 index 2ec238bfe..000000000 --- a/packet-sender/Cargo.lock +++ /dev/null @@ -1,1734 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.59.0", -] - -[[package]] -name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "async-trait" -version = "0.1.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - -[[package]] -name = "cc" -version = "1.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" - -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link 0.1.3", -] - -[[package]] -name = "clap" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "clap_lex" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "config" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" -dependencies = [ - "async-trait", - "json5", - "lazy_static", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "yaml-rust", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossterm" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" -dependencies = [ - "bitflags 2.9.1", - "crossterm_winapi", - "libc", - "mio 0.8.11", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - -[[package]] -name = "dlv-list" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "enable-ansi-support" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea7457668b3da8a4b702f3d79e131aa3e81cd7e81cc95fb2d54fce9f182ecc77" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.9.1", - "libc", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.4", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.59.0", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-multimap" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" -dependencies = [ - "dlv-list", - "hashbrown 0.12.3", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "packet-sender-rs" -version = "0.1.0" -dependencies = [ - "anyhow", - "bincode", - "byteorder", - "chrono", - "clap", - "config", - "crossterm", - "dirs", - "enable-ansi-support", - "libc", - "rand", - "ratatui", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pest" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" -dependencies = [ - "memchr", - "thiserror 2.0.12", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "pest_meta" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "ratatui" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" -dependencies = [ - "bitflags 2.9.1", - "cassowary", - "crossterm", - "indoc", - "itertools", - "lru", - "paste", - "stability", - "strum", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "redox_syscall" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "ron" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" -dependencies = [ - "base64", - "bitflags 1.3.2", - "serde", -] - -[[package]] -name = "rust-ini" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" - -[[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" -dependencies = [ - "libc", - "mio 0.8.11", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "stability" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.104", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tokio" -version = "1.46.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio 1.0.4", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.104", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] diff --git a/packet-sender/Cargo.toml b/packet-sender/Cargo.toml deleted file mode 100644 index ec4cba8d3..000000000 --- a/packet-sender/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "packet-sender-rs" -version = "0.1.0" -edition = "2021" -authors = ["Hyperloop UPV Team"] -description = "Network packet sender for testing Hyperloop backend and frontend" - -[dependencies] -# Async runtime -tokio = { version = "1.35", features = ["full"] } - -# Folder directories -dirs = "5.0" - -# Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -bincode = "1.3" -byteorder = "1.5" - -# CLI and configuration -clap = { version = "4.4", features = ["derive"] } -config = "0.13" -anyhow = "1.0" -thiserror = "1.0" - -# Logging -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -enable-ansi-support = "0.3" - -# Random generation -rand = "0.8" - -# Time handling -chrono = "0.4" - -# System calls (for macOS socket configuration) -libc = "0.2" - -# Optional TUI -ratatui = { version = "0.25", optional = true } -crossterm = { version = "0.27", optional = true } - -[features] -default = [] -tui = ["ratatui", "crossterm"] - -[[bin]] -name = "packet-sender" -path = "src/main.rs" diff --git a/packet-sender/RANDOM_FEATURE.md b/packet-sender/RANDOM_FEATURE.md deleted file mode 100644 index 41a9668de..000000000 --- a/packet-sender/RANDOM_FEATURE.md +++ /dev/null @@ -1,41 +0,0 @@ -# Random Packet Generation Feature - -The packet-sender now supports sending random packets from all boards when no specific board is specified. - -## Usage - -### Command Line - -```bash -# Send random packets from all boards at default rate (100 pps) -packet-sender random - -# Send random packets from all boards at specific rate -packet-sender random --rate 200 - -# Send random packets from a specific board (existing behavior) -packet-sender random --board LCU --rate 150 -``` - -### Interactive Mode - -```bash -# Start interactive mode -packet-sender interactive - -# In the interactive prompt: -> random # Random from all boards at 100 pps -> random 200 # Random from all boards at 200 pps -> random LCU # Random from LCU board at 100 pps -> random LCU 150 # Random from LCU board at 150 pps -``` - -## How it Works - -When no board is specified, the packet-sender will: -1. Randomly select a board from all available boards -2. Generate a random data packet from that board -3. Send the packet with appropriate random values for all measurements -4. Repeat the process at the specified rate - -This simulates a more realistic scenario where packets arrive from different boards in an unpredictable order. \ No newline at end of file diff --git a/packet-sender/README.md b/packet-sender/README.md deleted file mode 100644 index b51273750..000000000 --- a/packet-sender/README.md +++ /dev/null @@ -1,232 +0,0 @@ -# Hyperloop Packet Sender (Rust) - -A professional network packet sender for testing the Hyperloop backend and frontend. This tool simulates board communications by generating and sending packets from simulated board IPs to the backend. - -## Features - -- **macOS-optimized**: Special handling for macOS localhost networking -- **Proper IP simulation**: Each board uses its actual IP from the ADJ configuration -- **Multiple packet types**: Support for data, protection, and other packet types -- **Flexible generation modes**: Random, sine wave, and sequence patterns -- **Interactive CLI**: User-friendly command-line interface -- **High performance**: Built with Rust and Tokio for efficient async networking - -## Architecture - -The packet sender is built with a modular architecture: - -- **ADJ Module**: Parses and manages board configurations -- **Network Module**: Handles UDP sockets with macOS-specific optimizations -- **Generator Module**: Creates packets with realistic data -- **CLI Module**: Provides interactive and batch operation modes - -## Key Improvements Over Go Version - -1. **Correct Network Configuration**: Uses actual board IPs instead of hardcoded addresses -2. **macOS Compatibility**: Proper socket options for localhost communication -3. **Type Safety**: Rust's type system prevents packet format errors -4. **Better Performance**: Zero-copy packet generation where possible -5. **Comprehensive Error Handling**: No silent failures - -## Building - -```bash -cargo build --release -``` - -## Quick Start - -### Testing Without Backend - -1. **Start a test listener** (in one terminal): - -```bash -./target/release/packet-sender listen -# This starts a UDP listener on 127.0.0.9:8000 -``` - -2. **Send packets** (in another terminal): - -```bash -# Send random packets from BCU at 10 packets/second -./target/release/packet-sender random -b BCU -r 10 - -# Or use interactive mode -./target/release/packet-sender -> random BCU 20 -``` - -## Usage - -### Dev Mode vs Production Mode - -The packet-sender now supports two modes: - -- **Production Mode (default)**: Sends packets that can be captured by the backend's packet sniffer. Requires board IPs to be configured on network interfaces. -- **Dev Mode**: Sends packets directly to the backend's UDP server. Use this for local development. - -```bash -# Production mode (for use with packet sniffer) -./target/release/packet-sender random - -# Dev mode (for use with UDP server) -./target/release/packet-sender --dev random -``` - -### Interactive Mode - -```bash -./target/release/packet-sender interactive -# Or in dev mode -./target/release/packet-sender --dev interactive -``` - -Interactive mode commands: - -- `help` / `h` - Show available commands -- `list` / `l` - List all boards -- `board` / `b ` - Show board information -- `manual` / `m ` - Manually build and send a packet with custom values -- `random` / `r [rate]` - Start random packet generation -- `simulate` / `sim` / `s ` - Start simulation (modes: random, sine, sequence) -- `quit` / `q` / `exit` - Exit the program - -#### Manual Packet Sending - -The new `manual` command provides an interactive way to send packets with custom values: - -1. Select the board: `manual VCU` -2. Choose a packet from the displayed list -3. Select random or custom values -4. If custom, enter values for each variable with: - - Type information (uint8, float32, bool, enum, etc.) - - Valid ranges based on the data type - - Units (if available in ADJ) - - Safe/warning ranges (if configured in ADJ) - -Example session: - -``` -> manual VCU -Available packets for board VCU: - -Data packets: - [1] temperature_data (ID: 100) - 3 variables - [2] pressure_data (ID: 101) - 2 variables - -Select packet number (1-2): 1 - -Selected packet: temperature_data (ID: 100) -This packet has 3 variables - -Generate (r)andom or (c)ustom values? c - -Variable: temp_sensor_1 (ID: 1001) -Type: Float32 -Units: °C -Safe range: 20.0 to 80.0 -32-bit floating point -Enter value: 25.5 - -Packet sent successfully! -``` - -### Random Generation - -```bash -# Generate packets from all boards at 100 pps -./target/release/packet-sender random - -# Generate packets from specific board at 200 pps -./target/release/packet-sender random -b BCU -r 200 - -# Dev mode with specific board -./target/release/packet-sender --dev random -b BCU -r 200 -``` - -### Board Simulation - -```bash -# Simulate BCU board with random data -./target/release/packet-sender board BCU --mode random - -# Simulate with sine wave pattern -./target/release/packet-sender board BCU --mode sine - -# Dev mode simulation -./target/release/packet-sender --dev board BCU --mode sine -``` - -### List Available Boards - -```bash -./target/release/packet-sender list -``` - -## Configuration - -The tool automatically loads the ADJ (Abstract Data JSON) from the backend directory. You can specify a custom path: - -```bash -./target/release/packet-sender --adj-path /path/to/adj -``` - -## macOS-Specific Features - -The packet sender includes special handling for macOS: - -- Sets `SO_REUSEADDR` and `SO_REUSEPORT` for multiple binds -- Configures `SO_NOSIGPIPE` to prevent crashes -- Optimizes buffer sizes for localhost communication -- Properly handles the `lo0` loopback interface - -## Packet Format - -Packets follow the backend's binary format: - -- 2 bytes: Packet ID (little-endian) -- Variable data based on packet type and definition - -## Development - -### Adding New Packet Types - -1. Update the `PacketType` enum in `src/adj/packet.rs` -2. Create a new generator in `src/generator/` -3. Add handling in `PacketGenerator::generate_packet()` - -### Testing with Backend - -#### Production Mode (with Sniffer) - -1. Start the backend with `dev_mode = false` in config.toml -2. Ensure network interfaces are configured with board IPs -3. Run the packet sender without the `--dev` flag -4. Monitor backend logs for received packets - -#### Dev Mode (with UDP Server) - -1. Start the backend with `dev_mode = true` in config.toml or dev-config.toml -2. Run the packet sender with the `--dev` flag -3. Monitor backend logs for received packets - -## Troubleshooting - -### "Address already in use" errors - -- The tool sets SO_REUSEADDR/PORT, but check for other processes -- Use `lsof -i :PORT` to find conflicting processes - -### Packets not reaching backend - -- Verify backend is listening on the correct address/port -- Check firewall settings (especially on macOS) -- Use Wireshark to monitor localhost traffic - -## Future Enhancements - -- [ ] TUI mode with real-time statistics -- [ ] Packet replay from captured files -- [ ] Fault injection scenarios -- [ ] Performance benchmarking mode -- [ ] Integration tests with backend diff --git a/packet-sender/go.mod b/packet-sender/go.mod new file mode 100644 index 000000000..b513866fd --- /dev/null +++ b/packet-sender/go.mod @@ -0,0 +1,34 @@ +module packet_sender + +go 1.23.0 + +toolchain go1.24.2 + +require github.com/HyperloopUPV-H8/h9-backend v0.0.0-00010101000000-000000000000 + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/tools v0.13.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) + +replace github.com/HyperloopUPV-H8/h9-backend => ../backend diff --git a/packet-sender/go.sum b/packet-sender/go.sum new file mode 100644 index 000000000..688732b2b --- /dev/null +++ b/packet-sender/go.sum @@ -0,0 +1,145 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/packet-sender/main.go b/packet-sender/main.go new file mode 100644 index 000000000..9a52ccb99 --- /dev/null +++ b/packet-sender/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "log" + "net" + boardpkg "packet_sender/pkg/board" + "packet_sender/pkg/listener" + "packet_sender/pkg/sender" + "path" + "path/filepath" + "strings" + + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" +) + +func main() { + adj := getADJ() + conns := getConns(adj) + + fmt.Print("[Warning] This program must start before the backend. If the backend is terminated, this program must be restarted. \n \n") + fmt.Println("[Tip] You may send and listen to packets at the same time, on different terminals.") + input := getBinaryInput("Select mode:\n1) Send packets\n2) Listen packets") + + switch input { + case "1": + input := getBinaryInput("Do you want to send random or custom packets?\n1) Random\n2) Custom") + sender.Start(conns, input) + case "2": + listener.Start(conns, adj) + } +} + +func getConns(adj adj_module.ADJ) []boardpkg.BoardConn { + conns := make([]boardpkg.BoardConn, 0) + + for _, board := range adj.Boards { + conn := getConn(board.IP, 0, adj.Info.Addresses["backend"], adj.Info.Ports["UDP"]) + conns = append(conns, boardpkg.BoardConn{ + UDPConn: conn, + Packets: []adj_module.Packet{}, + Board: board, + }) + } + + return conns +} + +func getConn(lip string, lport uint16, rip string, rport uint16) *net.UDPConn { + laddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", lip, lport)) + if err != nil { + log.Fatalf("resolve address: %s\n", err) + } + raddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", rip, rport)) + if err != nil { + log.Fatalf("resolve address: %s\n", err) + } + conn, err := net.DialUDP("udp", laddr, raddr) + + if err != nil { + log.Fatal("Error creating udp connection", err) + } + + return conn +} + +// getADJ loads the same ADJ used by backend directly from backend/cmd/adj. +func getADJ() adj_module.ADJ { + adjPath, err := filepath.Abs(path.Join("..", "backend", "cmd", "adj")) + if err != nil { + log.Fatalf("Failed to resolve ADJ path: %v", err) + } + adj_module.RepoPath = adjPath + string(filepath.Separator) + + adj, err := adj_module.NewADJ("") + if err != nil { + log.Fatalf("Failed to load ADJ: %v\n", err) + } + + return adj +} + +func getBinaryInput(msg string) string { + + fmt.Println(msg) + var input string + _, err := fmt.Scan(&input) + if err != nil { + log.Fatalf("failed to read input: %v", err) + } + + for { + input = strings.TrimSpace(input) + if input != "1" && input != "2" { + log.Fatal("invalid input: use 1 or 2") + } else { + break + } + } + return input +} diff --git a/packet-sender/main_test.go b/packet-sender/main_test.go new file mode 100644 index 000000000..8b9af7fa0 --- /dev/null +++ b/packet-sender/main_test.go @@ -0,0 +1,206 @@ +package main + +import ( + "encoding/binary" + boardpkg "packet_sender/pkg/board" + sender "packet_sender/pkg/sender" + "testing" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" +) + +func TestGetBoardPackets_FiltersOnlyDataPackets(t *testing.T) { + board := adj_module.Board{ + Name: "VCU", + Packets: []adj_module.Packet{ + {Id: abstraction.PacketId(100), Name: "DataA", Type: "data", VariablesIds: []string{"a"}}, + {Id: abstraction.PacketId(200), Name: "OrderA", Type: "order", VariablesIds: []string{"b"}}, + {Id: abstraction.PacketId(300), Name: "StateOrderA", Type: "stateOrder", VariablesIds: []string{"c"}}, + {Id: abstraction.PacketId(400), Name: "DataB", Type: "data", VariablesIds: []string{"d"}}, + }, + } + + bc := boardpkg.BoardConn{Board: board} + boardpkg.LoadDataPackets(&bc) + + if len(bc.Packets) != 2 { + t.Fatalf("expected 2 data packets, got %d", len(bc.Packets)) + } + + if bc.Packets[0].Type != "data" || bc.Packets[1].Type != "data" { + t.Fatalf("expected all packets to be type=data, got %q and %q", bc.Packets[0].Type, bc.Packets[1].Type) + } + + if bc.Packets[0].Id != 100 || bc.Packets[1].Id != 400 { + t.Fatalf("unexpected packet IDs: got %d and %d", bc.Packets[0].Id, bc.Packets[1].Id) + } +} + +func TestGetBoardPackets_EmptyInputProducesEmptyOutput(t *testing.T) { + bc := boardpkg.BoardConn{ + Board: adj_module.Board{ + Name: "EmptyBoard", + Packets: nil, + }, + } + + boardpkg.LoadDataPackets(&bc) + + if len(bc.Packets) != 0 { + t.Fatalf("expected 0 packets, got %d", len(bc.Packets)) + } +} + +func TestCreateRandomPacket_NoPackets_ReturnsNil(t *testing.T) { + bc := boardpkg.BoardConn{Packets: nil} + got := sender.CreateRandomPacket(&bc) + if got != nil { + t.Fatalf("expected nil, got %v", got) + } +} + +func TestCreateRandomPacket_NoVariables_ReturnsNil(t *testing.T) { + bc := boardpkg.BoardConn{ + Packets: []adj_module.Packet{ + { + Id: abstraction.PacketId(42), + Type: "data", + Variables: nil, + VariablesIds: nil, + }, + }, + } + + got := sender.CreateRandomPacket(&bc) + if got != nil { + t.Fatalf("expected nil when packet has no variables, got %v", got) + } +} + +func TestCreateRandomPacket_StringMeasurement_ReturnsNil(t *testing.T) { + bc := boardpkg.BoardConn{ + Packets: []adj_module.Packet{ + { + Id: abstraction.PacketId(7), + Type: "data", + VariablesIds: []string{"s"}, + Variables: []adj_module.Measurement{ + {Id: "s", Name: "S", Type: "string"}, + }, + }, + }, + } + + got := sender.CreateRandomPacket(&bc) + if got != nil { + t.Fatalf("expected nil for string measurement, got %v", got) + } +} + +func TestCreateRandomPacket_BoolPacket_HasIDAndPayload(t *testing.T) { + bc := boardpkg.BoardConn{ + Packets: []adj_module.Packet{ + { + Id: abstraction.PacketId(513), // 0x0201 + Type: "data", + VariablesIds: []string{"b"}, + Variables: []adj_module.Measurement{ + {Id: "b", Name: "B", Type: "bool"}, + }, + }, + }, + } + + got := sender.CreateRandomPacket(&bc) + if got == nil { + t.Fatal("expected non-nil packet") + } + + // 2 bytes ID + 1 byte bool + if len(got) != 3 { + t.Fatalf("expected len=3, got %d", len(got)) + } + + id := binary.LittleEndian.Uint16(got[:2]) + if id != 513 { + t.Fatalf("expected id=513, got %d", id) + } + + if got[2] != 0 && got[2] != 1 { + t.Fatalf("expected bool payload byte 0 or 1, got %d", got[2]) + } +} + +func TestCreateRandomPacket_EnumPacket_HasIDAndEnumByte(t *testing.T) { + bc := boardpkg.BoardConn{ + Packets: []adj_module.Packet{ + { + Id: abstraction.PacketId(10), + Type: "data", + VariablesIds: []string{"mode"}, + Variables: []adj_module.Measurement{ + {Id: "mode", Name: "Mode", Type: "enum(OFF,ON,FAULT)"}, + }, + }, + }, + } + + got := sender.CreateRandomPacket(&bc) + if got == nil { + t.Fatal("expected non-nil packet") + } + + // 2 bytes ID + 1 byte enum + if len(got) != 3 { + t.Fatalf("expected len=3, got %d", len(got)) + } + + id := binary.LittleEndian.Uint16(got[:2]) + if id != 10 { + t.Fatalf("expected id=10, got %d", id) + } + + enumVal := got[2] + if enumVal > 2 { + t.Fatalf("expected enum byte in [0..2], got %d", enumVal) + } +} + +func TestCreateRandomPacket_NumericPacket_HasIDAndNumericPayload(t *testing.T) { + bc := boardpkg.BoardConn{ + Packets: []adj_module.Packet{ + { + Id: abstraction.PacketId(20), + Type: "data", + VariablesIds: []string{"rpm"}, + Variables: []adj_module.Measurement{ + { + Id: "rpm", + Name: "RPM", + Type: "uint16", + WarningRange: []*float64{}, // force fallback path + }, + }, + }, + }, + } + + got := sender.CreateRandomPacket(&bc) + if got == nil { + t.Fatal("expected non-nil packet") + } + + // 2 bytes ID + 2 bytes uint16 + if len(got) != 4 { + t.Fatalf("expected len=4, got %d", len(got)) + } + + id := binary.LittleEndian.Uint16(got[:2]) + if id != 20 { + t.Fatalf("expected id=20, got %d", id) + } + + // just ensure numeric payload exists and is parseable + _ = binary.LittleEndian.Uint16(got[2:4]) +} \ No newline at end of file diff --git a/packet-sender/package.json b/packet-sender/package.json deleted file mode 100644 index ef6e4db22..000000000 --- a/packet-sender/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "packet-sender", - "version": "1.0.0", - "private": true, - "author": "Hyperloop UPV Team", - "license": "MIT", - "scripts": { - "build": "cargo build --release", - "dev": "cargo run", - "test": "cargo test" - } -} diff --git a/packet-sender/pkg/board/board.go b/packet-sender/pkg/board/board.go new file mode 100644 index 000000000..59d204850 --- /dev/null +++ b/packet-sender/pkg/board/board.go @@ -0,0 +1,36 @@ +package board + +import ( + "net" + + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" +) + +type BoardConn struct { + UDPConn *net.UDPConn + Packets []adj_module.Packet + Board adj_module.Board + + TCPListener *net.TCPListener + TCPConn net.Conn +} + +func LoadDataPackets(boardConn *BoardConn) { + packets := make([]adj_module.Packet, 0) + + for _, packet := range boardConn.Board.Packets { + if packet.Type != "data" { + continue + } + + packets = append(packets, adj_module.Packet{ + Id: packet.Id, + Name: packet.Name, + Type: packet.Type, + Variables: packet.Variables, + VariablesIds: packet.VariablesIds, + }) + } + + boardConn.Packets = packets +} diff --git a/packet-sender/pkg/listener/listener.go b/packet-sender/pkg/listener/listener.go new file mode 100644 index 000000000..5dd1f1a3d --- /dev/null +++ b/packet-sender/pkg/listener/listener.go @@ -0,0 +1,108 @@ +package listener + +import ( + "encoding/binary" + "fmt" + "log" + "net" + boardpkg "packet_sender/pkg/board" + + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" +) + +func Start(conns []boardpkg.BoardConn, adj adj_module.ADJ) { + tcpPort := adj.Info.Ports["TCP_SERVER"] + for i := range conns { + if err := startTCPListener(&conns[i], tcpPort); err != nil { + log.Printf("TCP listener error: %v", err) + } + } + + defer func() { + for _, c := range conns { + if c.TCPConn != nil { + c.TCPConn.Close() + } + if c.TCPListener != nil { + c.TCPListener.Close() + } + } + }() + + // Get the list of packets for each board + for i := range conns { + boardpkg.LoadDataPackets(&conns[i]) + go startTCPConnection(&conns[i]) + } + + select {} +} + +func startTCPListener(board *boardpkg.BoardConn, backendPort uint16) error { + + addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", board.Board.IP, backendPort)) + if err != nil { + return fmt.Errorf("resolve tcp addr for %s: %w", board.Board.Name, err) + } + + ln, err := net.ListenTCP("tcp", addr) + if err != nil { + return fmt.Errorf("listen tcp for %s: %w", board.Board.Name, err) + } + + board.TCPListener = ln + log.Printf("[%s] TCP listening on %s", board.Board.Name, ln.Addr().String()) + return nil +} + +func startTCPConnection(board *boardpkg.BoardConn) { + if board.TCPListener == nil { + return + } + + conn, err := board.TCPListener.AcceptTCP() + if err != nil { + log.Printf("[%s] TCP accept error: %v", board.Board.Name, err) + return + } + + board.TCPConn = conn + log.Printf("[%s] TCP connected: local=%s remote=%s", + board.Board.Name, conn.LocalAddr().String(), conn.RemoteAddr().String()) + + go readTCPPackets(board) +} + +func readTCPPackets(board *boardpkg.BoardConn) { + if board.TCPConn == nil { + return + } + + buf := make([]byte, 2048) + + // Read packets in a loop + for { + n, err := board.TCPConn.Read(buf) + if err != nil { + log.Printf("[%s] TCP read closed/error: %v", board.Board.Name, err) + return + } + if n < 2 { + log.Printf("[%s] TCP read %d bytes (too short for packet id)", board.Board.Name, n) + continue + } + + id := binary.LittleEndian.Uint16(buf[:2]) + order := findOrderByID(board, id) + log.Printf("[%s] TCP: packet_id=%d order=%s", board.Board.Name, id, order) + } +} + +func findOrderByID(board *boardpkg.BoardConn, id uint16) string { + for _, p := range board.Board.Packets { + if uint16(p.Id) == id { + return p.Name + } + } + return "unknown" +} \ No newline at end of file diff --git a/packet-sender/pkg/sender/customSender.go b/packet-sender/pkg/sender/customSender.go new file mode 100644 index 000000000..a07386ab7 --- /dev/null +++ b/packet-sender/pkg/sender/customSender.go @@ -0,0 +1,105 @@ +package sender + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "os" + boardpkg "packet_sender/pkg/board" + "strconv" + "strings" +) + +func CustomSender(conns []boardpkg.BoardConn) { + reader := bufio.NewReader(os.Stdin) + for { + // Show available boards + fmt.Println("Available boards:") + for i, c := range conns { + fmt.Printf("[%d] %s\n", i, c.Board.Name) + } + fmt.Print("Select board index: ") + boardIdxStr, _ := reader.ReadString('\n') + boardIdx, err := strconv.Atoi(strings.TrimSpace(boardIdxStr)) + if err != nil || boardIdx < 0 || boardIdx >= len(conns) { + fmt.Println("Invalid board index") + continue + } + board := &conns[boardIdx] + + // Show available packets + fmt.Println("Available packets:") + for i, p := range board.Packets { + fmt.Printf("[%d] ID: %d, Nombre: %s\n", i, p.Id, p.Name) + } + fmt.Print("Select packet index: ") + packetIdxStr, _ := reader.ReadString('\n') + packetIdx, err := strconv.Atoi(strings.TrimSpace(packetIdxStr)) + if err != nil || packetIdx < 0 || packetIdx >= len(board.Packets) { + fmt.Println("Invalid packet index") + continue + } + packet := board.Packets[packetIdx] + + // Ask values for each variable + buff := bytes.NewBuffer(make([]byte, 0)) + binary.Write(buff, binary.LittleEndian, packet.Id) + + for _, v := range packet.Variables { + fmt.Printf("Enter value for %s (%s): ", v.Name, v.Type) + valStr, _ := reader.ReadString('\n') + valStr = strings.TrimSpace(valStr) + writeUserValueAsBytes(valStr, v.Type, buff) + } + + // Send packet + _, err = board.UDPConn.Write(buff.Bytes()) + if err != nil { + fmt.Println("Error sending packet:", err) + } else { + fmt.Println("Packet sent successfully.") + } + } +} + +func writeUserValueAsBytes(valStr, typ string, buff *bytes.Buffer) { + switch typ { + case "uint8": + v, _ := strconv.ParseUint(valStr, 10, 8) + binary.Write(buff, binary.LittleEndian, uint8(v)) + case "uint16": + v, _ := strconv.ParseUint(valStr, 10, 16) + binary.Write(buff, binary.LittleEndian, uint16(v)) + case "uint32": + v, _ := strconv.ParseUint(valStr, 10, 32) + binary.Write(buff, binary.LittleEndian, uint32(v)) + case "uint64": + v, _ := strconv.ParseUint(valStr, 10, 64) + binary.Write(buff, binary.LittleEndian, uint64(v)) + case "int8": + v, _ := strconv.ParseInt(valStr, 10, 8) + binary.Write(buff, binary.LittleEndian, int8(v)) + case "int16": + v, _ := strconv.ParseInt(valStr, 10, 16) + binary.Write(buff, binary.LittleEndian, int16(v)) + case "int32": + v, _ := strconv.ParseInt(valStr, 10, 32) + binary.Write(buff, binary.LittleEndian, int32(v)) + case "int64": + v, _ := strconv.ParseInt(valStr, 10, 64) + binary.Write(buff, binary.LittleEndian, int64(v)) + case "float32": + v, _ := strconv.ParseFloat(valStr, 32) + binary.Write(buff, binary.LittleEndian, float32(v)) + case "float64": + v, _ := strconv.ParseFloat(valStr, 64) + binary.Write(buff, binary.LittleEndian, v) + case "bool": + v := valStr == "true" || valStr == "1" + binary.Write(buff, binary.LittleEndian, v) + default: + // Add extra handling for enums and other types here. + binary.Write(buff, binary.LittleEndian, uint8(0)) + } +} diff --git a/packet-sender/pkg/sender/randomSender.go b/packet-sender/pkg/sender/randomSender.go new file mode 100644 index 000000000..2457bee57 --- /dev/null +++ b/packet-sender/pkg/sender/randomSender.go @@ -0,0 +1,179 @@ +package sender + +import ( + "bytes" + "encoding/binary" + "fmt" + "log" + "math" + "math/rand" + "os" + "os/signal" + boardpkg "packet_sender/pkg/board" + "strings" + "time" +) + +func RandomSender(conns []boardpkg.BoardConn) { + count := make(chan struct{}, 10000) + start := time.Now() + prev := time.Now() + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + for range ticker.C { + // Get a random board to send the packet from + randomIndex := rand.Int63n(int64(len(conns))) + randomBoard := conns[randomIndex] + + packet := CreateRandomPacket(&randomBoard) + fmt.Println(time.Since(prev)) + prev = time.Now() + + if len(packet) < 2 { + continue + } + + fmt.Printf("Sending packet ID: %d, size: %d\n", binary.LittleEndian.Uint16(packet), len(packet)) + _, err := randomBoard.UDPConn.Write(packet) + if err != nil { + continue + } + + count <- struct{}{} + } + }() + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + defer signal.Stop(interrupt) + + sent := 0 + for { + select { + case <-count: + sent++ + case <-interrupt: + fmt.Printf("Sent=%d, Elapsed=%v\n", sent, time.Since(start)) + return + } + } +} + +func CreateRandomPacket(board *boardpkg.BoardConn) []byte { + if len(board.Packets) == 0 { + return nil + } + + randomIndex := rand.Int63n(int64(len(board.Packets))) + randomPacket := board.Packets[randomIndex] + + if len(randomPacket.VariablesIds) == 0 { + log.Printf("The packet with ID %d has no measurements\n", randomPacket.Id) + return nil + } + + buff := bytes.NewBuffer(make([]byte, 0)) + + binary.Write(buff, binary.LittleEndian, randomPacket.Id) + + for _, measurement := range randomPacket.Variables { + switch { + case strings.Contains(measurement.Type, "enum"): + n := enumOptionCount(measurement.Type) + binary.Write(buff, binary.LittleEndian, uint8(rand.Intn(n))) + case measurement.Type == "bool": + binary.Write(buff, binary.LittleEndian, rand.Intn(2) == 1) + case measurement.Type == "string": + return nil + default: + var number float64 + // If warning bounds are unavailable, fall back to full type range. + if len(measurement.WarningRange) == 0 { + number = mapNumberToRange(rand.Float64(), nil, measurement.Type) + } else if measurement.WarningRange[0] != nil && measurement.WarningRange[1] != nil { + low := *measurement.WarningRange[0] * 0.8 + high := *measurement.WarningRange[1] * 1.2 + number = mapNumberToRange(rand.Float64(), []*float64{&low, &high}, measurement.Type) + } else { + number = mapNumberToRange(rand.Float64(), nil, measurement.Type) + } + writeNumberAsBytes(number, measurement.Type, buff) + } + } + return buff.Bytes() +} + +func enumOptionCount(t string) int { + trimmed := strings.TrimSuffix(strings.TrimPrefix(t, "enum("), ")") + trimmed = strings.ReplaceAll(trimmed, " ", "") + if trimmed == "" { + return 0 + } + return len(strings.Split(trimmed, ",")) +} + +func mapNumberToRange(number float64, numberRange []*float64, numberType string) float64 { + if len(numberRange) == 0 { + return number * getTypeMaxValue(numberType) + } + return (number * (*numberRange[1] - *numberRange[0])) + *numberRange[0] +} + +func getTypeMaxValue(numberType string) float64 { + switch numberType { + case "uint8": + return math.MaxUint8 + case "uint16": + return math.MaxUint16 + case "uint32": + return math.MaxUint32 + case "uint64": + return math.MaxUint64 + case "int8": + return math.MaxInt8 + case "int16": + return math.MaxInt16 + case "int32": + return math.MaxInt32 + case "int64": + return math.MaxInt64 + case "float32": + return math.MaxFloat32 + case "float64": + return math.MaxFloat64 + case "bool": + return math.MaxUint8 + default: + return math.MaxUint8 + } +} + +func writeNumberAsBytes(number float64, numberType string, buff *bytes.Buffer) { + switch numberType { + case "uint8": + binary.Write(buff, binary.LittleEndian, uint8(number)) + case "uint16": + binary.Write(buff, binary.LittleEndian, uint16(number)) + case "uint32": + binary.Write(buff, binary.LittleEndian, uint32(number)) + case "uint64": + binary.Write(buff, binary.LittleEndian, uint64(number)) + case "int8": + binary.Write(buff, binary.LittleEndian, int8(number)) + case "int16": + binary.Write(buff, binary.LittleEndian, int16(number)) + case "int32": + binary.Write(buff, binary.LittleEndian, int32(number)) + case "int64": + binary.Write(buff, binary.LittleEndian, int64(number)) + case "float32": + binary.Write(buff, binary.LittleEndian, float32(number)) + case "float64": + binary.Write(buff, binary.LittleEndian, number) + case "bool": + binary.Write(buff, binary.LittleEndian, uint8(number)) + default: + binary.Write(buff, binary.LittleEndian, uint8(number)) + } +} diff --git a/packet-sender/pkg/sender/sender.go b/packet-sender/pkg/sender/sender.go new file mode 100644 index 000000000..b06446f43 --- /dev/null +++ b/packet-sender/pkg/sender/sender.go @@ -0,0 +1,26 @@ +package sender + +import ( + boardpkg "packet_sender/pkg/board" +) + +func Start(conns []boardpkg.BoardConn, input string) { + + // Get the list of packets for each board + for i := range conns { + boardpkg.LoadDataPackets(&conns[i]) + } + + defer func() { + for _, c := range conns { + c.UDPConn.Close() + } + }() + + switch input { + case "1": + RandomSender(conns) + case "2": + CustomSender(conns) + } +} diff --git a/packet-sender/src/adj/board.rs b/packet-sender/src/adj/board.rs deleted file mode 100644 index 2abd11633..000000000 --- a/packet-sender/src/adj/board.rs +++ /dev/null @@ -1,48 +0,0 @@ -use serde::{Deserialize, Serialize}; -use super::packet::Packet; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Board { - pub name: String, - pub id: u16, - pub ip: String, - pub packets: Vec, -} - -impl Board { - pub fn get_data_packets(&self) -> Vec<&Packet> { - self.packets - .iter() - .filter(|p| p.packet_type == super::packet::PacketType::Data) - .collect() - } - - pub fn get_protection_packets(&self) -> Vec<&Packet> { - self.packets - .iter() - .filter(|p| p.packet_type == super::packet::PacketType::Protection) - .collect() - } - - pub fn get_order_packets(&self) -> Vec<&Packet> { - self.packets - .iter() - .filter(|p| p.packet_type == super::packet::PacketType::Order) - .collect() - } - - pub fn get_info_packets(&self) -> Vec<&Packet> { - self.packets - .iter() - .filter(|p| p.packet_type == super::packet::PacketType::Info) - .collect() - } - - pub fn find_packet_by_id(&self, id: u16) -> Option<&Packet> { - self.packets.iter().find(|p| p.id == id) - } - - pub fn find_packet_by_name(&self, name: &str) -> Option<&Packet> { - self.packets.iter().find(|p| p.name == name) - } -} \ No newline at end of file diff --git a/packet-sender/src/adj/mod.rs b/packet-sender/src/adj/mod.rs deleted file mode 100644 index 4cf3ca530..000000000 --- a/packet-sender/src/adj/mod.rs +++ /dev/null @@ -1,239 +0,0 @@ -use anyhow::{Result, Context}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::Path; -use tokio::fs; -use tracing::{debug, info}; - -pub mod board; -pub mod packet; - -pub use board::Board; -pub use packet::{Packet, PacketType, Variable, ValueType}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ADJ { - pub info: Info, - pub boards: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Info { - pub addresses: HashMap, - pub ports: HashMap, -} - -pub async fn load_adj(adj_path: &Path) -> Result { - info!("Loading ADJ from {:?}", adj_path); - - // Load general_info.json - let info_path = adj_path.join("general_info.json"); - let info_content = fs::read_to_string(&info_path) - .await - .context(format!("Failed to read general_info.json from {:?}", info_path))?; - let general_info: serde_json::Value = serde_json::from_str(&info_content) - .context("Failed to parse general_info.json")?; - - // Convert to our Info structure - let info = Info { - addresses: serde_json::from_value(general_info["addresses"].clone())?, - ports: serde_json::from_value(general_info["ports"].clone())?, - }; - - debug!("Loaded info: {:?}", info); - - // Load boards.json - let boards_path = adj_path.join("boards.json"); - let boards_content = fs::read_to_string(&boards_path) - .await - .context("Failed to read boards.json")?; - let board_map: HashMap = serde_json::from_str(&boards_content) - .context("Failed to parse boards.json")?; - - // Load each board - let mut boards = Vec::new(); - for (board_name, board_file) in board_map { - let board_path = adj_path.join(&board_file); - if let Ok(board) = load_board_from_file(&board_path, &board_name).await { - boards.push(board); - } - } - - info!("Loaded {} boards", boards.len()); - - Ok(ADJ { info, boards }) -} - -async fn load_board_from_file(board_config_path: &Path, board_name: &str) -> Result { - debug!("Loading board: {} from {:?}", board_name, board_config_path); - - // Load board configuration - let board_config_content = fs::read_to_string(&board_config_path) - .await - .context(format!("Failed to read board config for {}", board_name))?; - let board_config: serde_json::Value = serde_json::from_str(&board_config_content)?; - - let id = board_config["board_id"].as_u64().unwrap_or(0) as u16; - let ip = board_config["board_ip"].as_str().unwrap_or("127.0.0.1").to_string(); - - // Get the board directory - let board_dir = board_config_path.parent() - .ok_or_else(|| anyhow::anyhow!("Invalid board config path"))?; - - // First, load all measurements into a map - let mut measurements_map: HashMap = HashMap::new(); - - if let Some(measurement_files) = board_config["measurements"].as_array() { - for measurement_file in measurement_files { - if let Some(filename) = measurement_file.as_str() { - let measurement_path = board_dir.join(filename); - if let Ok(measurements_content) = fs::read_to_string(&measurement_path).await { - if let Ok(measurements) = serde_json::from_str::>(&measurements_content) { - for measurement in measurements { - if let Some(id) = measurement["id"].as_str() { - measurements_map.insert(id.to_string(), measurement); - } - } - } - } - } - } - } - - // Load all packet files - let mut all_packets = Vec::new(); - - if let Some(packet_files) = board_config["packets"].as_array() { - for packet_file in packet_files { - if let Some(filename) = packet_file.as_str() { - let packet_path = board_dir.join(filename); - if let Ok(packets_content) = fs::read_to_string(&packet_path).await { - if let Ok(packet_defs) = serde_json::from_str::>(&packets_content) { - for packet_def in packet_defs { - if let Ok(packet) = parse_packet_with_measurements(&packet_def, &measurements_map) { - all_packets.push(packet); - } - } - } - } - } - } - } - - Ok(Board { - name: board_name.to_string(), - id, - ip, - packets: all_packets, - }) -} - -fn parse_packet_with_measurements( - packet_def: &serde_json::Value, - measurements_map: &HashMap -) -> Result { - let id = packet_def["id"].as_u64().ok_or_else(|| anyhow::anyhow!("Missing packet id"))? as u16; - let name = packet_def["name"].as_str().unwrap_or("Unknown").to_string(); - let packet_type_str = packet_def["type"].as_str().unwrap_or("data"); - - let packet_type = match packet_type_str { - "data" => PacketType::Data, - "protection" => PacketType::Protection, - "order" => PacketType::Order, - "info" => PacketType::Info, - _ => PacketType::Data, - }; - - let mut variables = Vec::new(); - let mut variables_ids = Vec::new(); - - if let Some(var_names) = packet_def["variables"].as_array() { - for (idx, var_name) in var_names.iter().enumerate() { - if let Some(var_id) = var_name.as_str() { - if let Some(measurement) = measurements_map.get(var_id) { - let type_str = measurement["type"].as_str().unwrap_or("uint32"); - let value_type = if type_str == "enum" { - // Parse enum with its values - if let Some(enum_values) = measurement["enumValues"].as_array() { - let values: Vec = enum_values.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(); - if values.is_empty() { - // Fallback to at least one value to prevent empty range panic - ValueType::Enum(vec!["Unknown".to_string()]) - } else { - ValueType::Enum(values) - } - } else { - // Fallback to at least one value to prevent empty range panic - ValueType::Enum(vec!["Unknown".to_string()]) - } - } else { - parse_value_type(type_str)? - }; - - // Parse out_of_range field if it exists - let mut warning_range = None; - if let Some(out_of_range) = measurement["out_of_range"].as_object() { - if let Some(warning) = out_of_range.get("warning") { - warning_range = parse_range(warning); - } - } - - let variable = Variable { - id: idx as u16, - name: measurement["name"].as_str().unwrap_or(var_id).to_string(), - value_type, - units: measurement["displayUnits"].as_str().map(|s| s.to_string()), - safe_range: parse_range(&measurement["safeRange"]), - warning_range, - }; - variables.push(variable); - variables_ids.push(idx as u16); - } - } - } - } - - Ok(Packet { - id, - name, - packet_type, - variables, - variables_ids, - }) -} - -fn parse_value_type(type_str: &str) -> Result { - match type_str { - "uint8" => Ok(ValueType::UInt8), - "uint16" => Ok(ValueType::UInt16), - "uint32" => Ok(ValueType::UInt32), - "uint64" => Ok(ValueType::UInt64), - "int8" => Ok(ValueType::Int8), - "int16" => Ok(ValueType::Int16), - "int32" => Ok(ValueType::Int32), - "int64" => Ok(ValueType::Int64), - "float32" => Ok(ValueType::Float32), - "float64" => Ok(ValueType::Float64), - "bool" => Ok(ValueType::Bool), - s if s.starts_with("enum") => { - // This case handles enum types that might come from other sources - // We provide a default value to prevent empty range panic - Ok(ValueType::Enum(vec!["Unknown".to_string()])) - } - _ => Ok(ValueType::UInt32), // Default - } -} - -fn parse_range(range_val: &serde_json::Value) -> Option<[Option; 2]> { - if let Some(arr) = range_val.as_array() { - if arr.len() >= 2 { - return Some([ - arr[0].as_f64(), - arr[1].as_f64(), - ]); - } - } - None -} \ No newline at end of file diff --git a/packet-sender/src/adj/packet.rs b/packet-sender/src/adj/packet.rs deleted file mode 100644 index 7d53b83e9..000000000 --- a/packet-sender/src/adj/packet.rs +++ /dev/null @@ -1,159 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum PacketType { - Data, - Protection, - Order, - Info, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Packet { - pub id: u16, - pub name: String, - #[serde(rename = "type")] - pub packet_type: PacketType, - pub variables: Vec, - #[serde(default)] - pub variables_ids: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Variable { - pub id: u16, - pub name: String, - #[serde(rename = "type")] - pub value_type: ValueType, - #[serde(default)] - pub units: Option, - #[serde(default)] - pub safe_range: Option<[Option; 2]>, - #[serde(default)] - pub warning_range: Option<[Option; 2]>, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum ValueType { - UInt8, - UInt16, - UInt32, - UInt64, - Int8, - Int16, - Int32, - Int64, - Float32, - Float64, - Bool, - Enum(Vec), - String, -} - -impl Serialize for ValueType { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - ValueType::UInt8 => serializer.serialize_str("uint8"), - ValueType::UInt16 => serializer.serialize_str("uint16"), - ValueType::UInt32 => serializer.serialize_str("uint32"), - ValueType::UInt64 => serializer.serialize_str("uint64"), - ValueType::Int8 => serializer.serialize_str("int8"), - ValueType::Int16 => serializer.serialize_str("int16"), - ValueType::Int32 => serializer.serialize_str("int32"), - ValueType::Int64 => serializer.serialize_str("int64"), - ValueType::Float32 => serializer.serialize_str("float32"), - ValueType::Float64 => serializer.serialize_str("float64"), - ValueType::Bool => serializer.serialize_str("bool"), - ValueType::String => serializer.serialize_str("string"), - ValueType::Enum(values) => { - let enum_str = format!("enum({})", values.join(",")); - serializer.serialize_str(&enum_str) - } - } - } -} - -impl<'de> Deserialize<'de> for ValueType { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - match s.as_str() { - "uint8" => Ok(ValueType::UInt8), - "uint16" => Ok(ValueType::UInt16), - "uint32" => Ok(ValueType::UInt32), - "uint64" => Ok(ValueType::UInt64), - "int8" => Ok(ValueType::Int8), - "int16" => Ok(ValueType::Int16), - "int32" => Ok(ValueType::Int32), - "int64" => Ok(ValueType::Int64), - "float32" => Ok(ValueType::Float32), - "float64" => Ok(ValueType::Float64), - "bool" => Ok(ValueType::Bool), - "string" => Ok(ValueType::String), - s if s.starts_with("enum(") && s.ends_with(")") => { - let content = &s[5..s.len()-1]; - let values: Vec = content - .split(',') - .map(|v| v.trim().to_string()) - .collect(); - Ok(ValueType::Enum(values)) - } - _ => Err(serde::de::Error::custom(format!("Unknown value type: {}", s))), - } - } -} - -impl ValueType { - pub fn size_bytes(&self) -> usize { - match self { - ValueType::UInt8 | ValueType::Int8 | ValueType::Bool => 1, - ValueType::UInt16 | ValueType::Int16 => 2, - ValueType::UInt32 | ValueType::Int32 | ValueType::Float32 => 4, - ValueType::UInt64 | ValueType::Int64 | ValueType::Float64 => 8, - ValueType::Enum(_) => 1, // Enums are typically stored as uint8 - ValueType::String => 0, // Variable size - } - } - - pub fn max_value(&self) -> f64 { - match self { - ValueType::UInt8 => u8::MAX as f64, - ValueType::UInt16 => u16::MAX as f64, - ValueType::UInt32 => u32::MAX as f64, - ValueType::UInt64 => u64::MAX as f64, - ValueType::Int8 => i8::MAX as f64, - ValueType::Int16 => i16::MAX as f64, - ValueType::Int32 => i32::MAX as f64, - ValueType::Int64 => i64::MAX as f64, - ValueType::Float32 => f32::MAX as f64, - ValueType::Float64 => f64::MAX, - ValueType::Bool => 1.0, - ValueType::Enum(values) => values.len() as f64 - 1.0, - ValueType::String => 0.0, - } - } - - pub fn min_value(&self) -> f64 { - match self { - ValueType::UInt8 => 0.0, - ValueType::UInt16 => 0.0, - ValueType::UInt32 => 0.0, - ValueType::UInt64 => 0.0, - ValueType::Int8 => i8::MIN as f64, - ValueType::Int16 => i16::MIN as f64, - ValueType::Int32 => i32::MIN as f64, - ValueType::Int64 => i64::MIN as f64, - ValueType::Float32 => f32::MIN as f64, - ValueType::Float64 => f64::MIN, - ValueType::Bool => 0.0, - ValueType::Enum(_) => 0.0, - ValueType::String => 0.0, - } - } -} \ No newline at end of file diff --git a/packet-sender/src/cli/manual.rs b/packet-sender/src/cli/manual.rs deleted file mode 100644 index 8148476e1..000000000 --- a/packet-sender/src/cli/manual.rs +++ /dev/null @@ -1,203 +0,0 @@ -use anyhow::Result; -use std::io::{self, Write}; -use crate::adj::{Board, Packet, Variable, ValueType}; -use crate::generator::{encode_packet_header, encode_value}; - -pub struct ManualPacketBuilder; - -impl ManualPacketBuilder { - pub fn build_packet_interactive(board: &Board) -> Result> { - // First, let user select a packet - let packet = Self::select_packet(board)?; - - println!("\nSelected packet: {} (ID: {})", packet.name, packet.id); - println!("This packet has {} variables", packet.variables.len()); - - if packet.variables.is_empty() { - // Empty packet, just return header - return Ok(encode_packet_header(packet.id)); - } - - // Ask if user wants random or custom values - let use_custom = Self::ask_random_or_custom()?; - - let mut packet_data = encode_packet_header(packet.id); - - if use_custom { - // Get custom values for each variable - for var in &packet.variables { - let value = Self::get_custom_value(var)?; - let encoded = encode_value(value, &var.value_type)?; - packet_data.extend(encoded); - } - } else { - // Generate random values - use crate::generator::RandomValueGenerator; - let generator = RandomValueGenerator::new(); - - for var in &packet.variables { - let value = generator.generate_for_variable(var)?; - let encoded = encode_value(value, &var.value_type)?; - packet_data.extend(encoded); - } - } - - Ok(packet_data) - } - - fn select_packet(board: &Board) -> Result<&Packet> { - println!("\nAvailable packets for board {}:", board.name); - - let mut all_packets = vec![]; - - // Group by type - let data_packets = board.get_data_packets(); - let protection_packets = board.get_protection_packets(); - let order_packets = board.get_order_packets(); - let info_packets = board.get_info_packets(); - - if !data_packets.is_empty() { - println!("\nData packets:"); - for packet in data_packets { - let idx = all_packets.len(); - all_packets.push(packet); - println!(" [{}] {} (ID: {}) - {} variables", - idx + 1, packet.name, packet.id, packet.variables.len()); - } - } - - if !protection_packets.is_empty() { - println!("\nProtection packets:"); - for packet in protection_packets { - let idx = all_packets.len(); - all_packets.push(packet); - println!(" [{}] {} (ID: {}) - {} variables", - idx + 1, packet.name, packet.id, packet.variables.len()); - } - } - - if !order_packets.is_empty() { - println!("\nOrder packets:"); - for packet in order_packets { - let idx = all_packets.len(); - all_packets.push(packet); - println!(" [{}] {} (ID: {}) - {} variables", - idx + 1, packet.name, packet.id, packet.variables.len()); - } - } - - if !info_packets.is_empty() { - println!("\nInfo packets:"); - for packet in info_packets { - let idx = all_packets.len(); - all_packets.push(packet); - println!(" [{}] {} (ID: {}) - {} variables", - idx + 1, packet.name, packet.id, packet.variables.len()); - } - } - - loop { - print!("\nSelect packet number (1-{}): ", all_packets.len()); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - if let Ok(num) = input.trim().parse::() { - if num > 0 && num <= all_packets.len() { - return Ok(all_packets[num - 1]); - } - } - - println!("Invalid selection. Please enter a number between 1 and {}", all_packets.len()); - } - } - - fn ask_random_or_custom() -> Result { - loop { - print!("\nGenerate (r)andom or (c)ustom values? "); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - match input.trim().to_lowercase().as_str() { - "r" | "random" => return Ok(false), - "c" | "custom" => return Ok(true), - _ => println!("Please enter 'r' for random or 'c' for custom"), - } - } - } - - fn get_custom_value(var: &Variable) -> Result { - println!("\nVariable: {} (ID: {})", var.name, var.id); - println!("Type: {:?}", var.value_type); - - // Show units if available - if let Some(ref units) = var.units { - println!("Units: {}", units); - } - - // Show safe/warning ranges if available - if let Some(range) = &var.safe_range { - if let (Some(min), Some(max)) = (&range[0], &range[1]) { - println!("Safe range: {} to {}", min, max); - } - } - if let Some(range) = &var.warning_range { - if let (Some(min), Some(max)) = (&range[0], &range[1]) { - println!("Warning range: {} to {}", min, max); - } - } - - // Show type-specific info - match &var.value_type { - ValueType::UInt8 => println!("Valid range: 0 to 255"), - ValueType::UInt16 => println!("Valid range: 0 to 65535"), - ValueType::UInt32 => println!("Valid range: 0 to 4294967295"), - ValueType::UInt64 => println!("Valid range: 0 to 18446744073709551615"), - ValueType::Int8 => println!("Valid range: -128 to 127"), - ValueType::Int16 => println!("Valid range: -32768 to 32767"), - ValueType::Int32 => println!("Valid range: -2147483648 to 2147483647"), - ValueType::Int64 => println!("Valid range: -9223372036854775808 to 9223372036854775807"), - ValueType::Float32 => println!("32-bit floating point"), - ValueType::Float64 => println!("64-bit floating point"), - ValueType::Bool => println!("Boolean: enter 0 for false, 1 for true"), - ValueType::Enum(values) => { - println!("Enum values:"); - for (i, val) in values.iter().enumerate() { - println!(" {} = {}", i, val); - } - } - ValueType::String => println!("String type not supported for manual input"), - } - - loop { - print!("Enter value: "); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - if let Ok(value) = input.trim().parse::() { - // Validate value based on type - match &var.value_type { - ValueType::UInt8 if value >= 0.0 && value <= 255.0 => return Ok(value), - ValueType::UInt16 if value >= 0.0 && value <= 65535.0 => return Ok(value), - ValueType::UInt32 if value >= 0.0 && value <= 4294967295.0 => return Ok(value), - ValueType::UInt64 if value >= 0.0 && value <= u64::MAX as f64 => return Ok(value), - ValueType::Int8 if value >= -128.0 && value <= 127.0 => return Ok(value), - ValueType::Int16 if value >= -32768.0 && value <= 32767.0 => return Ok(value), - ValueType::Int32 if value >= i32::MIN as f64 && value <= i32::MAX as f64 => return Ok(value), - ValueType::Int64 if value >= i64::MIN as f64 && value <= i64::MAX as f64 => return Ok(value), - ValueType::Float32 | ValueType::Float64 => return Ok(value), - ValueType::Bool if value == 0.0 || value == 1.0 => return Ok(value), - ValueType::Enum(values) if value >= 0.0 && (value as usize) < values.len() => return Ok(value), - _ => println!("Value out of range for type {:?}", var.value_type), - } - } else { - println!("Invalid number format"); - } - } - } -} \ No newline at end of file diff --git a/packet-sender/src/cli/mod.rs b/packet-sender/src/cli/mod.rs deleted file mode 100644 index 87d27c39b..000000000 --- a/packet-sender/src/cli/mod.rs +++ /dev/null @@ -1,178 +0,0 @@ -use anyhow::Result; -use std::io::{self, Write}; -use tokio::task; - -use crate::network::PacketSender; - -mod manual; -use manual::ManualPacketBuilder; - -pub struct InteractiveMode { - sender: PacketSender, -} - -impl InteractiveMode { - pub fn new(sender: PacketSender) -> Self { - Self { sender } - } - - pub async fn run(self) -> Result<()> { - println!("Hyperloop Packet Sender - Interactive Mode"); - println!("Type 'help' for available commands"); - - loop { - print!("> "); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - let parts: Vec<&str> = input.trim().split_whitespace().collect(); - if parts.is_empty() { - continue; - } - - match parts[0] { - "help" | "h" => self.show_help(), - "list" | "l" => self.list_boards(), - "board" | "b" => { - if parts.len() >= 2 { - self.show_board_info(parts[1]); - } else { - println!("Usage: board "); - } - } - "manual" | "m" => { - if parts.len() >= 2 { - self.send_manual_packet(parts[1]).await?; - } else { - println!("Usage: manual "); - } - } - "random" | "r" => { - if parts.len() == 1 { - // No board specified - random from all boards - let rate = 100; - let mut sender = self.sender.clone(); - - println!("Starting random generation from all boards at {} pps (Ctrl+C to stop)", rate); - - task::spawn(async move { - let _ = sender.start_random_all(rate).await; - }); - } else { - // Check if first argument is a number (rate) or board name - if let Ok(rate) = parts[1].parse::() { - // First argument is rate - random from all boards - let mut sender = self.sender.clone(); - - println!("Starting random generation from all boards at {} pps (Ctrl+C to stop)", rate); - - task::spawn(async move { - let _ = sender.start_random_all(rate).await; - }); - } else { - // First argument is board name - let rate = parts.get(2) - .and_then(|s| s.parse().ok()) - .unwrap_or(100); - - let mut sender = self.sender.clone(); - let board = parts[1].to_string(); - - println!("Starting random generation for {} at {} pps (Ctrl+C to stop)", board, rate); - - task::spawn(async move { - let _ = sender.start_random_single(&board, rate).await; - }); - } - } - } - "simulate" | "sim" | "s" => { - if parts.len() >= 3 { - let mut sender = self.sender.clone(); - let board = parts[1].to_string(); - let mode = parts[2].to_string(); - - println!("Starting {} simulation for {} (Ctrl+C to stop)", mode, board); - - task::spawn(async move { - let _ = sender.simulate_board(&board, &mode).await; - }); - } else { - println!("Usage: simulate "); - println!("Modes: random, sine, sequence"); - } - } - "quit" | "q" | "exit" => { - println!("Goodbye!"); - break; - } - _ => { - println!("Unknown command: {}", parts[0]); - println!("Type 'help' for available commands"); - } - } - } - - Ok(()) - } - - fn show_help(&self) { - println!("Available commands:"); - println!(" help, h - Show this help message"); - println!(" list, l - List all boards"); - println!(" board, b - Show board information"); - println!(" manual, m - Manually build and send a packet"); - println!(" random, r [rate] - Start random packet generation"); - println!(" simulate, sim, s - Start simulation (modes: random, sine, sequence)"); - println!(" quit, q, exit - Exit the program"); - } - - fn list_boards(&self) { - println!("Available boards:"); - for board in &self.sender.adj.boards { - println!(" {} - {} ({} packets)", board.name, board.ip, board.packets.len()); - } - } - - fn show_board_info(&self, board_name: &str) { - if let Some(board) = self.sender.adj.boards.iter().find(|b| b.name == board_name) { - println!("Board: {} (ID: {}, IP: {})", board.name, board.id, board.ip); - println!("Packets ({}):", board.packets.len()); - - println!("\nData packets:"); - for packet in board.get_data_packets() { - println!(" [{}] {} ({} variables)", packet.id, packet.name, packet.variables.len()); - } - - println!("\nProtection packets:"); - for packet in board.get_protection_packets() { - println!(" [{}] {} ({} variables)", packet.id, packet.name, packet.variables.len()); - } - } else { - println!("Board '{}' not found", board_name); - } - } - - async fn send_manual_packet(&self, board_name: &str) -> Result<()> { - // Find the board - let board = self.sender.adj.boards.iter() - .find(|b| b.name == board_name) - .ok_or_else(|| anyhow::anyhow!("Board '{}' not found", board_name))?; - - // Build packet interactively - match ManualPacketBuilder::build_packet_interactive(board) { - Ok(packet_data) => { - // Send the packet - match self.sender.send_raw_packet(board_name, packet_data).await { - Ok(_) => println!("\nPacket sent successfully!"), - Err(e) => println!("\nError sending packet: {}", e), - } - } - Err(e) => println!("\nError building packet: {}", e), - } - - Ok(()) - } -} \ No newline at end of file diff --git a/packet-sender/src/generator/data.rs b/packet-sender/src/generator/data.rs deleted file mode 100644 index 3614e11fa..000000000 --- a/packet-sender/src/generator/data.rs +++ /dev/null @@ -1,113 +0,0 @@ -use super::{encode_packet_header, encode_value, RandomValueGenerator}; -use crate::adj::{Board, Packet, ValueType, Variable}; -use anyhow::Result; - -#[derive(Clone)] -pub struct DataPacketGenerator { - board: Board, - random_gen: RandomValueGenerator, -} - -impl DataPacketGenerator { - pub fn new(board: Board) -> Self { - Self { - board, - random_gen: RandomValueGenerator::new(), - } - } - - pub fn generate(&self, packet: &Packet) -> Result> { - let mut buffer = encode_packet_header(packet.id); - - // Handle packets with no variables (like FAULT packet with ID 0) - if packet.variables.is_empty() { - return Ok(buffer); - } - - for variable in &packet.variables { - let value = self.random_gen.generate_for_variable(variable)?; - let encoded = encode_value(value, &variable.value_type)?; - buffer.extend_from_slice(&encoded); - } - - Ok(buffer) - } - - pub fn generate_sine_packet(&self, packet: &Packet, time: f64) -> Result> { - let mut buffer = encode_packet_header(packet.id); - - // Handle packets with no variables - if packet.variables.is_empty() { - return Ok(buffer); - } - - for (i, variable) in packet.variables.iter().enumerate() { - let value = self.generate_sine_value(variable, time, i as f64)?; - let encoded = encode_value(value, &variable.value_type)?; - buffer.extend_from_slice(&encoded); - } - - Ok(buffer) - } - - fn generate_sine_value(&self, variable: &Variable, time: f64, offset: f64) -> Result { - let frequency = 0.1 + offset * 0.05; // Different frequencies for each variable - let phase = offset * std::f64::consts::PI / 4.0; - let sine = (time * frequency * 2.0 * std::f64::consts::PI + phase).sin(); - - // Map sine wave (-1 to 1) to variable range - let normalized = (sine + 1.0) / 2.0; // 0 to 1 - - // Try warning range first - if let Some(warning_range) = &variable.warning_range { - if let (Some(min), Some(max)) = (warning_range[0], warning_range[1]) { - if min < max { - let type_min = variable.value_type.min_value(); - let type_max = variable.value_type.max_value(); - let clamped_min = min.max(type_min); - let clamped_max = max.min(type_max); - return Ok(clamped_min + normalized * (clamped_max - clamped_min)); - } - } - } - - // Try safe range - if let Some(safe_range) = &variable.safe_range { - if let (Some(min), Some(max)) = (safe_range[0], safe_range[1]) { - if min < max { - let type_min = variable.value_type.min_value(); - let type_max = variable.value_type.max_value(); - let clamped_min = min.max(type_min); - let clamped_max = max.min(type_max); - return Ok(clamped_min + normalized * (clamped_max - clamped_min)); - } - } - } - - // Fallback to reasonable type-based range - match &variable.value_type { - ValueType::Bool => Ok(if normalized > 0.5 { 1.0 } else { 0.0 }), - ValueType::Enum(variants) => { - let max_idx = if variants.is_empty() { - 0.0 - } else { - (variants.len() - 1) as f64 - }; - Ok((normalized * max_idx).round()) - } - ValueType::UInt8 | ValueType::UInt16 | ValueType::UInt32 => { - let max = variable.value_type.max_value().min(1000.0); - Ok(normalized * max) - } - ValueType::Int8 | ValueType::Int16 | ValueType::Int32 => { - // Map to -500 to 500 range - Ok(-500.0 + normalized * 1000.0) - } - ValueType::Float32 | ValueType::Float64 => { - // Map to -100 to 100 range for floats - Ok(-100.0 + normalized * 200.0) - } - _ => Ok(normalized * 10000.0), - } - } -} diff --git a/packet-sender/src/generator/mod.rs b/packet-sender/src/generator/mod.rs deleted file mode 100644 index 0bc37733b..000000000 --- a/packet-sender/src/generator/mod.rs +++ /dev/null @@ -1,113 +0,0 @@ -use anyhow::Result; -use byteorder::{LittleEndian, WriteBytesExt}; -use rand::Rng; -use std::io::Cursor; - -mod data; -mod protection; -mod random; - -pub use data::DataPacketGenerator; -pub use protection::ProtectionPacketGenerator; -pub use random::RandomValueGenerator; - -use crate::adj::{Board, Packet, PacketType, ValueType}; - -#[derive(Clone)] -pub struct PacketGenerator { - board: Board, - data_generator: DataPacketGenerator, - protection_generator: ProtectionPacketGenerator, -} - -impl PacketGenerator { - pub fn new(board: Board) -> Self { - Self { - board: board.clone(), - data_generator: DataPacketGenerator::new(board.clone()), - protection_generator: ProtectionPacketGenerator::new(board.clone()), - } - } - - pub fn generate_random_data_packet(&self) -> Result> { - let data_packets = self.board.get_data_packets(); - if data_packets.is_empty() { - return Err(anyhow::anyhow!( - "No data packets available for board {}", - self.board.name - )); - } - - // For now, just pick any data packet - we handle empty packets in the generator - let packet = data_packets[rand::thread_rng().gen_range(0..data_packets.len())]; - self.generate_packet(packet) - } - - pub fn generate_random_protection_packet(&self) -> Result> { - let protection_packets = self.board.get_protection_packets(); - if protection_packets.is_empty() { - return Err(anyhow::anyhow!("No protection packets available")); - } - - let packet = protection_packets[rand::thread_rng().gen_range(0..protection_packets.len())]; - self.generate_packet(packet) - } - - pub fn generate_packet_with_id(&self, packet_id: u16) -> Result> { - let packet = self - .board - .find_packet_by_id(packet_id) - .ok_or_else(|| anyhow::anyhow!("Packet with ID {} not found", packet_id))?; - - self.generate_packet(packet) - } - - pub fn generate_sine_data_packet(&self, time: f64) -> Result> { - let data_packets = self.board.get_data_packets(); - if data_packets.is_empty() { - return Err(anyhow::anyhow!("No data packets available")); - } - - let packet = data_packets[rand::thread_rng().gen_range(0..data_packets.len())]; - self.data_generator.generate_sine_packet(packet, time) - } - - fn generate_packet(&self, packet: &Packet) -> Result> { - match packet.packet_type { - PacketType::Data => self.data_generator.generate(packet), - PacketType::Protection => self.protection_generator.generate(packet), - _ => Err(anyhow::anyhow!( - "Unsupported packet type: {:?}", - packet.packet_type - )), - } - } -} - -pub fn encode_packet_header(packet_id: u16) -> Vec { - let mut buffer = vec![]; - buffer.write_u16::(packet_id).unwrap(); - buffer -} - -pub fn encode_value(value: f64, value_type: &ValueType) -> Result> { - let mut cursor = Cursor::new(Vec::new()); - - match value_type { - ValueType::UInt8 => cursor.write_u8(value as u8)?, - ValueType::UInt16 => cursor.write_u16::(value as u16)?, - ValueType::UInt32 => cursor.write_u32::(value as u32)?, - ValueType::UInt64 => cursor.write_u64::(value as u64)?, - ValueType::Int8 => cursor.write_i8(value as i8)?, - ValueType::Int16 => cursor.write_i16::(value as i16)?, - ValueType::Int32 => cursor.write_i32::(value as i32)?, - ValueType::Int64 => cursor.write_i64::(value as i64)?, - ValueType::Float32 => cursor.write_f32::(value as f32)?, - ValueType::Float64 => cursor.write_f64::(value)?, - ValueType::Bool => cursor.write_u8(if value > 0.5 { 1 } else { 0 })?, - ValueType::Enum(_) => cursor.write_u8(value as u8)?, - ValueType::String => return Err(anyhow::anyhow!("String encoding not supported")), - } - - Ok(cursor.into_inner()) -} diff --git a/packet-sender/src/generator/protection.rs b/packet-sender/src/generator/protection.rs deleted file mode 100644 index 2df32bbb6..000000000 --- a/packet-sender/src/generator/protection.rs +++ /dev/null @@ -1,28 +0,0 @@ -use super::encode_packet_header; -use crate::adj::{Board, Packet}; -use anyhow::Result; - -#[derive(Clone)] -pub struct ProtectionPacketGenerator { - board: Board, -} - -impl ProtectionPacketGenerator { - pub fn new(board: Board) -> Self { - Self { board } - } - - pub fn generate(&self, packet: &Packet) -> Result> { - // Protection packets typically contain status or fault information - // For now, we'll generate a simple protection packet with header only - let buffer = encode_packet_header(packet.id); - - // TODO: Add protection-specific data based on packet definition - // This might include: - // - Fault codes - // - Status flags - // - Protection state - - Ok(buffer) - } -} diff --git a/packet-sender/src/generator/random.rs b/packet-sender/src/generator/random.rs deleted file mode 100644 index 8a668598a..000000000 --- a/packet-sender/src/generator/random.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::adj::{ValueType, Variable}; -use anyhow::Result; -use rand::Rng; - -#[derive(Clone)] -pub struct RandomValueGenerator; - -impl RandomValueGenerator { - pub fn new() -> Self { - Self - } - - pub fn generate_for_variable(&self, variable: &Variable) -> Result { - let mut rng = rand::thread_rng(); - - // Check if we have warning ranges - if let Some(warning_range) = &variable.warning_range { - if let (Some(min), Some(max)) = (warning_range[0], warning_range[1]) { - // Ensure the range is valid - if min < max { - // Clamp to type bounds - let type_min = variable.value_type.min_value(); - let type_max = variable.value_type.max_value(); - let clamped_min = min.max(type_min); - let clamped_max = max.min(type_max); - - // 90% chance to be within warning range - if rng.gen_bool(0.9) { - return Ok(rng.gen_range(clamped_min..=clamped_max)); - } else { - // 10% chance to be slightly outside (but still within type bounds) - let range = clamped_max - clamped_min; - let extended_min = (clamped_min - range * 0.1).max(type_min); - let extended_max = (clamped_max + range * 0.1).min(type_max); - return Ok(rng.gen_range(extended_min..=extended_max)); - } - } - } - } - - // Check safe range - if let Some(safe_range) = &variable.safe_range { - if let (Some(min), Some(max)) = (safe_range[0], safe_range[1]) { - if min < max { - let type_min = variable.value_type.min_value(); - let type_max = variable.value_type.max_value(); - let clamped_min = min.max(type_min); - let clamped_max = max.min(type_max); - return Ok(rng.gen_range(clamped_min..=clamped_max)); - } - } - } - - // Fallback to type-based generation with reasonable defaults - self.generate_for_type(&variable.value_type) - } - - pub fn generate_for_type(&self, value_type: &ValueType) -> Result { - let mut rng = rand::thread_rng(); - let value = match value_type { - ValueType::Bool => { - if rng.gen_bool(0.5) { - 1.0 - } else { - 0.0 - } - } - ValueType::Enum(variants) => { - if variants.is_empty() { - 0.0 - } else { - rng.gen_range(0..variants.len()) as f64 - } - } - _ => { - // For numeric types, generate reasonable values - let min = value_type.min_value(); - let max = value_type.max_value(); - - // For integer types, generate values in a reasonable range - match value_type { - ValueType::UInt8 | ValueType::UInt16 | ValueType::UInt32 => { - // Generate values between 0 and 1000 or type max, whichever is smaller - let reasonable_max = max.min(1000.0); - rng.gen_range(0.0..=reasonable_max) - } - ValueType::Int8 | ValueType::Int16 | ValueType::Int32 => { - // Generate values between -500 and 500 or type bounds - let reasonable_min = min.max(-500.0); - let reasonable_max = max.min(500.0); - rng.gen_range(reasonable_min..=reasonable_max) - } - ValueType::Float32 | ValueType::Float64 => { - // Generate reasonable float values between -1000 and 1000 - rng.gen_range(-1000.0..=1000.0) - } - _ => { - // For large integer types, use a smaller range - rng.gen_range(0.0..=10000.0) - } - } - } - }; - - Ok(value) - } - - pub fn generate_fault_value(&self) -> f64 { - // Generate values that are likely to trigger faults - if rand::thread_rng().gen_bool(0.5) { - // Very high value - f64::MAX * 0.9 - } else { - // Very low value (potentially negative for signed types) - -f64::MAX * 0.9 - } - } -} diff --git a/packet-sender/src/logger.rs b/packet-sender/src/logger.rs deleted file mode 100644 index f50863b99..000000000 --- a/packet-sender/src/logger.rs +++ /dev/null @@ -1,27 +0,0 @@ -#[cfg(windows)] -use enable_ansi_support; - -use tracing_subscriber::fmt::format::Writer; -use tracing_subscriber::fmt::time::FormatTime; - -struct CompactTimer; - -// Format time as HH:MM:SS.mmm or 14:59:59.999 -impl FormatTime for CompactTimer { - fn format_time(&self, w: &mut Writer<'_>) -> Result<(), std::fmt::Error> { - let now = chrono::Local::now(); - write!(w, "{}", now.format("%H:%M:%S%.3f")) - } -} - -pub fn init(log_level: &str) { - // Enable ANSI support on Windows before initializing logger - #[cfg(windows)] - let _ = enable_ansi_support::enable_ansi_support(); - - // Initialize logging - tracing_subscriber::fmt() - .with_env_filter(log_level) - .with_timer(CompactTimer) - .init(); -} diff --git a/packet-sender/src/main.rs b/packet-sender/src/main.rs deleted file mode 100644 index cfb4306c7..000000000 --- a/packet-sender/src/main.rs +++ /dev/null @@ -1,178 +0,0 @@ -use anyhow::Result; -use clap::{Parser, Subcommand}; -use dirs; -use std::path::PathBuf; -use tracing::info; - -mod adj; -mod cli; -mod generator; -mod logger; -mod network; -mod test_listener; - -use crate::cli::InteractiveMode; -use crate::network::PacketSender; - -fn get_default_adj_path() -> PathBuf { - dirs::cache_dir() - .expect("Failed to get cache directory") - .join("hyperloop-control-station") - .join("adj") -} - -#[derive(Parser)] -#[command(name = "packet-sender")] -#[command(about = "Hyperloop packet sender for testing backend and frontend", long_about = None)] -struct Cli { - // Path to AD JSON directory - #[arg(short, long)] - adj_path: Option, - - /// Log level (trace, debug, info, warn, error) - #[arg(short, long, default_value = "info")] - log_level: String, - - /// Backend address (overrides ADJ if specified) - #[arg(short, long)] - backend_address: Option, - - /// Backend UDP port (overrides ADJ if specified) - #[arg(short = 'p', long)] - backend_port: Option, - - /// Use dev mode (UDP server) instead of packet sniffer - #[arg(short = 'd', long, default_value_t = false)] - dev: bool, - - #[command(subcommand)] - command: Option, -} - -#[derive(Subcommand)] -enum Commands { - /// Run in interactive mode - Interactive, - - /// Send random packets continuously - Random { - /// Packets per second - #[arg(short, long, default_value_t = 100)] - rate: u32, - - /// Specific board to simulate (if not specified, all boards) - #[arg(short, long)] - board: Option, - }, - - /// Send packets from a specific board - Board { - /// Board name - name: String, - - /// Packet sending mode - #[arg(short, long, default_value = "random")] - mode: String, - }, - - /// List available boards and packets - List, - - /// Start a test UDP listener (for testing without backend) - Listen { - /// Address to listen on - #[arg(short, long, default_value = "127.0.0.9:8000")] - address: String, - }, -} - -#[tokio::main] -async fn main() -> Result<()> { - let cli = Cli::parse(); - - logger::init(&cli.log_level); - - let adj_path = cli.adj_path.unwrap_or_else(get_default_adj_path); - - info!("Starting Hyperloop packet sender"); - info!("Loading ADJ from: {:?}", adj_path); - - // Load ADJ - let adj = adj::load_adj(&adj_path).await?; - info!("Loaded {} boards from ADJ", adj.boards.len()); - - // Get backend address and port from ADJ or CLI args - let backend_address = cli.backend_address.unwrap_or_else(|| { - adj.info - .addresses - .get("backend") - .cloned() - .unwrap_or_else(|| "127.0.0.9".to_string()) - }); - - let backend_port = cli - .backend_port - .unwrap_or_else(|| adj.info.ports.get("UDP").copied().unwrap_or(8000)); - - info!("Backend address: {}:{}", backend_address, backend_port); - info!("Dev mode: {}", cli.dev); - - // Create packet sender - let mut sender = - PacketSender::new(&backend_address, backend_port, adj.clone(), cli.dev).await?; - - match cli.command { - None | Some(Commands::Interactive) => { - info!("Starting interactive mode"); - let interactive = InteractiveMode::new(sender); - interactive.run().await?; - } - - Some(Commands::Random { rate, board }) => { - info!( - "Starting random packet generation at {} packets/second", - rate - ); - if let Some(board_name) = board { - sender.start_random_single(&board_name, rate).await?; - } else { - sender.start_random_all(rate).await?; - } - } - - Some(Commands::Board { name, mode }) => { - info!("Simulating board {} in {} mode", name, mode); - sender.simulate_board(&name, &mode).await?; - } - - Some(Commands::List) => { - println!("Available boards:"); - for board in &adj.boards { - println!( - " - {} (IP: {}, {} packets)", - board.name, - board.ip, - board.packets.len() - ); - } - } - - Some(Commands::Listen { address }) => { - println!("Starting test listener on {}", address); - println!("Press Ctrl+C to stop"); - - // Run listener in a blocking thread - let _handle = std::thread::spawn(move || { - if let Err(e) = test_listener::start_test_listener(&address) { - eprintln!("Listener error: {}", e); - } - }); - - // Wait for Ctrl+C - tokio::signal::ctrl_c().await?; - println!("\nShutting down listener..."); - std::process::exit(0); - } - } - Ok(()) -} diff --git a/packet-sender/src/network/macos.rs b/packet-sender/src/network/macos.rs deleted file mode 100644 index e4e3d97e7..000000000 --- a/packet-sender/src/network/macos.rs +++ /dev/null @@ -1,90 +0,0 @@ -use anyhow::{Context, Result}; -#[cfg(unix)] -use std::os::unix::io::AsRawFd; -use tokio::net::UdpSocket; - -/// Configure socket with macOS-specific options for localhost networking -pub fn configure_socket(socket: &UdpSocket) -> Result<()> { - #[cfg(unix)] - { - let fd = socket.as_raw_fd(); - - // Set SO_REUSEADDR to allow multiple binds to same address - set_socket_option(fd, libc::SOL_SOCKET, libc::SO_REUSEADDR, 1) - .context("Failed to set SO_REUSEADDR")?; - - // Set SO_REUSEPORT for macOS (allows multiple sockets on same port) - #[cfg(target_os = "macos")] - set_socket_option(fd, libc::SOL_SOCKET, libc::SO_REUSEPORT, 1) - .context("Failed to set SO_REUSEPORT")?; - - // Disable SIGPIPE on macOS to prevent crashes - #[cfg(target_os = "macos")] - set_socket_option(fd, libc::SOL_SOCKET, libc::SO_NOSIGPIPE, 1) - .context("Failed to set SO_NOSIGPIPE")?; - - // Set receive buffer size for better performance - set_socket_option(fd, libc::SOL_SOCKET, libc::SO_RCVBUF, 1024 * 1024) - .context("Failed to set SO_RCVBUF")?; - - // Set send buffer size - set_socket_option(fd, libc::SOL_SOCKET, libc::SO_SNDBUF, 1024 * 1024) - .context("Failed to set SO_SNDBUF")?; - } - - Ok(()) -} - -#[cfg(unix)] -fn set_socket_option(fd: i32, level: i32, option: i32, value: i32) -> Result<()> { - unsafe { - let value_ptr = &value as *const i32 as *const libc::c_void; - let value_len = std::mem::size_of::() as libc::socklen_t; - - let result = libc::setsockopt(fd, level, option, value_ptr, value_len); - - if result < 0 { - return Err(std::io::Error::last_os_error().into()); - } - } - - Ok(()) -} - -/// Get macOS-specific loopback interface information -pub fn get_loopback_info() -> Result { - Ok(LoopbackInfo { - interface: "lo0".to_string(), - addresses: vec!["127.0.0.1".to_string(), "::1".to_string()], - }) -} - -pub struct LoopbackInfo { - pub interface: String, - pub addresses: Vec, -} - -/// Check if an IP address is a valid localhost address on macOS -pub fn is_localhost(ip: &str) -> bool { - match ip { - "127.0.0.1" | "localhost" | "::1" => true, - addr if addr.starts_with("127.0.0.") || addr.starts_with("127.0.1.") => true, - _ => false, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_localhost_detection() { - assert!(is_localhost("127.0.0.1")); - assert!(is_localhost("127.0.0.6")); - assert!(is_localhost("127.0.1.1")); - assert!(is_localhost("localhost")); - assert!(is_localhost("::1")); - assert!(!is_localhost("192.168.1.1")); - assert!(!is_localhost("10.0.0.1")); - } -} diff --git a/packet-sender/src/network/mod.rs b/packet-sender/src/network/mod.rs deleted file mode 100644 index 4306f5e7e..000000000 --- a/packet-sender/src/network/mod.rs +++ /dev/null @@ -1,195 +0,0 @@ -use anyhow::{Context, Result}; -use rand::Rng; -use std::net::SocketAddr; -use std::sync::Arc; -use tokio::net::UdpSocket; -use tokio::sync::Mutex; -use tokio::time::{interval, Duration}; -use tracing::{debug, error, info, trace}; - -mod macos; -mod sender; - -pub use sender::PacketSender; - -use crate::adj::{Board, ADJ}; -use crate::generator::PacketGenerator; - -pub struct NetworkManager { - backend_addr: SocketAddr, - adj: ADJ, - sockets: Arc>>, - dev_mode: bool, -} - -struct BoardSocket { - board: Board, - socket: Arc, - generator: PacketGenerator, -} - -impl NetworkManager { - pub async fn new( - backend_host: &str, - backend_port: u16, - adj: ADJ, - dev_mode: bool, - ) -> Result { - let backend_addr = format!("{}:{}", backend_host, backend_port) - .parse() - .context("Invalid backend address")?; - - let sockets = Arc::new(Mutex::new(Vec::new())); - - Ok(Self { - backend_addr, - adj, - sockets, - dev_mode, - }) - } - - pub async fn initialize_boards(&mut self) -> Result<()> { - let mut sockets = self.sockets.lock().await; - - for board in &self.adj.boards { - info!( - "Initializing socket for board {} ({})", - board.name, board.ip - ); - - // Create socket with board's IP - let socket = create_board_socket(&board.ip, &self.backend_addr, self.dev_mode).await?; - - let board_socket = BoardSocket { - board: board.clone(), - socket: Arc::new(socket), - generator: PacketGenerator::new(board.clone()), - }; - - sockets.push(board_socket); - } - - info!( - "Initialized {} board sockets (dev_mode: {})", - sockets.len(), - self.dev_mode - ); - Ok(()) - } - - pub async fn send_packet(&self, board_name: &str, packet_data: Vec) -> Result<()> { - let sockets = self.sockets.lock().await; - - let board_socket = sockets - .iter() - .find(|bs| bs.board.name == board_name) - .ok_or_else(|| anyhow::anyhow!("Board {} not found", board_name))?; - - match board_socket.socket.send(&packet_data).await { - Ok(bytes_sent) => { - info!("Sent {} bytes from board {}", bytes_sent, board_name); - Ok(()) - } - Err(e) => { - error!("Failed to send packet from {}: {}", board_name, e); - Err(e.into()) - } - } - } - - pub async fn start_random_generation(&self, rate: u32) -> Result<()> { - let interval_ms = 1000 / rate; - let mut interval = interval(Duration::from_millis(interval_ms as u64)); - - info!("Starting packet generation at {} packets/second", rate); - - loop { - interval.tick().await; - - let sockets = self.sockets.lock().await; - if sockets.is_empty() { - error!("No board sockets available"); - continue; - } - - // Select random board - let idx = { - let mut rng = rand::thread_rng(); - rng.gen_range(0..sockets.len()) - }; - let board_socket = &sockets[idx]; - - // Generate random packet - let packet_data = match board_socket.generator.generate_random_data_packet() { - Ok(data) => data, - Err(e) => { - error!( - "Failed to generate packet for board {}: {}", - board_socket.board.name, e - ); - continue; - } - }; - - // Send packet - match board_socket.socket.send(&packet_data).await { - Ok(bytes_sent) => { - info!( - "Sent {} bytes from board {}", - bytes_sent, board_socket.board.name - ); - } - Err(e) => { - error!( - "Failed to send packet from {}: {}", - board_socket.board.name, e - ); - } - } - } - } -} - -async fn create_board_socket( - board_ip: &str, - backend_addr: &SocketAddr, - dev_mode: bool, -) -> Result { - // In dev mode, we can bind to any available port on the board IP - // In production mode (sniffer), we must bind to the exact board IP - let bind_addr = if dev_mode || macos::is_localhost(board_ip) { - // For localhost addresses in dev mode, use the board IP - format!("{}:0", board_ip) - } else { - // For non-localhost addresses, we need to ensure the packets come from the right IP - // Note: This requires the host to have these IPs configured on network interfaces - format!("{}:0", board_ip) - }; - - debug!( - "Creating socket: {} -> {} (dev_mode: {})", - bind_addr, backend_addr, dev_mode - ); - - let socket = UdpSocket::bind(&bind_addr) - .await - .context(format!("Failed to bind to {}. In production mode, ensure the board IP {} is configured on a network interface", bind_addr, board_ip))?; - - // Apply macOS-specific configurations - macos::configure_socket(&socket)?; - - // Connect to backend - socket - .connect(backend_addr) - .await - .context("Failed to connect to backend")?; - - let local_addr = socket.local_addr()?; - info!( - "Socket created: {} -> {} (dev_mode: {})", - local_addr, backend_addr, dev_mode - ); - - Ok(socket) -} diff --git a/packet-sender/src/network/sender.rs b/packet-sender/src/network/sender.rs deleted file mode 100644 index c7e872124..000000000 --- a/packet-sender/src/network/sender.rs +++ /dev/null @@ -1,179 +0,0 @@ -use anyhow::Result; -use std::sync::Arc; -use tokio::time::{interval, Duration}; -use tokio::sync::RwLock; -use tracing::{info, error, debug}; - -use crate::adj::ADJ; -use crate::generator::PacketGenerator; -use super::NetworkManager; - -#[derive(Clone)] -pub struct PacketSender { - network: Arc, - pub adj: ADJ, - generators: Arc>>, -} - -impl PacketSender { - pub async fn new(backend_host: &str, backend_port: u16, adj: ADJ, dev_mode: bool) -> Result { - let mut network = NetworkManager::new(backend_host, backend_port, adj.clone(), dev_mode).await?; - network.initialize_boards().await?; - - let generators = Arc::new(RwLock::new(Vec::new())); - - // Create generators for each board - { - let mut gens = generators.write().await; - for board in &adj.boards { - gens.push((board.name.clone(), PacketGenerator::new(board.clone()))); - } - } - - Ok(Self { - network: Arc::new(network), - adj, - generators, - }) - } - - pub async fn start_random_all(&mut self, rate: u32) -> Result<()> { - self.network.start_random_generation(rate).await - } - - pub async fn start_random_single(&mut self, board_name: &str, rate: u32) -> Result<()> { - let generators = self.generators.read().await; - let (_, generator) = generators - .iter() - .find(|(name, _)| name == board_name) - .ok_or_else(|| anyhow::anyhow!("Board {} not found", board_name))?; - - let network = self.network.clone(); - let board_name = board_name.to_string(); - let generator = generator.clone(); - - let interval_ms = 1000 / rate; - let mut interval = interval(Duration::from_millis(interval_ms as u64)); - - info!("Starting random generation for {} at {} packets/second", board_name, rate); - - loop { - interval.tick().await; - - if let Ok(packet_data) = generator.generate_random_data_packet() { - if let Err(e) = network.send_packet(&board_name, packet_data).await { - error!("Failed to send packet: {}", e); - } - } - } - } - - pub async fn simulate_board(&mut self, board_name: &str, mode: &str) -> Result<()> { - match mode { - "random" => self.simulate_random(board_name).await, - "sine" => self.simulate_sine(board_name).await, - "sequence" => self.simulate_sequence(board_name).await, - _ => Err(anyhow::anyhow!("Unknown simulation mode: {}", mode)), - } - } - - async fn simulate_random(&self, board_name: &str) -> Result<()> { - let generators = self.generators.read().await; - let (_, generator) = generators - .iter() - .find(|(name, _)| name == board_name) - .ok_or_else(|| anyhow::anyhow!("Board {} not found", board_name))?; - - let network = self.network.clone(); - let board_name = board_name.to_string(); - let generator = generator.clone(); - - let mut interval = interval(Duration::from_millis(10)); - - info!("Starting random simulation for {}", board_name); - - loop { - interval.tick().await; - - if let Ok(packet_data) = generator.generate_random_data_packet() { - if let Err(e) = network.send_packet(&board_name, packet_data).await { - error!("Failed to send packet: {}", e); - } - } - } - } - - async fn simulate_sine(&self, board_name: &str) -> Result<()> { - let generators = self.generators.read().await; - let (_, generator) = generators - .iter() - .find(|(name, _)| name == board_name) - .ok_or_else(|| anyhow::anyhow!("Board {} not found", board_name))?; - - let network = self.network.clone(); - let board_name = board_name.to_string(); - let generator = generator.clone(); - - let mut interval = interval(Duration::from_millis(10)); - let start_time = std::time::Instant::now(); - - info!("Starting sine wave simulation for {}", board_name); - - loop { - interval.tick().await; - - let elapsed = start_time.elapsed().as_secs_f64(); - if let Ok(packet_data) = generator.generate_sine_data_packet(elapsed) { - if let Err(e) = network.send_packet(&board_name, packet_data).await { - error!("Failed to send packet: {}", e); - } - } - } - } - - async fn simulate_sequence(&self, board_name: &str) -> Result<()> { - let board = self.adj.boards - .iter() - .find(|b| b.name == board_name) - .ok_or_else(|| anyhow::anyhow!("Board {} not found", board_name))?; - - let data_packets = board.get_data_packets(); - if data_packets.is_empty() { - return Err(anyhow::anyhow!("No data packets found for board {}", board_name)); - } - - let generators = self.generators.read().await; - let (_, generator) = generators - .iter() - .find(|(name, _)| name == board_name) - .ok_or_else(|| anyhow::anyhow!("Board {} not found", board_name))?; - - let network = self.network.clone(); - let board_name = board_name.to_string(); - let generator = generator.clone(); - - let mut interval = interval(Duration::from_millis(100)); - let mut packet_index = 0; - - info!("Starting sequence simulation for {} ({} packets)", board_name, data_packets.len()); - - loop { - interval.tick().await; - - let packet = data_packets[packet_index]; - if let Ok(packet_data) = generator.generate_packet_with_id(packet.id) { - if let Err(e) = network.send_packet(&board_name, packet_data).await { - error!("Failed to send packet: {}", e); - } else { - debug!("Sent packet {} ({}) from {}", packet.id, packet.name, board_name); - } - } - - packet_index = (packet_index + 1) % data_packets.len(); - } - } - - pub async fn send_raw_packet(&self, board_name: &str, packet_data: Vec) -> Result<()> { - self.network.send_packet(board_name, packet_data).await - } -} \ No newline at end of file diff --git a/packet-sender/src/test_listener.rs b/packet-sender/src/test_listener.rs deleted file mode 100644 index 9c20180d2..000000000 --- a/packet-sender/src/test_listener.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::net::UdpSocket; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -pub fn start_test_listener(addr: &str) -> std::io::Result<()> { - println!("Starting test UDP listener on {}", addr); - - let socket = UdpSocket::bind(addr)?; - println!("Listening for packets..."); - - let mut buf = [0u8; 65536]; - let packet_count = Arc::new(AtomicU64::new(0)); - let start_time = Instant::now(); - - // Print stats periodically - let count_clone = packet_count.clone(); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_secs(5)); - let count = count_clone.load(Ordering::Relaxed); - let elapsed = start_time.elapsed().as_secs_f64(); - let rate = count as f64 / elapsed; - println!("Received {} packets ({:.1} pps)", count, rate); - }); - - loop { - match socket.recv_from(&mut buf) { - Ok((size, src)) => { - packet_count.fetch_add(1, Ordering::Relaxed); - - if size >= 2 { - let packet_id = u16::from_le_bytes([buf[0], buf[1]]); - println!("Packet from {}: ID={}, size={} bytes", src, packet_id, size); - - // Print first few bytes - if size > 2 { - print!(" Data: "); - for i in 2..size.min(18) { - print!("{:02x} ", buf[i]); - } - if size > 18 { - print!("..."); - } - println!(); - } - } - } - Err(e) => { - eprintln!("Error receiving packet: {}", e); - } - } - } -} diff --git a/packet-sender/test_manual.sh b/packet-sender/test_manual.sh deleted file mode 100755 index b47ba7851..000000000 --- a/packet-sender/test_manual.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -echo "Testing manual packet sending feature..." -echo "" -echo "Make sure the backend is running before testing!" -echo "" -echo "To test the manual packet sending:" -echo "1. Run: cargo run -- interactive" -echo "2. Type: list (to see available boards)" -echo "3. Type: manual (e.g., manual VCU)" -echo "4. Follow the prompts to select a packet" -echo "5. Choose random or custom values" -echo "6. If custom, enter values for each variable" -echo "" -echo "Example commands to try:" -echo " list" -echo " board VCU" -echo " manual VCU" \ No newline at end of file diff --git a/packet-sender/test_ranges.py b/packet-sender/test_ranges.py deleted file mode 100755 index 268a64e73..000000000 --- a/packet-sender/test_ranges.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to verify that packet sender respects measurement ranges. -""" - -import subprocess -import json -import socket -import struct -import threading -import time -from collections import defaultdict - -# Test configuration -TEST_DURATION = 5 # seconds -UDP_PORT = 9999 - -# Storage for received values -received_values = defaultdict(list) -stop_flag = threading.Event() - -def parse_packet(data, board_ip): - """Parse a packet and extract values.""" - if len(data) < 2: - return - - packet_id = struct.unpack('>H', data[:2])[0] - offset = 2 - - # For simplicity, just extract float32 values - values = [] - while offset + 4 <= len(data): - try: - value = struct.unpack('>f', data[offset:offset+4])[0] - values.append(value) - offset += 4 - except: - break - - if values: - key = f"{board_ip}_packet_{packet_id}" - received_values[key].extend(values) - -def udp_listener(): - """Listen for UDP packets.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.bind(('127.0.0.1', UDP_PORT)) - sock.settimeout(0.1) - - print(f"Listening on UDP port {UDP_PORT}...") - - while not stop_flag.is_set(): - try: - data, addr = sock.recvfrom(4096) - parse_packet(data, addr[0]) - except socket.timeout: - continue - except Exception as e: - print(f"Error receiving packet: {e}") - - sock.close() - -def main(): - # Start UDP listener - listener_thread = threading.Thread(target=udp_listener) - listener_thread.start() - - # Give listener time to start - time.sleep(0.5) - - # Start packet sender in random mode - print("Starting packet sender...") - process = subprocess.Popen([ - './target/release/packet-sender', - '-b', '127.0.0.1', - '-p', str(UDP_PORT), - '-d', # Dev mode to send directly - 'random', - '--rate', '10' # 10 packets per second - ]) - - # Let it run for a while - print(f"Collecting packets for {TEST_DURATION} seconds...") - time.sleep(TEST_DURATION) - - # Stop packet sender - process.terminate() - process.wait() - - # Stop listener - stop_flag.set() - listener_thread.join() - - # Analyze results - print("\nAnalysis of received values:") - print("-" * 50) - - for key, values in received_values.items(): - if values: - min_val = min(values) - max_val = max(values) - avg_val = sum(values) / len(values) - - print(f"\n{key}:") - print(f" Samples: {len(values)}") - print(f" Min: {min_val:.2f}") - print(f" Max: {max_val:.2f}") - print(f" Avg: {avg_val:.2f}") - - # Check for reasonable ranges - if max_val > 10000: - print(f" ⚠️ Warning: Very high values detected!") - if min_val < -10000: - print(f" ⚠️ Warning: Very low values detected!") - - # Show value distribution - if len(values) > 10: - sorted_vals = sorted(values) - p10 = sorted_vals[len(sorted_vals)//10] - p90 = sorted_vals[9*len(sorted_vals)//10] - print(f" 10th percentile: {p10:.2f}") - print(f" 90th percentile: {p90:.2f}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/packet-sender/testadj.py b/packet-sender/testadj.py new file mode 100644 index 000000000..82802cd34 --- /dev/null +++ b/packet-sender/testadj.py @@ -0,0 +1,121 @@ +import os +import json + + +def validate_json_structure(data): + errors = [] + + if "board_id" in data: + if not isinstance(data["board_id"], int): + errors.append(f"'board_id' debe ser un entero, pero se encontró: {type(data['board_id']).__name__}.") + + if "board_ip" in data: + if not isinstance(data["board_ip"], str): + errors.append(f"'board_ip' debe ser una cadena, pero se encontró: {type(data['board_ip']).__name__}.") + + + if "measurements" in data: + if not isinstance(data["measurements"], list): + errors.append("La clave 'measurements' debe ser una lista.") + else: + for measurement in data["measurements"]: + if isinstance(measurement, dict): + required_keys = ["id", "name", "type", "podUnits", "displayUnits"] + for key in required_keys: + if key not in measurement: + errors.append(f"Falta la clave '{key}' en un objeto de 'measurements'.") + if "id" in measurement and not isinstance(measurement["id"], str): + errors.append(f"El 'id' debe ser una cadena en: {measurement}") + if "name" in measurement and not isinstance(measurement["name"], str): + errors.append(f"El 'name' debe ser una cadena en: {measurement}") + if "type" in measurement and not isinstance(measurement["type"], str): + errors.append(f"El 'type' debe ser una cadena en: {measurement}") + if "safeRange" in measurement and not isinstance(measurement.get("safeRange", []), list): + errors.append(f"'safeRange' debe ser una lista en: {measurement}") + if "warningRange" in measurement and not isinstance(measurement.get("warningRange", []), list): + errors.append(f"'warningRange' debe ser una lista en: {measurement}") + if "displayUnits" in measurement and not isinstance(measurement["displayUnits"], str): + errors.append(f"El 'podUnits' debe ser una cadena en: {measurement}") + if "podUnits" in measurement and not isinstance(measurement["podUnits"], str): + errors.append(f"El 'podUnits' debe ser una cadena en: {measurement}") #esto se puede quitar (json 516) + elif not isinstance(measurement, str): + errors.append("Cada elemento en 'measurements' debe ser un objeto o una cadena (nombre de archivo).") + + + if "packets" in data: + if not isinstance(data["packets"], list): + errors.append("La clave 'packets' debe ser una lista.") + else: + for packet in data["packets"]: + if isinstance(packet, dict): + required_keys = ["id", "name", "type","variable"] + for key in required_keys: + if key not in packet: + errors.append(f"Falta la clave '{key}' en un objeto de 'packets'.") + if "id" in packet and not isinstance(packet["id"], str): + errors.append(f"El 'id' debe ser una cadena en: {packet}") + if "name" in packet and not isinstance(packet["name"], str): + errors.append(f"El 'name' debe ser una cadena en: {packet}") + if "type" in packet and not isinstance(packet["type"], str): + errors.append(f"El 'type' debe ser una cadena en: {packet}") + if "variables" in packet: + if not isinstance(packet["variables"], list): + errors.append(f"'variables' debe ser una lista en: {packet}") + else: + for variable in packet["variables"]: + if not isinstance(variable, dict): + errors.append(f"Cada elemento en 'variables' debe ser un objeto en: {packet}") + if "name" not in variable: + errors.append(f"Falta la clave 'name' en un objeto de 'variables' en: {packet}") + elif not isinstance(packet, str): + errors.append("Cada elemento en 'packets' debe ser un objeto o una cadena (nombre de archivo).") + + return errors + + + +def validate_json_folder(folder_path): + boards_file_path = os.path.join(folder_path, "boards.json") + + try: + with open(boards_file_path, 'r') as boards_file: + boards_data = json.load(boards_file) + boards = boards_data.get("boards", {}) + + + board_keys = list(boards.keys()) + duplicate_keys = [key for key in board_keys if board_keys.count(key) > 1] + + if duplicate_keys: + print(f"Error: El archivo boards.json contiene claves duplicadas: {', '.join(duplicate_keys)}") + return + + except json.JSONDecodeError as e: + print(f"Error al decodificar JSON en {boards_file_path}: {e}") + return + except Exception as e: + print(f"Error al procesar el archivo {boards_file_path}: {e}") + return + + + for board_name, board_file_path in boards.items(): + full_path = os.path.join(folder_path, board_file_path) + try: + with open(full_path, 'r') as board_file: + data = json.load(board_file) + errors = validate_json_structure(data) + if errors: + print(f"Errores encontrados en {full_path} para la placa '{board_name}':") + for error in errors: + print(f"- {error}") + + except json.JSONDecodeError as e: + print(f"Error al decodificar JSON en {full_path}: {e}") + except Exception as e: + print(f"Error al procesar el archivo {full_path}: {e}") + + +if os.path.exists('./adj/') == False: + print("La carpeta ./adj/ no existe") +if __name__ == "__main__": + validate_json_folder("./adj/") From c4caf31695d247da5f786709fd3b01d545e953db Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:56:43 +0100 Subject: [PATCH 08/37] feat: add .gitignore for packet-sender --- packet-sender/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 packet-sender/.gitignore diff --git a/packet-sender/.gitignore b/packet-sender/.gitignore new file mode 100644 index 000000000..6d23150b2 --- /dev/null +++ b/packet-sender/.gitignore @@ -0,0 +1,2 @@ +# Rust target directory +/target/ \ No newline at end of file From 708f32713813ce90732aa0d7c50ed35fca671402 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:58:38 +0100 Subject: [PATCH 09/37] feat: add no-sandbox fix for linux --- electron-app/main.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/electron-app/main.js b/electron-app/main.js index ef5841b58..fd7a0a40d 100644 --- a/electron-app/main.js +++ b/electron-app/main.js @@ -15,6 +15,22 @@ import { createWindow } from "./src/windows/mainWindow.js"; const { autoUpdater } = pkg; +// Disable sandbox for Linux +if (process.platform === "linux") { + try { + const userns = fs + .readFileSync("/proc/sys/kernel/unprivileged_userns_clone", "utf8") + .trim(); + if (userns === "0") { + app.commandLine.appendSwitch("no-sandbox"); + } + } catch (e) {} + + if (process.getuid && process.getuid() === 0) { + app.commandLine.appendSwitch("no-sandbox"); + } +} + // Setup IPC handlers for renderer process communication setupIpcHandlers(); From a50bdc2059ccb7e4399260bb9cfd29721e286afe Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:46:54 +0100 Subject: [PATCH 10/37] feat: some packet-sender tweaks --- backend/cmd/main.go | 2 +- backend/cmd/orchestrator.go | 2 +- backend/cmd/setup_transport.go | 2 +- backend/cmd/setup_vehicle.go | 2 +- backend/internal/pod_data/measurement.go | 2 +- backend/internal/pod_data/pod_data.go | 2 +- backend/{internal => pkg}/adj/adj.go | 0 backend/{internal => pkg}/adj/boards.go | 0 backend/{internal => pkg}/adj/git.go | 0 backend/{internal => pkg}/adj/models.go | 0 electron-app/build.mjs | 106 +++++++++------------ electron-app/src/menu/menu.js | 2 +- electron-app/src/processes/packetSender.js | 16 +++- electron-app/src/utils/paths.js | 7 -- go.work | 1 + packet-sender/.gitignore | 2 - packet-sender/main.go | 14 +-- packet-sender/package.json | 13 +++ 18 files changed, 87 insertions(+), 86 deletions(-) rename backend/{internal => pkg}/adj/adj.go (100%) rename backend/{internal => pkg}/adj/boards.go (100%) rename backend/{internal => pkg}/adj/git.go (100%) rename backend/{internal => pkg}/adj/models.go (100%) delete mode 100644 packet-sender/.gitignore create mode 100644 packet-sender/package.json diff --git a/backend/cmd/main.go b/backend/cmd/main.go index addc5c879..b31a17d63 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -5,12 +5,12 @@ import ( "os" "os/signal" - adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/flags" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory" vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport" "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" trace "github.com/rs/zerolog/log" diff --git a/backend/cmd/orchestrator.go b/backend/cmd/orchestrator.go index 180b26c8d..6d34b064a 100644 --- a/backend/cmd/orchestrator.go +++ b/backend/cmd/orchestrator.go @@ -7,11 +7,11 @@ import ( "runtime/pprof" "strings" - adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/flags" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" "github.com/HyperloopUPV-H8/h9-backend/pkg/logger" data_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/data" order_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/order" diff --git a/backend/cmd/setup_transport.go b/backend/cmd/setup_transport.go index c86f9270e..9c9a5b628 100644 --- a/backend/cmd/setup_transport.go +++ b/backend/cmd/setup_transport.go @@ -7,12 +7,12 @@ import ( "net" "time" - adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/utils" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/tcp" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/udp" diff --git a/backend/cmd/setup_vehicle.go b/backend/cmd/setup_vehicle.go index b8a7b62e0..8fe8999e2 100644 --- a/backend/cmd/setup_vehicle.go +++ b/backend/cmd/setup_vehicle.go @@ -12,12 +12,12 @@ import ( h "github.com/HyperloopUPV-H8/h9-backend/pkg/http" "github.com/HyperloopUPV-H8/h9-backend/pkg/websocket" - adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" "github.com/HyperloopUPV-H8/h9-backend/pkg/broker" connection_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/connection" data_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/data" diff --git a/backend/internal/pod_data/measurement.go b/backend/internal/pod_data/measurement.go index 446b88611..b2581c4e3 100644 --- a/backend/internal/pod_data/measurement.go +++ b/backend/internal/pod_data/measurement.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" "github.com/HyperloopUPV-H8/h9-backend/internal/utils" + "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" ) const EnumType = "enum" diff --git a/backend/internal/pod_data/pod_data.go b/backend/internal/pod_data/pod_data.go index 7cafa1411..3f091aaaa 100644 --- a/backend/internal/pod_data/pod_data.go +++ b/backend/internal/pod_data/pod_data.go @@ -3,8 +3,8 @@ package pod_data import ( "github.com/HyperloopUPV-H8/h9-backend/internal/utils" - "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" + "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" ) func NewPodData(adjBoards map[string]adj.Board, globalUnits map[string]utils.Operations) (PodData, error) { diff --git a/backend/internal/adj/adj.go b/backend/pkg/adj/adj.go similarity index 100% rename from backend/internal/adj/adj.go rename to backend/pkg/adj/adj.go diff --git a/backend/internal/adj/boards.go b/backend/pkg/adj/boards.go similarity index 100% rename from backend/internal/adj/boards.go rename to backend/pkg/adj/boards.go diff --git a/backend/internal/adj/git.go b/backend/pkg/adj/git.go similarity index 100% rename from backend/internal/adj/git.go rename to backend/pkg/adj/git.go diff --git a/backend/internal/adj/models.go b/backend/pkg/adj/models.go similarity index 100% rename from backend/internal/adj/models.go rename to backend/pkg/adj/models.go diff --git a/electron-app/build.mjs b/electron-app/build.mjs index f83964a19..b4dff1753 100644 --- a/electron-app/build.mjs +++ b/electron-app/build.mjs @@ -5,7 +5,7 @@ */ import { execSync } from "child_process"; -import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync } from "fs"; +import { cpSync, existsSync, mkdirSync, rmSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { logger } from "./src/utils/logger.js"; @@ -20,6 +20,7 @@ const CONFIG = { type: "go", path: join(ROOT, "backend"), // Root of backend (where package.json is) output: join(__dirname, "binaries"), + entry: "./cmd", commands: ["pnpm run build:ci"], platforms: [ { @@ -52,18 +53,43 @@ const CONFIG = { }, ], }, - "packet-sender": { - type: "rust", - path: join(ROOT, "packet-sender"), - output: join(__dirname, "binaries"), - commands: ["pnpm run build"], - binaryPath: "target/release/packet-sender", - platforms: [ - { id: "win64", ext: ".exe", tags: ["win", "windows"] }, - { id: "linux64", ext: "", tags: ["linux"] }, - { id: "mac64", ext: "", tags: ["mac", "macos"] }, - ], - }, + // "packet-sender": { + // type: "go", + // path: join(ROOT, "packet-sender"), + // output: join(__dirname, "binaries"), + // entry: ".", + // commands: ["pnpm run build:ci"], + // platforms: [ + // { + // id: "win64", + // goos: "windows", + // goarch: "amd64", + // ext: ".exe", + // tags: ["win", "windows"], + // }, + // { + // id: "linux64", + // goos: "linux", + // goarch: "amd64", + // ext: "", + // tags: ["linux"], + // }, + // { + // id: "mac64", + // goos: "darwin", + // goarch: "amd64", + // ext: "", + // tags: ["mac", "macos"], + // }, + // { + // id: "macArm", + // goos: "darwin", + // goarch: "arm64", + // ext: "", + // tags: ["mac", "macos"], + // }, + // ], + // }, "testing-view": { type: "frontend", path: join(ROOT, "frontend/testing-view"), @@ -98,8 +124,8 @@ const run = (cmd, cwd, env = {}) => { } }; -const buildBackend = (config, requestedPlatforms, extraArgs = "") => { - logger.info("Building Backend (Go)..."); +const buildGo = (name, config, requestedPlatforms, extraArgs = "") => { + logger.info(`Building ${name} (Go)...`); mkdirSync(config.output, { recursive: true }); const targets = config.platforms.filter((p) => { @@ -112,22 +138,15 @@ const buildBackend = (config, requestedPlatforms, extraArgs = "") => { return p.tags.some((tag) => requestedPlatforms.includes(tag)); }); - if (targets.length === 0) { - logger.error( - `No matching platforms found for: ${requestedPlatforms.join(", ")}` - ); - return false; - } - let success = true; for (const p of targets) { - const filename = `backend-${p.goos}-${p.goarch}${p.ext}`; + const filename = `${name}-${p.goos}-${p.goarch}${p.ext}`; logger.step(`Building ${p.goos}/${p.goarch}...`); + const entryPath = config.entry || "."; + for (const cmd of config.commands) { - // cmd is like "pnpm run build:ci --" - // We append the output flag and target directory - const buildCmd = `${cmd} -o "${join(config.output, filename)}" ${extraArgs} ./cmd`; + const buildCmd = `${cmd} -o "${join(config.output, filename)}" ${extraArgs} ${entryPath}`; const result = run(buildCmd, config.path, { GOOS: p.goos, @@ -145,37 +164,6 @@ const buildBackend = (config, requestedPlatforms, extraArgs = "") => { return success; }; -const buildRust = (name, config, requestedPlatforms, extraArgs = "") => { - logger.info(`Building ${name} (Rust)...`); - mkdirSync(config.output, { recursive: true }); - - for (const cmd of config.commands) { - // Only append extra args to build commands - const finalCmd = cmd.includes("build") ? `${cmd} ${extraArgs}` : cmd; - if (!run(finalCmd, config.path)) return false; - } - - const isWin = - process.platform === "win32" || - (requestedPlatforms && requestedPlatforms.includes("win")); - const ext = isWin ? ".exe" : ""; - - // Check for source binary - const sourceBin = join(config.path, config.binaryPath + ext); - const destName = `packet-sender${ext}`; - const destPath = join(config.output, destName); - - logger.step(`Copying binary to ${destPath}...`); - - if (existsSync(sourceBin)) { - copyFileSync(sourceBin, destPath); - return true; - } else { - logger.error(`Rust binary not found at ${sourceBin}`); - return false; - } -}; - const buildFrontend = (name, config, extraArgs = "") => { if (config.optional && !existsSync(join(config.path, "package.json"))) { logger.warning(`Skipping ${name} (not initialized)`); @@ -252,9 +240,7 @@ logger.header("Hyperloop Control Station Build"); let success = true; if (config.type === "go") { - success = buildBackend(config, requestedPlatforms, extraArgs); - } else if (config.type === "rust") { - success = buildRust(key, config, requestedPlatforms, extraArgs); + success = buildGo(key, config, requestedPlatforms, extraArgs); } else if (config.type === "frontend") { success = buildFrontend(key, config, extraArgs); if (success && !config.optional) frontendBuilt = true; diff --git a/electron-app/src/menu/menu.js b/electron-app/src/menu/menu.js index c0436ab79..64f776da9 100644 --- a/electron-app/src/menu/menu.js +++ b/electron-app/src/menu/menu.js @@ -83,7 +83,7 @@ function createMenu(mainWindow) { } const packetSenderProcess = getPacketSenderProcess(); if (!packetSenderProcess || packetSenderProcess.killed) { - startPacketSender(["random"]); + startPacketSender(); } }, }, diff --git a/electron-app/src/processes/packetSender.js b/electron-app/src/processes/packetSender.js index e6efc5cf9..74b653fa1 100644 --- a/electron-app/src/processes/packetSender.js +++ b/electron-app/src/processes/packetSender.js @@ -12,16 +12,18 @@ import { getBinaryPath } from "../utils/paths.js"; // Store the packet sender process instance let packetSenderProcess = null; +// Default arguments for packet sender +const DEFAULT_ARGS = ["1", "1"]; // Send mode, Random type + /** * Starts the packet sender process by spawning the packet-sender binary with optional arguments. * Sets up event handlers for stdout and stderr with appropriate logging. * @param {string[]} [args=[]] - Optional array of command-line arguments to pass to the packet-sender binary. * @returns {import("child_process").ChildProcessWithoutNullStreams | null} The spawned ChildProcess object, or null if the binary is not found. * @example - * const process = startPacketSender(["--port", "8080"]); * startPacketSender(); */ -function startPacketSender(args = []) { +function startPacketSender(args = DEFAULT_ARGS) { // Get the path to the packet-sender binary const packetSenderBin = getBinaryPath("packet-sender"); @@ -44,6 +46,14 @@ function startPacketSender(args = []) { // Log stdout output from packet sender process.stdout.on("data", (data) => { logger.packetSender.info(`${data.toString().trim()}`); + + if (data.toString().includes("1) Send packets")) { + process.stdin.write("1\n"); + } + + if (data.toString().includes("1) Random")) { + process.stdin.write("1\n"); + } }); // Log stderr output as errors @@ -90,7 +100,7 @@ function restartPacketSender() { // Wait before starting new process to ensure cleanup setTimeout(() => { // Start with help arguments - startPacketSender(["random"]); + startPacketSender(); }, 500); } } diff --git a/electron-app/src/utils/paths.js b/electron-app/src/utils/paths.js index f7bf70033..1d44ceb39 100644 --- a/electron-app/src/utils/paths.js +++ b/electron-app/src/utils/paths.js @@ -41,13 +41,6 @@ function getBinaryPath(name) { const arch = process.arch; const ext = platform === "win32" ? ".exe" : ""; - if (name === "packet-sender") { - if (!app.isPackaged) { - return path.join(getAppPath(), "binaries", `${name}${ext}`); - } - return path.join(process.resourcesPath, "binaries", `${name}${ext}`); - } - const goosMap = { win32: "windows", darwin: "darwin", diff --git a/go.work b/go.work index 521811c76..957518330 100644 --- a/go.work +++ b/go.work @@ -2,4 +2,5 @@ go 1.23.1 use ( ./backend + ./packet-sender ) diff --git a/packet-sender/.gitignore b/packet-sender/.gitignore deleted file mode 100644 index 6d23150b2..000000000 --- a/packet-sender/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Rust target directory -/target/ \ No newline at end of file diff --git a/packet-sender/main.go b/packet-sender/main.go index 9a52ccb99..f748519f3 100644 --- a/packet-sender/main.go +++ b/packet-sender/main.go @@ -7,8 +7,6 @@ import ( boardpkg "packet_sender/pkg/board" "packet_sender/pkg/listener" "packet_sender/pkg/sender" - "path" - "path/filepath" "strings" adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj" @@ -66,11 +64,13 @@ func getConn(lip string, lport uint16, rip string, rport uint16) *net.UDPConn { // getADJ loads the same ADJ used by backend directly from backend/cmd/adj. func getADJ() adj_module.ADJ { - adjPath, err := filepath.Abs(path.Join("..", "backend", "cmd", "adj")) - if err != nil { - log.Fatalf("Failed to resolve ADJ path: %v", err) - } - adj_module.RepoPath = adjPath + string(filepath.Separator) + // adjPath, err := filepath.Abs(path.Join("..", "backend", "cmd", "adj")) + // if err != nil { + // log.Fatalf("Failed to resolve ADJ path: %v", err) + // } + // adj_module.RepoPath = adjPath + string(filepath.Separator) + + // Uses the same ADJ RepoPath as the backend by default adj, err := adj_module.NewADJ("") if err != nil { diff --git a/packet-sender/package.json b/packet-sender/package.json new file mode 100644 index 000000000..4842b09d2 --- /dev/null +++ b/packet-sender/package.json @@ -0,0 +1,13 @@ +{ + "name": "packet-sender", + "version": "1.0.0", + "private": true, + "author": "Hyperloop UPV Team", + "license": "MIT", + "scripts": { + "build": "go build -o packet-sender main.go", + "build:ci": "go build", + "dev": "go run main.go", + "test": "go test ./..." + } +} From 715edba7a9f549d49517e8183602479202a8e108 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:18:14 +0100 Subject: [PATCH 11/37] feat: add global boards state --- .../settings/MultiCheckboxField.tsx | 36 +++++++++++-------- .../src/components/settings/SettingsForm.tsx | 11 ++++-- frontend/testing-view/src/constants/boards.ts | 10 ------ .../src/constants/settingsSchema.ts | 6 ++-- .../components/FilterCategoryItem.tsx | 4 +-- .../filtering/components/FilterController.tsx | 5 +-- .../filtering/store/filteringSlice.ts | 15 ++++---- .../rightSidebar/sections/CommandsSection.tsx | 26 ++++++++------ .../sections/TelemetrySection.tsx | 28 ++++++++------- .../testing-view/src/hooks/useBoardData.ts | 2 ++ .../src/hooks/useTransformedBoards.ts | 10 +++++- frontend/testing-view/src/lib/utils.ts | 8 ++--- .../src/store/slices/catalogSlice.ts | 6 ++++ packet-sender/package.json | 1 - 14 files changed, 97 insertions(+), 71 deletions(-) delete mode 100644 frontend/testing-view/src/constants/boards.ts diff --git a/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx b/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx index eb3980a2f..8ece18567 100644 --- a/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx +++ b/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx @@ -20,21 +20,27 @@ export const MultiCheckboxField = ({
- {field.options?.map((opt) => ( -
- handleToggle(opt, !!checked)} - /> - -
- ))} + {!field.options || field.options.length === 0 ? ( +

+ No boards detected. Connect to the backend to see available options. +

+ ) : ( + field.options?.map((opt) => ( +
+ handleToggle(opt, !!checked)} + /> + +
+ )) + )}
); diff --git a/frontend/testing-view/src/components/settings/SettingsForm.tsx b/frontend/testing-view/src/components/settings/SettingsForm.tsx index 96357b57e..c69512c55 100644 --- a/frontend/testing-view/src/components/settings/SettingsForm.tsx +++ b/frontend/testing-view/src/components/settings/SettingsForm.tsx @@ -1,5 +1,7 @@ import { get, set } from "lodash"; -import { SETTINGS_SCHEMA } from "../../constants/settingsSchema"; +import { useMemo } from "react"; +import { getSettingsSchema } from "../../constants/settingsSchema"; +import { useStore } from "../../store/store"; import type { ConfigData } from "../../types/common/config"; import type { SettingField } from "../../types/common/settings"; import { BooleanField } from "./BooleanField"; @@ -23,6 +25,9 @@ export const SettingsForm = ({ config, onChange }: SettingsFormProps) => { onChange(nextConfig); }; + const boards = useStore((s) => s.boards); + const schema = useMemo(() => getSettingsSchema(boards), [boards]); + const renderField = (field: SettingField) => { const currentValue = get(config, field.path); @@ -94,9 +99,9 @@ export const SettingsForm = ({ config, onChange }: SettingsFormProps) => { return (
- {SETTINGS_SCHEMA.map((section) => ( + {schema.map((section) => (
-

+

{section.title}

diff --git a/frontend/testing-view/src/constants/boards.ts b/frontend/testing-view/src/constants/boards.ts deleted file mode 100644 index 68a8a4127..000000000 --- a/frontend/testing-view/src/constants/boards.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** List of names of available boards. */ -export const BOARD_NAMES: readonly string[] = [ - "BCU", // Battery Control Unit - "PCU", // Propulsion Control Unit - "LCU", // Levitation Control Unit - "HVSCU", // High Voltage System Control Unit - "BMSL", // Battery Management System Level - "VCU", // Vehicle Control Unit - "HVSCU-Cabinet", // High Voltage System Control Unit Cabinet -]; diff --git a/frontend/testing-view/src/constants/settingsSchema.ts b/frontend/testing-view/src/constants/settingsSchema.ts index eecc75f85..28561ea8c 100644 --- a/frontend/testing-view/src/constants/settingsSchema.ts +++ b/frontend/testing-view/src/constants/settingsSchema.ts @@ -1,8 +1,8 @@ import type { SettingsSection } from "../types/common/settings"; -import { BOARD_NAMES } from "./boards"; +import type { BoardName } from "../types/data/board"; /** Settings form is generated from this schema. */ -export const SETTINGS_SCHEMA: SettingsSection[] = [ +export const getSettingsSchema = (boards: BoardName[]): SettingsSection[] => [ { title: "Vehicle Configuration", fields: [ @@ -10,7 +10,7 @@ export const SETTINGS_SCHEMA: SettingsSection[] = [ label: "Boards", path: "vehicle.boards", type: "multi-checkbox", - options: BOARD_NAMES as string[], + options: boards, }, ], }, diff --git a/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx b/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx index 747f80aff..05f2a2467 100644 --- a/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx +++ b/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx @@ -22,7 +22,7 @@ export const FilterCategoryItem = ({ category }: FilterCategoryItemProps) => { const toggleCategoryFilter = useStore((s) => s.toggleCategoryFilter); const toggleItemFilter = useStore((s) => s.toggleItemFilter); - const items = useStore((s) => s.getCatalog(scope)[category]); + const items = useStore((s) => s.getCatalog(scope)[category]) || []; const totalItems = items.length; const selectedIds = useStore( @@ -61,7 +61,7 @@ export const FilterCategoryItem = ({ category }: FilterCategoryItemProps) => {
- {items.map((item) => ( + {items?.map((item) => ( { const { isOpen, scope } = useStore((s) => s.filterDialog); const close = useStore((s) => s.closeFilterDialog); + const boards = useStore((s) => s.boards); + const clearFilters = useStore((s) => s.clearFilters); const selectAllFilters = useStore((s) => s.selectAllFilters); @@ -20,7 +21,7 @@ export const FilterController = () => { onClose={close} onClearAll={() => clearFilters(scope)} onSelectAll={() => selectAllFilters(scope)} - categories={BOARD_NAMES} + categories={boards} FilterCategoryComponent={FilterCategoryItem} /> ); diff --git a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts index 1bcc4cc6d..ecb047379 100644 --- a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts +++ b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts @@ -135,7 +135,7 @@ export const createFilteringSlice: StateCreator< const currentWorkspaceFilters = get().workspaceFilters[workspaceId] || {}; const currentTabFilter = - currentWorkspaceFilters[scope] || createEmptyFilter(); + currentWorkspaceFilters[scope] || createEmptyFilter(get().boards); const currentCategoryIds = currentTabFilter[category] || []; @@ -157,13 +157,13 @@ export const createFilteringSlice: StateCreator< const items = get().getCatalog(scope); - const fullFilter = createFullFilter(items); + const fullFilter = createFullFilter(items, get().boards); get().updateFilters(scope, fullFilter); }, clearFilters: (scope) => { const workspaceId = get().getActiveWorkspaceId(); if (!workspaceId) return; - const emptyFilter = createEmptyFilter(); + const emptyFilter = createEmptyFilter(get().boards); get().updateFilters(scope, emptyFilter); }, toggleCategoryFilter: (scope, category, checked) => { @@ -173,7 +173,8 @@ export const createFilteringSlice: StateCreator< const catalog = get().getCatalog(scope); const currentFilters = - get().workspaceFilters[workspaceId]?.[scope] || createEmptyFilter(); + get().workspaceFilters[workspaceId]?.[scope] || + createEmptyFilter(get().boards); const newItems = checked ? catalog?.[category]?.map((item) => item.id) || [] @@ -196,9 +197,9 @@ export const createFilteringSlice: StateCreator< if (Object.keys(currentFilters).length === 0) { set({ workspaceFilters: generateInitialFilters({ - commands: createFullFilter(commands), - telemetry: createFullFilter(telemetry), - logs: createFullFilter(telemetry), + commands: createFullFilter(commands, get().boards), + telemetry: createFullFilter(telemetry, get().boards), + logs: createFullFilter(telemetry, get().boards), }), }); } diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx index 60b1a0876..59b3dbaff 100644 --- a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx @@ -1,15 +1,19 @@ -import { BOARD_NAMES } from "../../../../../constants/boards"; +import { useStore } from "../../../../../store/store"; import type { CommandCatalogItem } from "../../../../../types/data/commandCatalogItem"; import { CommandItem } from "../tabs/commands/CommandItem"; import { Tab } from "../tabs/Tab"; -export const CommandsSection = () => ( - ( - - )} - /> -); +export const CommandsSection = () => { + const boards = useStore((s) => s.boards); + + return ( + ( + + )} + /> + ); +}; diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx index 4798da54d..3cb19cfce 100644 --- a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx @@ -1,16 +1,20 @@ -import { BOARD_NAMES } from "../../../../../constants/boards"; +import { useStore } from "../../../../../store/store"; import type { TelemetryCatalogItem } from "../../../../../types/data/telemetryCatalogItem"; import { Tab } from "../tabs/Tab"; import { TelemetryItem } from "../tabs/telemetry/TelemetryItem"; -export const TelemetrySection = () => ( - ( - - )} - virtualized - /> -); +export const TelemetrySection = () => { + const boards = useStore((s) => s.boards); + + return ( + ( + + )} + virtualized + /> + ); +}; diff --git a/frontend/testing-view/src/hooks/useBoardData.ts b/frontend/testing-view/src/hooks/useBoardData.ts index 22ff3d8f9..3f40f4cfc 100644 --- a/frontend/testing-view/src/hooks/useBoardData.ts +++ b/frontend/testing-view/src/hooks/useBoardData.ts @@ -90,6 +90,8 @@ export function useBoardData( logger.testingView.log("[useBoardData] Commands data processed"); + console.log("availableBoards", availableBoards); + return { telemetryCatalog: telemetryCatalogResult, commandsCatalog: commandsCatalogResult, diff --git a/frontend/testing-view/src/hooks/useTransformedBoards.ts b/frontend/testing-view/src/hooks/useTransformedBoards.ts index c8fc04c40..aa7cda62b 100644 --- a/frontend/testing-view/src/hooks/useTransformedBoards.ts +++ b/frontend/testing-view/src/hooks/useTransformedBoards.ts @@ -12,6 +12,7 @@ export function useTransformedBoards( const setTelemetryCatalog = useStore((s) => s.setTelemetryCatalog); const setCommandsCatalog = useStore((s) => s.setCommandsCatalog); + const setBoards = useStore((s) => s.setBoards); const initializeWorkspaceFilters = useStore( (s) => s.initializeWorkspaceFilters, ); @@ -25,7 +26,14 @@ export function useTransformedBoards( setTelemetryCatalog(transformedBoards.telemetryCatalog); setCommandsCatalog(transformedBoards.commandsCatalog); - initializeWorkspaceFilters(); + setBoards(Array.from(transformedBoards.boards)); + + const hasTelemetryData = + Object.keys(transformedBoards.telemetryCatalog).length > 0; + const hasCommandsData = + Object.keys(transformedBoards.commandsCatalog).length > 0; + + if (hasTelemetryData && hasCommandsData) initializeWorkspaceFilters(); }, [ transformedBoards, setTelemetryCatalog, diff --git a/frontend/testing-view/src/lib/utils.ts b/frontend/testing-view/src/lib/utils.ts index fe0306338..adbcdfc2f 100644 --- a/frontend/testing-view/src/lib/utils.ts +++ b/frontend/testing-view/src/lib/utils.ts @@ -1,5 +1,4 @@ import { ACRONYMS } from "../constants/acronyms"; -import { BOARD_NAMES } from "../constants/boards"; import { variablesBadgeClasses } from "../constants/variablesBadgeClasses"; import type { FilterScope, @@ -29,8 +28,8 @@ export const generateInitialFilters = ( ); }; -export const createEmptyFilter = (): TabFilter => { - return BOARD_NAMES.reduce((acc, category) => { +export const createEmptyFilter = (boards: BoardName[]): TabFilter => { + return boards.reduce((acc, category) => { acc[category] = []; return acc; }, {} as TabFilter); @@ -38,8 +37,9 @@ export const createEmptyFilter = (): TabFilter => { export const createFullFilter = ( dataSource: Record, + boards: BoardName[], ): TabFilter => { - return BOARD_NAMES.reduce((acc, category) => { + return boards.reduce((acc, category) => { acc[category] = dataSource[category]?.map((item) => item.id) || []; return acc; }, {} as TabFilter); diff --git a/frontend/testing-view/src/store/slices/catalogSlice.ts b/frontend/testing-view/src/store/slices/catalogSlice.ts index e2e60017f..dcd365534 100644 --- a/frontend/testing-view/src/store/slices/catalogSlice.ts +++ b/frontend/testing-view/src/store/slices/catalogSlice.ts @@ -15,6 +15,10 @@ export interface CatalogSlice { setTelemetryCatalog: ( telemetryCatalog: Record, ) => void; + + // Boards + boards: BoardName[]; + setBoards: (boards: BoardName[]) => void; } export const createCatalogSlice: StateCreator = ( @@ -24,4 +28,6 @@ export const createCatalogSlice: StateCreator = ( telemetryCatalog: {} as Record, setCommandsCatalog: (commandsCatalog) => set({ commandsCatalog }), setTelemetryCatalog: (telemetryCatalog) => set({ telemetryCatalog }), + boards: [] as BoardName[], + setBoards: (boards) => set({ boards }), }); diff --git a/packet-sender/package.json b/packet-sender/package.json index 4842b09d2..d96551e08 100644 --- a/packet-sender/package.json +++ b/packet-sender/package.json @@ -7,7 +7,6 @@ "scripts": { "build": "go build -o packet-sender main.go", "build:ci": "go build", - "dev": "go run main.go", "test": "go test ./..." } } From 8168f40a2d2420879ac3a0e7b9c650b858863108 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:28:37 +0100 Subject: [PATCH 12/37] feat: increase backend resolving time --- electron-app/src/processes/backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js index 1e215619d..c994a94e3 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -86,7 +86,7 @@ function startBackend() { // If the backend didn't fail in this period of time, resolve the promise setTimeout(() => { resolve(backendProcess); - }, 1000); + }, 2000); // Handle process exit backendProcess.on("close", (code) => { From b6e9f00a79ce141cc3e843a34e8a7fc33c302cb7 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:37:34 +0100 Subject: [PATCH 13/37] fix: workflows --- .github/workflows/build.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2564f79db..37d11b4c4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -57,7 +57,6 @@ jobs: - uses: dorny/paths-filter@v3 id: filter with: - ref: "production" filters: | backend: - 'backend/**/*' From eb85a77697d21bc1e1ec7b7b6802f7a6a38d09bb Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:41:31 +0100 Subject: [PATCH 14/37] fix --- .github/workflows/build.yaml | 2 +- .../workspace/store/workspacesSlice.ts | 6 ++--- frontend/testing-view/src/lib/utils.test.ts | 24 +++++++++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 37d11b4c4..dbbfce409 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -112,7 +112,7 @@ jobs: with: workflow: build.yaml branch: production - workflow_conclusion: success + workflow_conclusion: completed name: backend-${{ matrix.platform }} path: backend/cmd diff --git a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts index ea09a6844..28a1be861 100644 --- a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts +++ b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts @@ -63,9 +63,9 @@ export const createWorkspacesSlice: StateCreator< const newWorkspaceFilters = { ...state.workspaceFilters, [newWorkspaceId]: { - commands: createFullFilter(commands), - telemetry: createFullFilter(telemetry), - logs: createFullFilter(telemetry), + commands: createFullFilter(commands, get().boards), + telemetry: createFullFilter(telemetry, get().boards), + logs: createFullFilter(telemetry, get().boards), }, }; diff --git a/frontend/testing-view/src/lib/utils.test.ts b/frontend/testing-view/src/lib/utils.test.ts index 947b6c5c7..fa667af7a 100644 --- a/frontend/testing-view/src/lib/utils.test.ts +++ b/frontend/testing-view/src/lib/utils.test.ts @@ -105,7 +105,17 @@ describe("getTypeBadgeClass", () => { describe("emptyFilter", () => { it("should return the correct empty filter", () => { - expect(createEmptyFilter()).toStrictEqual({ + const boards = [ + "BCU", + "PCU", + "LCU", + "HVSCU", + "HVSCU-Cabinet", + "BMSL", + "VCU", + ]; + + expect(createEmptyFilter(boards)).toStrictEqual({ BCU: [], PCU: [], LCU: [], @@ -133,7 +143,17 @@ describe("fullFilter", () => { VCU: [], }; - expect(createFullFilter(testDataSource)).toStrictEqual({ + const boards = [ + "BCU", + "PCU", + "LCU", + "HVSCU", + "HVSCU-Cabinet", + "BMSL", + "VCU", + ]; + + expect(createFullFilter(testDataSource, boards)).toStrictEqual({ BCU: [1], PCU: [2], LCU: [3], From 47156f5f16bc67f45f3a6128db3cf37aa77b6882 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:54:52 +0100 Subject: [PATCH 15/37] Update build.yaml --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index dbbfce409..c6a335ac0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -53,6 +53,8 @@ jobs: competition-view: ${{ steps.filter.outputs.competition-view == 'true' || github.event.inputs.rebuild-competition-view == 'true' || inputs.build-competition-view == true }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: dorny/paths-filter@v3 id: filter From 7ccfe4ba9907591c5ab0f1f09a1abe6327754ca2 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:56:16 +0100 Subject: [PATCH 16/37] check --- .../testing-view/src/features/workspace/store/workspacesSlice.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts index 28a1be861..191ab3c75 100644 --- a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts +++ b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts @@ -68,6 +68,7 @@ export const createWorkspacesSlice: StateCreator< logs: createFullFilter(telemetry, get().boards), }, }; + // test // Initialize expanded items for the new workspace const newExpandedItems = { From 913fe95a44b5a683694812720229852d20d03d47 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:01:40 +0100 Subject: [PATCH 17/37] Update build.yaml --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c6a335ac0..516ddbdc7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -59,6 +59,7 @@ jobs: - uses: dorny/paths-filter@v3 id: filter with: + base: ${{ github.event.before }} filters: | backend: - 'backend/**/*' From 322c199d92259d52570f0e9b2a9634f9e027d850 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:02:37 +0100 Subject: [PATCH 18/37] fix --- .../testing-view/src/features/workspace/store/workspacesSlice.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts index 191ab3c75..28a1be861 100644 --- a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts +++ b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts @@ -68,7 +68,6 @@ export const createWorkspacesSlice: StateCreator< logs: createFullFilter(telemetry, get().boards), }, }; - // test // Initialize expanded items for the new workspace const newExpandedItems = { From c55f0ac82508e3d521a45e01554d24d0a075ac5b Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:43:31 +0100 Subject: [PATCH 19/37] feat: add building to frontend testing --- .github/workflows/frontend-tests.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/frontend-tests.yaml b/.github/workflows/frontend-tests.yaml index 149d955df..0599a5097 100644 --- a/.github/workflows/frontend-tests.yaml +++ b/.github/workflows/frontend-tests.yaml @@ -41,5 +41,8 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --filter=testing-view --filter=ui --filter=core + - name: Build frontend + run: pnpm build --filter="./frontend/**" + - name: Run tests run: pnpm test --filter="./frontend/**" From 225cbe5a2c23df9abadea0b8b44aa34a146ad8b9 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:54:13 +0100 Subject: [PATCH 20/37] feat: increase backend resolving time --- electron-app/src/processes/backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js index c994a94e3..747336e3d 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -86,7 +86,7 @@ function startBackend() { // If the backend didn't fail in this period of time, resolve the promise setTimeout(() => { resolve(backendProcess); - }, 2000); + }, 4000); // Handle process exit backendProcess.on("close", (code) => { From 6840db855b67b7bf8560122fe930c48b1d800689 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:11:57 +0100 Subject: [PATCH 21/37] feat: include rpm and pacman distributives --- electron-app/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electron-app/package.json b/electron-app/package.json index a37d20f36..30344aa16 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -106,7 +106,9 @@ "linux": { "target": [ "AppImage", - "deb" + "deb", + "rpm", + "pacman" ], "icon": "icons/512x512.png", "category": "Utility", From 4740414d812f30fca609c7989d171cdd12e88ac4 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:20:39 +0100 Subject: [PATCH 22/37] fix: include dependencies and new files --- .github/workflows/release.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index abcc3c225..01f375565 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -86,6 +86,12 @@ jobs: echo "Updated version to:" cat package.json | grep version + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y rpm libarchive-tools + # Download ONLY the appropriate backend for this platform - name: Download Linux backend if: runner.os == 'Linux' @@ -182,6 +188,8 @@ jobs: electron-app/dist/*.exe electron-app/dist/*.AppImage electron-app/dist/*.deb + electron-app/dist/*.rpm + electron-app/dist/*.pacman electron-app/dist/*.dmg electron-app/dist/*.zip electron-app/dist/*.yml From 33630b216013516a9d2cef649e40851ccd5cb713 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:30:59 +0100 Subject: [PATCH 23/37] Update README.md --- electron-app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-app/README.md b/electron-app/README.md index 854930745..a881da8e5 100644 --- a/electron-app/README.md +++ b/electron-app/README.md @@ -79,7 +79,7 @@ pnpm run dist:linux # Linux On macOS, the backend requires the loopback address `127.0.0.9` to be configured. If you encounter a "can't assign requested address" error when starting the backend, run: ``` -sudo ipconfig set en0 INFORM 127.0.0.9 +sudo ifconfig lo0 alias 127.0.0.9 up ``` ## Available Scripts From ee6441cdc57928ab8886e616febfdcbe9a49488f Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:33:43 +0100 Subject: [PATCH 24/37] Update README.md --- electron-app/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/electron-app/README.md b/electron-app/README.md index a881da8e5..7034c163e 100644 --- a/electron-app/README.md +++ b/electron-app/README.md @@ -89,6 +89,7 @@ sudo ifconfig lo0 alias 127.0.0.9 up - `pnpm start` - Run application in development mode - `pnpm run dist` - Build production executable - `pnpm test` - Run tests +- `pnpm build-icons` - build icon from the icon.png file in the `/electron-app` folder ...and many custom variations (see package.json) # Only works and makes sense after running `pnpm run dist` From ef5bd72490e123896ec9e82c74c66fedacbdd2e7 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:34:46 +0100 Subject: [PATCH 25/37] feat: add stale boards indication --- .../filtering/components/FilterController.tsx | 6 +++ .../filtering/components/FilterDialog.tsx | 30 ++++++++++- .../filtering/store/filteringSlice.ts | 3 +- .../rightSidebar/tabs/TabHeader.tsx | 53 +++++++++++++------ frontend/testing-view/src/lib/utils.ts | 8 +++ 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/frontend/testing-view/src/features/filtering/components/FilterController.tsx b/frontend/testing-view/src/features/filtering/components/FilterController.tsx index 538cef4a9..fdea53a8d 100644 --- a/frontend/testing-view/src/features/filtering/components/FilterController.tsx +++ b/frontend/testing-view/src/features/filtering/components/FilterController.tsx @@ -1,3 +1,5 @@ +import { useShallow } from "zustand/shallow"; +import { detectExtraBoards } from "../../../lib/utils"; import { useStore } from "../../../store/store"; import { FilterCategoryItem } from "./FilterCategoryItem"; import { FilterDialog } from "./FilterDialog"; @@ -7,12 +9,15 @@ export const FilterController = () => { const close = useStore((s) => s.closeFilterDialog); const boards = useStore((s) => s.boards); + const activeFilters = useStore(useShallow((s) => s.getActiveFilters(scope))); const clearFilters = useStore((s) => s.clearFilters); const selectAllFilters = useStore((s) => s.selectAllFilters); if (!scope) return null; + const extraBoards = detectExtraBoards(activeFilters, boards); + return ( { onClearAll={() => clearFilters(scope)} onSelectAll={() => selectAllFilters(scope)} categories={boards} + extraCategories={extraBoards} FilterCategoryComponent={FilterCategoryItem} /> ); diff --git a/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx b/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx index 6c72ba88c..f211b2b33 100644 --- a/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx +++ b/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx @@ -6,6 +6,7 @@ import { DialogHeader, DialogTitle, } from "@workspace/ui"; +import { AlertTriangle } from "@workspace/ui/icons"; import { type ComponentType } from "react"; import type { BoardName } from "../../../types/data/board"; @@ -17,6 +18,7 @@ interface FilterDialogProps { onClearAll: () => void; onSelectAll: () => void; categories: readonly BoardName[]; + extraCategories: readonly BoardName[]; FilterCategoryComponent: ComponentType<{ category: BoardName }>; } @@ -28,11 +30,13 @@ export const FilterDialog = ({ onClearAll, onSelectAll, categories, + extraCategories, FilterCategoryComponent, }: FilterDialogProps) => { + console.log(extraCategories); return ( - + {title} {description && {description}} @@ -47,6 +51,30 @@ export const FilterDialog = ({
+ {extraCategories.length > 0 && ( +
+
+ + Stale filters detected +
+

+ The following boards are in your saved filters but not in the + current configuration:{" "} + + {extraCategories.join(", ")} + +

+ +
+ )} +
{categories.map((category) => ( diff --git a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts index ecb047379..58cefaccc 100644 --- a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts +++ b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts @@ -38,7 +38,7 @@ export interface FilteringSlice { workspaceFilters: Record; initializeWorkspaceFilters: () => void; updateFilters: (scope: FilterScope, filters: TabFilter) => void; - getActiveFilters: (scope: FilterScope) => TabFilter | undefined; + getActiveFilters: (scope: FilterScope | null) => TabFilter | undefined; /** Filter Actions */ selectAllFilters: (scope: FilterScope) => void; @@ -229,6 +229,7 @@ export const createFilteringSlice: StateCreator< // Helper getters getActiveFilters: (scope) => { const id = get().getActiveWorkspaceId(); + if (!scope) return {}; return id ? get().workspaceFilters[id]?.[scope] : undefined; }, diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx index 08730d14c..e93c10110 100644 --- a/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx @@ -1,5 +1,7 @@ import { Button } from "@workspace/ui"; -import { ListFilterPlus } from "@workspace/ui/icons"; +import { AlertTriangle, ListFilterPlus } from "@workspace/ui/icons"; +import { useShallow } from "zustand/shallow"; +import { detectExtraBoards } from "../../../../../lib/utils"; import { useStore } from "../../../../../store/store"; import type { SidebarTab } from "../../../types/sidebar"; @@ -13,23 +15,40 @@ export const TabHeader = ({ title, scope }: TabHeaderProps) => { const totalCount = useStore((state) => state.getTotalCount(scope)); const filteredCount = useStore((state) => state.getFilteredCount(scope)); + const boards = useStore((s) => s.boards); + const activeFilters = useStore(useShallow((s) => s.getActiveFilters(scope))); + const extraBoards = detectExtraBoards(activeFilters, boards); + return ( -
-

- {title} - - {filteredCount} / {totalCount} - -

- +
+
+

+ {title} + + {filteredCount} / {totalCount} + +

+ +
+ + {/* Warning for stale boards */} + {extraBoards.length > 0 && ( +
+ + {extraBoards.length} stale board(s) affecting counts +
+ )}
); }; diff --git a/frontend/testing-view/src/lib/utils.ts b/frontend/testing-view/src/lib/utils.ts index adbcdfc2f..0df30e9c3 100644 --- a/frontend/testing-view/src/lib/utils.ts +++ b/frontend/testing-view/src/lib/utils.ts @@ -119,3 +119,11 @@ export const formatTimestamp = (ts: MessageTimestamp) => { if (!ts) return "00:00:00"; return `${ts.hour.toString().padStart(2, "0")}:${ts.minute.toString().padStart(2, "0")}:${ts.second.toString().padStart(2, "0")}`; }; + +export const detectExtraBoards = ( + activeFilters: TabFilter | undefined, + boards: BoardName[], +) => + Object.keys(activeFilters || {}).filter( + (key) => !boards.includes(key), + ) as BoardName[]; From e2fc74e5c45e76216556df70f34461db2ab1e779 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:57:43 +0100 Subject: [PATCH 26/37] feat: add backend logs window to electron --- electron-app/main.js | 16 +++++++++--- electron-app/package.json | 1 + electron-app/preload.js | 3 +++ electron-app/src/processes/backend.js | 34 +++++++++++++++++++++++--- electron-app/src/windows/logWindow.js | 26 ++++++++++++++++++++ electron-app/src/windows/mainWindow.js | 14 +++++++---- pnpm-lock.yaml | 17 +++++++++++++ 7 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 electron-app/src/windows/logWindow.js diff --git a/electron-app/main.js b/electron-app/main.js index fd7a0a40d..ea52752c4 100644 --- a/electron-app/main.js +++ b/electron-app/main.js @@ -4,13 +4,14 @@ * Handles application lifecycle, initialization, and cleanup of processes and windows. */ -import { app, BrowserWindow, dialog } from "electron"; +import { app, BrowserWindow, dialog, screen } from "electron"; import pkg from "electron-updater"; import { getConfigManager } from "./src/config/configInstance.js"; import { setupIpcHandlers } from "./src/ipc/handlers.js"; import { startBackend, stopBackend } from "./src/processes/backend.js"; import { stopPacketSender } from "./src/processes/packetSender.js"; import { logger } from "./src/utils/logger.js"; +import { createLogWindow } from "./src/windows/logWindow.js"; import { createWindow } from "./src/windows/mainWindow.js"; const { autoUpdater } = pkg; @@ -38,15 +39,22 @@ app.setName("hyperloop-control-station"); // App lifecycle: wait for Electron to be ready app.whenReady().then(async () => { + // Get the screen width and height + // Only can be used inside app.whenReady() + const { width: screenWidth, height: screenHeight } = + screen.getPrimaryDisplay().workAreaSize; + // Initialize ConfigManager and ensure config exists BEFORE starting backend logger.electron.header("Initializing configuration..."); // Get ConfigManager instance (creates config from template if needed) await getConfigManager(); logger.electron.header("Configuration ready"); + const logWindow = createLogWindow(screenWidth, screenHeight); + // Start backend process try { - await startBackend(); + await startBackend(logWindow); logger.electron.header("Backend process spawned"); } catch (error) { // Start backend already shows these errors @@ -54,7 +62,9 @@ app.whenReady().then(async () => { } // Create main application window - createWindow(); + const mainWindow = createWindow(screenWidth, screenHeight); + mainWindow.maximize(); + logger.electron.header("Main application window created"); // Updater setup diff --git a/electron-app/package.json b/electron-app/package.json index 30344aa16..c5957648d 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", + "ansi-to-html": "^0.7.2", "electron-store": "^11.0.2", "electron-updater": "^6.7.3", "picocolors": "^1.1.1" diff --git a/electron-app/preload.js b/electron-app/preload.js index 761fa8dcf..2bc27909c 100644 --- a/electron-app/preload.js +++ b/electron-app/preload.js @@ -36,4 +36,7 @@ contextBridge.exposeInMainWorld("electronAPI", { importConfig: () => ipcRenderer.invoke("import-config"), // Open folder selection dialog selectFolder: () => ipcRenderer.invoke("select-folder"), + // Receive log message from backend + onLog: (callback) => + ipcRenderer.on("log", (_event, value) => callback(value)), }); diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js index 747336e3d..9075f3b45 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -4,6 +4,7 @@ * Handles starting, stopping, and restarting the backend process with proper error handling and logging. */ +import AnsiToHtml from "ansi-to-html"; import { spawn } from "child_process"; import { app, dialog } from "electron"; import fs from "fs"; @@ -15,6 +16,9 @@ import { getUserConfigPath, } from "../utils/paths.js"; +// Create ANSI to HTML converter +const convert = new AnsiToHtml(); + // Get the application root path const appPath = getAppPath(); @@ -30,7 +34,7 @@ let lastBackendError = null; * @example * startBackend(); */ -function startBackend() { +function startBackend(logWindow = null) { return new Promise((resolve, reject) => { // Get paths for binary and config const backendBin = getBinaryPath("backend"); @@ -63,6 +67,12 @@ function startBackend() { // Log stdout output from backend backendProcess.stdout.on("data", (data) => { logger.backend.info(`${data.toString().trim()}`); + + // Send log message to log window + if (logWindow) { + const htmlData = convert.toHtml(data.toString().trim()); + logWindow.webContents.send("log", htmlData); + } }); // Capture stderr output (where Go errors/panics are written) @@ -71,6 +81,12 @@ function startBackend() { logger.backend.error(errorMsg); // Store the last error message lastBackendError = errorMsg; + + // Send error message to log window + if (logWindow) { + const htmlError = convert.toHtml(errorMsg); + logWindow.webContents.send("log", htmlError); + } }); // Handle spawn errors @@ -120,8 +136,20 @@ function stopBackend() { // Only stop if process exists and is still running if (backendProcess && !backendProcess.killed) { logger.backend.info("Stopping backend..."); - // Send termination signal - backendProcess.kill("SIGTERM"); + + backendProcess.stdin.end(); + + const fallbackTimer = setTimeout(() => { + if (backendProcess && !backendProcess.killed) { + logger.backend.warning( + "Backend did not exit gracefully, force killing..." + ); + backendProcess.kill("SIGKILL"); + } + }, 2000); + + fallbackTimer.unref(); + // Clear the process reference backendProcess = null; } diff --git a/electron-app/src/windows/logWindow.js b/electron-app/src/windows/logWindow.js new file mode 100644 index 000000000..7aac0e928 --- /dev/null +++ b/electron-app/src/windows/logWindow.js @@ -0,0 +1,26 @@ +import { BrowserWindow } from "electron"; +import path from "path"; + +import { getAppPath } from "../utils/paths.js"; + +// Get the application root path +const appPath = getAppPath(); + +export const createLogWindow = (screenWidth, screenHeight) => { + const logWindow = new BrowserWindow({ + x: Math.floor(screenWidth * 0.65), + y: 0, + width: Math.floor(screenWidth * 0.35), + height: screenHeight, + title: "Backend Logs", + webPreferences: { + preload: path.join(appPath, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + }, + }); + + logWindow.loadFile(path.join(appPath, "src/logs/logs.html")); + + return logWindow; +}; diff --git a/electron-app/src/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js index 259f8dc79..213dc03e9 100644 --- a/electron-app/src/windows/mainWindow.js +++ b/electron-app/src/windows/mainWindow.js @@ -24,13 +24,15 @@ let currentView = "testing-view"; * @example * createWindow(); */ -function createWindow() { +function createWindow(screenWidth, screenHeight) { // Create new browser window with configuration mainWindow = new BrowserWindow({ - width: 1920, - height: 1080, - minWidth: 1280, - minHeight: 720, + x: 0, + y: 0, + width: screenWidth, + height: screenHeight, + minWidth: 800, + minHeight: 600, webPreferences: { // Path to preload script for secure IPC preload: path.join(appPath, "preload.js"), @@ -60,6 +62,8 @@ function createWindow() { mainWindow.on("closed", () => { mainWindow = null; }); + + return mainWindow; } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9698bd788..29d83d227 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: '@iarna/toml': specifier: ^2.2.5 version: 2.2.5 + ansi-to-html: + specifier: ^0.7.2 + version: 0.7.2 electron-store: specifier: ^11.0.2 version: 11.0.2 @@ -1805,6 +1808,11 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansi-to-html@0.7.2: + resolution: {integrity: sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==} + engines: {node: '>=8.0.0'} + hasBin: true + app-builder-bin@4.0.0: resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==} @@ -2392,6 +2400,9 @@ packages: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -6218,6 +6229,10 @@ snapshots: ansi-styles@6.2.3: {} + ansi-to-html@0.7.2: + dependencies: + entities: 2.2.0 + app-builder-bin@4.0.0: {} app-builder-bin@5.0.0-alpha.12: {} @@ -7045,6 +7060,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + entities@2.2.0: {} + env-paths@2.2.1: {} env-paths@3.0.0: {} From df01f6b6ebfb90f461e7ba752257a1a0a329f1b9 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:40:35 +0100 Subject: [PATCH 27/37] feat: fix menus --- electron-app/src/menu/menu.js | 14 +++++++++++--- electron-app/src/windows/mainWindow.js | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/electron-app/src/menu/menu.js b/electron-app/src/menu/menu.js index 64f776da9..c2000533d 100644 --- a/electron-app/src/menu/menu.js +++ b/electron-app/src/menu/menu.js @@ -30,7 +30,11 @@ function createMenu(mainWindow) { { label: "Reload", accelerator: "CmdOrCtrl+R", - click: () => mainWindow.reload(), + click: (_, browserWindow) => { + if (browserWindow) { + browserWindow.reload(); + } + }, }, { type: "separator" }, { @@ -61,7 +65,11 @@ function createMenu(mainWindow) { { label: "Toggle DevTools", accelerator: "F12", - click: () => mainWindow.webContents.toggleDevTools(), + click: (_, browserWindow) => { + if (browserWindow) { + browserWindow.webContents.toggleDevTools(); + } + }, }, ], }, @@ -118,7 +126,7 @@ function createMenu(mainWindow) { ]; const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); + return menu; } export { createMenu }; diff --git a/electron-app/src/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js index 213dc03e9..7b9d8a5f0 100644 --- a/electron-app/src/windows/mainWindow.js +++ b/electron-app/src/windows/mainWindow.js @@ -51,7 +51,8 @@ function createWindow(screenWidth, screenHeight) { loadView(currentView); // Create application menu - createMenu(mainWindow); + const menu = createMenu(mainWindow); + mainWindow.setApplicationMenu(menu); // Open DevTools in development mode if (!app.isPackaged) { From 050b5112f269d08615e3fe1378e74473695fe223 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:43:33 +0100 Subject: [PATCH 28/37] feat: update README.md --- electron-app/src/windows/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/electron-app/src/windows/README.md b/electron-app/src/windows/README.md index bb4e4ec5a..c13435b7a 100644 --- a/electron-app/src/windows/README.md +++ b/electron-app/src/windows/README.md @@ -4,24 +4,25 @@ Window management module for the Electron application. Handles creation, configu ## Overview -Manages the primary Electron `BrowserWindow` instance and provides functionality for switching between different application views (Competition View and Testing View). +Manages the primary and logs Electron `BrowserWindow` instances and provides functionality for switching between different application views (Competition View and Testing View). ## Files +- `logWindow.js` - Backend logs and messages - `mainWindow.js` - Main window creation and management ## Window Configuration - **Default Size**: 1920x1080 pixels -- **Minimum Size**: 1280x720 pixels +- **Minimum Size**: 800x600 pixels - **Title**: "Hyperloop Control Station" - **Background Color**: `#1a1a1a` (dark theme) - **Security**: Context isolation enabled, node integration disabled ## Available Views -- **Ethernet View** (default) - Testing interface, loads from `renderer/ethernet-view/index.html` -- **Control Station** - Competition interface, loads from `renderer/control-station/index.html` +- **Testing View** (default) - Testing interface, loads from `renderer/testing-view/index.html` +- **Competition View** - Competition interface, loads from `renderer/competition-view/index.html` ## Functions From e4ce2d00393124c62adf495127696585685ab274 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:03:40 +0100 Subject: [PATCH 29/37] fix: logs path --- electron-app/src/windows/logWindow.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electron-app/src/windows/logWindow.js b/electron-app/src/windows/logWindow.js index 7aac0e928..fe0d09106 100644 --- a/electron-app/src/windows/logWindow.js +++ b/electron-app/src/windows/logWindow.js @@ -20,7 +20,8 @@ export const createLogWindow = (screenWidth, screenHeight) => { }, }); - logWindow.loadFile(path.join(appPath, "src/logs/logs.html")); + const logFilePath = path.join(appPath, "src", "logs", "logs.html"); + logWindow.loadFile(logFilePath); return logWindow; }; From e804e4b9d472d09ca94f602f1c5f657f5b3382ab Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:33:52 +0100 Subject: [PATCH 30/37] feat: rename folders --- electron-app/src/log-viewer/index.html | 34 ++++++++++++++++++++++++++ electron-app/src/windows/logWindow.js | 2 +- electron-app/src/windows/mainWindow.js | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 electron-app/src/log-viewer/index.html diff --git a/electron-app/src/log-viewer/index.html b/electron-app/src/log-viewer/index.html new file mode 100644 index 000000000..2513be5f8 --- /dev/null +++ b/electron-app/src/log-viewer/index.html @@ -0,0 +1,34 @@ + + + + Backend Logs + + + +
+ + + + diff --git a/electron-app/src/windows/logWindow.js b/electron-app/src/windows/logWindow.js index fe0d09106..4c1c0e27e 100644 --- a/electron-app/src/windows/logWindow.js +++ b/electron-app/src/windows/logWindow.js @@ -20,7 +20,7 @@ export const createLogWindow = (screenWidth, screenHeight) => { }, }); - const logFilePath = path.join(appPath, "src", "logs", "logs.html"); + const logFilePath = path.join(appPath, "src", "log-viewer", "index.html"); logWindow.loadFile(logFilePath); return logWindow; diff --git a/electron-app/src/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js index 7b9d8a5f0..a6b41380c 100644 --- a/electron-app/src/windows/mainWindow.js +++ b/electron-app/src/windows/mainWindow.js @@ -52,7 +52,7 @@ function createWindow(screenWidth, screenHeight) { // Create application menu const menu = createMenu(mainWindow); - mainWindow.setApplicationMenu(menu); + mainWindow.setMenu(menu); // Open DevTools in development mode if (!app.isPackaged) { From 8b64a887353f1a59378c04c36779b175973b378a Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:44:50 +0100 Subject: [PATCH 31/37] fix: scrolling --- electron-app/src/log-viewer/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-app/src/log-viewer/index.html b/electron-app/src/log-viewer/index.html index 2513be5f8..c9496d24a 100644 --- a/electron-app/src/log-viewer/index.html +++ b/electron-app/src/log-viewer/index.html @@ -27,7 +27,7 @@ // Use innerHTML to render the colors, but be careful of XSS if logs are untrusted entry.innerHTML = data; container.appendChild(entry); - container.scrollTop = container.scrollHeight; + window.scrollTo(0, document.body.scrollHeight); }); From e5bc6cc18cec3a956725e6a3884cadb576649796 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:04:08 +0100 Subject: [PATCH 32/37] feat: make main window reload after restart, handle better backend start and restart --- electron-app/src/ipc/handlers.js | 11 ++- electron-app/src/processes/backend.js | 88 +++++++++++++------ electron-app/src/windows/mainWindow.js | 14 ++- frontend/testing-view/src/hooks/useAppMode.ts | 1 - 4 files changed, 81 insertions(+), 33 deletions(-) diff --git a/electron-app/src/ipc/handlers.js b/electron-app/src/ipc/handlers.js index 2aefe2bf4..79d8c1075 100644 --- a/electron-app/src/ipc/handlers.js +++ b/electron-app/src/ipc/handlers.js @@ -19,6 +19,7 @@ import { getCurrentView, getMainWindow, loadView, + reloadWindow, } from "../windows/mainWindow.js"; /** @@ -61,7 +62,10 @@ function setupIpcHandlers() { ipcMain.handle("save-config", async (event, config) => { try { await writeConfig(config); - restartBackend(); + await restartBackend(); + + reloadWindow(); + return true; } catch (error) { logger.electron.error("Error saving config:", error); @@ -96,7 +100,10 @@ function setupIpcHandlers() { ipcMain.handle("import-config", async () => { try { await importConfig(); - restartBackend(); + await restartBackend(); + + reloadWindow(); + return true; } catch (error) { logger.electron.error("Error importing config:", error); diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js index 9075f3b45..3c2c9720d 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -25,6 +25,9 @@ const appPath = getAppPath(); // Store the backend process instance let backendProcess = null; +// Common log window instance for all backend processes +let storedLogWindow = null; + // Store error messages (keep last 10 lines to avoid memory issues) let lastBackendError = null; @@ -34,7 +37,13 @@ let lastBackendError = null; * @example * startBackend(); */ -function startBackend(logWindow = null) { +async function startBackend(logWindow = null) { + if (logWindow) { + storedLogWindow = logWindow; + } + + const currentLogWindow = logWindow || storedLogWindow; + return new Promise((resolve, reject) => { // Get paths for binary and config const backendBin = getBinaryPath("backend"); @@ -69,9 +78,9 @@ function startBackend(logWindow = null) { logger.backend.info(`${data.toString().trim()}`); // Send log message to log window - if (logWindow) { + if (currentLogWindow && !currentLogWindow.isDestroyed()) { const htmlData = convert.toHtml(data.toString().trim()); - logWindow.webContents.send("log", htmlData); + currentLogWindow.webContents.send("log", htmlData); } }); @@ -83,9 +92,9 @@ function startBackend(logWindow = null) { lastBackendError = errorMsg; // Send error message to log window - if (logWindow) { + if (currentLogWindow && !currentLogWindow.isDestroyed()) { const htmlError = convert.toHtml(errorMsg); - logWindow.webContents.send("log", htmlError); + currentLogWindow.webContents.send("log", htmlError); } }); @@ -102,7 +111,7 @@ function startBackend(logWindow = null) { // If the backend didn't fail in this period of time, resolve the promise setTimeout(() => { resolve(backendProcess); - }, 4000); + }, 2000); // Handle process exit backendProcess.on("close", (code) => { @@ -127,32 +136,46 @@ function startBackend(logWindow = null) { } /** - * Stops the backend process by sending a SIGTERM signal. + * Stops the backend process by sending a SIGTERM and std.in.end() signal. + * If the process does not exit gracefully after defined time, it will be force killed. * @returns {void} * @example * stopBackend(); */ -function stopBackend() { - // Only stop if process exists and is still running - if (backendProcess && !backendProcess.killed) { - logger.backend.info("Stopping backend..."); - - backendProcess.stdin.end(); - - const fallbackTimer = setTimeout(() => { - if (backendProcess && !backendProcess.killed) { - logger.backend.warning( - "Backend did not exit gracefully, force killing..." - ); - backendProcess.kill("SIGKILL"); - } - }, 2000); +async function stopBackend() { + return new Promise((resolve, reject) => { + const localBackendProcess = backendProcess; - fallbackTimer.unref(); + // Only stop if process exists and is still running + if (localBackendProcess && !localBackendProcess.killed) { + logger.backend.info("Stopping backend..."); - // Clear the process reference - backendProcess = null; - } + localBackendProcess.once("close", () => { + // Clear the process reference + if (localBackendProcess === backendProcess) { + backendProcess = null; + } + resolve(); + }); + + localBackendProcess.kill("SIGTERM"); + localBackendProcess.stdin.end(); + + const fallbackTimer = setTimeout(() => { + if (localBackendProcess && !localBackendProcess.killed) { + logger.backend.warning( + "Backend did not exit gracefully, force killing..." + ); + localBackendProcess.kill("SIGKILL"); + } + }, 2000); + + fallbackTimer.unref(); + } else { + logger.backend.warning("Backend process not found, skipping stop..."); + resolve(); + } + }); } /** @@ -161,11 +184,18 @@ function stopBackend() { * @example * restartBackend(); */ -function restartBackend() { +async function restartBackend() { // Stop current process first - stopBackend(); + await stopBackend(); + // Start a new process - startBackend(); + try { + await startBackend(); + logger.electron.info("Backend restarted successfully"); + } catch (error) { + logger.electron.error("Failed to restart backend:", error); + throw error; // Let the IPC handler know it failed + } } export { restartBackend, startBackend, stopBackend }; diff --git a/electron-app/src/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js index a6b41380c..ae5fc29b2 100644 --- a/electron-app/src/windows/mainWindow.js +++ b/electron-app/src/windows/mainWindow.js @@ -98,6 +98,18 @@ function loadView(view) { } } +/** + * Reloads the main window. + * @returns {void} + * @example + * reloadWindow(); + */ +function reloadWindow() { + if (mainWindow) { + mainWindow.reload(); + } +} + /** * Returns the name of the currently loaded view. * @returns {string} The current view name (e.g., "ethernet-view", "control-station"). @@ -124,4 +136,4 @@ function getMainWindow() { return mainWindow; } -export { createWindow, getCurrentView, getMainWindow, loadView }; +export { createWindow, getCurrentView, getMainWindow, loadView, reloadWindow }; diff --git a/frontend/testing-view/src/hooks/useAppMode.ts b/frontend/testing-view/src/hooks/useAppMode.ts index e390a0cc4..4e279b3be 100644 --- a/frontend/testing-view/src/hooks/useAppMode.ts +++ b/frontend/testing-view/src/hooks/useAppMode.ts @@ -31,7 +31,6 @@ export function useAppMode( // logger.testingView.log("[DEBUG] isDev", isDev); // logger.testingView.log("[DEBUG] isLoading", isLoading); // logger.testingView.log("[DEBUG] hasData", hasData); - // logger.testingView.log("[DEBUG] backendConnected", backendConnected); // logger.testingView.log("[DEBUG] hasError", hasError); if (isLoading || isRestarting) return "loading"; From 58bff09e2607ba0fa96271bd63ec6db38dae2085 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:05:52 +0100 Subject: [PATCH 33/37] fix: fs import --- electron-app/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/electron-app/main.js b/electron-app/main.js index ea52752c4..c468fbbe3 100644 --- a/electron-app/main.js +++ b/electron-app/main.js @@ -6,6 +6,7 @@ import { app, BrowserWindow, dialog, screen } from "electron"; import pkg from "electron-updater"; +import fs from "fs"; import { getConfigManager } from "./src/config/configInstance.js"; import { setupIpcHandlers } from "./src/ipc/handlers.js"; import { startBackend, stopBackend } from "./src/processes/backend.js"; From 86f0f5df389e67f121883b3b4f83d9869a36da23 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:16:45 +0100 Subject: [PATCH 34/37] docs: update README.md --- electron-app/src/windows/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electron-app/src/windows/README.md b/electron-app/src/windows/README.md index c13435b7a..7698de6aa 100644 --- a/electron-app/src/windows/README.md +++ b/electron-app/src/windows/README.md @@ -30,6 +30,10 @@ Manages the primary and logs Electron `BrowserWindow` instances and provides fun Creates and initializes the main application window. Loads default view, sets up menu, and opens DevTools in development mode. +### `reloadWindow()` + +Reloads main window. + ### `loadView(view)` Switches the main window to display a different view. Updates window title and validates view file exists. From aa5ab6f2795cd1012271f93deb29873aeb736e4a Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:07:31 +0100 Subject: [PATCH 35/37] feat: implement icons-master --- frontend/frontend-kit/ui/package.json | 5 ++++- frontend/frontend-kit/ui/src/icons/arrows.ts | 2 +- pnpm-lock.yaml | 10 ++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/frontend-kit/ui/package.json b/frontend/frontend-kit/ui/package.json index 9c26d70ce..78d4fb424 100644 --- a/frontend/frontend-kit/ui/package.json +++ b/frontend/frontend-kit/ui/package.json @@ -5,7 +5,9 @@ "private": true, "scripts": { "preinstall": "npx only-allow pnpm", - "lint": "eslint ." + "lint": "eslint .", + "icon:add": "icons-master add", + "icon:remove": "icons-master remove" }, "dependencies": { "@radix-ui/react-checkbox": "^1.3.3", @@ -34,6 +36,7 @@ "zustand": "^5.0.11" }, "devDependencies": { + "@maximka76667/icons-master": "^1.0.1", "@tailwindcss/postcss": "^4.1.18", "@turbo/gen": "^2.8.3", "@types/node": "^25.2.0", diff --git a/frontend/frontend-kit/ui/src/icons/arrows.ts b/frontend/frontend-kit/ui/src/icons/arrows.ts index 5f6067552..7c8bbb5dc 100644 --- a/frontend/frontend-kit/ui/src/icons/arrows.ts +++ b/frontend/frontend-kit/ui/src/icons/arrows.ts @@ -2,8 +2,8 @@ export { ChevronDown, ChevronLeft, ChevronRight, - ChevronsUpDown, ChevronUp, + ChevronsUpDown, Play, RefreshCw, TrendingDown, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29d83d227..0597f9802 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,9 @@ importers: specifier: ^5.0.11 version: 5.0.11(@types/react@19.2.11)(immer@11.1.0)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: + '@maximka76667/icons-master': + specifier: ^1.0.1 + version: 1.0.1 '@tailwindcss/postcss': specifier: ^4.1.18 version: 4.1.18 @@ -756,6 +759,10 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} + '@maximka76667/icons-master@1.0.1': + resolution: {integrity: sha512-2mQG0k3p3c2b8KctuH/1KGgy0nlBMTyzC0hguEDNNA3bYzwiehwGJ63bspbQvAbNHxENJ8YhdJFvWX4tDK+W/g==} + hasBin: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4244,6 +4251,7 @@ packages: tar@7.5.7: resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me temp-file@3.4.0: resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} @@ -5189,6 +5197,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@maximka76667/icons-master@1.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 From cc058d95570f8d78299ddc3e20c655877c078956 Mon Sep 17 00:00:00 2001 From: Maxim <74974283+maximka76667@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:19:47 +0100 Subject: [PATCH 36/37] docs: add README.md --- frontend/frontend-kit/ui/README.md | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 frontend/frontend-kit/ui/README.md diff --git a/frontend/frontend-kit/ui/README.md b/frontend/frontend-kit/ui/README.md new file mode 100644 index 000000000..713357c0a --- /dev/null +++ b/frontend/frontend-kit/ui/README.md @@ -0,0 +1,40 @@ +# UI Package - Frontend Kit + +This package is the main UI and React shared component library for the Hyperloop Control Station. It provides reusable, styled components and hooks built on top of **React**, **Shadcn/UI**, and **Tailwind CSS**. + +## Project Guidelines + +### React-Only Logic + +> This package is specifically for **UI and React-related components**. +> +> - If your logic requires React hooks (`useState`, `useEffect`, etc.) or TSX, it belongs here. +> - **If your logic does NOT need React** (e.g., websocket connections), it should be implemented in the `@workspace/core` package. This keeps the codebase clean and allows for better reuse across different environments. + +--- + +## Icon Management + +We use a custom Rust-based tool, **icons-master**, to manage our Lucide icon exports. This tool helps keep our icons organized by category and ensures we don't have duplicate exports. + +> **⚠️ Windows Support Only** +> +> The `icons-master` CLI currently only supports **Windows**. If you are on macOS or Linux, you must manually update the `.ts` files in `src/icons/`. + +### Usage + +Here are the scripts you can run: + +```bash +# Install dependencies +pnpm install + +# Run linter +pnpm lint + +# Add a new icon from Lucide (Example: pnpm icon:add arrow-up src/icons) +pnpm icon:add src/icons + +# Remove an existing icon export (Example: pnpm icon:remove arrow-up src/icons) +pnpm icon:remove src/icons +``` From 162c815c16609567331b3a89e279e6a11115631d Mon Sep 17 00:00:00 2001 From: Vasyl Date: Mon, 2 Mar 2026 20:40:26 +0100 Subject: [PATCH 37/37] chore(backend): move adj pkg --- backend/{internal => pkg}/adj/adj.go | 0 backend/{internal => pkg}/adj/boards.go | 0 backend/{internal => pkg}/adj/git.go | 0 backend/{internal => pkg}/adj/models.go | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename backend/{internal => pkg}/adj/adj.go (100%) rename backend/{internal => pkg}/adj/boards.go (100%) rename backend/{internal => pkg}/adj/git.go (100%) rename backend/{internal => pkg}/adj/models.go (100%) diff --git a/backend/internal/adj/adj.go b/backend/pkg/adj/adj.go similarity index 100% rename from backend/internal/adj/adj.go rename to backend/pkg/adj/adj.go diff --git a/backend/internal/adj/boards.go b/backend/pkg/adj/boards.go similarity index 100% rename from backend/internal/adj/boards.go rename to backend/pkg/adj/boards.go diff --git a/backend/internal/adj/git.go b/backend/pkg/adj/git.go similarity index 100% rename from backend/internal/adj/git.go rename to backend/pkg/adj/git.go diff --git a/backend/internal/adj/models.go b/backend/pkg/adj/models.go similarity index 100% rename from backend/internal/adj/models.go rename to backend/pkg/adj/models.go