diff --git a/.github/workflows/build-backend.yaml b/.github/workflows/build-backend.yaml new file mode 100644 index 000000000..c5a1c3b74 --- /dev/null +++ b/.github/workflows/build-backend.yaml @@ -0,0 +1,148 @@ +name: Build backend + +on: + workflow_call: + secrets: + SECRET: + required: true + workflow_dispatch: + +env: + SECRET: ${{ secrets.SECRET }} + +jobs: + build-backend-linux: + name: "Build backend for linux" + runs-on: ubuntu-latest + + # Runs on alpine because it is easier to staticly link the library + container: + image: golang:alpine + + env: + BACKEND_DIR: ./backend + + steps: + - name: "Install packages" + run: apk update && apk add --no-cache libpcap-dev musl-dev gcc go + + - uses: actions/checkout@v3 + + - name: "Create output path" + working-directory: "${{env.BACKEND_DIR}}" + run: mkdir ./output + + - name: "Load secret" + working-directory: "${{env.BACKEND_DIR}}" + run: | + ./load-secret.sh "$SECRET" + + - name: "Build (64 bit)" + working-directory: "${{env.BACKEND_DIR}}/cmd" + env: + CGO_ENABLED: 1 + GOARCH: amd64 + GOOS: linux + run: | + go build -ldflags '-linkmode external -extldflags "-static"' -o ../output/backend-linux-64 + + - name: "Upload build" + uses: actions/upload-artifact@v3 + with: + name: backend-linux + path: "${{env.BACKEND_DIR}}/output/*" + + build-backend-windows: + name: "Build backend for windows" + runs-on: windows-latest + + env: + BACKEND_DIR: ".\\backend" + + steps: + - uses: actions/checkout@v3 + + - name: "Setup Go" + uses: actions/setup-go@v4 + with: + go-version: "1.19" + cache-dependency-path: "${{env.BACKEND_DIR}}\\go.sum" + + - name: "Load secret" + working-directory: "${{env.BACKEND_DIR}}" + run: | + echo "${{env.SECRET}}" > ".\internal\excel\secret.json" && + echo "${{env.SECRET}}" > ".\internal\excel_adapter\internals\secret.json" && + echo "${{env.SECRET}}" > ".\pkg\excel\secret.json" && + echo "${{env.SECRET}}" > ".\pkg\excel_adapter\internals\secret.json" + + - name: "Create output path" + working-directory: "${{env.BACKEND_DIR}}" + run: mkdir .\output + + - name: "Build (64 bit)" + working-directory: "${{env.BACKEND_DIR}}\\cmd" + env: + CGO_ENABLED: 1 + GOARCH: amd64 + GOOS: windows + run: | + go build -o ..\output\backend-windows-64.exe + + - name: "Upload build" + uses: actions/upload-artifact@v3 + with: + name: backend-windows + path: "${{env.BACKEND_DIR}}\\output\\*" + + build-backend-mac: + name: "Build backend for macOS" + runs-on: macos-latest + + env: + BACKEND_DIR: ./backend + + steps: + - name: "Install packages" + run: brew install libpcap + + - name: "Setup Go" + uses: actions/setup-go@v4 + with: + go-version: "1.19" + cache-dependency-path: "${{env.BACKEND_DIR}}/go.sum" + + - uses: actions/checkout@v3 + + - name: "Create output path" + working-directory: "${{env.BACKEND_DIR}}" + run: mkdir ./output + + - name: "Load secret" + working-directory: "${{env.BACKEND_DIR}}" + run: | + ./load-secret.sh "$SECRET" + + - name: "Build (64 bit)" + working-directory: "${{env.BACKEND_DIR}}/cmd" + env: + CGO_ENABLED: 1 + GOARCH: amd64 + GOOS: darwin + run: | + go build -o ../output/backend-macos-64 + + - name: "Build (apple 64 bit)" + working-directory: "${{env.BACKEND_DIR}}/cmd" + env: + CGO_ENABLED: 1 + GOARCH: arm64 + GOOS: darwin + run: | + go build -o ../output/backend-macos-m1-64 + + - name: "Upload build" + uses: actions/upload-artifact@v3 + with: + name: backend-macos + path: "${{env.BACKEND_DIR}}/output/*" diff --git a/.github/workflows/build-ethernet-view.yaml b/.github/workflows/build-ethernet-view.yaml new file mode 100644 index 000000000..2c8347c83 --- /dev/null +++ b/.github/workflows/build-ethernet-view.yaml @@ -0,0 +1,42 @@ +name: Build ethernet view + +on: + workflow_call: + workflow_dispatch: + +jobs: + build-ethernet-view: + name: "Build ethernet view" + runs-on: ubuntu-latest + + env: + FRONTEND_DIR: ./ethernet-view + COMMON_DIR: ./common-front + + steps: + - uses: actions/checkout@v3 + + - name: "Install common front dependencies" + working-directory: "${{env.COMMON_DIR}}" + run: npm install + + + - name: "Build common front" + working-directory: "${{env.COMMON_DIR}}" + run: npm run build + + - name: "Install ethernet view dependencies" + working-directory: "${{env.FRONTEND_DIR}}" + run: npm install + + + - name: "Build ethernet view" + working-directory: "${{env.FRONTEND_DIR}}" + run: npm run build + + + - name: "Upload build" + uses: actions/upload-artifact@v3 + with: + name: ethernet-view + path: "${{env.FRONTEND_DIR}}/static/*" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 5ab6d802d..000000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,84 +0,0 @@ -name: Build - -on: - workflow_call: - secrets: - SECRET: - required: true - -jobs: - build-linux: - name: "Build for linux" - runs-on: ubuntu-latest - - container: - image: golang:alpine - - steps: - - name: "Install packages" - run: apk update && apk add --no-cache libpcap-dev musl-dev gcc go - - - uses: actions/checkout@v3 - - - name: "Create output path" - run: mkdir ./output - - - name: "Load secret" - env: - SECRET: ${{ secrets.SECRET }} - run: | - echo "$SECRET" > ./src/excel/secret.json - echo "$SECRET" > ./src/excel_adapter/internals/secret.json - - - name: "Build for amd46 (64 bit)" - working-directory: ./src - env: - CGO_ENABLED: 1 - GOARCH: amd64 - GOOS: linux - run: | - go build -ldflags '-linkmode external -extldflags "-static"' -o ../output/backend-linux-64 - - - name: "Uplaod artifacts" - uses: actions/upload-artifact@v2 - with: - name: backend-linux - path: ./output/backend-linux-64 - - build-windows: - name: "Build for windows" - runs-on: windows-latest - - steps: - - uses: actions/checkout@v3 - - - name: "Setup Go" - uses: actions/setup-go@v4 - with: - go-version: "1.20" - cache-dependency-path: ".\\src\\go.sum" - - - name: "Load secret" - env: - SECRET: ${{ secrets.SECRET }} - run: | - echo "$env:SECRET" > .\src\excel\secret.json - echo "$env:SECRET" > .\src\excel_adapter\internals\secret.json - - - name: "Create output path" - run: mkdir .\output - - - name: "Build for amd46 (64 bit)" - working-directory: .\src - env: - CGO_ENABLED: 1 - GOARCH: amd64 - GOOS: windows - run: | - go build -ldflags '-linkmode external -extldflags "-static"' -o ..\output\backend-windows-64.exe - - - name: "Uplaod artifacts" - uses: actions/upload-artifact@v2 - with: - name: backend-windows - path: ./output/backend-windows-64.exe diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml deleted file mode 100644 index d689c7731..000000000 --- a/.github/workflows/pre-release.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: Create Release - -on: - push: - branches: - - main - - develop - -jobs: - build: - name: "Build" - uses: ./.github/workflows/build.yaml - secrets: - SECRET: ${{ secrets.GOOGLE_DRIVE_API }} - - release: - name: "Release" - needs: [build] - runs-on: ubuntu-latest - - steps: - - name: "Checkout" - uses: actions/checkout@v2 - - - name: "Download linux release" - uses: actions/download-artifact@v2 - with: - name: backend-linux - path: ./output - - - name: "Download windows release" - uses: actions/download-artifact@v2 - with: - name: backend-windows - path: ./output - - - name: "Create release" - uses: marvinpinto/action-automatic-releases@latest - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - title: preview - prerelease: true - automatic_release_tag: latest - files: | - output/backend-linux-64 - output/backend-windows-64.exe - examples/config.toml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index a806fac9e..000000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: Create Release - -on: - push: - tags: - - v* - -jobs: - build: - name: "Build" - uses: ./.github/workflows/build.yaml - secrets: - SECRET: ${{ secrets.GOOGLE_DRIVE_API }} - - release: - name: "Release" - needs: [build] - runs-on: ubuntu-latest - - steps: - - name: "Checkout" - uses: actions/checkout@v2 - - - name: "Download linux release" - uses: actions/download-artifact@v2 - with: - name: backend-linux - path: ./output - - - name: "Download windows release" - uses: actions/download-artifact@v2 - with: - name: backend-windows - path: ./output - - - name: "Create release" - uses: marvinpinto/action-automatic-releases@latest - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - prerelease: false - files: | - output/backend-linux-64 - output/backend-windows-64.exe - examples/config.toml diff --git a/.gitignore b/.gitignore index c741adab2..c86d00d2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ build -secret.json \ No newline at end of file + +# GOOGLE API KEY +secret.json + +# MacOS Files +.DS_Store diff --git a/backend/.gitignore b/backend/.gitignore index 25d9d64fb..efee91423 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -10,6 +10,8 @@ trace.json Backend-H8 backend !build +# MacOS build +cmd # EXCEL *.xlsx @@ -20,4 +22,4 @@ static downloads -audience_static \ No newline at end of file +audience_static diff --git a/backend/cmd/config.toml b/backend/cmd/config.toml index 1c87cd422..01c4d73bb 100644 --- a/backend/cmd/config.toml +++ b/backend/cmd/config.toml @@ -10,17 +10,6 @@ programable_boards = "/uploadableBoards" connections = "/backend" file_server = "/" -[server.audience] -address = "192.168.0.9:4000" -static = "./mobile_front" - -[server.audience.endpoints] -pod_data = "/podDataStructure" -order_data = "/orderStructures" -programable_boards = "/uploadableBoards" -connections = "/backend" -file_server = "/" - [vehicle] boards = ["BLCU"] @@ -46,9 +35,7 @@ add_state_orders_id_key = "add_state_orders" remove_state_orders_id_key = "remove_state_orders" [excel.download] -#id = "1XE9V2PI0hwSdAC8P6MePnSLyzADqsdWCOlx_kct7dps" -id="1b_nOrWqjMLOSEFIV9dMUObnJ15J7ypmF-KVJ4qztAtw" -#id = "1BEwASubu0el9oQA6PSwVKaNU-Q6gbJ40JR6kgqguKYE" +id="1NyNaAOw_6iWtnCpEg73AtSSFx1fMdhPRmmdOhjgjCZI" name = "ade.xlsx" path = "." diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 181ee9050..57659ec06 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -2,14 +2,18 @@ package main import ( "bufio" + "encoding/binary" "flag" "fmt" + "log" "net" "os" "os/signal" "path" "runtime" + "runtime/pprof" "strings" + "time" blcuPackage "github.com/HyperloopUPV-H8/h9-backend/internal/blcu" "github.com/HyperloopUPV-H8/h9-backend/internal/common" @@ -29,11 +33,21 @@ import ( "github.com/HyperloopUPV-H8/h9-backend/internal/state_space_logger" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory" "github.com/HyperloopUPV-H8/h9-backend/internal/value_logger" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/message_parser" vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" "github.com/HyperloopUPV-H8/h9-backend/internal/ws_handle" + "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/sniffer" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/tcp" + blcu_packet "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/blcu" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" + info_packet "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/info" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/order" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/state" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/presentation" "github.com/fatih/color" + "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "github.com/pelletier/go-toml/v2" trace "github.com/rs/zerolog/log" @@ -41,8 +55,10 @@ import ( var traceLevel = flag.String("trace", "info", "set the trace level (\"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\")") var traceFile = flag.String("log", "trace.json", "set the trace log file") +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") func main() { + flag.Parse() traceFile := initTrace(*traceLevel, *traceFile) defer traceFile.Close() @@ -52,41 +68,37 @@ func main() { defer RemovePid(pidPath) runtime.GOMAXPROCS(runtime.NumCPU()) - flag.Parse() - + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } config := getConfig("./config.toml") - // excelAdapter := excel_adapter.New(config.Excel) - // boards := excelAdapter.GetBoards() - // globalInfo := excelAdapter.GetGlobalInfo() - file, err := excel.Download(excel.DownloadConfig(config.Excel.Download)) - if err != nil { trace.Fatal().Err(err).Msg("downloading file") } ade, err := ade.CreateADE(file) - if err != nil { trace.Fatal().Err(err).Msg("creating ade") } info, err := info.NewInfo(ade.Info) - if err != nil { trace.Fatal().Err(err).Msg("creating info") } podData, err := pod_data.NewPodData(ade.Boards, info.Units) - if err != nil { fmt.Println(err) trace.Fatal().Err(err).Msg("creating podData") } - dataOnlyPodData := pod_data.GetDataOnlyPodData(podData) - dev, err := selectDev() if err != nil { trace.Fatal().Err(err).Msg("Error selecting device") @@ -97,50 +109,21 @@ func main() { connectionTransfer := connection_transfer.New(config.Connections) vehicleOrders, err := vehicle_models.NewVehicleOrders(podData.Boards, config.Excel.Parse.Global.BLCUAddressKey) - if err != nil { trace.Fatal().Err(err).Msg("creating vehicleOrders") } - orderTransfer, orderChannel := order_transfer.New() - - vehicle := vehicle.New(vehicle.VehicleConstructorArgs{ - PodData: podData, - Config: config.Vehicle, - Boards: podData.Boards, - Info: info, - OnConnectionChange: func(board string, isConnected bool) { - if !isConnected { - orderTransfer.ClearOrders(board) - } - connectionTransfer.Update(board, isConnected) - }, - }) - - var blcu blcuPackage.BLCU - blcuAddr, useBlcu := info.Addresses.Boards["BLCU"] - - if useBlcu { - blcu = blcuPackage.NewBLCU(net.TCPAddr{ - IP: blcuAddr, - Port: int(info.Ports.TFTP), - }, info.BoardIds, config.BLCU) - - blcu.SetSendOrder(vehicle.SendOrder) - } - - vehicleUpdates := make(chan vehicle_models.PacketUpdate, 1) - vehicleProtections := make(chan any) - vehicleTransmittedOrders := make(chan vehicle_models.PacketUpdate) - blcuAckChan := make(chan struct{}) - stateOrdersChan := make(chan message_parser.StateOrdersAdapter) - stateSpaceChan := make(chan vehicle_models.StateSpace) - + // <--- data transfer ---> dataTransfer := data_transfer.New(config.DataTransfer) go dataTransfer.Run() + // <--- message transfer ---> messageTransfer := message_transfer.New(config.Messages) + // <--- update factory ---> + updateFactory := update_factory.NewFactory() + + // <--- logger ---> packetLogger := packet_logger.NewPacketLogger(podData.Boards, config.PacketLogger) valueLogger := value_logger.NewValueLogger(podData.Boards, config.ValueLogger) orderLogger := order_logger.NewOrderLogger(podData.Boards, config.OrderLogger) @@ -154,69 +137,165 @@ func main() { "protections": &protectionLogger, "stateSpace": &stateSpaceLogger, } - loggerHandler := logger_handler.NewLoggerHandler(loggers, config.LoggerHandler) - websocketBroker := ws_handle.New() - defer websocketBroker.Close() + // <--- order transfer ---> + idToBoard := make(map[uint16]string) + for _, board := range podData.Boards { + for _, packet := range board.Packets { + idToBoard[packet.Id] = board.Name + } + } + orderTransfer, orderChannel := order_transfer.New(idToBoard) - if useBlcu { - websocketBroker.RegisterHandle(&blcu, config.BLCU.Topics.Upload, config.BLCU.Topics.Download) + // <--- blcu ---> + var blcu blcuPackage.BLCU + blcuAddr, useBlcu := info.Addresses.Boards["BLCU"] + + // <--- transport ---> + orders := make(map[abstraction.PacketId]struct{}) + for _, board := range podData.Boards { + for _, packet := range board.Packets { + if packet.Type == "order" { + orders[abstraction.PacketId(packet.Id)] = struct{}{} + } + } } - websocketBroker.RegisterHandle(&connectionTransfer, config.Connections.UpdateTopic, "connection/update") - websocketBroker.RegisterHandle(&dataTransfer, "podData/update") - websocketBroker.RegisterHandle(&loggerHandler, config.LoggerHandler.Topics.Enable) - websocketBroker.RegisterHandle(&messageTransfer, "message/update") - websocketBroker.RegisterHandle(&orderTransfer, config.Orders.SendTopic, "order/stateOrders") + transp := transport.NewTransport() + + prev := time.Now() + transp.SetAPI(&TransportAPI{ + OnNotification: func(notification abstraction.TransportNotification) { + packet := notification.(transport.PacketNotification) + switch p := packet.Packet.(type) { + case *data.Packet: + fmt.Println(time.Since(prev)) + prev = time.Now() + if _, ok := orders[p.Id()]; ok { + loggerHandler.Log(order_logger.LoggableOrder(*p)) + return + } + + update := updateFactory.NewUpdate(p) + dataTransfer.Update(update) + + loggerHandler.Log(packet_logger.ToLoggablePacket(p)) + + for id, value := range p.GetValues() { + loggerHandler.Log(value_logger.ToLoggableValue(string(id), value, p.Timestamp())) + } + case *info_packet.Packet: + messageTransfer.SendMessage(p) + loggerHandler.Log(protection_logger.LoggableInfo(*p)) + case *protection.Packet: + messageTransfer.SendMessage(p) + loggerHandler.Log(protection_logger.LoggableProtection(*p)) + case *blcu_packet.Ack: + if useBlcu { + blcu.NotifyAck() + } + case *state.Space: + for _, row := range p.State() { + loggerHandler.Log(state_space_logger.LoggableStateSpaceRow(row)) + } + case *order.Add: + orderTransfer.AddStateOrders(*p) + case *order.Remove: + orderTransfer.RemoveStateOrders(*p) + } + }, - go vehicle.Listen(vehicleUpdates, vehicleTransmittedOrders, vehicleProtections, blcuAckChan, stateOrdersChan, stateSpaceChan) + OnConnectionUpdate: func(target abstraction.TransportTarget, isConnected bool) {}, + }) - go startPacketUpdateRoutine(vehicleUpdates, &dataTransfer, &loggerHandler) - go startMessagesRoutine(vehicleProtections, &messageTransfer, &loggerHandler) - go startOrderRoutine(orderChannel, &vehicle, &loggerHandler) + // Load and set packet decoder and encoder + decoder, encoder := getTransportDecEnc(info, podData) + transp.WithDecoder(decoder).WithEncoder(encoder) - go func() { - for order := range vehicleTransmittedOrders { - loggable := order_logger.LoggableTransmittedOrder(order) - loggerHandler.Log(loggable) + // Set package id to target map + for _, board := range podData.Boards { + for _, packet := range board.Packets { + transp.SetIdTarget(abstraction.PacketId(packet.Id), abstraction.TransportTarget(board.Name)) } - }() + } - if useBlcu { - go func() { - for range blcuAckChan { - blcu.NotifyAck() - } - }() + // Start handling TCP client connections + backendTcpClientAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", info.Addresses.Backend, info.Ports.TcpClient)) + if err != nil { + panic("Failed to resolve local backend TCP client address") + } + serverTargets := make(map[string]abstraction.TransportTarget) + for _, board := range podData.Boards { + if !common.Contains(config.Vehicle.Boards, board.Name) { + serverTargets[fmt.Sprintf("%s:%d", info.Addresses.Boards[board.Name], info.Ports.TcpClient)] = abstraction.TransportTarget(board.Name) + continue + } + go transp.HandleClient(tcp.NewClient(backendTcpClientAddr), abstraction.TransportTarget(board.Name), "tcp", string(info.Addresses.Boards[board.Name])) } - go func() { - for stateSpace := range stateSpaceChan { - for _, row := range stateSpace { - loggerHandler.Log(state_space_logger.LoggableStateSpaceRow(row)) - } + // Start handling TCP server connections + go transp.HandleServer(tcp.NewServer(serverTargets), "tcp", fmt.Sprintf("%s:%d", info.Addresses.Backend, info.Ports.TcpServer)) - } - }() + // Start handling the sniffer + source, err := pcap.OpenLive(dev.Name, 1500, true, pcap.BlockForever) + if err != nil { + panic("failed to obtain sniffer source: " + err.Error()) + } + boardIps := make([]net.IP, 0) + for _, board := range info.Addresses.Boards { + boardIps = append(boardIps, board) + } + err = source.SetBPFFilter(getFilter(boardIps, info.Addresses.Backend, info.Ports.UDP, info.Ports.TcpClient, info.Ports.TcpServer)) + if err != nil { + panic("failed to compile bpf filter") + } + go transp.HandleSniffer(sniffer.New(source, &layers.LayerTypeEthernet)) + // <--- order transfer ---> go func() { - for stateOrders := range stateOrdersChan { - switch stateOrders.Action { - case message_parser.AddStateOrderKind: - orderTransfer.AddStateOrders(stateOrders.StateOrders) - case message_parser.RemoveStateOrderKind: - orderTransfer.RemoveStateOrders(stateOrders.StateOrders) + for order := range orderChannel { + err := transp.SendMessage(transport.NewPacketMessage(&order)) + if err != nil { + trace.Error().Any("order", order).Err(err).Msg("error sending order") } + + loggerHandler.Log(order_logger.LoggableOrder(order)) } }() + // <--- blcu ---> + if useBlcu { + blcu = blcuPackage.NewBLCU(net.TCPAddr{ + IP: blcuAddr, + Port: int(info.Ports.TFTP), + }, info.BoardIds, config.BLCU) + + blcu.SetSendOrder(func(o *data.Packet) error { + return transp.SendMessage(transport.NewPacketMessage(o)) + }) + } + + // <--- websocket broker ---> + websocketBroker := ws_handle.New() + defer websocketBroker.Close() + + if useBlcu { + websocketBroker.RegisterHandle(&blcu, config.BLCU.Topics.Upload, config.BLCU.Topics.Download) + } + + websocketBroker.RegisterHandle(&connectionTransfer, config.Connections.UpdateTopic, "connection/update") + websocketBroker.RegisterHandle(&dataTransfer, "podData/update") + websocketBroker.RegisterHandle(&loggerHandler, config.LoggerHandler.Topics.Enable) + websocketBroker.RegisterHandle(&messageTransfer, "message/update") + websocketBroker.RegisterHandle(&orderTransfer, config.Orders.SendTopic, "order/stateOrders") + uploadableBords := common.Filter(common.Keys(info.Addresses.Boards), func(item string) bool { return item != config.Excel.Parse.Global.BLCUAddressKey }) endpointData := server.EndpointData{ - PodData: dataOnlyPodData, + PodData: pod_data.GetDataOnlyPodData(podData), OrderData: vehicleOrders, ProgramableBoards: uploadableBords, } @@ -345,42 +424,144 @@ func getConfig(path string) Config { return config } -func startPacketUpdateRoutine(vehicleUpdates <-chan vehicle_models.PacketUpdate, dataTransfer *data_transfer.DataTransfer, loggerHandler *logger_handler.LoggerHandler) { - updateFactory := update_factory.NewFactory() +func getTransportDecEnc(info info.Info, podData pod_data.PodData) (*presentation.Decoder, *presentation.Encoder) { + decoder := presentation.NewDecoder(binary.LittleEndian) + encoder := presentation.NewEncoder(binary.LittleEndian) + + dataDecoder := data.NewDecoder(binary.LittleEndian) + dataEncoder := data.NewEncoder(binary.LittleEndian) + + ids := make([]abstraction.PacketId, 0) + for _, board := range podData.Boards { + for _, packet := range board.Packets { + descriptor := make(data.Descriptor, len(packet.Measurements)) + for i, measurement := range packet.Measurements { + switch meas := measurement.(type) { + case pod_data.NumericMeasurement: + switch meas.Type { + case "uint8": + descriptor[i] = data.NewNumericDescriptor[uint8](data.ValueName(meas.Id)) + case "uint16": + descriptor[i] = data.NewNumericDescriptor[uint16](data.ValueName(meas.Id)) + case "uint32": + descriptor[i] = data.NewNumericDescriptor[uint32](data.ValueName(meas.Id)) + case "uint64": + descriptor[i] = data.NewNumericDescriptor[uint64](data.ValueName(meas.Id)) + case "int8": + descriptor[i] = data.NewNumericDescriptor[int8](data.ValueName(meas.Id)) + case "int16": + descriptor[i] = data.NewNumericDescriptor[int16](data.ValueName(meas.Id)) + case "int32": + descriptor[i] = data.NewNumericDescriptor[int32](data.ValueName(meas.Id)) + case "int64": + descriptor[i] = data.NewNumericDescriptor[int64](data.ValueName(meas.Id)) + case "float32": + descriptor[i] = data.NewNumericDescriptor[float32](data.ValueName(meas.Id)) + case "float64": + descriptor[i] = data.NewNumericDescriptor[float64](data.ValueName(meas.Id)) + default: + panic(fmt.Sprintf("unexpected numeric type for %s: %s", meas.Id, meas.Type)) + } + case pod_data.BooleanMeasurement: + descriptor[i] = data.NewBooleanDescriptor(data.ValueName(meas.Id)) + case pod_data.EnumMeasurement: + enumDescriptor := make(data.EnumDescriptor, len(meas.Options)) + for j, option := range meas.Options { + enumDescriptor[j] = data.EnumVariant(option) + } + descriptor[i] = data.NewEnumDescriptor(data.ValueName(meas.Id), enumDescriptor) + default: + panic(fmt.Sprintf("unexpected measurement type: %T", measurement)) + } + } + dataDecoder.SetDescriptor(abstraction.PacketId(packet.Id), descriptor) + dataEncoder.SetDescriptor(abstraction.PacketId(packet.Id), descriptor) + ids = append(ids, abstraction.PacketId(packet.Id)) + } + } - for packetUpdate := range vehicleUpdates { - update := updateFactory.NewUpdate(packetUpdate) - dataTransfer.Update(update) + for _, id := range ids { + decoder.SetPacketDecoder(id, dataDecoder) + encoder.SetPacketEncoder(id, dataEncoder) + } - loggerHandler.Log(packet_logger.ToLoggablePacket(packetUpdate)) + decoder.SetPacketDecoder(abstraction.PacketId(info.MessageIds.BlcuAck), blcu_packet.NewDecoder()) - for id, value := range packetUpdate.Values { - loggerHandler.Log(value_logger.ToLoggableValue(id, value, packetUpdate.Metadata.Timestamp)) - } - } + decoder.SetPacketDecoder(abstraction.PacketId(info.MessageIds.Info), info_packet.NewDecoder(0)) + + stateOrdersDecoder := order.NewDecoder(binary.LittleEndian) + stateOrdersDecoder.SetActionId(abstraction.PacketId(info.MessageIds.AddStateOrder), stateOrdersDecoder.DecodeAdd) + stateOrdersDecoder.SetActionId(abstraction.PacketId(info.MessageIds.RemoveStateOrder), stateOrdersDecoder.DecodeRemove) + decoder.SetPacketDecoder(abstraction.PacketId(info.MessageIds.AddStateOrder), stateOrdersDecoder) + decoder.SetPacketDecoder(abstraction.PacketId(info.MessageIds.RemoveStateOrder), stateOrdersDecoder) + + protectionDecoder := protection.NewDecoder() + protectionDecoder.SetSeverity(abstraction.PacketId(info.MessageIds.Warning), protection.SeverityWarning) + protectionDecoder.SetSeverity(abstraction.PacketId(info.MessageIds.Fault), protection.SeverityFault) + + return decoder, encoder } -func startMessagesRoutine(vehicleMessages <-chan any, messageTransfer *message_transfer.MessageTransfer, loggerHandler *logger_handler.LoggerHandler) { - for message := range vehicleMessages { - messageTransfer.SendMessage(message) +type TransportAPI struct { + OnNotification func(abstraction.TransportNotification) + OnConnectionUpdate func(abstraction.TransportTarget, bool) +} - switch msg := message.(type) { - case vehicle_models.InfoMessage: - loggerHandler.Log(protection_logger.LoggableInfo(msg)) - case vehicle_models.ProtectionMessage: - loggerHandler.Log(protection_logger.LoggableProtection(msg)) - } - } +func (api *TransportAPI) Notification(notification abstraction.TransportNotification) { + api.OnNotification(notification) } -func startOrderRoutine(orderChannel <-chan vehicle_models.Order, vehicle *vehicle.Vehicle, loggerHandler *logger_handler.LoggerHandler) { - for ord := range orderChannel { - err := vehicle.SendOrder(ord) +func (api *TransportAPI) ConnectionUpdate(target abstraction.TransportTarget, isConnected bool) { + api.OnConnectionUpdate(target, isConnected) +} - if err != nil { - trace.Error().Any("order", ord).Msg("error sending order") - } +func getFilter(boardAddrs []net.IP, backendAddr net.IP, udpPort uint16, tcpClientPort uint16, tcpServerPort uint16) string { + ipipFilter := getIPIPfilter() + udpFilter := getUDPFilter(boardAddrs, udpPort) + tcpFilter := getTCPFilter(boardAddrs, tcpServerPort, tcpClientPort) + // noBackend := "not host 192.168.0.9" - loggerHandler.Log(order_logger.LoggableOrder(ord)) - } + // filter := fmt.Sprintf("((%s) or (%s) or (%s)) and (%s)", ipipFilter, udpFilter, tcpFilter, noBackend) + + filter := fmt.Sprintf("(%s) or (%s) or (%s)", ipipFilter, udpFilter, tcpFilter) + + trace.Trace().Any("addrs", boardAddrs).Str("filter", filter).Msg("new filter") + return filter +} + +func getIPIPfilter() string { + return "ip[9] == 4" +} + +func getUDPFilter(addrs []net.IP, port uint16) string { + udpPort := fmt.Sprintf("udp port %d", port) + udpAddrs := common.Map(addrs, func(addr net.IP) string { + return fmt.Sprintf("(src host %s)", addr) + }) + + udpAddrsStr := strings.Join(udpAddrs, " or ") + + return fmt.Sprintf("(%s) and (%s)", udpPort, udpAddrsStr) +} + +func getTCPFilter(addrs []net.IP, serverPort uint16, clientPort uint16) string { + ports := fmt.Sprintf("tcp port %d or %d", serverPort, clientPort) + notSynFinRst := "tcp[tcpflags] & (tcp-fin | tcp-syn | tcp-rst) == 0" + notJustAck := "tcp[tcpflags] | tcp-ack != 16" + nonZeroPayload := "tcp[tcpflags] & tcp-push != 0" + + srcAddresses := common.Map(addrs, func(addr net.IP) string { + return fmt.Sprintf("(src host %s)", addr) + }) + + srcAddressesStr := strings.Join(srcAddresses, " or ") + + dstAddresses := common.Map(addrs, func(addr net.IP) string { + return fmt.Sprintf("(dst host %s)", addr) + }) + + dstAddressesStr := strings.Join(dstAddresses, " or ") + + filter := fmt.Sprintf("(%s) and (%s) and (%s) and (%s) and (%s) and (%s)", ports, notSynFinRst, notJustAck, nonZeroPayload, srcAddressesStr, dstAddressesStr) + return filter } diff --git a/backend/internal/blcu/blcu.go b/backend/internal/blcu/blcu.go index 72e4c2735..13631d949 100644 --- a/backend/internal/blcu/blcu.go +++ b/backend/internal/blcu/blcu.go @@ -3,8 +3,8 @@ package blcu import ( "net" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" wsModels "github.com/HyperloopUPV-H8/h9-backend/internal/ws_handle/models" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" "github.com/rs/zerolog" trace "github.com/rs/zerolog/log" ) @@ -14,7 +14,7 @@ type BLCU struct { boardToId map[string]uint16 ackChannel chan struct{} - sendOrder func(models.Order) error + sendOrder func(*data.Packet) error config BLCUConfig @@ -30,7 +30,7 @@ func NewBLCU(laddr net.TCPAddr, boardIds map[string]uint16, config BLCUConfig) B ackChannel: make(chan struct{}, BLCU_ACK_CHAN_BUF), trace: trace.With().Str("component", BLCU_COMPONENT_NAME).Logger(), config: config, - sendOrder: func(o models.Order) error { return nil }, + sendOrder: func(o *data.Packet) error { return nil }, } } @@ -38,7 +38,7 @@ func (blcu *BLCU) HandlerName() string { return BLCU_HANDLER_NAME } -func (blcu *BLCU) SetSendOrder(sendOrder func(o models.Order) error) { +func (blcu *BLCU) SetSendOrder(sendOrder func(o *data.Packet) error) { blcu.sendOrder = sendOrder } diff --git a/backend/internal/blcu/download.go b/backend/internal/blcu/download.go index 603bc3306..1a8140ee5 100644 --- a/backend/internal/blcu/download.go +++ b/backend/internal/blcu/download.go @@ -10,8 +10,9 @@ import ( "time" "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" wsModels "github.com/HyperloopUPV-H8/h9-backend/internal/ws_handle/models" + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" "github.com/pin/tftp/v3" ) @@ -61,23 +62,19 @@ func (blcu *BLCU) requestDownload(board string) error { return nil } -func (blcu *BLCU) createDownloadOrder(board string) (models.Order, error) { +func (blcu *BLCU) createDownloadOrder(board string) (*data.Packet, error) { boardId, ok := blcu.boardToId[board] if !ok { blcu.trace.Error().Str("board", board).Msg("board id not found") - return models.Order{}, fmt.Errorf("missing id for board %s", board) - } - - return models.Order{ - ID: blcu.config.Packets.Download.Id, - Fields: map[string]models.Field{ - blcu.config.Packets.Download.Field: { - Value: boardId, - IsEnabled: true, - }, - }, - }, nil + return data.NewPacket(0), fmt.Errorf("missing id for board %s", board) + } + + return data.NewPacketWithValues(abstraction.PacketId(blcu.config.Packets.Download.Id), map[data.ValueName]data.Value{ + data.ValueName(blcu.config.Packets.Download.Field): data.NewNumericValue[uint16](boardId), + }, map[data.ValueName]bool{ + data.ValueName(blcu.config.Packets.Download.Field): true, + }), nil } const FlashMemorySize = 786432 diff --git a/backend/internal/blcu/upload.go b/backend/internal/blcu/upload.go index 960452fae..98b42672e 100644 --- a/backend/internal/blcu/upload.go +++ b/backend/internal/blcu/upload.go @@ -10,8 +10,9 @@ import ( "time" "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" wsModels "github.com/HyperloopUPV-H8/h9-backend/internal/ws_handle/models" + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" "github.com/pin/tftp/v3" ) @@ -68,23 +69,19 @@ func (blcu *BLCU) requestUpload(board string) error { return nil } -func (blcu *BLCU) createUploadOrder(board string) (models.Order, error) { +func (blcu *BLCU) createUploadOrder(board string) (*data.Packet, error) { boardId, ok := blcu.boardToId[board] if !ok { blcu.trace.Error().Str("board", board).Msg("board id not found") - return models.Order{}, fmt.Errorf("missing id for board %s", board) - } - - return models.Order{ - ID: blcu.config.Packets.Upload.Id, - Fields: map[string]models.Field{ - blcu.config.Packets.Upload.Field: { - Value: boardId, - IsEnabled: true, - }, - }, - }, nil + return data.NewPacket(0), fmt.Errorf("missing id for board %s", board) + } + + return data.NewPacketWithValues(abstraction.PacketId(blcu.config.Packets.Upload.Id), map[data.ValueName]data.Value{ + data.ValueName(blcu.config.Packets.Upload.Field): data.NewNumericValue[uint16](boardId), + }, map[data.ValueName]bool{ + data.ValueName(blcu.config.Packets.Upload.Field): true, + }), nil } func (blcu *BLCU) WriteTFTP(reader io.Reader, size int, onProgress func(float64)) error { diff --git a/backend/internal/excel/ade/ade_test.go b/backend/internal/excel/ade/ade_test.go deleted file mode 100644 index eefd7069d..000000000 --- a/backend/internal/excel/ade/ade_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package ade - -import ( - "testing" - - "github.com/xuri/excelize/v2" -) - -func TestAde(t *testing.T) { - t.Run("correct ade is parsed without errors", func(t *testing.T) { - file, err := excelize.OpenFile("ade.xlsx") - - if err != nil { - t.Fatalf("opening file: %e", err) - } - - _, err = CreateADE(file) - - if err != nil { - t.Fatalf("creating ade: %e", err) - } - }) - -} diff --git a/backend/internal/message_logger/loggable_info.go b/backend/internal/message_logger/loggable_info.go index 9ce20f677..1bcbdc9ba 100644 --- a/backend/internal/message_logger/loggable_info.go +++ b/backend/internal/message_logger/loggable_info.go @@ -3,10 +3,10 @@ package protection_logger import ( "fmt" - vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/info" ) -type LoggableInfo vehicle_models.InfoMessage +type LoggableInfo info.Packet func (info LoggableInfo) Id() string { return "info" @@ -16,5 +16,5 @@ func (info LoggableInfo) Log() []string { date := fmt.Sprintf("%d/%d/%d", info.Timestamp.Day, info.Timestamp.Month, info.Timestamp.Year) time := fmt.Sprintf("%d:%d:%d", info.Timestamp.Hour, info.Timestamp.Minute, info.Timestamp.Second) datetime := fmt.Sprintf("%s %s", date, time) - return []string{datetime, "info", info.Board, info.Msg} + return []string{datetime, "info", fmt.Sprint(info.BoardId), string(info.Msg)} } diff --git a/backend/internal/message_logger/loggable_protection.go b/backend/internal/message_logger/loggable_protection.go index 741dca312..003f68d5c 100644 --- a/backend/internal/message_logger/loggable_protection.go +++ b/backend/internal/message_logger/loggable_protection.go @@ -3,13 +3,14 @@ package protection_logger import ( "fmt" - vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" ) -type LoggableProtection vehicle_models.ProtectionMessage +type LoggableProtection protection.Packet func (lp LoggableProtection) Id() string { - return lp.Kind + packet := protection.Packet(lp) + return string(packet.Severity()) } func (lp LoggableProtection) Log() []string { @@ -18,24 +19,25 @@ func (lp LoggableProtection) Log() []string { datetime := fmt.Sprintf("%s %s", date, time) data := getDataString(lp.Protection.Data) - return []string{datetime, lp.Kind, lp.Board, lp.Name, lp.Protection.Kind, data} + packet := protection.Packet(lp) + return []string{datetime, string(packet.Severity()), fmt.Sprint(lp.BoardId), string(lp.Protection.Name), string(lp.Protection.Type), data} } func getDataString(data any) string { switch castedData := data.(type) { - case vehicle_models.OutOfBounds: + case protection.OutOfBounds: return fmt.Sprintf("Got: %f Want: %f", castedData.Value, castedData.Bounds) - case vehicle_models.LowerBound: + case protection.LowerBound: return fmt.Sprintf("Got: %f Want: > %f", castedData.Value, castedData.Bound) - case vehicle_models.UpperBound: + case protection.UpperBound: return fmt.Sprintf("Got: %f Want: < %f", castedData.Value, castedData.Bound) - case vehicle_models.Equals: + case protection.Equals: return fmt.Sprintf("%f is not allowed", castedData.Value) - case vehicle_models.NotEquals: + case protection.NotEquals: return fmt.Sprintf("%f should be %f", castedData.Value, castedData.Want) - case vehicle_models.TimeLimit: + case protection.TimeAccumulation: return fmt.Sprintf("Value (%f) surpassed bound (%f) for %f", castedData.Value, castedData.Bound, castedData.TimeLimit) - case vehicle_models.Error: + case protection.ErrorHandler: return fmt.Sprint(castedData) default: return fmt.Sprintf("UNRECOGNIZED VIOLATION: %v", data) diff --git a/backend/internal/order_logger/loggable_order.go b/backend/internal/order_logger/loggable_order.go index fa7921a2d..939951924 100644 --- a/backend/internal/order_logger/loggable_order.go +++ b/backend/internal/order_logger/loggable_order.go @@ -4,26 +4,29 @@ import ( "fmt" "time" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" ) -type LoggableOrder models.Order +type LoggableOrder data.Packet func (lo LoggableOrder) Id() string { - return fmt.Sprint(lo.ID) + packet := data.Packet(lo) + return fmt.Sprint(packet.Id()) } func (lo LoggableOrder) Log() []string { - return []string{"[GUI]", time.Now().String(), "", "", "", fmt.Sprint(lo.ID), fmt.Sprint(lo.Fields)} + packet := data.Packet(lo) + return []string{"[GUI]", time.Now().String(), "", "", "", lo.Id(), fmt.Sprint(packet.GetValues())} } -type LoggableTransmittedOrder models.PacketUpdate +type LoggableTransmittedOrder data.Packet func (lto LoggableTransmittedOrder) Id() string { - return fmt.Sprint(lto.Metadata.ID) + packet := data.Packet(lto) + return fmt.Sprint(packet.Id()) } func (lto LoggableTransmittedOrder) Log() []string { - - return []string{"[TRANSMITTED]", fmt.Sprint(lto.Metadata.Timestamp), fmt.Sprint(lto.Metadata.From), fmt.Sprint(lto.Metadata.To), fmt.Sprint(lto.Metadata.SeqNum), fmt.Sprint(lto.Metadata.ID), fmt.Sprint(lto.Values)} + packet := data.Packet(lto) + return []string{"[TRANSMITTED]", fmt.Sprint(packet.Timestamp()), "--from--", "--to--", "--seq num--", lto.Id(), fmt.Sprint(packet.GetValues())} } diff --git a/backend/internal/order_transfer/models/order.go b/backend/internal/order_transfer/models/order.go new file mode 100644 index 000000000..c65ec6f6f --- /dev/null +++ b/backend/internal/order_transfer/models/order.go @@ -0,0 +1,12 @@ +package models + +type Order struct { + Id uint16 `json:"id"` + Fields map[string]Field `json:"fields"` +} + +type Field struct { + Value any `json:"value"` + IsEnabled bool `json:"isEnabled"` + Type string `json:"type"` +} diff --git a/backend/internal/order_transfer/order_transfer.go b/backend/internal/order_transfer/order_transfer.go index c019affff..eafa8058f 100644 --- a/backend/internal/order_transfer/order_transfer.go +++ b/backend/internal/order_transfer/order_transfer.go @@ -4,11 +4,14 @@ import ( "encoding/json" "sync" + "github.com/HyperloopUPV-H8/h9-backend/internal/order_transfer/models" wsModels "github.com/HyperloopUPV-H8/h9-backend/internal/ws_handle/models" + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "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/internal/common" "github.com/HyperloopUPV-H8/h9-backend/internal/common/observable" - vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" "github.com/rs/zerolog" trace "github.com/rs/zerolog/log" ) @@ -22,18 +25,20 @@ const ( type OrderTransfer struct { stateOrdersMx *sync.Mutex + idToBoard map[uint16]string stateOrders map[string][]uint16 stateOrdersObservable observable.ReplayObservable[map[string][]uint16] - channel chan<- vehicle_models.Order + channel chan<- data.Packet trace zerolog.Logger } -func New() (OrderTransfer, <-chan vehicle_models.Order) { +func New(idToBoard map[uint16]string) (OrderTransfer, <-chan data.Packet) { trace.Info().Msg("new order transfer") - channel := make(chan vehicle_models.Order, ORDER_CHAN_BUFFER) + channel := make(chan data.Packet, ORDER_CHAN_BUFFER) stateOrders := make(map[string][]uint16) return OrderTransfer{ stateOrdersMx: &sync.Mutex{}, + idToBoard: idToBoard, channel: channel, stateOrders: stateOrders, stateOrdersObservable: observable.NewReplayObservable(stateOrders), @@ -55,28 +60,66 @@ func (orderTransfer *OrderTransfer) handleSubscription(client wsModels.Client, m observable.HandleSubscribe[map[string][]uint16](&orderTransfer.stateOrdersObservable, msg, client) } -func (orderTransfer *OrderTransfer) AddStateOrders(stateOrders vehicle_models.StateOrdersMessage) { +func (orderTransfer *OrderTransfer) AddStateOrders(stateOrders order.Add) { orderTransfer.stateOrdersMx.Lock() defer orderTransfer.stateOrdersMx.Unlock() - orderTransfer.stateOrders[stateOrders.BoardId] = common.Union(orderTransfer.stateOrders[stateOrders.BoardId], stateOrders.Orders...) + for _, order := range stateOrders.Orders() { + orderTransfer.stateOrders[orderTransfer.idToBoard[uint16(order)]] = common.Union(orderTransfer.stateOrders[orderTransfer.idToBoard[uint16(order)]], uint16(order)) + } orderTransfer.stateOrdersObservable.Next(orderTransfer.stateOrders) } -func (orderTransfer *OrderTransfer) RemoveStateOrders(stateOrders vehicle_models.StateOrdersMessage) { +func (orderTransfer *OrderTransfer) RemoveStateOrders(stateOrders order.Remove) { orderTransfer.stateOrdersMx.Lock() defer orderTransfer.stateOrdersMx.Unlock() - orderTransfer.stateOrders[stateOrders.BoardId] = common.Subtract(orderTransfer.stateOrders[stateOrders.BoardId], stateOrders.Orders...) + for _, order := range stateOrders.Orders() { + orderTransfer.stateOrders[orderTransfer.idToBoard[uint16(order)]] = common.Subtract(orderTransfer.stateOrders[orderTransfer.idToBoard[uint16(order)]], uint16(order)) + } orderTransfer.stateOrdersObservable.Next(orderTransfer.stateOrders) } func (orderTransfer *OrderTransfer) handleOrder(topic string, payload json.RawMessage, source string) { - var order vehicle_models.Order + var order models.Order if err := json.Unmarshal(payload, &order); err != nil { orderTransfer.trace.Error().Stack().Err(err).Msg("") return } - orderTransfer.trace.Info().Str("source", source).Str("topic", topic).Uint16("id", order.ID).Msg("send order") - orderTransfer.channel <- order + orderTransfer.trace.Info().Str("source", source).Str("topic", topic).Uint16("id", order.Id).Msg("send order") + + values := make(map[data.ValueName]data.Value) + enabled := make(map[data.ValueName]bool) + for name, field := range order.Fields { + enabled[data.ValueName(name)] = field.IsEnabled + switch field.Type { + case "uint8": + values[data.ValueName(name)] = data.NewNumericValue[uint8](field.Value.(uint8)) + case "uint16": + values[data.ValueName(name)] = data.NewNumericValue[uint16](field.Value.(uint16)) + case "uint32": + values[data.ValueName(name)] = data.NewNumericValue[uint32](field.Value.(uint32)) + case "uint64": + values[data.ValueName(name)] = data.NewNumericValue[uint64](field.Value.(uint64)) + case "int8": + values[data.ValueName(name)] = data.NewNumericValue[int8](field.Value.(int8)) + case "int16": + values[data.ValueName(name)] = data.NewNumericValue[int16](field.Value.(int16)) + case "int32": + values[data.ValueName(name)] = data.NewNumericValue[int32](field.Value.(int32)) + case "int64": + values[data.ValueName(name)] = data.NewNumericValue[int64](field.Value.(int64)) + case "float32": + values[data.ValueName(name)] = data.NewNumericValue[float32](field.Value.(float32)) + case "float64": + values[data.ValueName(name)] = data.NewNumericValue[float64](field.Value.(float64)) + case "bool": + values[data.ValueName(name)] = data.NewBooleanValue(field.Value.(bool)) + case "enum": + values[data.ValueName(name)] = data.NewEnumValue(data.EnumVariant(field.Value.(string))) + default: + panic("unknown field type " + field.Type) + } + } + orderTransfer.channel <- *data.NewPacketWithValues(abstraction.PacketId(order.Id), values, enabled) } func (orderTransfer *OrderTransfer) HandlerName() string { diff --git a/backend/internal/packet/descriptors.go b/backend/internal/packet/descriptors.go deleted file mode 100644 index 763faa0d1..000000000 --- a/backend/internal/packet/descriptors.go +++ /dev/null @@ -1,6 +0,0 @@ -package packet - -type ValueDescriptor struct { - Name string - Type string -} diff --git a/backend/internal/packet/packet.go b/backend/internal/packet/packet.go deleted file mode 100644 index 0d683f93b..000000000 --- a/backend/internal/packet/packet.go +++ /dev/null @@ -1,27 +0,0 @@ -package packet - -import "time" - -type Packet struct { - Metadata Metadata - Payload []byte -} - -type Metadata struct { - From string - To string - ID uint16 - Timestamp time.Time - // TODO: generate a synthetic seq num for udp data - SeqNum uint32 -} - -func NewMetaData(from, to string, id uint16, seqNum uint32, timestamp time.Time) Metadata { - return Metadata{ - From: from, - To: to, - ID: id, - Timestamp: timestamp, - SeqNum: seqNum, - } -} diff --git a/backend/internal/packet/value.go b/backend/internal/packet/value.go deleted file mode 100644 index 923eed829..000000000 --- a/backend/internal/packet/value.go +++ /dev/null @@ -1,23 +0,0 @@ -package packet - -type Value interface { - Inner() any -} - -type Numeric float64 - -func (n Numeric) Inner() any { - return float64(n) -} - -type Boolean bool - -func (b Boolean) Inner() any { - return bool(b) -} - -type Enum string - -func (e Enum) Inner() any { - return string(e) -} diff --git a/backend/internal/packet_logger/loggable_update.go b/backend/internal/packet_logger/loggable_update.go index 814fe351b..f74b734cd 100644 --- a/backend/internal/packet_logger/loggable_update.go +++ b/backend/internal/packet_logger/loggable_update.go @@ -2,33 +2,31 @@ package packet_logger import ( "fmt" + "time" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" ) type LoggablePacket struct { - Metadata packet.Metadata - HexValue []byte + Packet abstraction.Packet } func (packet LoggablePacket) Id() string { - return fmt.Sprint(packet.Metadata.ID) + return fmt.Sprint(packet.Packet.Id()) } func (packet LoggablePacket) Log() []string { return []string{ - packet.Metadata.Timestamp.String(), - packet.Metadata.From, - packet.Metadata.To, - fmt.Sprintf("%d", packet.Metadata.ID), - fmt.Sprintf("%X", packet.HexValue), + time.Now().Format(time.RFC3339), // TODO: fetch timestamp + "--from--", // TODO: fetch from + "--to--", // TODO: fetch to + fmt.Sprint(packet.Id()), + "--hex--", // TODO: fetch hex vaue } } -func ToLoggablePacket(update vehicle_models.PacketUpdate) LoggablePacket { +func ToLoggablePacket(update abstraction.Packet) LoggablePacket { return LoggablePacket{ - Metadata: update.Metadata, - HexValue: update.HexValue, + Packet: update, } } diff --git a/backend/internal/pipe/config.go b/backend/internal/pipe/config.go deleted file mode 100644 index 470c5256a..000000000 --- a/backend/internal/pipe/config.go +++ /dev/null @@ -1,8 +0,0 @@ -package pipe - -type Config struct { - TcpClientTag string - TcpServerTag string - Mtu uint - KeepAliveProbes int -} diff --git a/backend/internal/pipe/constructor.go b/backend/internal/pipe/constructor.go deleted file mode 100644 index d5835c54e..000000000 --- a/backend/internal/pipe/constructor.go +++ /dev/null @@ -1,99 +0,0 @@ -package pipe - -import ( - "fmt" - "net" - "os/exec" - "time" - - "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/info" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - "github.com/rs/zerolog" - trace "github.com/rs/zerolog/log" -) - -func contains(boards []string, board string) bool { - for _, b := range boards { - if b == board { - return true - } - } - return false -} - -func CreatePipes(info info.Info, keepaliveInterval, writeTimeout *time.Duration, boards []string, dataChan chan<- packet.Packet, onConnectionChange func(string, bool), config Config, readers map[uint16]common.ReaderFrom, trace zerolog.Logger) map[string]*Pipe { - err := configKeepAliveProbes(config.KeepAliveProbes) - - if err != nil { - trace.Error().Err(err).Msg("configuring keep alive probes") - } - - i := 0 - pipes := make(map[string]*Pipe) - for board, ip := range info.Addresses.Boards { - ip := ip - if boards != nil && !contains(boards, board) { - continue - } - - raddr := net.TCPAddr{ - IP: ip, - Port: int(info.Ports.TcpServer), - } - - laddr := net.TCPAddr{ - IP: info.Addresses.Backend, - Port: int(info.Ports.TcpClient) + i, - } - - pipe, err := newPipe(laddr, raddr, keepaliveInterval, writeTimeout, config.Mtu, dataChan, readers, getOnConnectionChange(board, onConnectionChange)) - - if err != nil { - //TODO: how to handle this error - trace.Fatal().Stack().Err(err).Msg("error creating pipe") - } - - pipes[board] = pipe - i++ - } - - return pipes -} - -func configKeepAliveProbes(n int) error { - flag := fmt.Sprintf("net.ipv4.tcp_keepalive_probes=%d", n) - return exec.Command("sysctl", "-w", flag).Run() -} - -func newPipe(laddr net.TCPAddr, raddr net.TCPAddr, keepaliveInterval, writeTimeout *time.Duration, mtu uint, outputChan chan<- packet.Packet, readers map[uint16]common.ReaderFrom, onConnectionChange func(bool)) (*Pipe, error) { - trace.Info().Any("laddr", laddr).Any("raddr", raddr).Msg("new pipe") - - pipe := &Pipe{ - laddr: &laddr, - raddr: &raddr, - output: outputChan, - - readers: readers, - - isClosed: true, - mtu: int(mtu), - - onConnectionChange: onConnectionChange, - - keepaliveInterval: keepaliveInterval, - writeTiemout: writeTimeout, - - trace: trace.With().Str("component", "pipe").IPAddr("addr", raddr.IP).Logger(), - } - - go pipe.connect() - - return pipe, nil -} - -func getOnConnectionChange(board string, onConnectionChange func(string, bool)) func(bool) { - return func(state bool) { - onConnectionChange(board, state) - } -} diff --git a/backend/internal/pipe/pipe.go b/backend/internal/pipe/pipe.go deleted file mode 100644 index 4861af036..000000000 --- a/backend/internal/pipe/pipe.go +++ /dev/null @@ -1,183 +0,0 @@ -package pipe - -import ( - "encoding/binary" - "errors" - "net" - "time" - - "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - "github.com/rs/zerolog" -) - -const IdSize = 2 - -type Pipe struct { - conn *net.TCPConn - - laddr *net.TCPAddr - raddr *net.TCPAddr - - readers map[uint16]common.ReaderFrom - - isClosed bool - mtu int - - output chan<- packet.Packet - onConnectionChange func(bool) - - keepaliveInterval *time.Duration - writeTiemout *time.Duration - - trace zerolog.Logger -} - -func (pipe *Pipe) connect() { - pipe.trace.Debug().Msg("connecting") - dialer := net.Dialer{ - LocalAddr: pipe.laddr, - } - - for pipe.isClosed { - pipe.trace.Trace().Msg("dial") - - if pipe.writeTiemout != nil { - dialer.Deadline = time.Now().Add(*pipe.writeTiemout) - } - - if conn, err := dialer.Dial("tcp", pipe.raddr.String()); err == nil { - pipe.open(conn.(*net.TCPConn)) - } else { - pipe.trace.Trace().Stack().Err(err).Msg("dial failed") - } - } - pipe.trace.Info().Msg("connected") - - go pipe.listen() -} - -func (pipe *Pipe) keepalive(interval *time.Duration) { - ticker := time.NewTicker(*interval) - for range ticker.C { - _, err := pipe.Write([]byte{0x45, 0x00}) - if err != nil { - pipe.Close(true) - return - } - } - -} - -func (pipe *Pipe) open(conn *net.TCPConn) { - pipe.trace.Debug().Msg("open") - pipe.conn = conn - pipe.isClosed = false - pipe.conn.SetNoDelay(true) - pipe.onConnectionChange(!pipe.isClosed) - if pipe.keepaliveInterval != nil { - go pipe.keepalive(pipe.keepaliveInterval) - } -} - -func (pipe *Pipe) listen() { - pipe.trace.Info().Msg("start listening") - for { - idBuf := make([]byte, IdSize) - _, err := pipe.conn.Read(idBuf) - - if err != nil { - pipe.trace.Error().Stack().Err(err).Msg("") - pipe.Close(true) - return - } - - id := binary.LittleEndian.Uint16(idBuf) - - if id == 69 { - continue - } - - reader, ok := pipe.readers[id] - - if !ok { - pipe.trace.Error().Uint16("id", id).Msg("unknown id") - continue - } - - payloadBuf, err := reader.ReadFrom(pipe.conn) - - if err != nil { - pipe.trace.Error().Stack().Err(err).Msg("") - pipe.Close(true) - return - } - - if pipe.output == nil { - pipe.trace.Debug().Msg("no output configured") - continue - } - - pipe.trace.Trace().Msg("new message") - - totalMsg := make([]byte, 0) - totalMsg = append(totalMsg, idBuf...) - totalMsg = append(totalMsg, payloadBuf...) - - raw := pipe.getRaw(totalMsg) - pipe.output <- raw - } -} - -var syntheticSeqNum uint32 = 0 - -func (pipe *Pipe) getRaw(payload []byte) packet.Packet { - syntheticSeqNum++ - return packet.Packet{ - Metadata: packet.NewMetaData(pipe.raddr.String(), pipe.laddr.String(), binary.LittleEndian.Uint16(payload[0:2]), syntheticSeqNum, time.Now()), - Payload: payload[2:], - } -} - -func (pipe *Pipe) SendFault(from string, payload []byte) { - if from == pipe.raddr.String() { - return - } - - pipe.Write(append([]byte{0x02, 0x00}, payload...)) -} - -func (pipe *Pipe) Write(data []byte) (int, error) { - if pipe == nil || pipe.conn == nil { - err := errors.New("pipe is nil") - pipe.trace.Error().Stack().Err(err).Msg("") - return 0, err - } - - pipe.trace.Trace().Msg("write") - if pipe.writeTiemout != nil { - pipe.conn.SetWriteDeadline(time.Now().Add(*pipe.writeTiemout)) - } - return pipe.conn.Write(data) -} - -func (pipe *Pipe) Close(reconnect bool) error { - pipe.trace.Warn().Bool("reconnect", reconnect).Msg("close") - - err := pipe.conn.Close() - pipe.isClosed = err == nil - pipe.onConnectionChange(!pipe.isClosed) - - if reconnect { - go pipe.connect() - } - return err -} - -func (pipe *Pipe) Laddr() string { - return pipe.laddr.String() -} - -func (pipe *Pipe) Raddr() string { - return pipe.raddr.String() -} diff --git a/backend/internal/sniffer/config.go b/backend/internal/sniffer/config.go deleted file mode 100644 index 43cf40deb..000000000 --- a/backend/internal/sniffer/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package sniffer - -type Config struct { - TcpClientTag string - TcpServerTag string - UdpTag string - Mtu uint - Interface string -} diff --git a/backend/internal/sniffer/sniffer.go b/backend/internal/sniffer/sniffer.go deleted file mode 100644 index 86834f784..000000000 --- a/backend/internal/sniffer/sniffer.go +++ /dev/null @@ -1,222 +0,0 @@ -package sniffer - -import ( - "encoding/binary" - "errors" - "fmt" - "net" - "strings" - "time" - - "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/info" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - "github.com/google/gopacket" - "github.com/google/gopacket/layers" - "github.com/google/gopacket/pcap" - "github.com/rs/zerolog" - trace "github.com/rs/zerolog/log" -) - -const SNAPLEN = 1500 - -type Sniffer struct { - source *pcap.Handle - filter string - config Config - trace zerolog.Logger -} - -func CreateSniffer(info info.Info, config Config, trace zerolog.Logger) Sniffer { - ips := common.Values(info.Addresses.Boards) - filter := getFilter(ips, info.Addresses.Backend, info.Ports.UDP, info.Ports.TcpClient, info.Ports.TcpServer) - sniffer, err := newSniffer(filter, config) - - if err != nil { - trace.Fatal().Stack().Err(err).Msg("error creating sniffer") - } - - return *sniffer -} - -func newSniffer(filter string, config Config) (*Sniffer, error) { - trace.Info().Msg("new sniffer") - source, err := newSource(config, filter) - - if err != nil { - trace.Error().Stack().Err(err).Msg("") - return nil, err - } - - return &Sniffer{ - source: source, - filter: filter, - config: config, - trace: trace.With().Str("component", "sniffer").Str("dev", config.Interface).Logger(), - }, nil -} - -func newSource(config Config, filter string) (*pcap.Handle, error) { - source, err := obtainSource(config.Interface, filter, config.Mtu) - - if err != nil { - return nil, err - } - - return source, nil -} - -func getFilter(boardAddrs []net.IP, backendAddr net.IP, udpPort uint16, tcpClientPort uint16, tcpServerPort uint16) string { - ipipFilter := getIPIPfilter() - udpFilter := getUDPFilter(boardAddrs, udpPort) - tcpFilter := getTCPFilter(boardAddrs, tcpServerPort, tcpClientPort) - // noBackend := "not host 192.168.0.9" - - // filter := fmt.Sprintf("((%s) or (%s) or (%s)) and (%s)", ipipFilter, udpFilter, tcpFilter, noBackend) - - filter := fmt.Sprintf("(%s) or (%s) or (%s)", ipipFilter, udpFilter, tcpFilter) - - trace.Trace().Any("addrs", boardAddrs).Str("filter", filter).Msg("new filter") - return filter -} - -func getIPIPfilter() string { - return "ip[9] == 4" -} - -func getUDPFilter(addrs []net.IP, port uint16) string { - udpPort := fmt.Sprintf("udp port %d", port) - udpAddrs := common.Map(addrs, func(addr net.IP) string { - return fmt.Sprintf("(src host %s)", addr) - }) - - udpAddrsStr := strings.Join(udpAddrs, " or ") - - return fmt.Sprintf("(%s) and (%s)", udpPort, udpAddrsStr) -} - -func getTCPFilter(addrs []net.IP, serverPort uint16, clientPort uint16) string { - ports := fmt.Sprintf("tcp port %d or %d", serverPort, clientPort) - notSynFinRst := "tcp[tcpflags] & (tcp-fin | tcp-syn | tcp-rst) == 0" - notJustAck := "tcp[tcpflags] | tcp-ack != 16" - nonZeroPayload := "tcp[tcpflags] & tcp-push != 0" - - srcAddresses := common.Map(addrs, func(addr net.IP) string { - return fmt.Sprintf("(src host %s)", addr) - }) - - srcAddressesStr := strings.Join(srcAddresses, " or ") - - dstAddresses := common.Map(addrs, func(addr net.IP) string { - return fmt.Sprintf("(dst host %s)", addr) - }) - - dstAddressesStr := strings.Join(dstAddresses, " or ") - - filter := fmt.Sprintf("(%s) and (%s) and (%s) and (%s) and (%s) and (%s)", ports, notSynFinRst, notJustAck, nonZeroPayload, srcAddressesStr, dstAddressesStr) - return filter -} - -func obtainSource(dev string, filter string, mtu uint) (*pcap.Handle, error) { - trace.Debug().Str("dev", dev).Str("filter", filter).Msg("obtain source") - - source, err := pcap.OpenLive(dev, int32(mtu), true, pcap.BlockForever) - if err != nil { - return nil, err - } - - if err := source.SetBPFFilter(filter); err != nil { - return nil, err - } - - return source, nil -} - -func (sniffer *Sniffer) Listen(output chan<- packet.Packet) { - go sniffer.startReadLoop(output) -} - -func (sniffer *Sniffer) startReadLoop(output chan<- packet.Packet) { - for { - source, err := newSource(sniffer.config, sniffer.filter) - if err != nil { - continue - } - sniffer.source = source - - sniffer.trace.Info().Msg("start listening") - sniffer.read(output) - } - -} - -func (sniffer *Sniffer) read(output chan<- packet.Packet) { - for { - raw, _, err := sniffer.source.ReadPacketData() - if err != nil { - sniffer.trace.Error().Stack().Err(err).Msg("") - return - } - - sniffer.trace.Trace().Msg("read") - - packet := gopacket.NewPacket(raw, sniffer.source.LinkType(), gopacket.DecodeOptions{ - NoCopy: true, - }) - - rawPacket, err := sniffer.parseLayers(packet.Layers()) - if err != nil { - sniffer.trace.Error().Stack().Err(err).Msg("") - continue - } - - sniffer.trace.Trace().Msg("parsed") - output <- rawPacket - } -} - -var syntheticSeqNum uint32 = 0 - -func (sniffer *Sniffer) parseLayers(packetLayers []gopacket.Layer) (packet.Packet, error) { - timestamp := time.Now() - from := "" - to := "" - seqNum := syntheticSeqNum - var payload []byte - -layerLoop: - for _, layer := range packetLayers { - switch layer := layer.(type) { - case *layers.IPv4: - if layer.Protocol == 4 { - continue layerLoop - } - from = layer.SrcIP.String() - to = layer.DstIP.String() - case *layers.TCP: - seqNum = layer.Seq - payload = layer.Payload - break layerLoop - case *layers.UDP: - syntheticSeqNum++ - payload = layer.Payload - break layerLoop - } - } - - if from == "" || to == "" { - return packet.Packet{}, errors.New("failed to get flow") - } - - //Config endianess from config.toml - if len(payload) < 2 { - return packet.Packet{}, errors.New("payload smaller than 2") - } - - id := binary.LittleEndian.Uint16(payload[:2]) - - return packet.Packet{ - Metadata: packet.NewMetaData(from, to, id, seqNum, timestamp), - Payload: payload[2:], - }, nil -} diff --git a/backend/internal/update_factory/update_factory.go b/backend/internal/update_factory/update_factory.go index ae34042b3..ce1194fe7 100644 --- a/backend/internal/update_factory/update_factory.go +++ b/backend/internal/update_factory/update_factory.go @@ -1,17 +1,14 @@ package update_factory import ( - "fmt" "math" - "math/rand" "sync" "time" "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory/models" - vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" "github.com/rs/zerolog" trace "github.com/rs/zerolog/log" ) @@ -54,18 +51,18 @@ func NewFactory() *UpdateFactory { return factory } -func (factory *UpdateFactory) NewUpdate(packetUpdate vehicle_models.PacketUpdate) models.Update { - factory.updateCount(packetUpdate.Metadata.ID) +func (factory *UpdateFactory) NewUpdate(packet *data.Packet) models.Update { + factory.updateCount(uint16(packet.Id())) factory.averageMx.Lock() defer factory.averageMx.Unlock() return models.Update{ - Id: packetUpdate.Metadata.ID, - HexValue: fmt.Sprintf("%x", packetUpdate.HexValue), - Values: factory.getFields(packetUpdate.Metadata.ID, packetUpdate.Values), - Count: factory.getCount(packetUpdate.Metadata.ID), - CycleTime: factory.getCycleTime(packetUpdate.Metadata.ID, uint64(packetUpdate.Metadata.Timestamp.UnixNano())), + Id: uint16(packet.Id()), + HexValue: "", + Values: factory.getFields(uint16(packet.Id()), packet.GetValues()), + Count: factory.getCount(uint16(packet.Id())), + CycleTime: factory.getCycleTime(uint16(packet.Id()), uint64(packet.Timestamp().UnixNano())), } } @@ -129,39 +126,21 @@ func (factory *UpdateFactory) getCount(id uint16) uint64 { return factory.count[id] } -func (factory *UpdateFactory) getFields(id uint16, fields map[string]packet.Value) map[string]models.UpdateValue { +type numeric interface { + Value() float64 +} + +func (factory *UpdateFactory) getFields(id uint16, fields map[data.ValueName]data.Value) map[string]models.UpdateValue { updateFields := make(map[string]models.UpdateValue, len(fields)) for name, value := range fields { switch value := value.(type) { - case packet.Numeric: - switch name { - case "low_battery_temperature_1": - randOffset := (rand.Float64()*2 - 1) * 0.1 - randResult := float64(value) + 67 + randOffset - updateFields[name] = factory.getNumericField(id, name, packet.Numeric(randResult)) - // case "battery_temperature_1": - // randOffset := (rand.Float64()*2 - 1) * 0.1 - // randResult := float64(value) + 5 + randOffset - // updateFields[name] = factory.getNumericField(id, name, packet.Numeric(randResult)) - - // case "battery_temperature_2": - // randOffset := (rand.Float64()*2 - 1) * 0.1 - // randResult := float64(value) + 5 + randOffset - // updateFields[name] = factory.getNumericField(id, name, packet.Numeric(randResult)) - - // case "battery_temperature_3": - // randOffset := (rand.Float64()*2 - 1) * 0.1 - // randResult := float64(value) + 5 + randOffset - // updateFields[name] = factory.getNumericField(id, name, packet.Numeric(randResult)) - default: - updateFields[name] = factory.getNumericField(id, name, packet.Numeric(value)) - } - - case packet.Boolean: - updateFields[name] = models.BooleanValue(value) - case packet.Enum: - updateFields[name] = models.EnumValue(value) + case numeric: + updateFields[string(name)] = factory.getNumericField(id, string(name), value) + case data.BooleanValue: + updateFields[string(name)] = models.BooleanValue(value.Value()) + case data.EnumValue: + updateFields[string(name)] = models.EnumValue(value.Variant()) } } @@ -194,8 +173,8 @@ func replaceNaN() float64 { return 0 } -func (factory *UpdateFactory) getNumericField(id uint16, name string, value packet.Numeric) models.NumericValue { - lastVal := replaceInvalidNumber(float64(value)) +func (factory *UpdateFactory) getNumericField(id uint16, name string, value numeric) models.NumericValue { + lastVal := replaceInvalidNumber(value.Value()) avg := factory.getAverage(id, name) lastAvg := avg.Add(lastVal) diff --git a/backend/internal/value_logger/loggable_value.go b/backend/internal/value_logger/loggable_value.go index c04861f82..fe6f71382 100644 --- a/backend/internal/value_logger/loggable_value.go +++ b/backend/internal/value_logger/loggable_value.go @@ -4,12 +4,12 @@ import ( "fmt" "time" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" ) type LoggableValue struct { ValueId string - Value packet.Value + Value data.Value Timestamp time.Time } @@ -17,14 +17,28 @@ func (value LoggableValue) Id() string { return value.ValueId } +type numeric interface { + Value() float64 +} + func (value LoggableValue) Log() []string { - return []string{ + output := []string{ value.Timestamp.String(), - fmt.Sprintf("%v", value.Value), } + + switch v := value.Value.(type) { + case numeric: + output = append(output, fmt.Sprint(v.Value())) + case data.BooleanValue: + output = append(output, fmt.Sprint(v.Value())) + case data.EnumValue: + output = append(output, fmt.Sprint(v.Variant())) + } + + return output } -func ToLoggableValue(id string, value packet.Value, timestamp time.Time) LoggableValue { +func ToLoggableValue(id string, value data.Value, timestamp time.Time) LoggableValue { return LoggableValue{ ValueId: id, Value: value, diff --git a/backend/internal/vehicle/bitarray_parser.go b/backend/internal/vehicle/bitarray_parser.go deleted file mode 100644 index 8a4f5a484..000000000 --- a/backend/internal/vehicle/bitarray_parser.go +++ /dev/null @@ -1,102 +0,0 @@ -package vehicle - -import ( - "fmt" - "io" -) - -type BitarrayParser struct { - names map[uint16][]string -} - -func NewBitarrayParser(names map[uint16][]string) BitarrayParser { - return BitarrayParser{ - names: names, - } -} - -func (parser *BitarrayParser) Decode(id uint16, data io.Reader) (map[string]bool, error) { - name, ok := parser.names[id] - if !ok { - return nil, fmt.Errorf("value count for packet %d not found", id) - } - - return parser.decodeBitarray(name, data) -} - -func (decoder *BitarrayParser) decodeBitarray(names []string, data io.Reader) (map[string]bool, error) { - buf := make([]byte, (len(names)/8)+1) - n, err := data.Read(buf) - if err != nil { - return nil, err - } - - if n != len(buf) { - return nil, fmt.Errorf("invalid bitarray length %d/%d", n, len(buf)) - } - - return zip(names, readBits(buf)), nil -} - -func readBits(buf []byte) []bool { - bits := make([]bool, 0, len(buf)) - for _, b := range buf { - for j := 0; j < 8; j++ { - // TODO: test if this is the correct implementation - bits = append(bits, (b&(0b10000000>>j)) != 0) - } - } - return bits -} - -func zip[K comparable, V any](keys []K, values []V) map[K]V { - m := make(map[K]V, len(keys)) - for i, k := range keys { - m[k] = values[i] - } - return m -} - -func (parser *BitarrayParser) Encode(id uint16, enabled map[string]bool, data io.Writer) error { - names, ok := parser.names[id] - if !ok { - return fmt.Errorf("value names for packet %d not found", id) - } - - if len(names) == 0 { - return nil - } - - if len(enabled) != len(names) { - return fmt.Errorf("invalid value count %d/%d", len(enabled), len(names)) - } - - return parser.encodeBitarray(enabled, data) -} - -func (encoder *BitarrayParser) encodeBitarray(nameToEnable map[string]bool, data io.Writer) error { - buf := writeBits(nameToEnable) - - n, err := data.Write(buf) - if err != nil { - return err - } - - if n != len(buf) { - return fmt.Errorf("invalid bitarray length %d/%d", n, len(buf)) - } - - return nil -} - -func writeBits(nameToEnable map[string]bool) []byte { - buf := make([]byte, (len(nameToEnable)/8)+1) - i := 0 - for _, enabled := range nameToEnable { - if enabled { - buf[i/8] |= 0b10000000 >> (i % 8) - } - i++ - } - return buf -} diff --git a/backend/internal/vehicle/constructor.go b/backend/internal/vehicle/constructor.go deleted file mode 100644 index 7e064f1a4..000000000 --- a/backend/internal/vehicle/constructor.go +++ /dev/null @@ -1,150 +0,0 @@ -package vehicle - -import ( - "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/info" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - "github.com/HyperloopUPV-H8/h9-backend/internal/pipe" - "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" - "github.com/HyperloopUPV-H8/h9-backend/internal/sniffer" - "github.com/HyperloopUPV-H8/h9-backend/internal/unit_converter" - protection_parser "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/message_parser" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/packet_parser" - "github.com/rs/zerolog" - trace "github.com/rs/zerolog/log" -) - -const ( - UPDATE_CHAN_BUF_SIZE = 100 -) - -type VehicleConstructorArgs struct { - Boards []pod_data.Board - Info info.Info - PodData pod_data.PodData - Config Config - OnConnectionChange func(string, bool) -} - -func New(args VehicleConstructorArgs) Vehicle { - trace.Trace().Msg("creating vehicle") - - vehicleTrace := trace.With().Str("component", "vehicle").Logger() - dataChan := make(chan packet.Packet, UPDATE_CHAN_BUF_SIZE) - - packetParser, err := packet_parser.CreatePacketParser(args.Info, args.Boards, vehicleTrace) - - if err != nil { - vehicleTrace.Fatal().Err(err).Msg("error creating packetParser") - } - - names, err := getPacketToValuesNames(args.Info, args.Boards) - - if err != nil { - vehicleTrace.Error().Err(err).Msg("error getting packet to values names") - } - - snifferConfig := getSnifferConfig(args.Config) - pipesConfig := getPipesConfig(args.Config) - - messageIds := common.NewSet[uint16]() - messageIds.Add(args.Info.MessageIds.AddStateOrder) - messageIds.Add(args.Info.MessageIds.RemoveStateOrder) - messageIds.Add(args.Info.MessageIds.BlcuAck) - messageIds.Add(args.Info.MessageIds.Fault) - messageIds.Add(args.Info.MessageIds.Warning) - messageIds.Add(args.Info.MessageIds.Info) - - vehicle := Vehicle{ - podConverter: unit_converter.NewUnitConverter("pod", args.Boards, args.Info.Units), - displayConverter: unit_converter.NewUnitConverter("display", args.Boards, args.Info.Units), - backendAddr: args.Info.Addresses.Backend, - - sniffer: sniffer.CreateSniffer(args.Info, snifferConfig, vehicleTrace), - pipes: pipe.CreatePipes(args.Info, args.Config.Network.GetKeepAliveInterval(), args.Config.Network.GetWriteTimeout(), args.Config.Boards, dataChan, args.OnConnectionChange, pipesConfig, newPipeReaders(args.Info.MessageIds), vehicleTrace), - - dataIds: getBoardIdsFromType(args.Boards, "data", vehicleTrace), - orderIds: getBoardIdsFromType(args.Boards, "order", vehicleTrace), - messageIds: messageIds, - blcuAckId: args.Info.MessageIds.BlcuAck, - addStateOrdersId: args.Info.MessageIds.AddStateOrder, - removeStateOrdersId: args.Info.MessageIds.RemoveStateOrder, - stateSpaceId: args.Info.MessageIds.StateSpace, - - packetParser: packetParser, - messageParser: protection_parser.NewMessageParser(args.Info, args.PodData), - bitarrayParser: NewBitarrayParser(names), - - dataChan: dataChan, - - idToBoard: getIdToBoard(args.Boards, vehicleTrace), - onConnectionChange: args.OnConnectionChange, - trace: vehicleTrace, - } - - vehicle.sniffer.Listen(dataChan) - - return vehicle -} - -func getSnifferConfig(config Config) sniffer.Config { - return sniffer.Config{ - TcpClientTag: config.Network.TcpClientTag, - TcpServerTag: config.Network.TcpServerTag, - UdpTag: config.Network.UdpTag, - Mtu: config.Network.Mtu, - Interface: config.Network.Interface, - } -} - -func getPipesConfig(config Config) pipe.Config { - return pipe.Config{ - TcpClientTag: config.Network.TcpClientTag, - TcpServerTag: config.Network.TcpServerTag, - Mtu: config.Network.Mtu, - KeepAliveProbes: config.Network.KeepAliveProbes, - } -} - -func getPacketToValuesNames(info info.Info, boards []pod_data.Board) (map[uint16][]string, error) { - names := make(map[uint16][]string) - for _, board := range boards { - for _, packet := range board.Packets { - names[packet.Id] = getNamesFromValues(packet.Measurements) - } - } - - return names, nil -} - -func getNamesFromValues(measurements []pod_data.Measurement) []string { - names := make([]string, len(measurements)) - for i, m := range measurements { - names[i] = m.GetId() - } - return names -} - -func getIdToBoard(boards []pod_data.Board, trace zerolog.Logger) map[uint16]string { - idToBoard := make(map[uint16]string) - for _, board := range boards { - for _, packet := range board.Packets { - idToBoard[packet.Id] = board.Name - } - } - return idToBoard -} - -func getBoardIdsFromType(boards []pod_data.Board, kind string, trace zerolog.Logger) common.Set[uint16] { - ids := common.NewSet[uint16]() - - for _, board := range boards { - for _, packet := range board.Packets { - if packet.Type == kind { - ids.Add(packet.Id) - } - } - } - - return ids -} diff --git a/backend/internal/vehicle/message_parser/constructor.go b/backend/internal/vehicle/message_parser/constructor.go deleted file mode 100644 index 42143551a..000000000 --- a/backend/internal/vehicle/message_parser/constructor.go +++ /dev/null @@ -1,48 +0,0 @@ -package message_parser - -import ( - "github.com/HyperloopUPV-H8/h9-backend/internal/info" - "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" - "github.com/rs/zerolog" - trace "github.com/rs/zerolog/log" -) - -func NewMessageParser(info info.Info, podData pod_data.PodData) MessageParser { - parserLogger := trace.With().Str("component", "protection parser").Logger() - - idToBoard := getIdToBoard(info.BoardIds, parserLogger) - - return MessageParser{ - infoId: info.MessageIds.Info, - warningId: info.MessageIds.Warning, - faultId: info.MessageIds.Fault, - idToBoardId: getIdToBoardId(info.BoardIds, podData), - boardIdToName: idToBoard, - trace: parserLogger, - addStateOrderId: info.MessageIds.AddStateOrder, - removeStateOrderId: info.MessageIds.RemoveStateOrder, - } -} - -func getIdToBoardId(boardId map[string]uint16, podData pod_data.PodData) map[uint16]uint16 { - idToBoardId := make(map[uint16]uint16) - for _, board := range podData.Boards { - board := board - for _, packet := range board.Packets { - packet := packet - idToBoardId[packet.Id] = boardId[board.Name] - } - } - return idToBoardId -} - -func getIdToBoard(boardToId map[string]uint16, trace zerolog.Logger) map[uint16]string { - idToBoard := make(map[uint16]string) - - for board, id := range boardToId { - - idToBoard[id] = board - } - - return idToBoard -} diff --git a/backend/internal/vehicle/message_parser/message_adapter.go b/backend/internal/vehicle/message_parser/message_adapter.go deleted file mode 100644 index 8f45eb148..000000000 --- a/backend/internal/vehicle/message_parser/message_adapter.go +++ /dev/null @@ -1,30 +0,0 @@ -package message_parser - -import ( - "encoding/json" - - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" -) - -type InfoMessageAdapter struct { - BoardId uint16 `json:"boardId"` - Timestamp models.Timestamp `json:"timestamp"` - Msg string `json:"msg"` -} - -type ProtectionMessageAdapter struct { - BoardId uint16 `json:"boardId"` - Timestamp models.Timestamp `json:"timestamp"` - Protection ProtectionAdapter `json:"protection"` -} - -type ProtectionAdapter struct { - Name string `json:"name"` - Type string `json:"type"` - Data *json.RawMessage `json:"data"` -} - -type StateOrdersAdapter struct { - Action string - StateOrders models.StateOrdersMessage -} diff --git a/backend/internal/vehicle/message_parser/message_parser.go b/backend/internal/vehicle/message_parser/message_parser.go deleted file mode 100644 index 756df6bb2..000000000 --- a/backend/internal/vehicle/message_parser/message_parser.go +++ /dev/null @@ -1,189 +0,0 @@ -package message_parser - -import ( - "bytes" - "encoding/binary" - "encoding/json" - "fmt" - - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" - "github.com/rs/zerolog" -) - -type MessageParser struct { - infoId uint16 - warningId uint16 - faultId uint16 - errorId uint16 - addStateOrderId uint16 - removeStateOrderId uint16 - idToBoardId map[uint16]uint16 - boardIdToName map[uint16]string - trace zerolog.Logger -} - -func (parser *MessageParser) Parse(id uint16, raw []byte) (any, error) { - kind, err := parser.getKind(id) - - if err != nil { - parser.trace.Error().Err(err).Msg("error getting kind") - return models.ProtectionMessage{}, err - } - - if kind == AddStateOrderKind || kind == RemoveStateOrderKind { - parsed, err := parser.toStateOrder(kind, raw) - return StateOrdersAdapter{kind, parsed}, err - } - - if len(raw) < 2 { - return nil, fmt.Errorf("message too short (length %d)", len(raw)) - } - - if kind == infoKind { - return parser.toInfoMessage(kind, raw) - } - - return parser.toProtectionMessage(kind, raw) - -} - -func (parser *MessageParser) toStateOrder(kind string, payload []byte) (models.StateOrdersMessage, error) { - reader := bytes.NewReader(payload) - - if reader.Len() <= 1 { - return models.StateOrdersMessage{}, nil - } - - var ordersLen uint8 - err := binary.Read(reader, binary.LittleEndian, &ordersLen) - if err != nil { - return models.StateOrdersMessage{}, err - } - - orders := make([]uint16, ordersLen) - err = binary.Read(reader, binary.LittleEndian, &orders) - if err != nil { - return models.StateOrdersMessage{}, err - } - - boardId := parser.idToBoardId[orders[0]] - //TODO: check if board exists - - return models.StateOrdersMessage{ - BoardId: parser.boardIdToName[boardId], - Orders: orders, - }, nil -} - -func (parser *MessageParser) toInfoMessage(kind string, payload []byte) (models.InfoMessage, error) { - var adapter InfoMessageAdapter - err := json.Unmarshal(payload, &adapter) - - if err != nil { - parser.trace.Error().Err(err).Str("message", string(payload)).Msg("error parsing info message") - return models.InfoMessage{}, err - } - - name, ok := parser.boardIdToName[adapter.BoardId] - - if !ok { - parser.trace.Error().Uint16("board id", adapter.BoardId).Msg("board id not found") - name = "DEFAULT" - } - - return models.InfoMessage{ - Board: name, - Timestamp: adapter.Timestamp, - Msg: adapter.Msg, - Kind: "info", - }, nil - -} - -func (parser *MessageParser) toProtectionMessage(kind string, payload []byte) (models.ProtectionMessage, error) { - var adapter ProtectionMessageAdapter - err := json.Unmarshal(payload, &adapter) - - if err != nil { - parser.trace.Error().Err(err).Str("message", string(payload)).Msg("error parsing protection message") - return models.ProtectionMessage{}, err - } - - protection, err := getProtection(adapter.Protection.Type, *adapter.Protection.Data) - - if err != nil { - parser.trace.Error().Err(err).Msg("protection unmarshal failed") - } - - name, ok := parser.boardIdToName[adapter.BoardId] - - if !ok { - parser.trace.Error().Uint16("board id", adapter.BoardId).Msg("board id not found") - name = "DEFAULT" - } - - return models.ProtectionMessage{ - Kind: kind, - Board: name, - Name: adapter.Protection.Name, - Timestamp: adapter.Timestamp, - Protection: protection, - }, nil -} - -const AddStateOrderKind string = "addStateOrder" -const RemoveStateOrderKind string = "removeStateOrder" -const faultKind string = "fault" -const warningKind string = "warning" -const errorKind string = "error" -const infoKind string = "info" - -func (parser *MessageParser) getKind(id uint16) (string, error) { - switch id { - case parser.addStateOrderId: - return AddStateOrderKind, nil - case parser.removeStateOrderId: - return RemoveStateOrderKind, nil - case parser.faultId: - return faultKind, nil - case parser.warningId: - return warningKind, nil - case parser.errorId: - return errorKind, nil - case parser.infoId: - return infoKind, nil - } - - parser.trace.Error().Uint16("id", id).Msg("unrecognized message id") - return "", fmt.Errorf("unrecognized message id") - -} - -func getProtection(kind string, payload []byte) (models.Protection, error) { - switch kind { - case "OUT_OF_BOUNDS": - return parseProtection[models.OutOfBounds](kind, payload) - case "UPPER_BOUND": - return parseProtection[models.UpperBound](kind, payload) - case "LOWER_BOUND": - return parseProtection[models.LowerBound](kind, payload) - case "EQUALS": - return parseProtection[models.Equals](kind, payload) - case "NOT_EQUALS": - return parseProtection[models.NotEquals](kind, payload) - case "TIME_ACCUMULATION": - return parseProtection[models.TimeLimit](kind, payload) - case "ERROR_HANDLER": - return parseProtection[models.Error](kind, payload) - default: - return models.Protection{}, fmt.Errorf("protection kind not recognized: %s", kind) - } -} -func parseProtection[T any](kind string, payload []byte) (models.Protection, error) { - var data T - err := json.Unmarshal(payload, &data) - return models.Protection{ - Kind: kind, - Data: data, - }, err -} diff --git a/backend/internal/vehicle/models/messages.go b/backend/internal/vehicle/models/messages.go deleted file mode 100644 index c06d1cc77..000000000 --- a/backend/internal/vehicle/models/messages.go +++ /dev/null @@ -1,51 +0,0 @@ -package models - -type StateOrdersMessage struct { - BoardId string `json:"board"` - Orders []uint16 `json:"orders"` -} - -type InfoMessage struct { - Board string `json:"board"` - Timestamp Timestamp `json:"timestamp"` - Msg string `json:"msg"` - Kind string `json:"kind"` -} - -type ProtectionMessage struct { - Board string `json:"board"` - Name string `json:"name"` - Timestamp Timestamp `json:"timestamp"` - - Kind string `json:"kind"` - Protection Protection `json:"protection"` -} - -type Protection struct { - Kind string `json:"kind"` - Data any `json:"data"` -} - -type OutOfBounds struct { - Value float64 `json:"value"` - Bounds [2]float64 `json:"bounds"` -} -type LowerBound struct { - Value float64 `json:"value"` - Bound float64 `json:"bound"` -} -type UpperBound struct { - Value float64 `json:"value"` - Bound float64 `json:"bound"` -} -type Equals struct { - Value float64 `json:"value"` -} -type NotEquals struct { - Value float64 `json:"value"` - Want float64 `json:"want"` -} - -type Error = string - -type Info = string diff --git a/backend/internal/vehicle/models/order.go b/backend/internal/vehicle/models/order.go deleted file mode 100644 index e7d0ae24c..000000000 --- a/backend/internal/vehicle/models/order.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -import "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - -type Order struct { - ID uint16 `json:"id"` - Fields map[string]Field `json:"fields"` -} - -type Field struct { - Value any `json:"value"` - IsEnabled bool `json:"isEnabled"` -} - -type TransmittedOrder struct { - Metadata packet.Metadata - HexValue []byte - Values map[string]packet.Value -} diff --git a/backend/internal/vehicle/models/packet_update.go b/backend/internal/vehicle/models/packet_update.go deleted file mode 100644 index e5f3c2e58..000000000 --- a/backend/internal/vehicle/models/packet_update.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -import ( - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" -) - -type PacketUpdate struct { - Metadata packet.Metadata - HexValue []byte - Values map[string]packet.Value -} diff --git a/backend/internal/vehicle/models/spaceState.go b/backend/internal/vehicle/models/spaceState.go deleted file mode 100644 index 335127ac5..000000000 --- a/backend/internal/vehicle/models/spaceState.go +++ /dev/null @@ -1,17 +0,0 @@ -package models - -import ( - "bytes" - "encoding/binary" -) - -type StateSpace [8][15]float32 - -func NewStateSpace(buf []byte) StateSpace { - spaceState := StateSpace{} - r := bytes.NewBuffer(buf) - //TODO: check endianess - binary.Read(r, binary.LittleEndian, &spaceState) - - return spaceState -} diff --git a/backend/internal/vehicle/models/time_accumulation.go b/backend/internal/vehicle/models/time_accumulation.go deleted file mode 100644 index fecdb70bc..000000000 --- a/backend/internal/vehicle/models/time_accumulation.go +++ /dev/null @@ -1,7 +0,0 @@ -package models - -type TimeLimit struct { - Value float64 `json:"value"` - Bound float64 `json:"bound"` - TimeLimit float64 `json:"timelimit"` -} diff --git a/backend/internal/vehicle/models/timestamp.go b/backend/internal/vehicle/models/timestamp.go deleted file mode 100644 index e73967bf3..000000000 --- a/backend/internal/vehicle/models/timestamp.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -type Timestamp struct { - Counter uint16 `json:"counter"` - Second uint8 `json:"second"` - Minute uint8 `json:"minute"` - Hour uint8 `json:"hour"` - Day uint8 `json:"day"` - Month uint8 `json:"month"` - Year uint16 `json:"year"` -} diff --git a/backend/internal/vehicle/models/types.go b/backend/internal/vehicle/models/types.go deleted file mode 100644 index 08b19ee9a..000000000 --- a/backend/internal/vehicle/models/types.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -func IsNumeric(kind string) bool { - return (kind == "uint8" || - kind == "uint16" || - kind == "uint32" || - kind == "uint64" || - kind == "int8" || - kind == "int16" || - kind == "int32" || - kind == "int64" || - kind == "float32" || - kind == "float64") -} diff --git a/backend/internal/vehicle/packet_parser/constructor.go b/backend/internal/vehicle/packet_parser/constructor.go deleted file mode 100644 index 33aadb9ca..000000000 --- a/backend/internal/vehicle/packet_parser/constructor.go +++ /dev/null @@ -1,84 +0,0 @@ -package packet_parser - -import ( - "strings" - - "github.com/HyperloopUPV-H8/h9-backend/internal/info" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" - "github.com/rs/zerolog" -) - -func CreatePacketParser(info info.Info, boards []pod_data.Board, trace zerolog.Logger) (PacketParser, error) { - structures, err := getStructures(info, boards) - if err != nil { - return PacketParser{}, err - } - - return newPacketParser(structures, getEnumDescriptors(info, boards)), nil -} - -func newPacketParser(structures map[uint16][]packet.ValueDescriptor, enums map[string][]string) PacketParser { - return PacketParser{ - structures: structures, - valueParsers: map[string]parser{ - "uint8": numericParser[uint8]{}, - "uint16": numericParser[uint16]{}, - "uint32": numericParser[uint32]{}, - "uint64": numericParser[uint64]{}, - "int8": numericParser[int8]{}, - "int16": numericParser[int16]{}, - "int32": numericParser[int32]{}, - "int64": numericParser[int64]{}, - "float32": numericParser[float32]{}, - "float64": numericParser[float64]{}, - "bool": booleanParser{}, - "enum": enumParser{descriptors: enums}, - }, - } -} - -func getStructures(info info.Info, boards []pod_data.Board) (map[uint16][]packet.ValueDescriptor, error) { - structures := make(map[uint16][]packet.ValueDescriptor) - for _, board := range boards { - for _, packet := range board.Packets { - if packet.Type == "data" || packet.Type == "order" || packet.Type == "stateOrder" { - structures[packet.Id] = getDescriptor(packet.Measurements) - } - } - } - return structures, nil -} - -func getDescriptor(measurements []pod_data.Measurement) []packet.ValueDescriptor { - descriptor := make([]packet.ValueDescriptor, len(measurements)) - for i, meas := range measurements { - descriptor[i] = packet.ValueDescriptor{ - Name: meas.GetId(), - Type: getValueType(meas.GetType()), - } - } - return descriptor -} - -func getValueType(literal string) string { - if strings.HasPrefix(literal, "enum") { - return "enum" - } else { - return literal - } -} - -func getEnumDescriptors(info info.Info, boards []pod_data.Board) map[string][]string { - enums := make(map[string][]string) - for _, board := range boards { - for _, packet := range board.Packets { - for _, meas := range packet.Measurements { - if enumMeas, ok := meas.(pod_data.EnumMeasurement); ok { - enums[enumMeas.Id] = enumMeas.Options - } - } - } - } - return enums -} diff --git a/backend/internal/vehicle/packet_parser/descriptors.go b/backend/internal/vehicle/packet_parser/descriptors.go deleted file mode 100644 index d5ca1b2fd..000000000 --- a/backend/internal/vehicle/packet_parser/descriptors.go +++ /dev/null @@ -1,8 +0,0 @@ -package packet_parser - -type ValueDescriptor struct { - Name string - Type string -} - -type EnumDescriptor []string diff --git a/backend/internal/vehicle/packet_parser/packet_parser.go b/backend/internal/vehicle/packet_parser/packet_parser.go deleted file mode 100644 index fafff8183..000000000 --- a/backend/internal/vehicle/packet_parser/packet_parser.go +++ /dev/null @@ -1,80 +0,0 @@ -package packet_parser - -import ( - "bytes" - "fmt" - "io" - - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" -) - -type PacketParser struct { - structures map[uint16][]packet.ValueDescriptor - valueParsers map[string]parser - config Config -} - -func (parser *PacketParser) Decode(id uint16, raw []byte, metadata packet.Metadata) (models.PacketUpdate, error) { - structure, ok := parser.structures[id] - if !ok { - return models.PacketUpdate{}, fmt.Errorf("structure for packet %d not found", id) - } - - reader := bytes.NewReader(raw) - - values := make(map[string]packet.Value) - for _, descriptor := range structure { - value, err := parser.decodeValue(descriptor, reader) - if err != nil { - return models.PacketUpdate{}, err - } - - values[descriptor.Name] = value - } - - return models.PacketUpdate{ - Metadata: metadata, - HexValue: raw, - Values: values, - }, nil -} - -func (parser *PacketParser) decodeValue(descriptor packet.ValueDescriptor, reader io.Reader) (packet.Value, error) { - decoder, ok := parser.valueParsers[descriptor.Type] - if !ok { - return nil, fmt.Errorf("decoder for type %s not found", descriptor.Type) - } - - return decoder.decode(descriptor, parser.config.GetByteOrder(), reader) -} - -func (parser *PacketParser) Encode(id uint16, values map[string]packet.Value, writer io.Writer) error { - structure, ok := parser.structures[id] - if !ok { - return fmt.Errorf("structure for packet %d not found", id) - } - - for _, descriptor := range structure { - value, ok := values[descriptor.Name] - if !ok { - return fmt.Errorf("value for %s not found", descriptor.Name) - } - - err := parser.encodeValue(descriptor, value, writer) - if err != nil { - return err - } - } - - return nil -} - -func (parser *PacketParser) encodeValue(descriptor packet.ValueDescriptor, value packet.Value, writer io.Writer) error { - encoder, ok := parser.valueParsers[descriptor.Type] - if !ok { - return fmt.Errorf("encoder for type %s not found", descriptor.Type) - } - - return encoder.encode(descriptor, parser.config.GetByteOrder(), value, writer) -} diff --git a/backend/internal/vehicle/packet_parser/parsers.go b/backend/internal/vehicle/packet_parser/parsers.go deleted file mode 100644 index c5bc41a0f..000000000 --- a/backend/internal/vehicle/packet_parser/parsers.go +++ /dev/null @@ -1,87 +0,0 @@ -package packet_parser - -import ( - "encoding/binary" - "fmt" - "io" - - "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" -) - -type parser interface { - decode(packet.ValueDescriptor, binary.ByteOrder, io.Reader) (packet.Value, error) - encode(packet.ValueDescriptor, binary.ByteOrder, packet.Value, io.Writer) error -} - -type numericParser[T common.Numeric] struct{} - -func (parser numericParser[T]) decode(descriptor packet.ValueDescriptor, order binary.ByteOrder, data io.Reader) (packet.Value, error) { - var value T - err := binary.Read(data, order, &value) - if err != nil { - return packet.Numeric(0), err - } - - return packet.Numeric(value), nil -} - -func (parser numericParser[T]) encode(descriptor packet.ValueDescriptor, order binary.ByteOrder, value packet.Value, data io.Writer) error { - return binary.Write(data, order, (T)((value).(packet.Numeric))) -} - -type booleanParser struct{} - -func (parser booleanParser) decode(descriptor packet.ValueDescriptor, order binary.ByteOrder, data io.Reader) (packet.Value, error) { - var value bool - err := binary.Read(data, order, &value) - if err != nil { - return packet.Boolean(false), err - } - - return packet.Boolean(value), nil -} - -func (parser booleanParser) encode(descriptor packet.ValueDescriptor, order binary.ByteOrder, value packet.Value, data io.Writer) error { - return binary.Write(data, order, value) -} - -type enumParser struct { - descriptors map[string][]string -} - -func (parser enumParser) decode(descriptor packet.ValueDescriptor, order binary.ByteOrder, data io.Reader) (packet.Value, error) { - enum, ok := parser.descriptors[descriptor.Name] - if !ok { - return packet.Enum("Default"), fmt.Errorf("enum descriptor for %s not found", descriptor.Name) - } - - var value uint8 - err := binary.Read(data, order, &value) - if err != nil { - return packet.Enum("Default"), err - } - - if len(enum) < int(value) { - return packet.Enum("Default"), fmt.Errorf("option slice has length %d but index is %d", len(enum), value) - } - - return packet.Enum(enum[value]), nil -} - -func (parser enumParser) encode(descriptor packet.ValueDescriptor, order binary.ByteOrder, value packet.Value, data io.Writer) error { - enum, ok := parser.descriptors[descriptor.Name] - if !ok { - return fmt.Errorf("enum descriptor for %s not found", descriptor.Name) - } - - var index uint8 - for i, v := range enum { - if v == string(value.(packet.Enum)) { - index = (uint8)(i) - break - } - } - - return binary.Write(data, order, index) -} diff --git a/backend/internal/vehicle/pipe_readers.go b/backend/internal/vehicle/pipe_readers.go deleted file mode 100644 index 94c78d675..000000000 --- a/backend/internal/vehicle/pipe_readers.go +++ /dev/null @@ -1,130 +0,0 @@ -package vehicle - -import ( - "encoding/binary" - "fmt" - "io" - - "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/info" -) - -func newPipeReaders(messageIds info.MessageIds) map[uint16]common.ReaderFrom { - return map[uint16]common.ReaderFrom{ - 0: NewEmptyFrom(), - messageIds.Info: NewProtectionFrom(), - messageIds.Warning: NewProtectionFrom(), - messageIds.Fault: NewProtectionFrom(), - messageIds.BlcuAck: NewEmptyFrom(), - messageIds.AddStateOrder: NewStateOrderReaderFrom(), - messageIds.RemoveStateOrder: NewStateOrderReaderFrom(), - messageIds.StateSpace: NewStateSpaceReaderFrom(8, 15, 4), - } -} - -func NewProtectionFrom() ProtectionFrom { - return ProtectionFrom{} -} - -type ProtectionFrom struct{} - -func (rf ProtectionFrom) ReadFrom(r io.Reader) ([]byte, error) { - var protectionLen uint16 - err := binary.Read(r, binary.LittleEndian, &protectionLen) - if err != nil { - return nil, err - } - - protectionBuf := make([]byte, protectionLen) - n, err := r.Read(protectionBuf) - if err != nil { - return nil, err - } - - if n != int(protectionLen) { - return nil, io.ErrShortBuffer - } - - return protectionBuf, nil -} - -func NewEmptyFrom() EmptyFrom { - return EmptyFrom{} -} - -type EmptyFrom struct{} - -func (rf EmptyFrom) ReadFrom(r io.Reader) ([]byte, error) { - return []byte{}, nil -} - -func NewStateOrderReaderFrom() StateOrderReaderFrom { - return StateOrderReaderFrom{} -} - -const OrderNumByteSize = 1 -const OrderByteSize = 2 - -type StateOrderReaderFrom struct{} - -func (rf StateOrderReaderFrom) ReadFrom(r io.Reader) ([]byte, error) { - orderNumBuf := make([]byte, OrderNumByteSize) - n, err := r.Read(orderNumBuf) - - if n != len(orderNumBuf) { - return nil, fmt.Errorf("expected %d bytes, got %d", len(orderNumBuf), n) - } - - if err != nil { - return nil, err - } - - orderNum := orderNumBuf[0] - - orderIds := make([]byte, (orderNum * OrderByteSize)) - n, err = r.Read(orderIds) - - if err != nil { - return nil, err - } - - if n != len(orderIds) { - return nil, fmt.Errorf("expected %d bytes, got %d", len(orderIds), n) - } - - result := append([]byte{orderNum}, orderIds...) - - return result, nil -} - -// 8*15 float32 - -type StateSpaceReaderFrom struct { - rows int - cols int - variableSize int -} - -func NewStateSpaceReaderFrom(rows int, cols int, varSize int) StateSpaceReaderFrom { - return StateSpaceReaderFrom{ - rows: rows, - cols: cols, - variableSize: varSize, - } -} - -func (rf StateSpaceReaderFrom) ReadFrom(r io.Reader) ([]byte, error) { - size := rf.cols * rf.rows * rf.variableSize - stateSpaceBuf := make([]byte, size) - n, err := r.Read(stateSpaceBuf) - - if err != nil { - return nil, err - } - - if n != size { - return nil, fmt.Errorf("incorrect state space size: want %d got %d", size, n) - } - - return stateSpaceBuf, nil -} diff --git a/backend/internal/vehicle/vehicle.go b/backend/internal/vehicle/vehicle.go deleted file mode 100644 index 03a315ad2..000000000 --- a/backend/internal/vehicle/vehicle.go +++ /dev/null @@ -1,277 +0,0 @@ -package vehicle - -import ( - "bytes" - "encoding/binary" - "fmt" - "net" - "strings" - - "github.com/HyperloopUPV-H8/h9-backend/internal/common" - "github.com/HyperloopUPV-H8/h9-backend/internal/packet" - "github.com/HyperloopUPV-H8/h9-backend/internal/pipe" - "github.com/HyperloopUPV-H8/h9-backend/internal/sniffer" - "github.com/HyperloopUPV-H8/h9-backend/internal/unit_converter" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/message_parser" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" - "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/packet_parser" - "github.com/rs/zerolog" -) - -type Vehicle struct { - sniffer sniffer.Sniffer - pipes map[string]*pipe.Pipe - backendAddr net.IP - - displayConverter unit_converter.UnitConverter - podConverter unit_converter.UnitConverter - - dataIds common.Set[uint16] - orderIds common.Set[uint16] - messageIds common.Set[uint16] - blcuAckId uint16 - addStateOrdersId uint16 - removeStateOrdersId uint16 - stateSpaceId uint16 - - packetParser packet_parser.PacketParser - messageParser message_parser.MessageParser - bitarrayParser BitarrayParser - - dataChan chan packet.Packet - - idToBoard map[uint16]string - - onConnectionChange func(string, bool) - - trace zerolog.Logger -} - -// func (vehicle *Vehicle) propagateFault(source string, payload []byte) { -// for _, pipe := range vehicle.pipes { -// pipe.SendFault(source, payload) -// } -// } - -func (vehicle *Vehicle) Listen(updateChan chan<- models.PacketUpdate, transmittedOrderChan chan<- models.PacketUpdate, messageChan chan<- any, blcuAckChan chan<- struct{}, stateOrdersChan chan<- message_parser.StateOrdersAdapter, stateSpaceChan chan<- models.StateSpace) { - vehicle.trace.Debug().Msg("vehicle listening") - for packet := range vehicle.dataChan { - payloadCopy := make([]byte, len(packet.Payload)) - copy(payloadCopy, packet.Payload) - - if packet.Metadata.ID == 0 { - continue - } - - //TODO: add order decoding - switch id := packet.Metadata.ID; { - case vehicle.dataIds.Has(id): - update, err := vehicle.getUpdate(packet) - - if err != nil { - vehicle.trace.Error().Err(err).Msg("error decoding packet") - continue - } - - updateChan <- update - - case vehicle.orderIds.Has(id): - update, err := vehicle.getUpdate(packet) - - if err != nil { - vehicle.trace.Error().Err(err).Msg("error decoding packet") - continue - } - - transmittedOrderChan <- update - - case id == vehicle.stateSpaceId: - stateSpace := models.NewStateSpace(packet.Payload) - stateSpaceChan <- stateSpace - - case vehicle.messageIds.Has(id): - - if !strings.Contains(packet.Metadata.To, vehicle.backendAddr.String()) { - continue - } - - if id == vehicle.blcuAckId { - blcuAckChan <- struct{}{} - continue - } - - message, err := vehicle.messageParser.Parse(id, packet.Payload) - - if err != nil { - vehicle.trace.Error().Err(err).Msg("error decoding protection") - continue - } - - if id == vehicle.addStateOrdersId || id == vehicle.removeStateOrdersId { - stateOrders, ok := message.(message_parser.StateOrdersAdapter) - if !ok { - vehicle.trace.Error().Type("type", message).Uint16("id", id).Msg("invalid type for state orders") - continue - } - stateOrdersChan <- stateOrders - continue - } - - messageChan <- message - - default: - vehicle.trace.Error().Uint16("id", packet.Metadata.ID).Msg("raw id not recognized") - } - } -} - -func (vehicle *Vehicle) SendOrder(order models.Order) error { - vehicle.trace.Info().Uint16("id", order.ID).Msg("send order") - - board, ok := vehicle.idToBoard[order.ID] - - if !ok { - return fmt.Errorf("board for order id %d not found", order.ID) - } - - switch board { - case "BLCU": - return vehicle.sendOrderToBoard(order, "BLCU") - case "TCU": - return vehicle.sendOrderToBoard(order, "TCU") - default: - return vehicle.sendOrderToBoard(order, "VCU") - } -} - -func (vehicle *Vehicle) sendOrderToBoard(order models.Order, board string) error { - pipe, ok := vehicle.pipes[board] - - if !ok { - vehicle.trace.Error().Str("board", board).Msg("pipe not found") - return nil - } - - buf, err := vehicle.orderToBuf(order) - - if err != nil { - vehicle.trace.Error().Err(err).Msg("converting order to buf") - return err - } - - _, err = common.WriteAll(pipe, buf) - - return err -} - -func (vehicle *Vehicle) orderToBuf(order models.Order) ([]byte, error) { - values := getOrderValues(order, vehicle.trace) - convertedValues := vehicle.applyUnitConversion(values) - - buf := new(bytes.Buffer) - - idBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(idBuf, order.ID) - - err := vehicle.packetParser.Encode(order.ID, convertedValues, buf) - if err != nil { - vehicle.trace.Error().Err(err).Msg("error encoding order") - return nil, err - } - - enableBuf := new(bytes.Buffer) - vehicle.bitarrayParser.encodeBitarray(getOrderEnables(order), enableBuf) - - bufWithoutBitarray := append(idBuf, buf.Bytes()...) - return bufWithoutBitarray, nil -} - -func (vehicle *Vehicle) getUpdate(packet packet.Packet) (models.PacketUpdate, error) { - update, err := vehicle.packetParser.Decode(packet.Metadata.ID, packet.Payload, packet.Metadata) - - if err != nil { - return models.PacketUpdate{}, nil - } - - convertedValues := vehicle.applyUnitConversion(update.Values) - update.Values = convertedValues - - return update, nil -} - -func (vehicle *Vehicle) applyUnitConversion(values map[string]packet.Value) map[string]packet.Value { - newValues := make(map[string]packet.Value) - - for name, value := range values { - switch typedValue := value.(type) { - case packet.Numeric: - newValues[name] = vehicle.applyNumericConversion(name, float64(typedValue)) - default: - newValues[name] = typedValue - } - } - - return newValues -} - -func (vehicle *Vehicle) applyNumericConversion(name string, value float64) packet.Numeric { - valueInSIUnits, podErr := vehicle.podConverter.Revert(name, value) - - if podErr != nil { - vehicle.trace.Error().Err(podErr).Msg("error reverting podUnits") - } - - valueInDisplayUnits, displayErr := vehicle.displayConverter.Convert(name, valueInSIUnits) - - if displayErr != nil { - vehicle.trace.Error().Err(displayErr).Msg("error converting to displayUnits") - - } - - return packet.Numeric(valueInDisplayUnits) -} - -func (vehicle *Vehicle) getPipe(id uint16) (*pipe.Pipe, error) { - board, ok := vehicle.idToBoard[id] - if !ok { - return nil, fmt.Errorf("board for id %d not found", id) - } - - pipe, ok := vehicle.pipes[board] - if !ok { - return nil, fmt.Errorf("pipe for board %s not found", board) - } - - return pipe, nil -} - -func getOrderValues(order models.Order, trace zerolog.Logger) map[string]packet.Value { - values := make(map[string]packet.Value) - - for name, field := range order.Fields { - switch value := field.Value.(type) { - case float64: - values[name] = packet.Numeric(value) - case uint16: - values[name] = packet.Numeric(value) - case bool: - values[name] = packet.Boolean(value) - case string: - values[name] = packet.Enum(value) - default: - trace.Error().Str("name", name).Type("type", field.Value).Msg("order field value not recognized") - } - } - - return values -} - -func getOrderEnables(order models.Order) map[string]bool { - enables := make(map[string]bool, 0) - - for name, field := range order.Fields { - enables[name] = field.IsEnabled - } - - return enables -} diff --git a/backend/load-secret.sh b/backend/load-secret.sh new file mode 100755 index 000000000..105c658e3 --- /dev/null +++ b/backend/load-secret.sh @@ -0,0 +1,9 @@ +if [ $# -ne 1 ]; then + echo "expected at least one argument"; + exit 1; +fi + +echo "$1" > "./internal/excel/secret.json" +echo "$1" > "./internal/excel_adapter/internals/secret.json" +echo "$1" > "./pkg/excel/secret.json" +echo "$1" > "./pkg/excel_adapter/internals/secret.json" diff --git a/backend/pkg/abstraction/logger.go b/backend/pkg/abstraction/logger.go index 0c0ef6c40..24913fde8 100644 --- a/backend/pkg/abstraction/logger.go +++ b/backend/pkg/abstraction/logger.go @@ -16,6 +16,8 @@ type LoggerRequest interface { // Logger is the module in charge of storing and retrieving information from the // hard drive, making it persistent. type Logger interface { + Start(startKeys []LoggerName) error + Stop(stopKeys []LoggerName) error // PushRecord will store a record to disk PushRecord(LoggerRecord) error // PullRecord will retrieve a record from disk diff --git a/backend/pkg/abstraction/transport.go b/backend/pkg/abstraction/transport.go index fec954cac..9eb989a9c 100644 --- a/backend/pkg/abstraction/transport.go +++ b/backend/pkg/abstraction/transport.go @@ -32,4 +32,5 @@ type Transport interface { type TransportAPI interface { // Notification notifies the back-end that an event has been received Notification(TransportNotification) + ConnectionUpdate(TransportTarget, bool) } diff --git a/backend/pkg/excel/ade/ade_test.go b/backend/pkg/excel/ade/ade_test.go deleted file mode 100644 index eefd7069d..000000000 --- a/backend/pkg/excel/ade/ade_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package ade - -import ( - "testing" - - "github.com/xuri/excelize/v2" -) - -func TestAde(t *testing.T) { - t.Run("correct ade is parsed without errors", func(t *testing.T) { - file, err := excelize.OpenFile("ade.xlsx") - - if err != nil { - t.Fatalf("opening file: %e", err) - } - - _, err = CreateADE(file) - - if err != nil { - t.Fatalf("creating ade: %e", err) - } - }) - -} diff --git a/backend/pkg/logger/CHANGELOG.md b/backend/pkg/logger/CHANGELOG.md new file mode 100644 index 000000000..62c30baa5 --- /dev/null +++ b/backend/pkg/logger/CHANGELOG.md @@ -0,0 +1,49 @@ +# Logger Changelog + +## v0.0.0 + +### v0.3.0 + +#### v0.3.1 + +- Integrate abstraction +- "Keys" concept implementation +- Data sublogger + - Add constants + - Add map of valueTypes + +#### v0.3.2 + +- Errors and name publicity fixed + +#### v0.3.3 + +- Change from map to list +- Use more descriptive names +- Fix constants + +#### v0.3.4 + +- Change type check logic +- Handle writer errors +- Fix error printing +- Fix error handling logic +- Prevent concurrency-related errors +- Logic fixes + +## v1.0.0 + +### v1.1.0 + +#### v1.1.1 + +- Messages sublogger +- Handle json timestamp + +#### v1.1.2 + +- Order sublogger + +#### v1.1.3 + +- State sublogger diff --git a/backend/pkg/logger/data/logger.go b/backend/pkg/logger/data/logger.go new file mode 100644 index 000000000..823c3a238 --- /dev/null +++ b/backend/pkg/logger/data/logger.go @@ -0,0 +1,135 @@ +package data + +import ( + "encoding/csv" + "fmt" + "io" + "os" + "path" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/logger" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" +) + +const ( + Name abstraction.LoggerName = "data" +) + +// Logger is a struct that implements the abstraction.Logger interface +type Logger struct { + // An atomic boolean is used in order to use CompareAndSwap in the Start and Stop methods + running *atomic.Bool + fileLock *sync.RWMutex + // initialTime fixes the starting time of the log + initialTime time.Time + // valueFileSlice is a map that contains the file of each value + valueFileSlice map[data.ValueName]io.WriteCloser +} + +// Record is a struct that implements the abstraction.LoggerRecord interface +type Record struct { + packet *data.Packet +} + +func (data *Record) Name() abstraction.LoggerName { + return Name +} + +func (sublogger *Logger) Start() error { + if !sublogger.running.CompareAndSwap(false, true) { + fmt.Println("Logger already running") + return nil + } + sublogger.initialTime = time.Now() + + fmt.Println("Logger started") + return nil +} + +// numeric is an interface that allows to get the value of any numeric format +type numeric interface { + Value() float64 +} + +func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error { + if !sublogger.running.Load() { + return &logger.ErrLoggerNotRunning{ + Name: Name, + Timestamp: time.Now(), + } + } + + dataRecord, ok := record.(*Record) + if !ok { + return &logger.ErrWrongRecordType{ + Name: Name, + Timestamp: time.Now(), + Expected: &Record{}, + Received: record, + } + } + + valueMap := dataRecord.packet.GetValues() + + sublogger.fileLock.Lock() + defer sublogger.fileLock.Unlock() + + writerErr := error(nil) + for valueName, value := range valueMap { + var packet *Record + timestamp := packet.packet.Timestamp() + + var val string + + switch v := value.(type) { + case numeric: + val = strconv.FormatFloat(v.Value(), 'f', -1, 64) + + case data.BooleanValue: + val = strconv.FormatBool(v.Value()) + + case data.EnumValue: + val = string(v.Variant()) + } + + file, ok := sublogger.valueFileSlice[valueName] + if !ok { + f, err := os.Create(path.Join(string(valueName), fmt.Sprintf("%s_%s.csv", valueName, packet.packet.Timestamp().Format("3339")))) + if err != nil { + return &logger.ErrCreatingFile{ + Name: Name, + Timestamp: time.Now(), + Inner: err, + } + } + sublogger.valueFileSlice[valueName] = f + file = f + } + writer := csv.NewWriter(file) // TODO! use map/slice of writers + defer writer.Flush() + + err := writer.Write([]string{timestamp.Format(time.RFC3339), val}) + if err != nil { + writerErr = err + } + } + return writerErr +} + +func (sublogger *Logger) PullRecord(request abstraction.LoggerRequest) (abstraction.LoggerRecord, error) { + panic("TODO!") +} + +func Stop(sublogger *Logger) { + if !sublogger.running.CompareAndSwap(true, false) { + fmt.Println("Logger already stopped") + return + } + + fmt.Println("Logger stopped") +} diff --git a/backend/pkg/logger/errors.go b/backend/pkg/logger/errors.go new file mode 100644 index 000000000..dd181cafb --- /dev/null +++ b/backend/pkg/logger/errors.go @@ -0,0 +1,50 @@ +package logger + +import ( + "fmt" + "time" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +type ErrLoggerNotFound struct { + Name abstraction.LoggerName +} + +func (err ErrLoggerNotFound) Error() string { + return fmt.Sprintf("Logger %s not found", err.Name) +} + +type ErrCreatingFile struct { + Name abstraction.LoggerName + Timestamp time.Time + Inner error +} + +func (err *ErrCreatingFile) Error() string { + return fmt.Sprintf("Error creating file for %s logger at %s", err.Name, err.Timestamp.Format(time.RFC3339)) +} + +func (err *ErrCreatingFile) Unwrap() error { + return err.Inner +} + +type ErrLoggerNotRunning struct { + Name abstraction.LoggerName + Timestamp time.Time +} + +func (err *ErrLoggerNotRunning) Error() string { + return fmt.Sprintf("Logger %s not running at %s", err.Name, err.Timestamp.Format(time.RFC3339)) +} + +type ErrWrongRecordType struct { + Name abstraction.LoggerName + Timestamp time.Time + Expected abstraction.LoggerRecord + Received abstraction.LoggerRecord +} + +func (err *ErrWrongRecordType) Error() string { + return fmt.Sprintf("Wrong record type for logger %s at %s, expected %T, got %T", err.Name, err.Timestamp.Format(time.RFC3339), err.Expected.Name(), err.Received.Name()) +} diff --git a/backend/pkg/logger/logger.go b/backend/pkg/logger/logger.go new file mode 100644 index 000000000..f9efb34ba --- /dev/null +++ b/backend/pkg/logger/logger.go @@ -0,0 +1,82 @@ +package logger + +import ( + "fmt" + "sync" + "sync/atomic" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// Logger is a struct that implements the abstraction.Logger interface +type Logger struct { + // An atomic boolean is used in order to use CompareAndSwap in the Start and Stop methods + running *atomic.Bool + subloggersLock *sync.RWMutex + subloggers map[abstraction.LoggerName]abstraction.Logger +} + +var _ abstraction.Logger = &Logger{} + +func (logger *Logger) Start(startKeys []abstraction.LoggerName) error { + if logger.running.CompareAndSwap(false, true) { + fmt.Println("Logger already running") + return nil + } + + logger.subloggersLock.Lock() + defer logger.subloggersLock.Unlock() + + for _, name := range startKeys { + if sublogger, ok := logger.subloggers[name]; ok { + go sublogger.Start(nil) + } + } + + fmt.Println("Logger started") + return nil +} + +// PushRecord works as a proxy for the PushRecord method of the subloggers +func (logger *Logger) PushRecord(record abstraction.LoggerRecord) error { + loggerChecked, ok := logger.subloggers[record.Name()] + if !ok { + return ErrLoggerNotFound{record.Name()} + } + return loggerChecked.PushRecord(record) +} + +// PullRecord works as a proxy for the PullRecord method of the subloggers +func (logger *Logger) PullRecord(request abstraction.LoggerRequest) (abstraction.LoggerRecord, error) { + loggerChecked, ok := logger.subloggers[request.Name()] + if !ok { + return nil, ErrLoggerNotFound{request.Name()} + } + return loggerChecked.PullRecord(request) +} + +func (logger *Logger) Stop(stopKeys []abstraction.LoggerName) error { + logger.subloggersLock.Lock() + defer logger.subloggersLock.Unlock() + + if !logger.running.CompareAndSwap(true, false) { + fmt.Printf("Logger already stopped") + return nil + } + + var wg sync.WaitGroup + for _, sublogger := range stopKeys { + wg.Add(1) + + go func(sublogger abstraction.Logger) { + defer wg.Done() + sublogger.Stop(nil) + }(logger.subloggers[sublogger]) + } + // The waitgroup is used in order to wait for all the subloggers to stop + // before closing the main logger + wg.Wait() + + fmt.Printf("Logger stopped") + return nil +} diff --git a/backend/pkg/logger/messages/logger.go b/backend/pkg/logger/messages/logger.go new file mode 100644 index 000000000..933bd08f0 --- /dev/null +++ b/backend/pkg/logger/messages/logger.go @@ -0,0 +1,131 @@ +package messages + +import ( + "encoding/csv" + "fmt" + "io" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/logger" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/info" +) + +const ( + Name abstraction.LoggerName = "messages" +) + +// Record is a struct that implements the abstraction.LoggerRecord interface +type Record struct { + packet *info.Packet +} + +type Logger struct { + // An atomic boolean is used in order to use CompareAndSwap in the Start and Stop methods + running *atomic.Bool + fileLock *sync.RWMutex + // initialTime fixes the starting time of the log + initialTime time.Time + // infoIdMap is a map that contains the file of each info packet + infoIdMap map[abstraction.BoardId]io.WriteCloser + // BoardNames is a map that contains the common name of each board + boardNames map[abstraction.BoardId]string +} + +func NewLogger(boardMap map[abstraction.BoardId]string) *Logger { + return &Logger{ + running: &atomic.Bool{}, + fileLock: &sync.RWMutex{}, + infoIdMap: make(map[abstraction.BoardId]io.WriteCloser), + boardNames: boardMap, + } +} + +func (info *Record) Name() abstraction.LoggerName { + return Name +} + +func (sublogger *Logger) Start(boardMap map[abstraction.BoardId]string) error { + if !sublogger.running.CompareAndSwap(false, true) { + fmt.Println("Logger already running") + return nil + } + sublogger.initialTime = time.Now() + + NewLogger(boardMap) + + fmt.Println("Logger started") + return nil +} + +func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error { + if !sublogger.running.Load() { + return &logger.ErrLoggerNotRunning{ + Name: Name, + Timestamp: time.Now(), + } + } + + infoRecord, ok := record.(*Record) + if !ok { + return &logger.ErrWrongRecordType{ + Name: Name, + Timestamp: time.Now(), + Expected: &Record{}, + Received: record, + } + } + + boardId := infoRecord.packet.BoardId + timestamp := infoRecord.packet.Timestamp.ToTime().Format(time.RFC3339) + msg := string(infoRecord.packet.Msg) + + sublogger.fileLock.Lock() + defer sublogger.fileLock.Unlock() + + writerErr := error(nil) + + // The existence check is performed with the board ID + file, ok := sublogger.infoIdMap[boardId] + if !ok { + boardName, ok := sublogger.boardNames[boardId] + if !ok { + boardName = fmt.Sprint(boardId) + } + f, err := os.Create(fmt.Sprintf(boardName + "_" + timestamp + ".csv")) + if err != nil { + return &logger.ErrCreatingFile{ + Name: Name, + Timestamp: time.Now(), + Inner: err, + } + } + sublogger.infoIdMap[boardId] = f + file = f + } + writer := csv.NewWriter(file) + defer writer.Flush() + + err := writer.Write([]string{timestamp, msg}) + if err != nil { + writerErr = err + } + return writerErr +} + +func (sublogger *Logger) PullRecords() ([]abstraction.LoggerRecord, error) { + panic("TODO!") +} + +func (sublogger *Logger) Stop() error { + if !sublogger.running.CompareAndSwap(true, false) { + fmt.Println("Logger already stopped") + return nil + } + + fmt.Println("Logger stopped") + return nil +} diff --git a/backend/pkg/logger/order/logger.go b/backend/pkg/logger/order/logger.go new file mode 100644 index 000000000..5dec4e506 --- /dev/null +++ b/backend/pkg/logger/order/logger.go @@ -0,0 +1,100 @@ +package order + +import ( + "encoding/csv" + "fmt" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/logger" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" +) + +const ( + Name abstraction.LoggerName = "order" +) + +type Record struct { + packet *data.Packet +} + +func (order *Record) Name() abstraction.LoggerName { + return Name +} + +type Logger struct { + // An atomic boolean is used in order to use CompareAndSwap in the Start and Stop methods + running *atomic.Bool + fileLock *sync.RWMutex + // initialTime fixes the starting time of the log + initialTime time.Time + writer *csv.Writer +} + +func (sublogger *Logger) Start() error { + if !sublogger.running.CompareAndSwap(false, true) { + fmt.Println("Logger already running") + return nil + } + sublogger.initialTime = time.Now() + + file, err := os.Create(fmt.Sprintf("order_" + sublogger.initialTime.Format(time.RFC3339) + ".csv")) + if err != nil { + return &logger.ErrCreatingFile{ + Name: Name, + Timestamp: time.Now(), + Inner: err, + } + } + sublogger.writer = csv.NewWriter(file) + + fmt.Println("Logger started") + return nil +} + +func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error { + if !sublogger.running.Load() { + return &logger.ErrLoggerNotRunning{ + Name: Name, + Timestamp: time.Now(), + } + } + + orderRecord, ok := record.(*Record) + if !ok { + return &logger.ErrWrongRecordType{ + Name: Name, + Timestamp: time.Now(), + Expected: &Record{}, + Received: record, + } + } + + sublogger.fileLock.Lock() + defer sublogger.fileLock.Unlock() + + err := sublogger.writer.Write([]string{time.Now().Format(time.RFC3339), fmt.Sprint(orderRecord.packet.GetValues())}) + if err != nil { + return err + } + + defer sublogger.writer.Flush() + return nil +} + +func (sublogger *Logger) PullRecord() (abstraction.LoggerRecord, error) { + panic("TODO!") +} + +func (sublogger *Logger) Stop() error { + if !sublogger.running.CompareAndSwap(true, false) { + fmt.Println("Logger already stopped") + return nil + } + + fmt.Println("Logger stopped") + return nil +} diff --git a/backend/pkg/logger/state/logger.go b/backend/pkg/logger/state/logger.go new file mode 100644 index 000000000..dc17b64aa --- /dev/null +++ b/backend/pkg/logger/state/logger.go @@ -0,0 +1,93 @@ +package state + +import ( + "encoding/csv" + "fmt" + "os" + "sync/atomic" + "time" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/logger" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/state" +) + +const ( + Name abstraction.LoggerName = "state" +) + +type Record struct { + packet *state.Space +} + +func (record *Record) Name() abstraction.LoggerName { + return Name +} + +type Logger struct { + // An atomic boolean is used in order to use CompareAndSwap in the Start and Stop methods + running *atomic.Bool +} + +func (sublogger *Logger) Start() error { + if !sublogger.running.CompareAndSwap(false, true) { + fmt.Println("Logger already running") + return nil + } + + fmt.Println("Logger started") + return nil +} + +func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error { + if !sublogger.running.Load() { + return &logger.ErrLoggerNotRunning{ + Name: Name, + Timestamp: time.Now(), + } + } + + stateRecord, ok := record.(*Record) + if !ok { + return &logger.ErrWrongRecordType{ + Name: Name, + Timestamp: time.Now(), + Expected: &Record{}, + Received: record, + } + } + + file, err := os.Create(fmt.Sprintf("state_" + time.Now().Format(time.RFC3339) + ".csv")) + if err != nil { + return &logger.ErrCreatingFile{ + Name: Name, + Timestamp: time.Now(), + Inner: err, + } + } + writer := csv.NewWriter(file) + defer writer.Flush() + + for _, item := range stateRecord.packet.State() { + err = writer.Write([]string{fmt.Sprint(item)}) + if err != nil { + return err + } + } + + return nil +} + +func (sublogger *Logger) PullRecord() (abstraction.LoggerRecord, error) { + panic("TODO!") +} + +func (sublogger *Logger) Stop() error { + if !sublogger.running.CompareAndSwap(true, false) { + fmt.Println("Logger already stopped") + return nil + } + + fmt.Println("Logger stopped") + return nil +} diff --git a/backend/pkg/transport/constructor.go b/backend/pkg/transport/constructor.go new file mode 100644 index 000000000..d483861eb --- /dev/null +++ b/backend/pkg/transport/constructor.go @@ -0,0 +1,36 @@ +package transport + +import ( + "net" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/tftp" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/presentation" +) + +func NewTransport() *Transport { + return &Transport{ + connections: make(map[abstraction.TransportTarget]net.Conn), + idToTarget: make(map[abstraction.PacketId]abstraction.TransportTarget), + } +} + +func (transport *Transport) WithDecoder(decoder *presentation.Decoder) *Transport { + transport.decoder = decoder + return transport +} + +func (transport *Transport) WithEncoder(encoder *presentation.Encoder) *Transport { + transport.encoder = encoder + return transport +} + +func (transport *Transport) WithTFTP(client *tftp.Client) *Transport { + transport.tftp = client + return transport +} + +func (transport *Transport) SetIdTarget(id abstraction.PacketId, target abstraction.TransportTarget) *Transport { + transport.idToTarget[id] = target + return transport +} diff --git a/backend/pkg/transport/errors.go b/backend/pkg/transport/errors.go index 3edbb68ea..ad84f688d 100644 --- a/backend/pkg/transport/errors.go +++ b/backend/pkg/transport/errors.go @@ -9,9 +9,33 @@ import ( // ErrUnrecognizedEvent is returned when the event passed to transport // has an invalid type or it is not recognized. type ErrUnrecognizedEvent struct { - event abstraction.TransportEvent + Event abstraction.TransportEvent } func (err ErrUnrecognizedEvent) Error() string { - return fmt.Sprintf("unrecognized event %s", err.event) + return fmt.Sprintf("unrecognized event %s", err.Event) +} + +type ErrTargetAlreadyConnected struct { + Target abstraction.TransportTarget +} + +func (err ErrTargetAlreadyConnected) Error() string { + return fmt.Sprintf("%s is already connected", err.Target) +} + +type ErrUnrecognizedId struct { + Id abstraction.PacketId +} + +func (err ErrUnrecognizedId) Error() string { + return fmt.Sprintf("could not find target for packet with id %d", err.Id) +} + +type ErrConnClosed struct { + Target abstraction.TransportTarget +} + +func (err ErrConnClosed) Error() string { + return fmt.Sprintf("connection with %s is closed", err.Target) } diff --git a/backend/pkg/transport/messages.go b/backend/pkg/transport/messages.go index 62c340a94..9e4205b36 100644 --- a/backend/pkg/transport/messages.go +++ b/backend/pkg/transport/messages.go @@ -17,12 +17,12 @@ const ( // PacketMessage request a packet to be sent to the vehicle. type PacketMessage struct { - packet abstraction.Packet + abstraction.Packet } func NewPacketMessage(packet abstraction.Packet) PacketMessage { return PacketMessage{ - packet: packet, + Packet: packet, } } @@ -31,24 +31,16 @@ func (message PacketMessage) Event() abstraction.TransportEvent { return PacketEvent } -// Packet returns the packet associated with the message -func (message PacketMessage) Packet() abstraction.Packet { - return message.packet -} - -// Id returns the Id of the packet associated with the message -func (message PacketMessage) Id() abstraction.PacketId { - return message.packet.Id() -} - // FileWriteMessage request a file to be written to a specific target type FileWriteMessage struct { - data io.Reader + filename string + io.Reader } -func NewFileWriteMessage(data io.Reader) FileWriteMessage { +func NewFileWriteMessage(filename string, input io.Reader) FileWriteMessage { return FileWriteMessage{ - data: data, + filename: filename, + Reader: input, } } @@ -57,24 +49,20 @@ func (message FileWriteMessage) Event() abstraction.TransportEvent { return FileWriteEvent } -// Data returns the data to be written through tftp -func (message FileWriteMessage) Data() io.Reader { - return message.data -} - -// Read maps the message data read method so it can be used directly -func (message FileWriteMessage) Read(p []byte) (n int, err error) { - return message.data.Read(p) +func (message FileWriteMessage) Filename() string { + return message.filename } // FileReadMessage request a file to be read from the specific target. type FileReadMessage struct { - output io.Writer + filename string + io.Writer } -func NewFileReadMessage(output io.Writer) FileReadMessage { +func NewFileReadMessage(filename string, output io.Writer) FileReadMessage { return FileReadMessage{ - output: output, + filename: filename, + Writer: output, } } @@ -83,12 +71,6 @@ func (message FileReadMessage) Event() abstraction.TransportEvent { return FileReadEvent } -// Output returns where data read should be written -func (message FileReadMessage) Output() io.Writer { - return message.output -} - -// Write maps the message output write method -func (message FileReadMessage) Write(p []byte) (n int, err error) { - return message.output.Write(p) +func (message FileReadMessage) Filename() string { + return message.filename } diff --git a/backend/pkg/transport/network/sniffer/sniffer.go b/backend/pkg/transport/network/sniffer/sniffer.go index 235e798ed..7d77ad320 100644 --- a/backend/pkg/transport/network/sniffer/sniffer.go +++ b/backend/pkg/transport/network/sniffer/sniffer.go @@ -1,6 +1,8 @@ package sniffer import ( + "fmt" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network" "github.com/google/gopacket" "github.com/google/gopacket/layers" @@ -22,7 +24,7 @@ type Sniffer struct { // value is nil, the program tries to automatically detect the first layer from the source. // // The provided source should be already configured and ready to use, with the appropiate filters. -func New(source *pcap.Handle, firstLayer *gopacket.LayerType) (*Sniffer, error) { +func New(source *pcap.Handle, firstLayer *gopacket.LayerType) *Sniffer { first := source.LinkType().LayerType() if firstLayer != nil { first = *firstLayer @@ -34,7 +36,7 @@ func New(source *pcap.Handle, firstLayer *gopacket.LayerType) (*Sniffer, error) decoder: decoder, } - return sniffer, nil + return sniffer } // ReadNext pulls the next packet from the wire, decodes it and returns the socket it belongs to, @@ -76,6 +78,7 @@ layerLoop: } if socket.SrcPort == 0 && socket.DstPort == 0 { + fmt.Println(packetLayers) return network.Socket{}, data, ErrMissingPayload{packetLayers} } diff --git a/backend/pkg/transport/network/sniffer/sniffer_test.go b/backend/pkg/transport/network/sniffer/sniffer_test.go index a5eea9e2c..ae4bbdf77 100644 --- a/backend/pkg/transport/network/sniffer/sniffer_test.go +++ b/backend/pkg/transport/network/sniffer/sniffer_test.go @@ -57,10 +57,7 @@ func TestSniffer(t *testing.T) { } first := layers.LayerTypeEthernet - sniffer, err := sniffer.New(source, &first) - if err != nil { - t.Fatalf("error creating sniffer: %s", err) - } + sniffer := sniffer.New(source, &first) defer sniffer.Close() for _, test := range expected { diff --git a/backend/pkg/transport/network/tcp/client.go b/backend/pkg/transport/network/tcp/client.go index 2b51e9c78..ed66b4748 100644 --- a/backend/pkg/transport/network/tcp/client.go +++ b/backend/pkg/transport/network/tcp/client.go @@ -1,147 +1,106 @@ package tcp import ( + "context" "fmt" "net" "time" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" - "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network" ) -var ( - // CLIENT_INITIAL_BACKOFF is the starting value for the client backoff - CLIENT_INITIAL_BACKOFF = time.Millisecond * 100 - // CLIENT_MAX_BACKOFF is the max value for the client backoff - CLIENT_MAX_BACKOFF = time.Second * 5 +type backoffFunction = func(int) time.Duration + +const ( + defaultBackoffMin time.Duration = 100 * time.Millisecond + defaultBackoffExp float32 = 1.5 + defaultBackoffMax time.Duration = 5 * time.Second ) -// backoff is an abstraction over an exponential backoff algorithmn -type backoff struct { - base time.Duration - current time.Duration - rate float32 - max time.Duration -} +// NewExponBackoff returns an exponential backoff function with the given paramenters. +// +// It follows this formula: delay = (min * (exp ^ n); delay < max ? delay : max +func NewExponBackoff(min time.Duration, exp float32, max time.Duration) backoffFunction { + return func(n int) time.Duration { + curr := min + for i := 0; curr < max && i < n; i++ { + curr = time.Duration(exp * float32(curr)) + } + if curr > max { + return max + } -// next returns a channel where a message will be sent once the next period -// of time elapses. -func (b *backoff) next() <-chan time.Time { - timer := time.NewTimer(b.current) - return timer.C + return curr + } } -// reset resets the current value of the backoff to the default one -func (b *backoff) reset() { - b.current = b.base -} +// ClientConfig defines multiple configuration options for client operations +type ClientConfig struct { + net.Dialer -// increase sets the current value of the backoff to the next one -func (b *backoff) increase() { - b.current = time.Duration(float32(b.current) * b.rate) - if b.current > b.max { - b.current = b.max - } -} + // Context is the context used for the client. When cancelled, any attempts to connect to a + // remote will be cancelled + Context context.Context -// Assertion to check the TCPClient is a TCPSource -var _ Source = &Client{} + // Target is the transport target associated with this client + Target abstraction.TransportTarget -// Client is a TCPSource that creates connections with a server. -// -// Client must be used the same way as a regular TCPSource, extra methods provided -// are just to configure certain aspects about it. -type Client struct { - target abstraction.TransportTarget - raddr net.Addr - dialer net.Dialer - onConnection connectionCallback - onError errorCallback - retry backoff + // MaxRetries defines how many times might this client attempt to connect after a failed attempt + MaxRetries int + // CurrentRetries is how many times the client has tried to reconnect + CurrentRetries int + // Backoff specifies the backoff algorithm for this client + Backoff backoffFunction } -// NewClient creates a new TCPClient for the specified socket. -// -// It returns a non nil err if the one of the socket addresses are invalid. -// -// Target is used internally by the TransportModule to identify the client, it should follow -// this form: "client/" -func NewClient(target abstraction.TransportTarget, socket network.Socket) (*Client, error) { - laddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", socket.SrcIP, socket.SrcPort)) - if err != nil { - return nil, err - } - raddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", socket.DstIP, socket.DstPort)) - if err != nil { - return nil, err - } - - return &Client{ - target: target, - raddr: raddr, - dialer: net.Dialer{ - LocalAddr: laddr, - KeepAlive: -1, // Disable default keepalive (defaults to 15s) - Timeout: 0, // Default value -> No timeout +// NewClient inits a ClientConfig with good defaults and the provided information +func NewClient(local net.Addr) ClientConfig { + return ClientConfig{ + Dialer: net.Dialer{ + Timeout: time.Second, + KeepAlive: -1, + LocalAddr: local, }, - retry: backoff{ - base: CLIENT_INITIAL_BACKOFF, - current: CLIENT_INITIAL_BACKOFF, - rate: 2, - max: CLIENT_MAX_BACKOFF, - }, - }, nil -} -// SetTimeout sets the timeout for the connection to happen, if the connection -// isn't established before this time, it will generate an error and attempt to reconnect -// -// A timeout of 0 means no timeout -func (client *Client) SetTimeout(timeout time.Duration) { - client.dialer.Timeout = timeout -} + Context: context.TODO(), -// SetKeepalive sets the keepalive period for the connecion once it is established. -// -// A keepalive of 0 is a default keepalive (15s) and a negative keepalive disables it. -func (client *Client) SetKeepalive(keepalive time.Duration) { - client.dialer.KeepAlive = keepalive + MaxRetries: 30, + Backoff: NewExponBackoff(defaultBackoffMin, defaultBackoffExp, defaultBackoffMax), + } } -// SetOnConnection registers the callback that will be used when a connection is made -func (client *Client) SetOnConnection(callback connectionCallback) { - client.onConnection = callback +// Dial attempts to create a connection with the specified remote using the client configuration. +func (config ClientConfig) Dial(network, remote string) (net.Conn, error) { + var err error + for ; config.CurrentRetries != config.MaxRetries; config.CurrentRetries++ { + var conn net.Conn + conn, err = config.DialContext(config.Context, network, remote) + if err == nil { + return conn, nil + } + if config.Context.Err() != nil { + return nil, config.Context.Err() + } + + if netErr, ok := err.(net.Error); !ok || !netErr.Timeout() { + return nil, err + } + time.Sleep(config.Backoff(config.CurrentRetries)) + } + + return nil, ErrTooManyRetries{ + Max: config.MaxRetries, + Network: network, + Remote: remote, + } } -// SetOnError registers the callback that will be used when an error making the connection occurs. -func (client *Client) SetOnError(callback errorCallback) { - client.onError = callback +type ErrTooManyRetries struct { + Max int + Network string + Remote string } -// Run starts the connection of the client. -// -// If the connection fails, the client tries to reconnect using an exponential backoff algorithmn. -// -// Errors and connections are reported using the OnError and OnConnection callbacks, and should be -// set before calling Run. -// -// Run will block, callers should run this as a goroutine. The execution can be halted by sending a -// message to the cancel message. -func (client *Client) Run(cancel <-chan struct{}) { - client.retry.reset() - for { - backoffTimer := client.retry.next() - select { - case <-backoffTimer: - conn, err := client.dialer.Dial("tcp", client.raddr.String()) - if err == nil { - client.onConnection(client.target, conn.(*net.TCPConn)) - return - } - client.onError(client.target, err) - client.retry.increase() - case <-cancel: - return - } - } +func (err ErrTooManyRetries) Error() string { + return fmt.Sprintf("tried to reconnect over %d times to %s over %s", err.Max, err.Network, err.Remote) } diff --git a/backend/pkg/transport/network/tcp/conn.go b/backend/pkg/transport/network/tcp/conn.go new file mode 100644 index 000000000..35575bf34 --- /dev/null +++ b/backend/pkg/transport/network/tcp/conn.go @@ -0,0 +1,40 @@ +package tcp + +import ( + "errors" + "net" +) + +type connWithErr struct { + net.Conn + errors chan<- error +} + +func WithErrChan(conn net.Conn) (net.Conn, <-chan error) { + errChan := make(chan error, 1) + return &connWithErr{ + Conn: conn, + errors: errChan, + }, errChan +} + +func (conn *connWithErr) Read(b []byte) (n int, err error) { + n, err = conn.Conn.Read(b) + if err != nil && !errors.Is(err, net.ErrClosed) { + conn.errors <- err + } + return +} + +func (conn *connWithErr) Write(b []byte) (n int, err error) { + n, err = conn.Conn.Write(b) + if err != nil && !errors.Is(err, net.ErrClosed) { + conn.errors <- err + } + return +} + +func (conn *connWithErr) Close() error { + close(conn.errors) + return conn.Conn.Close() +} diff --git a/backend/pkg/transport/network/tcp/server.go b/backend/pkg/transport/network/tcp/server.go index 3e0080eaa..492691ad7 100644 --- a/backend/pkg/transport/network/tcp/server.go +++ b/backend/pkg/transport/network/tcp/server.go @@ -1,131 +1,64 @@ package tcp import ( + "context" "net" - "time" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" ) -// Address is an alias for string encoded network addresses (e.g. "127.0.0.1:4040") -type Address = string +type connectionCallback = func(target abstraction.TransportTarget, conn net.Conn) error -// serverTargets are the addresses are expected to connect and their respective target name -type serverTargets = map[Address]abstraction.TransportTarget +type address = string -// Assertion to check the TCPServer is a TCPSource -var _ Source = &Server{} +// ServerConfig defines several configuration options for TCP server operations +type ServerConfig struct { + net.ListenConfig -// Server is a TCPSource that gets connections from clients -// -// Server must be used as any other TCPSource. -type Server struct { - name abstraction.TransportTarget - targets serverTargets - listener *net.TCPListener - keepalive time.Duration - onConnection connectionCallback - onError errorCallback -} - -// NewServer creates a new TCPServer with the given name, local address and target connections. -// It returns a non nil error if it fails to resolve the local address or when the listener creation fails. -func NewServer(name abstraction.TransportTarget, laddr string, targets serverTargets) (*Server, error) { - localAddr, err := net.ResolveTCPAddr("tcp", laddr) - if err != nil { - return nil, err - } - - listener, err := net.ListenTCP("tcp", localAddr) - if err != nil { - return nil, err - } - - return &Server{ - name: name, - targets: targets, - listener: listener, - keepalive: 0, - }, nil -} + // Context is the context for calls used with this configuration. + // Cancelling the context means cancelling any runing tasks like listening to connections. + Context context.Context -// SetKeepalive sets the keepalive that will be applied to the connections established -func (server *Server) SetKeepalive(keepalive time.Duration) { - server.keepalive = keepalive + // Targets is a list of connections which this server accepts and their related transport targets + Targets map[address]abstraction.TransportTarget } -// SetOnConnection registers the callback that will be used when a connection is made -func (server *Server) SetOnConnection(callback connectionCallback) { - server.onConnection = callback -} +// NewServer inits a new ServerConfig with good defaults and the provided values +func NewServer(targets map[address]abstraction.TransportTarget) ServerConfig { + return ServerConfig{ + ListenConfig: net.ListenConfig{ + KeepAlive: -1, + }, -// SetOnError registers the callback that will be used when an error making the connection occurs. -func (server *Server) SetOnError(callback errorCallback) { - server.onError = callback -} + Context: context.TODO(), -// accept result is an auxiliary struct to pass the results of listener.Accept through a channel -type acceptResult struct { - conn *net.TCPConn - err error + Targets: targets, + } } -// Run starts the TCPServer. -// -// When a conneciton is established, it first checks if it's valid, then configures it and lastly -// notifies of the connection with the OnConnection callback. +// Listen listens for incoming TCP connections based on the server configuration. // -// Callers should make sure the OnConnection and the OnError callbacks are provided before calling Run. -// -// The server execution can be stopped by sending a message to the cancel channel. -func (server *Server) Run(cancel <-chan struct{}) { - defer server.listener.Close() - for { - acceptChan := make(chan acceptResult) - - go func(acceptChan chan<- acceptResult) { - conn, err := server.listener.AcceptTCP() - acceptChan <- acceptResult{conn, err} - }(acceptChan) - - var conn *net.TCPConn - var err error - select { - case result := <-acceptChan: - conn = result.conn - err = result.err - case <-cancel: - return - } +// connections are notified through the config connection callback. Any errors encountered +// are returned, stopping the listener in the process. +func (config ServerConfig) Listen(network string, local string, onConnection connectionCallback) error { + listener, err := config.ListenConfig.Listen(config.Context, network, local) + if err != nil { + return err + } + defer listener.Close() + for { + conn, err := listener.Accept() if err != nil { - server.onError(server.name, err) - continue + return err } - target, ok := server.targets[Address(conn.RemoteAddr().String())] + target, ok := config.Targets[conn.RemoteAddr().String()] if !ok { conn.Close() continue } - err = server.configureConn(conn) - if err != nil { - conn.Close() - server.onError(server.name, err) - continue - } - - server.onConnection(target, conn) - } -} - -// configureConn is a helper to apply all configuration to a connection. -func (server *Server) configureConn(conn *net.TCPConn) error { - err := conn.SetKeepAlive(server.keepalive > 0) - if err != nil { - return err + onConnection(target, conn) // TODO: handle error } - - return conn.SetKeepAlivePeriod(server.keepalive) } diff --git a/backend/pkg/transport/network/tcp/tcp.go b/backend/pkg/transport/network/tcp/tcp.go deleted file mode 100644 index 6c8366797..000000000 --- a/backend/pkg/transport/network/tcp/tcp.go +++ /dev/null @@ -1,87 +0,0 @@ -package tcp - -import ( - "net" - - "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" -) - -// These callbacks are used by the Source and the Conn structs -// connectionCallback is called when a source has obtained a new connection -type connectionCallback = func(abstraction.TransportTarget, *net.TCPConn) - -// errorCallback is called when a source encounters an error creating a connection -type errorCallback = func(abstraction.TransportTarget, error) - -// stateCallback is called when a Conn connection state changes -type stateCallback = func(abstraction.TransportTarget, bool) - -// Source is an interface for different ways of creating TCP connections. -// -// When using sources the caller should first use SetOnConnection and SetOnError -// to ensure both that connections are created correctly and any errors are handled. -// -// Once the callbacks are set, the consumer should execute Run in a goroutine, passing -// an optional channel to stop the execution of the Source -type Source interface { - // Run starts sourcing connections. Implementations might block, so it is advised - // to call this in a goroutine to prevent blocking. - // - // The provided channel is used to stop the runner, when a message is sent the - // Source stops creating connections. - Run(<-chan struct{}) - - // SetOnConnection gives the Source the callback it should call when a new connection - // is established. - SetOnConnection(connectionCallback) - - // SetOnError gives the Source the callback it should call when an error making a - // connection is encountered. - SetOnError(errorCallback) -} - -// Conn is a wrapper around a net.TCPConn which gives it an associated target and -// other callbacks -type Conn struct { - target abstraction.TransportTarget - conn *net.TCPConn - onConnUpdate stateCallback -} - -// NewConn creates a new Conn with the provided parameters -// -// target is the TransportTarget associated with this Conn -// -// conn is the actual network connection -// -// callback is the callback that will be called when the conn connects / disconnects -func NewConn(target abstraction.TransportTarget, conn *net.TCPConn, callback stateCallback) *Conn { - callback(target, true) - return &Conn{ - target: target, - conn: conn, - onConnUpdate: callback, - } -} - -// Target returns the connection TransportTarget -func (tcp *Conn) Target() abstraction.TransportTarget { - return tcp.target -} - -// Read maps the underlying conn Read method -func (tcp *Conn) Read(p []byte) (n int, err error) { - return tcp.conn.Read(p) -} - -// Write maps the underlying conn Write method -func (tcp *Conn) Write(p []byte) (n int, err error) { - return tcp.conn.Write(p) -} - -// Close maps the unterlying conn Close method, calling the appropiate callbacks -// in the process. -func (tcp *Conn) Close() error { - tcp.onConnUpdate(tcp.target, false) - return tcp.conn.Close() -} diff --git a/backend/pkg/transport/notifications.go b/backend/pkg/transport/notifications.go index c34728a5c..68b660341 100644 --- a/backend/pkg/transport/notifications.go +++ b/backend/pkg/transport/notifications.go @@ -4,12 +4,12 @@ import "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" // PacketNofitication notifies of an incoming message type PacketNotification struct { - packet abstraction.Packet + abstraction.Packet } func NewPacketNotification(packet abstraction.Packet) PacketNotification { return PacketNotification{ - packet: packet, + Packet: packet, } } @@ -17,13 +17,3 @@ func NewPacketNotification(packet abstraction.Packet) PacketNotification { func (notification PacketNotification) Event() abstraction.TransportEvent { return PacketEvent } - -// Packet returns the packet associated with the message -func (notification PacketNotification) Packet() abstraction.Packet { - return notification.packet -} - -// Id returns the id of the packet associated with the message -func (notification PacketNotification) Id() abstraction.PacketId { - return notification.packet.Id() -} diff --git a/backend/pkg/transport/packet/blcu/decoder.go b/backend/pkg/transport/packet/blcu/decoder.go new file mode 100644 index 000000000..273bf48c8 --- /dev/null +++ b/backend/pkg/transport/packet/blcu/decoder.go @@ -0,0 +1,20 @@ +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 new file mode 100644 index 000000000..ebe5c57c9 --- /dev/null +++ b/backend/pkg/transport/packet/blcu/packet.go @@ -0,0 +1,22 @@ +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/packet/data/codec.go b/backend/pkg/transport/packet/data/codec.go new file mode 100644 index 000000000..36583fd38 --- /dev/null +++ b/backend/pkg/transport/packet/data/codec.go @@ -0,0 +1,114 @@ +package data + +import ( + "encoding/binary" + "io" +) + +// valueDecoder is a function that tells how a value should be decoded based on its type +type valueDecoder func(endianness binary.ByteOrder, reader io.Reader) (Value, error) + +// valueEncoder is a function that tells how a value should be encoded based on its type +type valueEncoder func(endianness binary.ByteOrder, value Value, writer io.Writer) error + +// type assertions to check all variants of decodeNumeric are valueDecoders +var _ valueDecoder = decodeNumeric[uint8] +var _ valueDecoder = decodeNumeric[uint16] +var _ valueDecoder = decodeNumeric[uint32] +var _ valueDecoder = decodeNumeric[uint64] +var _ valueDecoder = decodeNumeric[int8] +var _ valueDecoder = decodeNumeric[int16] +var _ valueDecoder = decodeNumeric[int32] +var _ valueDecoder = decodeNumeric[int64] +var _ valueDecoder = decodeNumeric[float32] +var _ valueDecoder = decodeNumeric[float64] + +// decodeNumeric decodes the next numeric value based on the provided type +func decodeNumeric[N numeric](endianness binary.ByteOrder, reader io.Reader) (Value, error) { + var val N + err := binary.Read(reader, endianness, &val) + return NewNumericValue[N](val), err +} + +// type assertions to check all variants of encodeNumeric are valueEncoders +var _ valueEncoder = encodeNumeric[uint8] +var _ valueEncoder = encodeNumeric[uint16] +var _ valueEncoder = encodeNumeric[uint32] +var _ valueEncoder = encodeNumeric[uint64] +var _ valueEncoder = encodeNumeric[int8] +var _ valueEncoder = encodeNumeric[int16] +var _ valueEncoder = encodeNumeric[int32] +var _ valueEncoder = encodeNumeric[int64] +var _ valueEncoder = encodeNumeric[float32] +var _ valueEncoder = encodeNumeric[float64] + +// encodeNumeric encodes the numeric value into the provided writer +func encodeNumeric[N numeric](endianness binary.ByteOrder, value Value, writer io.Writer) error { + num, ok := value.(NumericValue[N]) + if !ok { + return ErrMismatchedTypes{Value: value} + } + return binary.Write(writer, endianness, num.inner) +} + +// type assertion to check decodeBool follows valueDecoder +var _ valueDecoder = decodeBool + +// decodeBool decodes the next boolean value +func decodeBool(endianness binary.ByteOrder, reader io.Reader) (Value, error) { + var val bool + err := binary.Read(reader, endianness, &val) + return NewBooleanValue(val), err +} + +// type assertion to check encodeBool follows valueEncoder +var _ valueEncoder = encodeBool + +// encodeBool encodes the provided value as a boolean into the writer +func encodeBool(endianness binary.ByteOrder, value Value, writer io.Writer) error { + boolean, ok := value.(BooleanValue) + if !ok { + return ErrMismatchedTypes{Value: value} + } + return binary.Write(writer, endianness, boolean.inner) +} + +// newDecodeEnum creates a new enum decoder for the given EnumDescriptor and name +func newDecodeEnum(name ValueName, descriptor EnumDescriptor) valueDecoder { + return func(endianness binary.ByteOrder, reader io.Reader) (Value, error) { + var variant uint8 + err := binary.Read(reader, endianness, &variant) + if err != nil { + return nil, err + } + variantIdx := int(variant) + + if variantIdx >= len(descriptor) { + return nil, ErrInvalidVariant{Name: name, Idx: variantIdx, Len: len(descriptor)} + } + + return NewEnumValue(descriptor[variantIdx]), nil + } +} + +// newEncodeEnum creates a new enum encoder for the given EnumDescriptor and name +func newEncodeEnum(name ValueName, descriptor EnumDescriptor) valueEncoder { + return func(endianness binary.ByteOrder, value Value, writer io.Writer) error { + enum, ok := value.(EnumValue) + if !ok { + return ErrMismatchedTypes{Value: value} + } + + variantIdx := -1 + for i, variant := range descriptor { + if variant == enum.Variant() { + variantIdx = i + } + } + if variantIdx < 0 { + return ErrUnexpectedVariant{Name: name, Variant: enum.Variant(), Descriptor: descriptor} + } + + return binary.Write(writer, endianness, uint8(variantIdx)) + } +} diff --git a/backend/pkg/transport/packet/data/decoder.go b/backend/pkg/transport/packet/data/decoder.go new file mode 100644 index 000000000..a4c11699a --- /dev/null +++ b/backend/pkg/transport/packet/data/decoder.go @@ -0,0 +1,48 @@ +package data + +import ( + "encoding/binary" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// Decoder is a decoder for data packets. It uses the decoder defined on the packet descriptor +type Decoder struct { + endianness binary.ByteOrder + idToDescription map[abstraction.PacketId]Descriptor +} + +// TODO: improve constructor +// NewDecoder creates a new Decoder +func NewDecoder(endianness binary.ByteOrder) *Decoder { + return &Decoder{ + endianness: endianness, + idToDescription: make(map[abstraction.PacketId]Descriptor), + } +} + +// SetDescriptor sets the descriptor for the given id +func (decoder *Decoder) SetDescriptor(id abstraction.PacketId, descriptor Descriptor) *Decoder { + decoder.idToDescription[id] = descriptor + return decoder +} + +// Decode decodes the next packet for the given id +func (decoder *Decoder) Decode(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) { + descriptor, ok := decoder.idToDescription[id] + if !ok { + return nil, ErrUnexpectedId{Id: id} + } + + packet := NewPacket(id) + for _, value := range descriptor { + val, err := value.Decode(decoder.endianness, reader) + if err != nil { + return packet, err + } + packet.SetValue(value.Name, val, true) + } + + return packet, nil +} diff --git a/backend/pkg/transport/packet/data/descriptors.go b/backend/pkg/transport/packet/data/descriptors.go new file mode 100644 index 000000000..1fbafa132 --- /dev/null +++ b/backend/pkg/transport/packet/data/descriptors.go @@ -0,0 +1,65 @@ +package data + +import ( + "encoding/binary" + "io" + "reflect" +) + +// Descriptor describes the structure of a data packet as an array of values +type Descriptor []valueDescriptor + +// valueDescriptor describes a value of a packet +type valueDescriptor struct { + Name ValueName + Type valueType + decode valueDecoder + encode valueEncoder +} + +// Decode decodes the next value from the reader using its decoding method +func (descriptor *valueDescriptor) Decode(endianness binary.ByteOrder, reader io.Reader) (Value, error) { + return descriptor.decode(endianness, reader) +} + +// Encode encodes the provided value into the writer using its encoding method +func (descriptor *valueDescriptor) Encode(endianness binary.ByteOrder, value Value, writer io.Writer) error { + return descriptor.encode(endianness, value, writer) +} + +// NewNumericDescriptor creates a new NumericDescriptor +// +// name is the name for the value +func NewNumericDescriptor[N numeric](name ValueName) valueDescriptor { + var n N + return valueDescriptor{ + Name: name, + Type: valueType(reflect.TypeOf(n).Name()), + decode: decodeNumeric[N], + encode: encodeNumeric[N], + } +} + +// NewBooleanDescriptor creates a new BooleanDescriptor +// +// name is the name for the value +func NewBooleanDescriptor(name ValueName) valueDescriptor { + return valueDescriptor{ + Name: name, + Type: BoolType, + decode: decodeBool, + encode: encodeBool, + } +} + +// NewEnumDescriptor creates a new EnumDescriptor +// +// name is the name for the value and descriptor is the variants the enum has +func NewEnumDescriptor(name ValueName, descriptor EnumDescriptor) valueDescriptor { + return valueDescriptor{ + Name: name, + Type: EnumType, + decode: newDecodeEnum(name, descriptor), + encode: newEncodeEnum(name, descriptor), + } +} diff --git a/backend/pkg/transport/packet/data/encoder.go b/backend/pkg/transport/packet/data/encoder.go new file mode 100644 index 000000000..59f6c6158 --- /dev/null +++ b/backend/pkg/transport/packet/data/encoder.go @@ -0,0 +1,57 @@ +package data + +import ( + "encoding/binary" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// Encoder is the encoder for data packets. It uses the encoder defined on the value descriptor. +type Encoder struct { + endianness binary.ByteOrder + idToDescription map[abstraction.PacketId]Descriptor +} + +// TODO: improve constructor +// NewEncoder creates a new Encoder +func NewEncoder(endianness binary.ByteOrder) *Encoder { + return &Encoder{ + endianness: endianness, + idToDescription: make(map[abstraction.PacketId]Descriptor), + } +} + +// SetDescriptor sets the descriptor for the given id +func (encoder *Encoder) SetDescriptor(id abstraction.PacketId, descriptor Descriptor) *Encoder { + encoder.idToDescription[id] = descriptor + return encoder +} + +// Encode encodes the given packet to the output Writer, returning any errors it encounters +func (encoder *Encoder) Encode(packet abstraction.Packet, output io.Writer) error { + data, ok := packet.(*Packet) + if !ok { + return ErrUnexpectedPacket{Packet: packet} + } + values := data.GetValues() + + descriptor, ok := encoder.idToDescription[data.Id()] + if !ok { + return ErrUnexpectedId{packet.Id()} + } + + for _, valueDescriptor := range descriptor { + value, ok := values[valueDescriptor.Name] + if !ok { + return ErrValueNotFound{Value: valueDescriptor, Packet: packet} + } + + err := valueDescriptor.Encode(encoder.endianness, value, output) + if err != nil { + return err + } + } + + return nil +} diff --git a/backend/pkg/transport/packet/data/errors.go b/backend/pkg/transport/packet/data/errors.go new file mode 100644 index 000000000..35a885cdf --- /dev/null +++ b/backend/pkg/transport/packet/data/errors.go @@ -0,0 +1,83 @@ +package data + +import ( + "fmt" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// ErrUnexpectedId is returned when an ID is not recognized or is not defined +type ErrUnexpectedId struct { + Id abstraction.PacketId +} + +func (err ErrUnexpectedId) Error() string { + return fmt.Sprintf("unexpected id %d", err.Id) +} + +// ErrUnexpectedValue is returned when a codec is not found for a value +type ErrUnexpectedValue struct { + Value valueDescriptor +} + +func (err ErrUnexpectedValue) Error() string { + return fmt.Sprintf("unexpected value %s with type %s", err.Value.Name, err.Value.Type) +} + +// ErrUndefinedEnum is returned when an enum is not defined +type ErrUndefinedEnum struct { + Name ValueName +} + +func (err ErrUndefinedEnum) Error() string { + return fmt.Sprintf("enum %s is not defined", err.Name) +} + +// ErrInvalidVariant is returned when trying to access an invalid enum variant +type ErrInvalidVariant struct { + Name ValueName + Idx int + Len int +} + +func (err ErrInvalidVariant) Error() string { + return fmt.Sprintf("enum %s has %d variants, but tried to access the %d variant", err.Name, err.Len, err.Idx+1) +} + +// ErrUnexpectedPacket is returned when a packet is given to an incorrect encoder +type ErrUnexpectedPacket struct { + Packet abstraction.Packet +} + +func (err ErrUnexpectedPacket) Error() string { + return fmt.Sprintf("expected data packet, got %T instead", err.Packet) +} + +// ErrValueNotFound is returned when the provided packet is meassing a value that should be present +type ErrValueNotFound struct { + Value valueDescriptor + Packet abstraction.Packet +} + +func (err ErrValueNotFound) Error() string { + return fmt.Sprintf("could not find value %s in packet %v", err.Value.Name, err.Packet) +} + +// ErrMismatchedTypes is returned when the provided value does not match the encoder type +type ErrMismatchedTypes struct { + Value Value +} + +func (err ErrMismatchedTypes) Error() string { + return fmt.Sprintf("type of value type %T does not match the one of the encoder", err.Value) +} + +type ErrUnexpectedVariant struct { + Name ValueName + Variant EnumVariant + Descriptor EnumDescriptor +} + +func (err ErrUnexpectedVariant) Error() string { + return fmt.Sprintf("tried to encode %s variant for %s enum, but possibilities are only %v", err.Variant, err.Name, err.Descriptor) +} diff --git a/backend/pkg/transport/packet/data/packet.go b/backend/pkg/transport/packet/data/packet.go new file mode 100644 index 000000000..d44f3c7df --- /dev/null +++ b/backend/pkg/transport/packet/data/packet.go @@ -0,0 +1,64 @@ +package data + +import ( + "time" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// Type assertion to check Data follows the abstraction.Packet interface +var _ abstraction.Packet = &Packet{} + +// Packet is a data packet containing multiple values +type Packet struct { + id abstraction.PacketId + values map[ValueName]Value + enabled map[ValueName]bool + timestamp time.Time +} + +// NewPacket creates a new data packet +func NewPacket(id abstraction.PacketId) *Packet { + return &Packet{ + id: id, + values: make(map[ValueName]Value), + enabled: make(map[ValueName]bool), + timestamp: time.Now(), + } +} + +// NewPacketWithValues creates a new data packet with the given values +func NewPacketWithValues(id abstraction.PacketId, values map[ValueName]Value, enabled map[ValueName]bool) *Packet { + return &Packet{ + id: id, + values: values, + enabled: enabled, + timestamp: time.Now(), + } +} + +// Id returns the id of the data packet +func (packet *Packet) Id() abstraction.PacketId { + return packet.id +} + +func (packet *Packet) Timestamp() time.Time { + return packet.timestamp +} + +// SetValue updates the value with the given name to the new value. It overwrites a value if it is already set +func (packet *Packet) SetValue(name ValueName, value Value, enable bool) *Packet { + packet.values[name] = value + packet.enabled[name] = enable + return packet +} + +// GetValues returns all values associated with the packet +func (packet *Packet) GetValues() map[ValueName]Value { + return packet.values +} + +func (packet *Packet) SetTimestamp(timestamp time.Time) *Packet { + packet.timestamp = timestamp + return packet +} diff --git a/backend/pkg/transport/packet/data/value.go b/backend/pkg/transport/packet/data/value.go new file mode 100644 index 000000000..ecdc70a07 --- /dev/null +++ b/backend/pkg/transport/packet/data/value.go @@ -0,0 +1,126 @@ +package data + +import ( + "reflect" +) + +// ValueName is the name of the value +type ValueName string + +// valueType is the variable type of a data packet value +type valueType string + +const ( + Uint8Type valueType = "uint8" + Uint16Type valueType = "uint16" + Uint32Type valueType = "uint32" + Uint64Type valueType = "uint64" + Int8Type valueType = "int8" + Int16Type valueType = "int16" + Int32Type valueType = "int32" + Int64Type valueType = "int64" + Float32Type valueType = "float32" + Float64Type valueType = "float64" + BoolType valueType = "bool" + EnumType valueType = "enum" +) + +// Value is an interface over all kinds of values +type Value interface { + Type() valueType +} + +// numeric is a helper interface to group any kind of numeric variable +type numeric interface { + uint8 | uint16 | uint32 | uint64 | int8 | int16 | int32 | int64 | float32 | float64 +} + +// type assertions to check numeric values follow the Value interface +var _ Value = NumericValue[uint8]{} +var _ Value = NumericValue[uint16]{} +var _ Value = NumericValue[uint32]{} +var _ Value = NumericValue[uint64]{} +var _ Value = NumericValue[int8]{} +var _ Value = NumericValue[int16]{} +var _ Value = NumericValue[int32]{} +var _ Value = NumericValue[int64]{} +var _ Value = NumericValue[float32]{} +var _ Value = NumericValue[float64]{} + +// NumericValue is a value holding a number +type NumericValue[N numeric] struct { + inner N +} + +// NewNumericValue creates a new numeric value +func NewNumericValue[N numeric](val N) NumericValue[N] { + return NumericValue[N]{ + inner: val, + } +} + +// Value returns the associated number, always as float64 +func (value NumericValue[N]) Value() float64 { + return float64(value.inner) +} + +// Type returns the type of the numeric value +func (value NumericValue[N]) Type() valueType { + return valueType(reflect.TypeOf(value.inner).Name()) +} + +// type assertion to check BooleanValue follows the Value interface +var _ Value = BooleanValue{} + +// BooleanValue is a value holding a bool +type BooleanValue struct { + inner bool +} + +// NewBooleanValue creates a new boolean value +func NewBooleanValue(val bool) BooleanValue { + return BooleanValue{ + inner: val, + } +} + +// Value returns the associated value +func (value BooleanValue) Value() bool { + return value.inner +} + +// Type returns the type of the boolean value +func (value BooleanValue) Type() valueType { + return BoolType +} + +// EnumVariant is one of the variants an enum can have +type EnumVariant string + +// EnumDescriptor describes the possible variants an enum might have +type EnumDescriptor []EnumVariant + +// type assertion to check EnumValue follows the Value interface +var _ Value = EnumValue{} + +// EnumValue is a value holding the variant of an enum +type EnumValue struct { + inner EnumVariant +} + +// NewEnumValue creates a new enum value +func NewEnumValue(val EnumVariant) EnumValue { + return EnumValue{ + inner: val, + } +} + +// Variant returns the variant of this value +func (value EnumValue) Variant() EnumVariant { + return value.inner +} + +// Type returns the type of the enum value +func (value EnumValue) Type() valueType { + return EnumType +} diff --git a/backend/pkg/transport/packet/info/decoder.go b/backend/pkg/transport/packet/info/decoder.go new file mode 100644 index 000000000..b05f0419d --- /dev/null +++ b/backend/pkg/transport/packet/info/decoder.go @@ -0,0 +1,46 @@ +package info + +import ( + "encoding/json" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +type delimiter = byte + +// Decoder is a decoder for info packets. It uses json to decode the information +type Decoder struct { + packetDelimiter delimiter +} + +// NewDecoder creates a new Decoder +func NewDecoder(delimiter delimiter) *Decoder { + return &Decoder{ + packetDelimiter: delimiter, + } +} + +// Decode decodes the next packet from the provided reader +func (decoder *Decoder) Decode(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) { + packet := NewPacket(id) + + // Read exactly up to the delimiter + packetBuf := make([]byte, 0) + readbuf := make([]byte, 1) + var readErr error + var n int + for { + n, readErr = reader.Read(readbuf) + if n < 1 || readbuf[0] == decoder.packetDelimiter { + break + } + packetBuf = append(packetBuf, readbuf[0]) + } + + err := json.Unmarshal(packetBuf, &packet) + if err != nil { + return packet, err + } + return packet, readErr +} diff --git a/backend/pkg/transport/packet/info/packet.go b/backend/pkg/transport/packet/info/packet.go new file mode 100644 index 000000000..92eebdaaa --- /dev/null +++ b/backend/pkg/transport/packet/info/packet.go @@ -0,0 +1,31 @@ +package info + +import ( + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + transport_packet "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet" +) + +// infoData is the contents of an info Packet +type infoData string + +// Packet is an info packet. +// +// info packets are used to send arbitrary strings for debugging purposes. +type Packet struct { + id abstraction.PacketId + BoardId abstraction.BoardId `json:"boardId"` + Timestamp transport_packet.Timestamp `json:"timestamp"` + Msg infoData `json:"msg"` +} + +// NewPacket creates a new info packet with the given ID +func NewPacket(id abstraction.PacketId) *Packet { + return &Packet{ + id: id, + } +} + +// Id returns the packet id +func (packet *Packet) Id() abstraction.PacketId { + return packet.id +} diff --git a/backend/pkg/transport/packet/order/codec.go b/backend/pkg/transport/packet/order/codec.go new file mode 100644 index 000000000..7ded0ecc0 --- /dev/null +++ b/backend/pkg/transport/packet/order/codec.go @@ -0,0 +1,46 @@ +package order + +import ( + "encoding/binary" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// TODO: make private +// DecodeAdd decodes the next stateOrderAdd request from the reader +func (decoder *Decoder) DecodeAdd(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) { + orders, err := decodeSlice[abstraction.PacketId](decoder.endianness, reader) + return &Add{ + id: id, + orders: orders, + }, err +} + +// TODO: make private +// DecodeRemove decodes the next stateOrderRemove request from the reader +func (decoder *Decoder) DecodeRemove(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) { + orders, err := decodeSlice[abstraction.PacketId](decoder.endianness, reader) + return &Remove{ + id: id, + orders: orders, + }, err +} + +// sliceLength represents the encoded length of an slice +type sliceLength uint16 + +// decodeSlice decodes the next slice from the reader +// +// to know the length of the slice, first the `sliceLength` is readed and used to read the rest of values +func decodeSlice[T any](endianness binary.ByteOrder, reader io.Reader) ([]T, error) { + var length sliceLength + err := binary.Read(reader, endianness, &length) + if err != nil { + return nil, err + } + + slice := make([]T, length) + err = binary.Read(reader, endianness, &slice) + return slice, err +} diff --git a/backend/pkg/transport/packet/order/decoder.go b/backend/pkg/transport/packet/order/decoder.go new file mode 100644 index 000000000..6fcccaead --- /dev/null +++ b/backend/pkg/transport/packet/order/decoder.go @@ -0,0 +1,43 @@ +package order + +import ( + "encoding/binary" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// actionDecoder is a decoder for a stateOrder action +type actionDecoder func(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) + +// Decoder decodes the state order actions +type Decoder struct { + idToAction map[abstraction.PacketId]actionDecoder + endianness binary.ByteOrder +} + +// TODO: improve constructor +// NewDecoder creates a new Decoder +func NewDecoder(endianness binary.ByteOrder) *Decoder { + return &Decoder{ + endianness: endianness, + idToAction: make(map[abstraction.PacketId]actionDecoder), + } +} + +// SetActionId sets the action decoder for the specified id +func (decoder *Decoder) SetActionId(id abstraction.PacketId, action actionDecoder) { + decoder.idToAction[id] = action +} + +// Decode decodes the next state order action from the reader. +// +// it uses the id to determine the kind of action. +func (decoder *Decoder) Decode(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) { + dec, ok := decoder.idToAction[id] + if !ok { + return nil, ErrUnexpectedId{Id: id} + } + + return dec(id, reader) +} diff --git a/backend/pkg/transport/packet/order/errors.go b/backend/pkg/transport/packet/order/errors.go new file mode 100644 index 000000000..74ac7ee04 --- /dev/null +++ b/backend/pkg/transport/packet/order/errors.go @@ -0,0 +1,16 @@ +package order + +import ( + "fmt" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// ErrUnexpectedId is returned when an ID is not recognized or is not defined +type ErrUnexpectedId struct { + Id abstraction.PacketId +} + +func (err ErrUnexpectedId) Error() string { + return fmt.Sprintf("unexpected id %d", err.Id) +} diff --git a/backend/pkg/transport/packet/order/packet.go b/backend/pkg/transport/packet/order/packet.go new file mode 100644 index 000000000..c982268e8 --- /dev/null +++ b/backend/pkg/transport/packet/order/packet.go @@ -0,0 +1,69 @@ +package order + +import "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + +// action represents an action to be performed to the current state orders +type action string + +const ( + ActionAdd action = "add" + ActionRemove action = "remove" +) + +// Add is a request to add the included orders to the current list of enabled state orders +type Add struct { + id abstraction.PacketId + orders []abstraction.PacketId +} + +// NewAdd creates a new Add packet +func NewAdd(id abstraction.PacketId, orders []abstraction.PacketId) *Add { + return &Add{ + id: id, + orders: orders, + } +} + +// Id returns the packet id +func (packet *Add) Id() abstraction.PacketId { + return packet.id +} + +// Orders returns the orders that need to be added +func (packet *Add) Orders() []abstraction.PacketId { + return packet.orders +} + +// Action returns the action kind for this request +func (packet *Add) Action() action { + return ActionAdd +} + +// Remove is a request to remove the included orders from the list of enabled state orders +type Remove struct { + id abstraction.PacketId + orders []abstraction.PacketId +} + +// NewRemove creates a new Add packet +func NewRemove(id abstraction.PacketId, orders []abstraction.PacketId) *Remove { + return &Remove{ + id: id, + orders: orders, + } +} + +// Id returns the packet id +func (packet *Remove) Id() abstraction.PacketId { + return packet.id +} + +// Orders returns the orders that need to be removed +func (packet *Remove) Orders() []abstraction.PacketId { + return packet.orders +} + +// Action returns the action kind for this request +func (packet *Remove) Action() action { + return ActionRemove +} diff --git a/backend/pkg/transport/packet/protection/decoder.go b/backend/pkg/transport/packet/protection/decoder.go new file mode 100644 index 000000000..0416872b0 --- /dev/null +++ b/backend/pkg/transport/packet/protection/decoder.go @@ -0,0 +1,98 @@ +package protection + +import ( + "encoding/json" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// protectionAdapter is used as an intermediate step when decoding the protection messages +type protectionAdapter struct { + Name ProtectionName `json:"name"` + Type kind `json:"type"` + Data *json.RawMessage `json:"data"` +} + +// UnmarshalJSON unmarshals the data in two passes to directly get the ProtectionData field +func (protection *Protection) UnmarshalJSON(data []byte) error { + var adapter protectionAdapter + err := json.Unmarshal(data, &adapter) + if err != nil { + return err + } + + protection.Name = adapter.Name + protection.Type = adapter.Type + + switch adapter.Type { + case OutOfBoundsKind: + protection.Data = new(OutOfBounds) + case UpperBoundKind: + protection.Data = new(UpperBound) + case LowerBoundKind: + protection.Data = new(LowerBound) + case EqualsKind: + protection.Data = new(Equals) + case NotEqualsKind: + protection.Data = new(NotEquals) + case TimeAccumulationKind: + protection.Data = new(TimeAccumulation) + case ErrorHandlerKind: + protection.Data = new(ErrorHandler) + default: + return ErrUnknownKind{Kind: adapter.Type} + } + + return json.Unmarshal(*adapter.Data, &protection.Data) +} + +type delimiter = byte + +// Decoder decodes protection messages +type Decoder struct { + idToSeverity map[abstraction.PacketId]severity + packetDelimiter delimiter +} + +// TODO: improve constructor +// NewDecoder creates a new Decoder +func NewDecoder() *Decoder { + return &Decoder{ + idToSeverity: make(map[abstraction.PacketId]severity), + } +} + +// SetSeverity sets the severity level for the given ID +func (decoder *Decoder) SetSeverity(id abstraction.PacketId, severity severity) { + decoder.idToSeverity[id] = severity +} + +// Decode decodes the next protection message from the reader using json +func (decoder *Decoder) Decode(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) { + severity, ok := decoder.idToSeverity[id] + if !ok { + return nil, ErrUnknownSeverity{Id: id} + } + + packet := NewPacket(id, severity) + + // Read exactly up to the delimiter + packetBuf := make([]byte, 0) + readbuf := make([]byte, 1) + var readErr error + var n int + for { + n, readErr = reader.Read(readbuf) + if n < 1 || readbuf[0] == decoder.packetDelimiter { + break + } + packetBuf = append(packetBuf, readbuf[0]) + } + + err := json.Unmarshal(packetBuf, &packet) + if err != nil { + return packet, err + } + return packet, readErr +} diff --git a/backend/pkg/transport/packet/protection/errors.go b/backend/pkg/transport/packet/protection/errors.go new file mode 100644 index 000000000..63b6ef03a --- /dev/null +++ b/backend/pkg/transport/packet/protection/errors.go @@ -0,0 +1,25 @@ +package protection + +import ( + "fmt" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// ErrUnknownKind is returned when the protection kind is not defined +type ErrUnknownKind struct { + Kind kind +} + +func (err ErrUnknownKind) Error() string { + return fmt.Sprintf("unrecognized kind %s", err.Kind) +} + +// ErrUnknownSeverity is returned when the severity of the protection is unknown +type ErrUnknownSeverity struct { + Id abstraction.PacketId +} + +func (err ErrUnknownSeverity) Error() string { + return fmt.Sprintf("unknown severity for id %d", err.Id) +} diff --git a/backend/pkg/transport/packet/protection/kind.go b/backend/pkg/transport/packet/protection/kind.go new file mode 100644 index 000000000..0eb951ace --- /dev/null +++ b/backend/pkg/transport/packet/protection/kind.go @@ -0,0 +1,102 @@ +package protection + +// severity is the level of the protection +type severity string + +const ( + SeverityWarning severity = "warning" + SeverityFault severity = "fault" + SeverityError severity = "error" +) + +// kind is the kind of protection +type kind string + +// ProtectionData is an interface common to all protection kinds +type ProtectionData interface { + Kind() kind +} + +const ( + OutOfBoundsKind kind = "OUT_OF_BOUNDS" + LowerBoundKind kind = "LOWER_BOUND" + UpperBoundKind kind = "UPPER_BOUND" + EqualsKind kind = "EQUALS" + NotEqualsKind kind = "NOT_EQUALS" + TimeAccumulationKind kind = "TIME_ACCUMULATION" + ErrorHandlerKind kind = "ERROR_HANDLER" +) + +// OutOfBounds is a protection thrown when the value gets out of an specific range +type OutOfBounds struct { + Value float64 `json:"value"` + Bounds [2]float64 `json:"bounds"` +} + +// Kind returns the protection kind +func (protection *OutOfBounds) Kind() kind { + return OutOfBoundsKind +} + +// UpperBound is a protection thrown when the value exceeds a threshold +type UpperBound struct { + Value float64 `json:"value"` + Bound float64 `json:"bound"` +} + +// Kind returns the protection kind +func (protection *UpperBound) Kind() kind { + return UpperBoundKind +} + +// LowerBound is a protection thrown when the value goes under a threshold +type LowerBound struct { + Value float64 `json:"value"` + Bound float64 `json:"bound"` +} + +// Kind returns the protection kind +func (protection *LowerBound) Kind() kind { + return LowerBoundKind +} + +// Equals is a protection thrown when a value is equal to some other value +type Equals struct { + Value float64 `json:"value"` +} + +// Kind returns the protection kind +func (protection *Equals) Kind() kind { + return EqualsKind +} + +// NotEquals is a protection thrown when a value is not equal to a target +type NotEquals struct { + Value float64 `json:"value"` + Want float64 `json:"want"` +} + +// Kind returns the protection kind +func (protection *NotEquals) Kind() kind { + return NotEqualsKind +} + +// TimeAccumulation is a protection thrown when the cummulative sum over a period of time exceeds a threshold +type TimeAccumulation struct { + Value float64 `json:"value"` + Bound float64 `json:"bound"` + TimeLimit float64 `json:"timelimit"` +} + +// Kind returns the protection kind +func (protection *TimeAccumulation) Kind() kind { + return TimeAccumulationKind +} + +// ErrorHandle is a protection thrown by the error handler +type ErrorHandler string + +// Kind returns the protection kind +func (protection *ErrorHandler) Kind() kind { + return ErrorHandlerKind +} diff --git a/backend/pkg/transport/packet/protection/packet.go b/backend/pkg/transport/packet/protection/packet.go new file mode 100644 index 000000000..910a33d3f --- /dev/null +++ b/backend/pkg/transport/packet/protection/packet.go @@ -0,0 +1,44 @@ +package protection + +import ( + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet" +) + +// Packet represents a protection packet. +// +// protection packets are encoded as json. +type Packet struct { + id abstraction.PacketId + BoardId abstraction.BoardId `json:"boardId"` + Timestamp packet.Timestamp `json:"timestamp"` + Protection Protection `json:"protection"` + severity severity +} + +func NewPacket(id abstraction.PacketId, severity severity) *Packet { + return &Packet{ + id: id, + severity: severity, + } +} + +// Severity returns the protection severity +func (packet *Packet) Severity() severity { + return packet.severity +} + +// Id returns the packet id +func (packet *Packet) Id() abstraction.PacketId { + return packet.id +} + +// ProtectionName is a name given to a protection to identify them +type ProtectionName string + +// Protection is the data of the proteciton that got thrown +type Protection struct { + Name ProtectionName `json:"name"` + Type kind `json:"type"` + Data ProtectionData `json:"data"` +} diff --git a/backend/pkg/transport/packet/state/decoder.go b/backend/pkg/transport/packet/state/decoder.go new file mode 100644 index 000000000..117554ada --- /dev/null +++ b/backend/pkg/transport/packet/state/decoder.go @@ -0,0 +1,30 @@ +package state + +import ( + "encoding/binary" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// Decoder decodes state space messages +type Decoder struct { + endianness binary.ByteOrder +} + +// NewDecoder creates a new Decoder +func NewDecoder(endianness binary.ByteOrder) *Decoder { + return &Decoder{ + endianness: endianness, + } +} + +// Decode decodes the next state space message in the stream +func (decoder *Decoder) Decode(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) { + stateSpace := Space{ + id: id, + } + + err := binary.Read(reader, decoder.endianness, &stateSpace.state) + return &stateSpace, err +} diff --git a/backend/pkg/transport/packet/state/packet.go b/backend/pkg/transport/packet/state/packet.go new file mode 100644 index 000000000..30a8d2319 --- /dev/null +++ b/backend/pkg/transport/packet/state/packet.go @@ -0,0 +1,28 @@ +package state + +import "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + +// Space represents the state space packet +// +// the state space represents the control parameters being used +type Space struct { + id abstraction.PacketId + state [8][15]float32 +} + +func NewSpace(id abstraction.PacketId, state [8][15]float32) *Space { + return &Space{ + id: id, + state: state, + } +} + +// Id returns the packet id +func (packet *Space) Id() abstraction.PacketId { + return packet.id +} + +// State returns the state space matrix +func (packet *Space) State() [8][15]float32 { + return packet.state +} diff --git a/backend/pkg/transport/packet/timestamp.go b/backend/pkg/transport/packet/timestamp.go new file mode 100644 index 000000000..b1426aa07 --- /dev/null +++ b/backend/pkg/transport/packet/timestamp.go @@ -0,0 +1,21 @@ +package packet + +import ( + "time" +) + +// Timestamp is a timestamp generated and sent by the boards +type Timestamp struct { + Counter uint16 `json:"counter"` + Second uint8 `json:"second"` + Minute uint8 `json:"minute"` + Hour uint8 `json:"hour"` + Day uint8 `json:"day"` + Month uint8 `json:"month"` + Year uint16 `json:"year"` +} + +// ToTime returns the timestamp as a time.Time object +func (timestamp *Timestamp) ToTime() time.Time { + return time.Date(int(timestamp.Year), time.Month(int(timestamp.Month)), int(timestamp.Day), int(timestamp.Hour), int(timestamp.Minute), int(timestamp.Second), int(timestamp.Counter), time.UTC) +} diff --git a/backend/pkg/transport/presentation/decoder.go b/backend/pkg/transport/presentation/decoder.go index b409c585c..3dc17b14e 100644 --- a/backend/pkg/transport/presentation/decoder.go +++ b/backend/pkg/transport/presentation/decoder.go @@ -1,3 +1,67 @@ package presentation -type PacketDecoder struct{} +import ( + "encoding/binary" + "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/info" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/order" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/state" +) + +// PacketDecoder is a common interface for packet-specific decoders +type PacketDecoder interface { + Decode(id abstraction.PacketId, reader io.Reader) (abstraction.Packet, error) +} + +// Type assertions to check packet decoders follows the Decoder interface +var _ PacketDecoder = &data.Decoder{} +var _ PacketDecoder = &blcu.Decoder{} +var _ PacketDecoder = &info.Decoder{} +var _ PacketDecoder = &order.Decoder{} +var _ PacketDecoder = &protection.Decoder{} +var _ PacketDecoder = &state.Decoder{} + +// Decoder is the root decoder, it takes the id of the packet and decodes the rest of it +type Decoder struct { + idToDecoder map[abstraction.PacketId]PacketDecoder + endianness binary.ByteOrder +} + +// TODO: improve constructor +// NewDecoder creates a new decoder with the given endianness +func NewDecoder(endianness binary.ByteOrder) *Decoder { + return &Decoder{ + idToDecoder: make(map[abstraction.PacketId]PacketDecoder), + endianness: endianness, + } +} + +// SetPacketDecoder sets the decoder for the specified id +func (decoder *Decoder) SetPacketDecoder(id abstraction.PacketId, dec PacketDecoder) { + decoder.idToDecoder[id] = dec +} + +// DecodeNext reads and decodes the next packet from the input stream, returning any errors encountered +// +// the decoder should have all the id decoders set before calling DecodeNext +func (decoder *Decoder) DecodeNext(reader io.Reader) (abstraction.Packet, error) { + var id abstraction.PacketId + err := binary.Read(reader, decoder.endianness, &id) + if err != nil { + return nil, err + } + + dec, ok := decoder.idToDecoder[id] + if !ok { + return nil, ErrUnexpectedId{Id: id} + } + + packet, err := dec.Decode(id, reader) + + return packet, err +} diff --git a/backend/pkg/transport/presentation/decoder_test.go b/backend/pkg/transport/presentation/decoder_test.go new file mode 100644 index 000000000..f75e78e44 --- /dev/null +++ b/backend/pkg/transport/presentation/decoder_test.go @@ -0,0 +1,1194 @@ +package presentation_test + +import ( + "bytes" + "encoding/binary" + "io" + "math" + "reflect" + "testing" + "time" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/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/info" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/order" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/protection" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/state" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/presentation" +) + +func TestDecoder(t *testing.T) { + type testcase struct { + name string + input io.Reader + output []abstraction.Packet + } + + 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: "info", + input: bytes.NewReader(join( + []byte{0x02, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "msg": "hello, world!\n" +}`, + ), + []byte{0x00}, + )), + output: []abstraction.Packet{ + func() abstraction.Packet { + p := info.NewPacket(2) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Msg = "hello, world!\n" + return p + }(), + }, + }, + { + name: "multiple info", + input: bytes.NewReader(join( + []byte{0x02, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "msg": "hello, world!\n" +}`, + ), + []byte{0x00}, + []byte{0x02, 0x00}, + []byte( + `{ + "boardId": 2, + "timestamp": { + "counter": 2, + "second": 3, + "minute": 4, + "hour": 5, + "day": 6, + "month": 7, + "year": 8 + }, + "msg": "goodbye, world :(\n" +}`, + ), + []byte{0x00}, + )), + output: []abstraction.Packet{ + func() abstraction.Packet { + p := info.NewPacket(2) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Msg = "hello, world!\n" + return p + }(), + func() abstraction.Packet { + p := info.NewPacket(2) + p.BoardId = 2 + p.Timestamp = packet.Timestamp{ + Counter: 2, + Second: 3, + Minute: 4, + Hour: 5, + Day: 6, + Month: 7, + Year: 8, + } + p.Msg = "goodbye, world :(\n" + return p + }(), + }, + }, + { + name: "state orders add", + input: bytes.NewReader([]byte{0x03, 0x00, 0x01, 0x00, 0xFF, 0xFF}), + output: []abstraction.Packet{ + order.NewAdd(3, []abstraction.PacketId{0xFFFF}), + }, + }, + { + name: "state orders remove", + input: bytes.NewReader([]byte{0x04, 0x00, 0x01, 0x00, 0xFF, 0xFF}), + output: []abstraction.Packet{ + order.NewRemove(4, []abstraction.PacketId{0xFFFF}), + }, + }, + { + name: "multiple state orders", + input: bytes.NewReader([]byte{ + 0x03, 0x00, 0x03, 0x00, 0xFF, 0xFF, 0xFE, 0xFF, 0xFD, 0xFF, + 0x04, 0x00, 0x03, 0x00, 0xFF, 0xFF, 0xFE, 0xFF, 0xFD, 0xFF, + }), + output: []abstraction.Packet{ + order.NewAdd(3, []abstraction.PacketId{0xFFFF, 0xFFFE, 0xFFFD}), + order.NewRemove(4, []abstraction.PacketId{0xFFFF, 0xFFFE, 0xFFFD}), + }, + }, + { + name: "all protection kinds", + input: bytes.NewReader(join( + []byte{0x05, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "out of bounds", + "type": "OUT_OF_BOUNDS", + "data": { + "value": 5.5, + "bounds": [0, 3] + } + } +}`, + ), + []byte{0x00}, + []byte{0x05, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "upper bound", + "type": "UPPER_BOUND", + "data": { + "value": 5.5, + "bound": 3 + } + } +}`, + ), + []byte{0x00}, + []byte{0x05, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "lower bound", + "type": "LOWER_BOUND", + "data": { + "value": 5.5, + "bound": 6 + } + } +}`, + ), + []byte{0x00}, + []byte{0x05, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "equals", + "type": "EQUALS", + "data": { + "value": 5.5 + } + } +}`, + ), + []byte{0x00}, + []byte{0x05, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "not equals", + "type": "NOT_EQUALS", + "data": { + "value": 5.5, + "want": 5 + } + } +}`, + ), + []byte{0x00}, + []byte{0x05, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "time accumulation", + "type": "TIME_ACCUMULATION", + "data": { + "value": 5.5, + "bound": 5, + "timelimit": 2.5 + } + } +}`, + ), + []byte{0x00}, + []byte{0x05, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "error handler", + "type": "ERROR_HANDLER", + "data": "something to warn about\n" + } +}`, + ), + []byte{0x00}, + )), + output: []abstraction.Packet{ + func() abstraction.Packet { + p := protection.NewPacket(5, protection.SeverityWarning) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "out of bounds", + Type: protection.OutOfBoundsKind, + Data: &protection.OutOfBounds{ + Value: 5.5, + Bounds: [2]float64{0, 3}, + }, + } + return p + }(), + func() abstraction.Packet { + p := protection.NewPacket(5, protection.SeverityWarning) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "upper bound", + Type: protection.UpperBoundKind, + Data: &protection.UpperBound{ + Value: 5.5, + Bound: 3, + }, + } + return p + }(), + func() abstraction.Packet { + p := protection.NewPacket(5, protection.SeverityWarning) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "lower bound", + Type: protection.LowerBoundKind, + Data: &protection.LowerBound{ + Value: 5.5, + Bound: 6, + }, + } + return p + }(), + func() abstraction.Packet { + p := protection.NewPacket(5, protection.SeverityWarning) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "equals", + Type: protection.EqualsKind, + Data: &protection.Equals{ + Value: 5.5, + }, + } + return p + }(), + func() abstraction.Packet { + p := protection.NewPacket(5, protection.SeverityWarning) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "not equals", + Type: protection.NotEqualsKind, + Data: &protection.NotEquals{ + Value: 5.5, + Want: 5, + }, + } + return p + }(), + func() abstraction.Packet { + p := protection.NewPacket(5, protection.SeverityWarning) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "time accumulation", + Type: protection.TimeAccumulationKind, + Data: &protection.TimeAccumulation{ + Value: 5.5, + Bound: 5, + TimeLimit: 2.5, + }, + } + return p + }(), + func() abstraction.Packet { + p := protection.NewPacket(5, protection.SeverityWarning) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + data := protection.ErrorHandler("something to warn about\n") + p.Protection = protection.Protection{ + Name: "error handler", + Type: protection.ErrorHandlerKind, + Data: &data, + } + return p + }(), + }, + }, + { + name: "protection severity", + input: bytes.NewReader(join( + []byte{0x05, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "out of bounds", + "type": "OUT_OF_BOUNDS", + "data": { + "value": 5.5, + "bounds": [0, 3] + } + } +}`, + ), + []byte{0x00}, + []byte{0x06, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "out of bounds", + "type": "OUT_OF_BOUNDS", + "data": { + "value": 5.5, + "bounds": [0, 3] + } + } +}`, + ), + []byte{0x00}, + []byte{0x07, 0x00}, + []byte( + `{ + "boardId": 1, + "timestamp": { + "counter": 1, + "second": 2, + "minute": 3, + "hour": 4, + "day": 5, + "month": 6, + "year": 7 + }, + "protection": { + "name": "out of bounds", + "type": "OUT_OF_BOUNDS", + "data": { + "value": 5.5, + "bounds": [0, 3] + } + } +}`, + ), + []byte{0x00}, + )), + output: []abstraction.Packet{ + func() abstraction.Packet { + p := protection.NewPacket(5, protection.SeverityWarning) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "out of bounds", + Type: protection.OutOfBoundsKind, + Data: &protection.OutOfBounds{ + Value: 5.5, + Bounds: [2]float64{0, 3}, + }, + } + return p + }(), + func() abstraction.Packet { + p := protection.NewPacket(6, protection.SeverityError) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "out of bounds", + Type: protection.OutOfBoundsKind, + Data: &protection.OutOfBounds{ + Value: 5.5, + Bounds: [2]float64{0, 3}, + }, + } + return p + }(), + func() abstraction.Packet { + p := protection.NewPacket(7, protection.SeverityFault) + p.BoardId = 1 + p.Timestamp = packet.Timestamp{ + Counter: 1, + Second: 2, + Minute: 3, + Hour: 4, + Day: 5, + Month: 6, + Year: 7, + } + p.Protection = protection.Protection{ + Name: "out of bounds", + Type: protection.OutOfBoundsKind, + Data: &protection.OutOfBounds{ + Value: 5.5, + Bounds: [2]float64{0, 3}, + }, + } + return p + }(), + }, + }, + { + name: "state space", + input: bytes.NewReader(join( + []byte{0x08, 0x00}, + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + toBinary(float32(5.5), endianness), + )), + output: []abstraction.Packet{ + func() abstraction.Packet { + space := [8][15]float32{} + for i := 0; i < 8; i++ { + for j := 0; j < 15; j++ { + space[i][j] = 5.5 + } + } + return state.NewSpace(8, space) + }(), + }, + }, + { + name: "data", + input: bytes.NewReader(join( + []byte{0x09, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint16(8), endianness), + toBinary(uint32(8), endianness), + toBinary(uint64(8), endianness), + []byte{0x0a, 0x00}, + toBinary(int8(8), endianness), + toBinary(int16(8), endianness), + toBinary(int32(8), endianness), + toBinary(int64(8), endianness), + []byte{0x0b, 0x00}, + toBinary(float32(8), endianness), + toBinary(float64(8), endianness), + []byte{0x0c, 0x00}, + []byte{0x01}, + []byte{0x0d, 0x00}, + toBinary(uint8(2), endianness), + []byte{0x0e, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(float64(8), endianness), + []byte{0x01}, + toBinary(uint8(0), endianness), + []byte{0x0f, 0x00}, + []byte{0x01}, + []byte{0x00}, + []byte{0x00}, + []byte{0x10, 0x00}, + toBinary(uint8(1), endianness), + toBinary(uint8(0), endianness), + toBinary(uint8(3), endianness), + toBinary(uint8(2), endianness), + toBinary(uint8(7), endianness), + []byte{0x11, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + []byte{0x12, 0x00}, + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + )), + output: []abstraction.Packet{ + data.NewPacketWithValues(9, map[data.ValueName]data.Value{ + "uint8": data.NewNumericValue[uint8](8), + "uint16": data.NewNumericValue[uint16](8), + "uint32": data.NewNumericValue[uint32](8), + "uint64": data.NewNumericValue[uint64](8), + }, map[data.ValueName]bool{ + "uint8": true, + "uint16": true, + "uint32": true, + "uint64": true, + }), + data.NewPacketWithValues(10, map[data.ValueName]data.Value{ + "int8": data.NewNumericValue[int8](8), + "int16": data.NewNumericValue[int16](8), + "int32": data.NewNumericValue[int32](8), + "int64": data.NewNumericValue[int64](8), + }, map[data.ValueName]bool{ + "int8": true, + "int16": true, + "int32": true, + "int64": true, + }), + data.NewPacketWithValues(11, map[data.ValueName]data.Value{ + "float32": data.NewNumericValue[float32](8), + "float64": data.NewNumericValue[float64](8), + }, map[data.ValueName]bool{ + "float32": true, + "float64": true, + }), + data.NewPacketWithValues(12, map[data.ValueName]data.Value{ + "bool": data.NewBooleanValue(true), + }, map[data.ValueName]bool{ + "bool": true, + }), + data.NewPacketWithValues(13, map[data.ValueName]data.Value{ + "enum": data.NewEnumValue("c"), + }, map[data.ValueName]bool{ + "enum": true, + }), + data.NewPacketWithValues(14, map[data.ValueName]data.Value{ + "uint8_1": data.NewNumericValue[uint8](8), + "uint8_2": data.NewNumericValue[uint8](8), + "float64": data.NewNumericValue[float64](8), + "bool": data.NewBooleanValue(true), + "enum": data.NewEnumValue("a"), + }, map[data.ValueName]bool{ + "uint8_1": true, + "uint8_2": true, + "float64": true, + "bool": true, + "enum": true, + }), + data.NewPacketWithValues(15, map[data.ValueName]data.Value{ + "bool_1": data.NewBooleanValue(true), + "bool_2": data.NewBooleanValue(false), + "bool_3": data.NewBooleanValue(false), + }, map[data.ValueName]bool{ + "bool_1": true, + "bool_2": true, + "bool_3": true, + }), + data.NewPacketWithValues(16, map[data.ValueName]data.Value{ + "enum_1": data.NewEnumValue("b"), + "enum_2": data.NewEnumValue("a"), + "enum_3": data.NewEnumValue("d"), + "enum_4": data.NewEnumValue("c"), + "enum_5": data.NewEnumValue("h"), + }, map[data.ValueName]bool{ + "enum_1": true, + "enum_2": true, + "enum_3": true, + "enum_4": true, + "enum_5": true, + }), + data.NewPacketWithValues(17, map[data.ValueName]data.Value{ + "uint8_1": data.NewNumericValue[uint8](8), + "uint8_2": data.NewNumericValue[uint8](8), + "uint8_3": data.NewNumericValue[uint8](8), + "uint8_4": data.NewNumericValue[uint8](8), + "uint8_5": data.NewNumericValue[uint8](8), + "uint8_6": data.NewNumericValue[uint8](8), + "uint8_7": data.NewNumericValue[uint8](8), + "uint8_8": data.NewNumericValue[uint8](8), + "uint8_9": data.NewNumericValue[uint8](8), + "uint8_10": data.NewNumericValue[uint8](8), + }, map[data.ValueName]bool{ + "uint8_1": true, + "uint8_2": true, + "uint8_3": true, + "uint8_4": true, + "uint8_5": true, + "uint8_6": true, + "uint8_7": true, + "uint8_8": true, + "uint8_9": true, + "uint8_10": true, + }), + data.NewPacketWithValues(18, map[data.ValueName]data.Value{ + "uint64_1": data.NewNumericValue[uint64](8), + "int64_1": data.NewNumericValue[int64](8), + "float64_1": data.NewNumericValue[float64](8), + "uint64_2": data.NewNumericValue[uint64](8), + "int64_2": data.NewNumericValue[int64](8), + "float64_2": data.NewNumericValue[float64](8), + "uint64_3": data.NewNumericValue[uint64](8), + "int64_3": data.NewNumericValue[int64](8), + "float64_3": data.NewNumericValue[float64](8), + "uint64_4": data.NewNumericValue[uint64](8), + "int64_4": data.NewNumericValue[int64](8), + "float64_4": data.NewNumericValue[float64](8), + "uint64_5": data.NewNumericValue[uint64](8), + "int64_5": data.NewNumericValue[int64](8), + "float64_5": data.NewNumericValue[float64](8), + "uint64_6": data.NewNumericValue[uint64](8), + "int64_6": data.NewNumericValue[int64](8), + "float64_6": data.NewNumericValue[float64](8), + }, map[data.ValueName]bool{ + "uint64_1": true, + "int64_1": true, + "float64_1": true, + "uint64_2": true, + "int64_2": true, + "float64_2": true, + "uint64_3": true, + "int64_3": true, + "float64_3": true, + "uint64_4": true, + "int64_4": true, + "float64_4": true, + "uint64_5": true, + "int64_5": true, + "float64_5": true, + "uint64_6": true, + "int64_6": true, + "float64_6": true, + }), + }, + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + decoder := getDecoder(endianness) + + for i := 0; i < len(test.output); i++ { + packet, err := decoder.DecodeNext(test.input) + if err != nil { + t.Fatalf("\nError decoding next (%d) packet: %s\n", i+1, err) + } + + if data, ok := test.output[i].(*data.Packet); ok { + data.SetTimestamp(time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) + } + + if data, ok := packet.(*data.Packet); ok { + data.SetTimestamp(time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)) + } + + if !reflect.DeepEqual(packet, test.output[i]) { + t.Fatalf("\npacket %d:\nGot %#v\nExpected %#v\n", i+1, packet, test.output[i]) + } + } + }) + } +} + +// getDecoder generates a mock Decoder with the following packet IDs: +// 1 - blcuAck +// 2 - info +// 3 - add state order +// 4 - remove state order +// 5 - protection warning +// 6 - protection error +// 7 - protection fault +// 8 - state space +// 9:=18 - data +func getDecoder(endianness binary.ByteOrder) *presentation.Decoder { + decoder := presentation.NewDecoder(endianness) + + decoder.SetPacketDecoder(1, blcu.NewDecoder()) + + decoder.SetPacketDecoder(2, info.NewDecoder(0)) + + ordersDecoder := order.NewDecoder(endianness) + ordersDecoder.SetActionId(3, ordersDecoder.DecodeAdd) + ordersDecoder.SetActionId(4, ordersDecoder.DecodeRemove) + decoder.SetPacketDecoder(3, ordersDecoder) + decoder.SetPacketDecoder(4, ordersDecoder) + + protectionDecoder := protection.NewDecoder() + protectionDecoder.SetSeverity(5, protection.SeverityWarning) + protectionDecoder.SetSeverity(6, protection.SeverityError) + protectionDecoder.SetSeverity(7, protection.SeverityFault) + decoder.SetPacketDecoder(5, protectionDecoder) + decoder.SetPacketDecoder(6, protectionDecoder) + decoder.SetPacketDecoder(7, protectionDecoder) + + decoder.SetPacketDecoder(8, state.NewDecoder(endianness)) + + dataDecoder := data.NewDecoder(endianness) + dataDecoder.SetDescriptor(9, data.Descriptor{ + data.NewNumericDescriptor[uint8]("uint8"), + data.NewNumericDescriptor[uint16]("uint16"), + data.NewNumericDescriptor[uint32]("uint32"), + data.NewNumericDescriptor[uint64]("uint64"), + }) + dataDecoder.SetDescriptor(10, data.Descriptor{ + data.NewNumericDescriptor[int8]("int8"), + data.NewNumericDescriptor[int16]("int16"), + data.NewNumericDescriptor[int32]("int32"), + data.NewNumericDescriptor[int64]("int64"), + }) + dataDecoder.SetDescriptor(11, data.Descriptor{ + data.NewNumericDescriptor[float32]("float32"), + data.NewNumericDescriptor[float64]("float64"), + }) + dataDecoder.SetDescriptor(12, data.Descriptor{ + data.NewBooleanDescriptor("bool"), + }) + dataDecoder.SetDescriptor(13, data.Descriptor{ + data.NewEnumDescriptor("enum", data.EnumDescriptor{"a", "b", "c", "d"}), + }) + dataDecoder.SetDescriptor(14, data.Descriptor{ + data.NewNumericDescriptor[uint8]("uint8_1"), + data.NewNumericDescriptor[uint8]("uint8_2"), + data.NewNumericDescriptor[float64]("float64"), + data.NewBooleanDescriptor("bool"), + data.NewEnumDescriptor("enum", data.EnumDescriptor{"a", "b", "c"}), + }) + dataDecoder.SetDescriptor(15, data.Descriptor{ + data.NewBooleanDescriptor("bool_1"), + data.NewBooleanDescriptor("bool_2"), + data.NewBooleanDescriptor("bool_3"), + }) + dataDecoder.SetDescriptor(16, data.Descriptor{ + data.NewEnumDescriptor("enum_1", data.EnumDescriptor{"a", "b", "c"}), + data.NewEnumDescriptor("enum_2", data.EnumDescriptor{"a", "b"}), + data.NewEnumDescriptor("enum_3", data.EnumDescriptor{"a", "b", "c", "d", "e"}), + data.NewEnumDescriptor("enum_4", data.EnumDescriptor{"a", "b", "c", "d"}), + data.NewEnumDescriptor("enum_5", data.EnumDescriptor{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}), + }) + dataDecoder.SetDescriptor(17, data.Descriptor{ + data.NewNumericDescriptor[uint8]("uint8_1"), + data.NewNumericDescriptor[uint8]("uint8_2"), + data.NewNumericDescriptor[uint8]("uint8_3"), + data.NewNumericDescriptor[uint8]("uint8_4"), + data.NewNumericDescriptor[uint8]("uint8_5"), + data.NewNumericDescriptor[uint8]("uint8_6"), + data.NewNumericDescriptor[uint8]("uint8_7"), + data.NewNumericDescriptor[uint8]("uint8_8"), + data.NewNumericDescriptor[uint8]("uint8_9"), + data.NewNumericDescriptor[uint8]("uint8_10"), + }) + dataDecoder.SetDescriptor(18, data.Descriptor{ + data.NewNumericDescriptor[uint64]("uint64_1"), + data.NewNumericDescriptor[int64]("int64_1"), + data.NewNumericDescriptor[float64]("float64_1"), + data.NewNumericDescriptor[uint64]("uint64_2"), + data.NewNumericDescriptor[int64]("int64_2"), + data.NewNumericDescriptor[float64]("float64_2"), + data.NewNumericDescriptor[uint64]("uint64_3"), + data.NewNumericDescriptor[int64]("int64_3"), + data.NewNumericDescriptor[float64]("float64_3"), + data.NewNumericDescriptor[uint64]("uint64_4"), + data.NewNumericDescriptor[int64]("int64_4"), + data.NewNumericDescriptor[float64]("float64_4"), + data.NewNumericDescriptor[uint64]("uint64_5"), + data.NewNumericDescriptor[int64]("int64_5"), + data.NewNumericDescriptor[float64]("float64_5"), + data.NewNumericDescriptor[uint64]("uint64_6"), + data.NewNumericDescriptor[int64]("int64_6"), + data.NewNumericDescriptor[float64]("float64_6"), + }) + decoder.SetPacketDecoder(9, dataDecoder) + decoder.SetPacketDecoder(10, dataDecoder) + decoder.SetPacketDecoder(11, dataDecoder) + decoder.SetPacketDecoder(12, dataDecoder) + decoder.SetPacketDecoder(13, dataDecoder) + decoder.SetPacketDecoder(14, dataDecoder) + decoder.SetPacketDecoder(15, dataDecoder) + decoder.SetPacketDecoder(16, dataDecoder) + decoder.SetPacketDecoder(17, dataDecoder) + decoder.SetPacketDecoder(18, dataDecoder) + + return decoder +} + +func join[T any](data ...[]T) []T { + output := make([]T, 0) + for _, d := range data { + output = append(output, d...) + } + return output +} + +func toBinary(n any, order binary.ByteOrder) []byte { + switch num := n.(type) { + case uint8: + return []byte{uint8(num)} + case uint16: + output := make([]byte, 2) + order.PutUint16(output, uint16(num)) + return output + case uint32: + output := make([]byte, 4) + order.PutUint32(output, uint32(num)) + return output + case uint64: + output := make([]byte, 8) + order.PutUint64(output, uint64(num)) + return output + case int8: + return []byte{uint8(num)} + case int16: + output := make([]byte, 2) + order.PutUint16(output, uint16(num)) + return output + case int32: + output := make([]byte, 4) + order.PutUint32(output, uint32(num)) + return output + case int64: + output := make([]byte, 8) + order.PutUint64(output, uint64(num)) + return output + case float32: + output := make([]byte, 4) + order.PutUint32(output, math.Float32bits(num)) + return output + case float64: + output := make([]byte, 8) + order.PutUint64(output, math.Float64bits(num)) + return output + default: + panic("must be a number") + } +} diff --git a/backend/pkg/transport/presentation/encoder.go b/backend/pkg/transport/presentation/encoder.go index 8b79cdce4..88e766267 100644 --- a/backend/pkg/transport/presentation/encoder.go +++ b/backend/pkg/transport/presentation/encoder.go @@ -1,3 +1,50 @@ package presentation -type PacketEncoder struct{} +import ( + "bytes" + "encoding/binary" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +type PacketEncoder interface { + Encode(abstraction.Packet, io.Writer) error +} + +type Encoder struct { + idToEncoder map[abstraction.PacketId]PacketEncoder + endianness binary.ByteOrder +} + +// TODO: improve constructor +// NewEncoder creates a new encoder with the given endianness +func NewEncoder(endianness binary.ByteOrder) *Encoder { + return &Encoder{ + idToEncoder: make(map[abstraction.PacketId]PacketEncoder), + endianness: endianness, + } +} + +// SetPacketEncoder sets the encoder for the specified id +func (encoder *Encoder) SetPacketEncoder(id abstraction.PacketId, enc PacketEncoder) { + encoder.idToEncoder[id] = enc +} + +// Encode encodes the provided packet into a byte slice, returning any errors +func (encoder *Encoder) Encode(packet abstraction.Packet) ([]byte, error) { + enc, ok := encoder.idToEncoder[packet.Id()] + if !ok { + return nil, ErrUnexpectedId{Id: packet.Id()} + } + + buffer := new(bytes.Buffer) + + err := binary.Write(buffer, encoder.endianness, packet.Id()) + if err != nil { + return buffer.Bytes(), err + } + + err = enc.Encode(packet, buffer) + return buffer.Bytes(), err +} diff --git a/backend/pkg/transport/presentation/encoder_test.go b/backend/pkg/transport/presentation/encoder_test.go new file mode 100644 index 000000000..8dcd7cc5e --- /dev/null +++ b/backend/pkg/transport/presentation/encoder_test.go @@ -0,0 +1,487 @@ +package presentation_test + +import ( + "encoding/binary" + "reflect" + "testing" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/packet/data" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/presentation" +) + +func TestEncoder(t *testing.T) { + type testcase struct { + name string + input []abstraction.Packet + output []byte + } + + endianness := binary.LittleEndian + + testcases := []testcase{ + { + name: "data 0", + input: []abstraction.Packet{ + data.NewPacketWithValues(9, map[data.ValueName]data.Value{ + "uint8": data.NewNumericValue[uint8](8), + "uint16": data.NewNumericValue[uint16](8), + "uint32": data.NewNumericValue[uint32](8), + "uint64": data.NewNumericValue[uint64](8), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x09, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint16(8), endianness), + toBinary(uint32(8), endianness), + toBinary(uint64(8), endianness), + )), + }, + { + name: "data 1", + input: []abstraction.Packet{ + data.NewPacketWithValues(10, map[data.ValueName]data.Value{ + "int8": data.NewNumericValue[int8](8), + "int16": data.NewNumericValue[int16](8), + "int32": data.NewNumericValue[int32](8), + "int64": data.NewNumericValue[int64](8), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x0a, 0x00}, + toBinary(int8(8), endianness), + toBinary(int16(8), endianness), + toBinary(int32(8), endianness), + toBinary(int64(8), endianness), + )), + }, + { + name: "data 2", + input: []abstraction.Packet{ + data.NewPacketWithValues(11, map[data.ValueName]data.Value{ + "float32": data.NewNumericValue[float32](8), + "float64": data.NewNumericValue[float64](8), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x0b, 0x00}, + toBinary(float32(8), endianness), + toBinary(float64(8), endianness), + )), + }, + { + name: "data 3", + input: []abstraction.Packet{ + data.NewPacketWithValues(12, map[data.ValueName]data.Value{ + "bool": data.NewBooleanValue(true), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x0c, 0x00}, + []byte{0x01}, + )), + }, + { + name: "data 4", + input: []abstraction.Packet{ + data.NewPacketWithValues(13, map[data.ValueName]data.Value{ + "enum": data.NewEnumValue("c"), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x0d, 0x00}, + toBinary(uint8(2), endianness), + )), + }, + { + name: "data 5", + input: []abstraction.Packet{ + data.NewPacketWithValues(14, map[data.ValueName]data.Value{ + "uint8_1": data.NewNumericValue[uint8](8), + "uint8_2": data.NewNumericValue[uint8](8), + "float64": data.NewNumericValue[float64](8), + "bool": data.NewBooleanValue(true), + "enum": data.NewEnumValue("a"), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x0e, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(float64(8), endianness), + []byte{0x01}, + toBinary(uint8(0), endianness), + )), + }, + { + name: "data 6", + input: []abstraction.Packet{ + data.NewPacketWithValues(15, map[data.ValueName]data.Value{ + "bool_1": data.NewBooleanValue(true), + "bool_2": data.NewBooleanValue(false), + "bool_3": data.NewBooleanValue(false), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x0f, 0x00}, + []byte{0x01}, + []byte{0x00}, + []byte{0x00}, + )), + }, + { + name: "data 7", + input: []abstraction.Packet{ + data.NewPacketWithValues(16, map[data.ValueName]data.Value{ + "enum_1": data.NewEnumValue("b"), + "enum_2": data.NewEnumValue("a"), + "enum_3": data.NewEnumValue("d"), + "enum_4": data.NewEnumValue("c"), + "enum_5": data.NewEnumValue("h"), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x10, 0x00}, + toBinary(uint8(1), endianness), + toBinary(uint8(0), endianness), + toBinary(uint8(3), endianness), + toBinary(uint8(2), endianness), + toBinary(uint8(7), endianness), + )), + }, + { + name: "data 8", + input: []abstraction.Packet{ + data.NewPacketWithValues(17, map[data.ValueName]data.Value{ + "uint8_1": data.NewNumericValue[uint8](8), + "uint8_2": data.NewNumericValue[uint8](8), + "uint8_3": data.NewNumericValue[uint8](8), + "uint8_4": data.NewNumericValue[uint8](8), + "uint8_5": data.NewNumericValue[uint8](8), + "uint8_6": data.NewNumericValue[uint8](8), + "uint8_7": data.NewNumericValue[uint8](8), + "uint8_8": data.NewNumericValue[uint8](8), + "uint8_9": data.NewNumericValue[uint8](8), + "uint8_10": data.NewNumericValue[uint8](8), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x11, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + )), + }, + { + name: "data 9", + input: []abstraction.Packet{ + data.NewPacketWithValues(18, map[data.ValueName]data.Value{ + "uint64_1": data.NewNumericValue[uint64](8), + "int64_1": data.NewNumericValue[int64](8), + "float64_1": data.NewNumericValue[float64](8), + "uint64_2": data.NewNumericValue[uint64](8), + "int64_2": data.NewNumericValue[int64](8), + "float64_2": data.NewNumericValue[float64](8), + "uint64_3": data.NewNumericValue[uint64](8), + "int64_3": data.NewNumericValue[int64](8), + "float64_3": data.NewNumericValue[float64](8), + "uint64_4": data.NewNumericValue[uint64](8), + "int64_4": data.NewNumericValue[int64](8), + "float64_4": data.NewNumericValue[float64](8), + "uint64_5": data.NewNumericValue[uint64](8), + "int64_5": data.NewNumericValue[int64](8), + "float64_5": data.NewNumericValue[float64](8), + "uint64_6": data.NewNumericValue[uint64](8), + "int64_6": data.NewNumericValue[int64](8), + "float64_6": data.NewNumericValue[float64](8), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x12, 0x00}, + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + )), + }, + { + name: "multiple", + input: []abstraction.Packet{ + data.NewPacketWithValues(9, map[data.ValueName]data.Value{ + "uint8": data.NewNumericValue[uint8](8), + "uint16": data.NewNumericValue[uint16](8), + "uint32": data.NewNumericValue[uint32](8), + "uint64": data.NewNumericValue[uint64](8), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(10, map[data.ValueName]data.Value{ + "int8": data.NewNumericValue[int8](8), + "int16": data.NewNumericValue[int16](8), + "int32": data.NewNumericValue[int32](8), + "int64": data.NewNumericValue[int64](8), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(11, map[data.ValueName]data.Value{ + "float32": data.NewNumericValue[float32](8), + "float64": data.NewNumericValue[float64](8), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(12, map[data.ValueName]data.Value{ + "bool": data.NewBooleanValue(true), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(13, map[data.ValueName]data.Value{ + "enum": data.NewEnumValue("c"), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(14, map[data.ValueName]data.Value{ + "uint8_1": data.NewNumericValue[uint8](8), + "uint8_2": data.NewNumericValue[uint8](8), + "float64": data.NewNumericValue[float64](8), + "bool": data.NewBooleanValue(true), + "enum": data.NewEnumValue("a"), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(15, map[data.ValueName]data.Value{ + "bool_1": data.NewBooleanValue(true), + "bool_2": data.NewBooleanValue(false), + "bool_3": data.NewBooleanValue(false), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(16, map[data.ValueName]data.Value{ + "enum_1": data.NewEnumValue("b"), + "enum_2": data.NewEnumValue("a"), + "enum_3": data.NewEnumValue("d"), + "enum_4": data.NewEnumValue("c"), + "enum_5": data.NewEnumValue("h"), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(17, map[data.ValueName]data.Value{ + "uint8_1": data.NewNumericValue[uint8](8), + "uint8_2": data.NewNumericValue[uint8](8), + "uint8_3": data.NewNumericValue[uint8](8), + "uint8_4": data.NewNumericValue[uint8](8), + "uint8_5": data.NewNumericValue[uint8](8), + "uint8_6": data.NewNumericValue[uint8](8), + "uint8_7": data.NewNumericValue[uint8](8), + "uint8_8": data.NewNumericValue[uint8](8), + "uint8_9": data.NewNumericValue[uint8](8), + "uint8_10": data.NewNumericValue[uint8](8), + }, make(map[data.ValueName]bool)), + data.NewPacketWithValues(18, map[data.ValueName]data.Value{ + "uint64_1": data.NewNumericValue[uint64](8), + "int64_1": data.NewNumericValue[int64](8), + "float64_1": data.NewNumericValue[float64](8), + "uint64_2": data.NewNumericValue[uint64](8), + "int64_2": data.NewNumericValue[int64](8), + "float64_2": data.NewNumericValue[float64](8), + "uint64_3": data.NewNumericValue[uint64](8), + "int64_3": data.NewNumericValue[int64](8), + "float64_3": data.NewNumericValue[float64](8), + "uint64_4": data.NewNumericValue[uint64](8), + "int64_4": data.NewNumericValue[int64](8), + "float64_4": data.NewNumericValue[float64](8), + "uint64_5": data.NewNumericValue[uint64](8), + "int64_5": data.NewNumericValue[int64](8), + "float64_5": data.NewNumericValue[float64](8), + "uint64_6": data.NewNumericValue[uint64](8), + "int64_6": data.NewNumericValue[int64](8), + "float64_6": data.NewNumericValue[float64](8), + }, make(map[data.ValueName]bool)), + }, + output: (join( + []byte{0x09, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint16(8), endianness), + toBinary(uint32(8), endianness), + toBinary(uint64(8), endianness), + []byte{0x0a, 0x00}, + toBinary(int8(8), endianness), + toBinary(int16(8), endianness), + toBinary(int32(8), endianness), + toBinary(int64(8), endianness), + []byte{0x0b, 0x00}, + toBinary(float32(8), endianness), + toBinary(float64(8), endianness), + []byte{0x0c, 0x00}, + []byte{0x01}, + []byte{0x0d, 0x00}, + toBinary(uint8(2), endianness), + []byte{0x0e, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(float64(8), endianness), + []byte{0x01}, + toBinary(uint8(0), endianness), + []byte{0x0f, 0x00}, + []byte{0x01}, + []byte{0x00}, + []byte{0x00}, + []byte{0x10, 0x00}, + toBinary(uint8(1), endianness), + toBinary(uint8(0), endianness), + toBinary(uint8(3), endianness), + toBinary(uint8(2), endianness), + toBinary(uint8(7), endianness), + []byte{0x11, 0x00}, + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + toBinary(uint8(8), endianness), + []byte{0x12, 0x00}, + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + toBinary(uint64(8), endianness), + toBinary(int64(8), endianness), + toBinary(float64(8), endianness), + )), + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + encoder := getEncoder(endianness) + + output := make([]byte, 0, len(test.output)) + for i := 0; i < len(test.input); i++ { + encoded, err := encoder.Encode(test.input[i]) + if err != nil { + t.Fatalf("\nError encoding (%d) packet: %s\n", i+1, err) + } + + output = append(output, encoded...) + + } + + if !reflect.DeepEqual(output, test.output) { + t.Fatalf("\nGot %#v\nExpected %#v\n", output, test.output) + } + }) + } +} + +// getEncoder generates a mock Encoder with the following packet IDs: +// 9:=18 - data +func getEncoder(endianness binary.ByteOrder) *presentation.Encoder { + encoder := presentation.NewEncoder(endianness) + + dataDecoder := data.NewEncoder(endianness) + dataDecoder.SetDescriptor(9, data.Descriptor{ + data.NewNumericDescriptor[uint8]("uint8"), + data.NewNumericDescriptor[uint16]("uint16"), + data.NewNumericDescriptor[uint32]("uint32"), + data.NewNumericDescriptor[uint64]("uint64"), + }) + dataDecoder.SetDescriptor(10, data.Descriptor{ + data.NewNumericDescriptor[int8]("int8"), + data.NewNumericDescriptor[int16]("int16"), + data.NewNumericDescriptor[int32]("int32"), + data.NewNumericDescriptor[int64]("int64"), + }) + dataDecoder.SetDescriptor(11, data.Descriptor{ + data.NewNumericDescriptor[float32]("float32"), + data.NewNumericDescriptor[float64]("float64"), + }) + dataDecoder.SetDescriptor(12, data.Descriptor{ + data.NewBooleanDescriptor("bool"), + }) + dataDecoder.SetDescriptor(13, data.Descriptor{ + data.NewEnumDescriptor("enum", data.EnumDescriptor{"a", "b", "c", "d"}), + }) + dataDecoder.SetDescriptor(14, data.Descriptor{ + data.NewNumericDescriptor[uint8]("uint8_1"), + data.NewNumericDescriptor[uint8]("uint8_2"), + data.NewNumericDescriptor[float64]("float64"), + data.NewBooleanDescriptor("bool"), + data.NewEnumDescriptor("enum", data.EnumDescriptor{"a", "b", "c"}), + }) + dataDecoder.SetDescriptor(15, data.Descriptor{ + data.NewBooleanDescriptor("bool_1"), + data.NewBooleanDescriptor("bool_2"), + data.NewBooleanDescriptor("bool_3"), + }) + dataDecoder.SetDescriptor(16, data.Descriptor{ + data.NewEnumDescriptor("enum_1", data.EnumDescriptor{"a", "b", "c"}), + data.NewEnumDescriptor("enum_2", data.EnumDescriptor{"a", "b"}), + data.NewEnumDescriptor("enum_3", data.EnumDescriptor{"a", "b", "c", "d", "e"}), + data.NewEnumDescriptor("enum_4", data.EnumDescriptor{"a", "b", "c", "d"}), + data.NewEnumDescriptor("enum_5", data.EnumDescriptor{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}), + }) + dataDecoder.SetDescriptor(17, data.Descriptor{ + data.NewNumericDescriptor[uint8]("uint8_1"), + data.NewNumericDescriptor[uint8]("uint8_2"), + data.NewNumericDescriptor[uint8]("uint8_3"), + data.NewNumericDescriptor[uint8]("uint8_4"), + data.NewNumericDescriptor[uint8]("uint8_5"), + data.NewNumericDescriptor[uint8]("uint8_6"), + data.NewNumericDescriptor[uint8]("uint8_7"), + data.NewNumericDescriptor[uint8]("uint8_8"), + data.NewNumericDescriptor[uint8]("uint8_9"), + data.NewNumericDescriptor[uint8]("uint8_10"), + }) + dataDecoder.SetDescriptor(18, data.Descriptor{ + data.NewNumericDescriptor[uint64]("uint64_1"), + data.NewNumericDescriptor[int64]("int64_1"), + data.NewNumericDescriptor[float64]("float64_1"), + data.NewNumericDescriptor[uint64]("uint64_2"), + data.NewNumericDescriptor[int64]("int64_2"), + data.NewNumericDescriptor[float64]("float64_2"), + data.NewNumericDescriptor[uint64]("uint64_3"), + data.NewNumericDescriptor[int64]("int64_3"), + data.NewNumericDescriptor[float64]("float64_3"), + data.NewNumericDescriptor[uint64]("uint64_4"), + data.NewNumericDescriptor[int64]("int64_4"), + data.NewNumericDescriptor[float64]("float64_4"), + data.NewNumericDescriptor[uint64]("uint64_5"), + data.NewNumericDescriptor[int64]("int64_5"), + data.NewNumericDescriptor[float64]("float64_5"), + data.NewNumericDescriptor[uint64]("uint64_6"), + data.NewNumericDescriptor[int64]("int64_6"), + data.NewNumericDescriptor[float64]("float64_6"), + }) + encoder.SetPacketEncoder(9, dataDecoder) + encoder.SetPacketEncoder(10, dataDecoder) + encoder.SetPacketEncoder(11, dataDecoder) + encoder.SetPacketEncoder(12, dataDecoder) + encoder.SetPacketEncoder(13, dataDecoder) + encoder.SetPacketEncoder(14, dataDecoder) + encoder.SetPacketEncoder(15, dataDecoder) + encoder.SetPacketEncoder(16, dataDecoder) + encoder.SetPacketEncoder(17, dataDecoder) + encoder.SetPacketEncoder(18, dataDecoder) + + return encoder +} diff --git a/backend/pkg/transport/presentation/errors.go b/backend/pkg/transport/presentation/errors.go new file mode 100644 index 000000000..5f93d29e1 --- /dev/null +++ b/backend/pkg/transport/presentation/errors.go @@ -0,0 +1,16 @@ +package presentation + +import ( + "fmt" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" +) + +// ErrUnexpectedId is returned when an ID is not recognized or is not defined +type ErrUnexpectedId struct { + Id abstraction.PacketId +} + +func (err ErrUnexpectedId) Error() string { + return fmt.Sprintf("unexpected id %d", err.Id) +} diff --git a/backend/pkg/transport/session/buffer.go b/backend/pkg/transport/session/buffer.go new file mode 100644 index 000000000..f630f7131 --- /dev/null +++ b/backend/pkg/transport/session/buffer.go @@ -0,0 +1,36 @@ +package session + +type Buffer struct { + data chan byte +} + +func NewBuffer(size int) *Buffer { + return &Buffer{ + data: make(chan byte, size), + } +} + +func (buffer *Buffer) Read(b []byte) (n int, err error) { + n = 0 + ok := true +loop: + for i := range b { + b[i], ok = <-buffer.data + n++ + if !ok { + break loop + } + } + + return n, nil +} + +func (buffer *Buffer) Write(b []byte) (n int, err error) { + n = 0 + for i := range b { + buffer.data <- b[i] + n++ + } + + return n, nil +} diff --git a/backend/pkg/transport/session/sniffer.go b/backend/pkg/transport/session/sniffer.go new file mode 100644 index 000000000..cd0dab2e2 --- /dev/null +++ b/backend/pkg/transport/session/sniffer.go @@ -0,0 +1,59 @@ +package session + +import ( + "fmt" + "io" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network" +) + +// conversationCallback is called when a new conversation is detected, the conversation socket, +// as well as the reader to get its packets, is provided through this callback. +type conversationCallback = func(socket network.Socket, reader io.Reader) + +// packetReader is a reader that generates packets from network connections +type packetReader interface { + ReadNext() (network.Socket, []byte, error) +} + +// Sniffer demux is a helper that takes all packets returned by the Sniffer and splits their +// contents onto buffers based on the source and destination IPs and ports +type SnifferDemux struct { + onConversation conversationCallback + conversations map[network.Socket]io.ReadWriter +} + +// NewSnifferDemux creates a new SnifferDemux with the provided onConversation callback +func NewSnifferDemux(onConversation conversationCallback) *SnifferDemux { + return &SnifferDemux{ + conversations: make(map[network.Socket]io.ReadWriter), + onConversation: onConversation, + } +} + +// ReadPackets consumes the provided PacketReader. +// +// ReadPackets will block until an error is returned, callers are advised to run this +// in a goroutine. +func (demux *SnifferDemux) ReadPackets(reader packetReader) error { + for { + socket, data, err := reader.ReadNext() + if err != nil { + fmt.Println(err) + return err + } + + conversation, ok := demux.conversations[socket] + if !ok { + demux.conversations[socket] = NewBuffer(1500) // TODO: this reader implementation does not exactly work as we want + conversation = demux.conversations[socket] + demux.onConversation(socket, conversation) + } + + _, err = conversation.Write(data) + if err != nil { + fmt.Println(err) + return err + } + } +} diff --git a/backend/pkg/transport/session/sniffer_test.go b/backend/pkg/transport/session/sniffer_test.go new file mode 100644 index 000000000..6c0e1cc39 --- /dev/null +++ b/backend/pkg/transport/session/sniffer_test.go @@ -0,0 +1,267 @@ +package session_test + +import ( + "errors" + "io" + "sync" + "testing" + + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network" + "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/session" +) + +type testInputChunk struct { + socket network.Socket + data string +} + +type testInput []testInputChunk + +type testInputAdapter struct { + chunks testInput + curr int +} + +func (input *testInputAdapter) ReadNext() (network.Socket, []byte, error) { + if input.curr >= len(input.chunks) { + return network.Socket{}, nil, errors.New("no more packets to read") + } + + chunk := input.chunks[input.curr] + input.curr++ + return chunk.socket, []byte(chunk.data), nil +} + +type testOutput = map[network.Socket]string + +type testcase struct { + name string + input testInput + output testOutput +} + +func TestSnifferDemux(t *testing.T) { + + tests := []testcase{ + { + name: "basic message", + input: testInput{ + { + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "Hello, world!", + }, + }, + output: testOutput{ + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }: "Hello, world!", + }, + }, + { + name: "multiple messages", + input: testInput{ + { + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "a", + }, + { + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "b", + }, + { + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "c", + }, + }, + output: testOutput{ + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }: "abc", + }, + }, + { + name: "multiple sockets", + input: testInput{ + { + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "a", + }, + { + network.Socket{ + SrcIP: "127.0.0.2", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "b", + }, + { + network.Socket{ + SrcIP: "127.0.0.3", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "c", + }, + }, + output: testOutput{ + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }: "a", + network.Socket{ + SrcIP: "127.0.0.2", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }: "b", + network.Socket{ + SrcIP: "127.0.0.3", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }: "c", + }, + }, + { + name: "multiple sockets, multiple messages", + input: testInput{ + { + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "a", + }, + { + network.Socket{ + SrcIP: "127.0.0.2", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "b", + }, + { + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }, + "c", + }, + }, + output: testOutput{ + network.Socket{ + SrcIP: "127.0.0.1", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }: "ac", + network.Socket{ + SrcIP: "127.0.0.2", + SrcPort: 3000, + DstIP: "127.0.0.1", + DstPort: 3001, + }: "b", + }, + }, + } + + for _, test := range tests { + t.Run(t.Name(), func(t *testing.T) { + mapMx := new(sync.Mutex) + outputMap := make(map[network.Socket]string) + + wg := new(sync.WaitGroup) + onConversation := func(socket network.Socket, reader io.Reader) { + wg.Add(1) + go func(socket network.Socket, reader io.Reader) { + defer wg.Done() + buf := make([]byte, 65536) + for { + n, err := reader.Read(buf) + if err != nil { + return + } + mapMx.Lock() + data := string(buf[:n]) + prev, ok := outputMap[socket] + if ok { + data = prev + data + } + outputMap[socket] = data + mapMx.Unlock() + } + }(socket, reader) + } + + demux := session.NewSnifferDemux(onConversation) + + data := testInputAdapter{test.input, 0} + + demux.ReadPackets(&data) + wg.Wait() + + if len(outputMap) > len(test.output) { + for socket, got := range outputMap { + if _, ok := test.output[socket]; !ok { + t.Fatalf("Unexpected message from socket %v: %s", socket, got) + } + expected := test.output[socket] + if expected != got { + t.Fatalf("expected != got for socket %v: (\"%s\" != \"%s\")", socket, expected, got) + } + } + } else { + for socket, expected := range test.output { + if _, ok := outputMap[socket]; !ok { + t.Fatalf("Expected message from socket %v: %s", socket, expected) + } + got := outputMap[socket] + if expected != got { + t.Fatalf("expected != got for socket %v: (\"%s\" != \"%s\")", socket, expected, got) + } + } + } + + }) + } +} diff --git a/backend/pkg/transport/session/socket.go b/backend/pkg/transport/session/socket.go deleted file mode 100644 index 3f9be7d56..000000000 --- a/backend/pkg/transport/session/socket.go +++ /dev/null @@ -1,3 +0,0 @@ -package session - -type SocketBuffer struct{} diff --git a/backend/pkg/transport/transport.go b/backend/pkg/transport/transport.go index b6fd7c5d3..c9431a1f5 100644 --- a/backend/pkg/transport/transport.go +++ b/backend/pkg/transport/transport.go @@ -1,6 +1,12 @@ package transport import ( + "errors" + "fmt" + "io" + "net" + "time" + "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network" "github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/sniffer" @@ -17,19 +23,80 @@ import ( // or notification has an associated event which is used to determine the // action to take. type Transport struct { - decoder *presentation.PacketDecoder - encoder *presentation.PacketEncoder + decoder *presentation.Decoder + encoder *presentation.Encoder - conversations map[network.Socket]*session.SocketBuffer + connections map[abstraction.TransportTarget]net.Conn - sniffer *sniffer.Sniffer - boards map[abstraction.BoardId]*tcp.Conn + idToTarget map[abstraction.PacketId]abstraction.TransportTarget tftp *tftp.Client api abstraction.TransportAPI } +// HandleClient connects to the specified client and handles its messages. This method blocks. +// This method will try to reconnect to the client if it disconnects mid way through, but after +// enough retries, it will stop. +func (transport *Transport) HandleClient(config tcp.ClientConfig, target abstraction.TransportTarget, network, remote string) error { + for { + conn, err := config.Dial(network, remote) + if err != nil { + if !errors.Is(err, error(tcp.ErrTooManyRetries{})) { + return err + } + + continue + } + + err = transport.handleTCPConn(target, conn) + if errors.Is(err, error(ErrTargetAlreadyConnected{})) { + return err + } + + // Wait before trying to reconnect + config.CurrentRetries = 5 + time.Sleep(config.Backoff(config.CurrentRetries)) + } +} + +// HandleServer creates a server on the specified address, listening for all incoming connections and +// handles them. +func (transport *Transport) HandleServer(config tcp.ServerConfig, network, local string) error { + return config.Listen(network, local, transport.handleTCPConn) +} + +// handleTCPConn is used to handle the specific TCP connections to the boards. It detects errors caused +// on concurrent reads and writes, so other routines should not worry about closing or handling errors +func (transport *Transport) handleTCPConn(target abstraction.TransportTarget, conn net.Conn) error { + if _, ok := transport.connections[target]; ok { + conn.Close() + return ErrTargetAlreadyConnected{Target: target} + } + + conn, errChan := tcp.WithErrChan(conn) + defer conn.Close() + + transport.connections[target] = conn + defer delete(transport.connections, target) + + transport.api.ConnectionUpdate(target, true) + defer transport.api.ConnectionUpdate(target, false) + + go func() { + for { + packet, err := transport.decoder.DecodeNext(conn) + if err != nil { + break + } + + transport.api.Notification(NewPacketNotification(packet)) + } + }() + + return <-errChan +} + // 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 { @@ -45,20 +112,82 @@ func (transport *Transport) SendMessage(message abstraction.TransportMessage) er } } +// handlePacketEvent is used to send an order to one of the connected boards func (transport *Transport) handlePacketEvent(message PacketMessage) error { - panic("TODO!") + target, ok := transport.idToTarget[message.Id()] + if !ok { + return ErrUnrecognizedId{Id: message.Id()} + } + + conn, ok := transport.connections[target] + if !ok { + return ErrConnClosed{Target: target} + } + + data, err := transport.encoder.Encode(message.Packet) + if err != nil { + return err + } + + totalWritten := 0 + for totalWritten < len(data) { + n, err := conn.Write(data[totalWritten:]) + totalWritten += n + if err != nil { + return err + } + } + + return nil } +// handleFileWrite writes a file through tftp to the blcu func (transport *Transport) handleFileWrite(message FileWriteMessage) error { - panic("TODO!") + _, err := transport.tftp.WriteFile(message.Filename(), tftp.BinaryMode, message) + return err } +// handleFileRead reads a file through tftp from the blcu func (transport *Transport) handleFileRead(message FileReadMessage) error { - panic("TODO!") + _, err := transport.tftp.ReadFile(message.Filename(), tftp.BinaryMode, message) + return err +} + +// HandleSniffer starts listening for packets on the provided sniffer and handles them. +func (transport *Transport) HandleSniffer(sniffer *sniffer.Sniffer) error { + // for { + // _, data, err := sniffer.ReadNext() + // if err != nil { + // return err + // } + + // packet, err := transport.decoder.DecodeNext(bytes.NewReader(data)) + // if err != nil { + // fmt.Println(err) + // continue + // } + + // transport.api.Notification(NewPacketNotification(packet)) + // } + return session.NewSnifferDemux(transport.handleConversation).ReadPackets(sniffer) +} + +// handleConversation is called when the sniffer detects a new conversation and handles its specific packets +func (transport *Transport) handleConversation(socket network.Socket, reader io.Reader) { + go func() { + for { + packet, err := transport.decoder.DecodeNext(reader) + if err != nil { + fmt.Println(err) + return // TODO: handle error + } + + transport.api.Notification(NewPacketNotification(packet)) + } + }() } // SetAPI sets the API that the Transport will use func (transport *Transport) SetAPI(api abstraction.TransportAPI) { transport.api = api - // TODO: make decoder use the api Notify method } diff --git a/backend/pkg/vehicle/vehicle.go b/backend/pkg/vehicle/vehicle.go index bc0d81a89..207293b99 100644 --- a/backend/pkg/vehicle/vehicle.go +++ b/backend/pkg/vehicle/vehicle.go @@ -35,3 +35,8 @@ func (vehicle *Vehicle) SendMessage(abstraction.TransportMessage) error { func (vehicle *Vehicle) SendPush(abstraction.BrokerPush) error { panic("TODO") } + +// ConnectionUpdate is the method invoked by transport to signal a connection state has changed +func (vehicle *Vehicle) ConnectionUpdate(target abstraction.TransportTarget, isConnected bool) { + panic("TODO") +} diff --git a/common-front/LICENSE b/common-front/LICENSE new file mode 100644 index 000000000..4433d1898 --- /dev/null +++ b/common-front/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 HyperloopUPV + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/common-front/README.md b/common-front/README.md index 4970f6d97..b393e8260 100644 --- a/common-front/README.md +++ b/common-front/README.md @@ -1,3 +1,2 @@ # common-front - Common code of th ev-frontend and the cs-frontend diff --git a/common-front/lib/adapters/PodData.ts b/common-front/lib/adapters/PodData.ts index 2e316659f..3c6ea48ff 100644 --- a/common-front/lib/adapters/PodData.ts +++ b/common-front/lib/adapters/PodData.ts @@ -50,7 +50,7 @@ export function createPodDataFromAdapter(adapter: PodDataAdapter): PodData { return { boards, packetToBoard, lastUpdates: {} }; } -function getPackets( +export function getPackets( boardName: string, packets: Record ): Packet[] { @@ -144,7 +144,7 @@ export function getBooleanMeasurement( // }; // } -function getPacketToBoard( +export function getPacketToBoard( boards: Record ): Record { let packetToBoard: PodData["packetToBoard"] = {}; @@ -158,7 +158,7 @@ function getPacketToBoard( return packetToBoard; } -function getMeasurementToPacket( +export function getMeasurementToPacket( packets: Record ): Record { let measurementToPacket: Board["measurementToPacket"] = {}; diff --git a/common-front/lib/broker/Broker.ts b/common-front/lib/broker/Broker.ts index fea9903e4..786e43e21 100644 --- a/common-front/lib/broker/Broker.ts +++ b/common-front/lib/broker/Broker.ts @@ -3,7 +3,7 @@ import { BackendMessage, Callback, Request, Topic } from "./types"; export class Broker { private webSocket!: WebSocket; - private typeToCallbacks: Map>> = new Map(); + private typeToCallbacks: Map[]> = new Map(); constructor(url: string, onOpen?: () => void, onClose?: () => void) { this.webSocket = new WebSocket(`ws://${url}`); diff --git a/common-front/lib/components/ColorfulChart/ColorfulChart.tsx b/common-front/lib/components/ColorfulChart/ColorfulChart.tsx index ae65140f8..615a21df9 100644 --- a/common-front/lib/components/ColorfulChart/ColorfulChart.tsx +++ b/common-front/lib/components/ColorfulChart/ColorfulChart.tsx @@ -10,7 +10,7 @@ const palette = ["#EE8735", "#51C6EB", "#7BEE35", "#e469ca"]; type Props = { className?: string; title: string; - items: Array; + items: LineDescription[]; length: number; height?: string; }; diff --git a/common-front/lib/components/ColorfulChart/Legend/Legend.tsx b/common-front/lib/components/ColorfulChart/Legend/Legend.tsx index 5a4005db3..282d34cf0 100644 --- a/common-front/lib/components/ColorfulChart/Legend/Legend.tsx +++ b/common-front/lib/components/ColorfulChart/Legend/Legend.tsx @@ -3,7 +3,7 @@ import { LegendItem } from "./LegendItem/LegendItem"; import { LineDescription } from "../../LinesChart/types"; type Props = { - items: Array; + items: LineDescription[]; }; export const Legend = ({ items }: Props) => { diff --git a/common-front/lib/components/Connections/Connections.tsx b/common-front/lib/components/Connections/Connections.tsx index 697151c5f..4d1dcd91c 100644 --- a/common-front/lib/components/Connections/Connections.tsx +++ b/common-front/lib/components/Connections/Connections.tsx @@ -7,7 +7,7 @@ export const Connections = () => { return (
- +
{Object.values(connections.boards).map((conn) => { return ( diff --git a/common-front/lib/components/Connections/useConnections.ts b/common-front/lib/components/Connections/useConnections.ts index 3df79d63e..31445f966 100644 --- a/common-front/lib/components/Connections/useConnections.ts +++ b/common-front/lib/components/Connections/useConnections.ts @@ -1,18 +1,16 @@ -import { useSelector } from "react-redux"; -import { useDispatch } from "react-redux"; -import { useSubscribe } from "../.."; -import { connectionsSlice } from "../../slices/connectionsSlice"; +import { useConnectionsStore, useSubscribe } from "../.."; export function useConnections() { - const dispatch = useDispatch(); + + // const setBoardConnections = useStore(state => state.setConnections); + // const connections = useStore(state => state.connections); + + const setBoardConnections = useConnectionsStore(state => state.setConnections); + const connections = useConnectionsStore(state => state.connections) useSubscribe("connection/update", (update) => { - dispatch(connectionsSlice.actions.updateBoardConnections(update)); + setBoardConnections(update) }); - return useSelector( - (state: { - connections: ReturnType; - }) => state.connections - ); + return connections; } diff --git a/common-front/lib/components/FormComponents/ExpandablePairs/ExpandablePairs.tsx b/common-front/lib/components/FormComponents/ExpandablePairs/ExpandablePairs.tsx index 765c7c377..9f8142cb3 100644 --- a/common-front/lib/components/FormComponents/ExpandablePairs/ExpandablePairs.tsx +++ b/common-front/lib/components/FormComponents/ExpandablePairs/ExpandablePairs.tsx @@ -2,7 +2,7 @@ import { useEffect, useReducer } from "react"; import styles from "./ExpandablePairs.module.scss"; import { Pair, PairType } from "./Pair/Pair"; import { Button } from ".."; -import { nanoid } from "@reduxjs/toolkit"; +import { nanoid } from "nanoid"; type Props = { leftColumnName: string; diff --git a/common-front/lib/components/LinesChart/LinesChart.tsx b/common-front/lib/components/LinesChart/LinesChart.tsx index 90b9950a3..f453943a3 100644 --- a/common-front/lib/components/LinesChart/LinesChart.tsx +++ b/common-front/lib/components/LinesChart/LinesChart.tsx @@ -5,7 +5,7 @@ import { LinesWithGrid } from "./LinesWithGrid/LinesWithGrid"; import { LineDescription } from "./types"; type Props = { - items: Array; + items: LineDescription[]; divisions: number; showGrid?: boolean; length: number; diff --git a/common-front/lib/components/LinesChart/VerticalAxis/VerticalAxis.tsx b/common-front/lib/components/LinesChart/VerticalAxis/VerticalAxis.tsx index fd38ea934..25a23302f 100644 --- a/common-front/lib/components/LinesChart/VerticalAxis/VerticalAxis.tsx +++ b/common-front/lib/components/LinesChart/VerticalAxis/VerticalAxis.tsx @@ -43,8 +43,8 @@ function getAxisMarks( min: number, max: number, divisions: number -): Array { - const marks: Array = []; +): AxisMark[] { + const marks: AxisMark[] = []; for (let i = 0; i < divisions + 1; i++) { marks.push({ diff --git a/common-front/lib/components/LinesChart/useLines.ts b/common-front/lib/components/LinesChart/useLines.ts index 628031814..bb9d23264 100644 --- a/common-front/lib/components/LinesChart/useLines.ts +++ b/common-front/lib/components/LinesChart/useLines.ts @@ -18,7 +18,7 @@ export function useLines( viewBoxWidth: number, viewBoxHeight: number, maxLineLength: number, - lineDescriptions: Array + lineDescriptions: LineDescription[] ) { const ref = useRef(null); const initialLargestRange = useMemo( @@ -31,7 +31,7 @@ export function useLines( const [collectiveRange, setCollectiveRange] = useState(initialLargestRange); - const lineInstancesRef = useRef>([]); + const lineInstancesRef = useRef([]); const updateLines = useCallback(() => { const [min, max] = getLargestRange( @@ -94,12 +94,12 @@ export function useLines( } function createLines( - descriptions: Array, - lines: Array, + descriptions: LineDescription[], + lines: Line[], length: number, width: number, height: number -): Array { +): Line[] { return descriptions.map((description) => { const newHandler = lines.find((line) => description.id == line.id)?.data ?? diff --git a/common-front/lib/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.tsx b/common-front/lib/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.tsx index c8a7d0479..80cd97ef6 100644 --- a/common-front/lib/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.tsx +++ b/common-front/lib/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.tsx @@ -10,7 +10,7 @@ export const InfoMessageView = ({ message, className }: Props) => { return (
{message.board}
-
{message.msg}
+
{message.payload}
); }; diff --git a/common-front/lib/components/MessagesContainer/Messages/MessageView/ProtectionMessageView/ProtectionMessageView.tsx b/common-front/lib/components/MessagesContainer/Messages/MessageView/ProtectionMessageView/ProtectionMessageView.tsx index abf32333c..eb3f8d9d9 100644 --- a/common-front/lib/components/MessagesContainer/Messages/MessageView/ProtectionMessageView/ProtectionMessageView.tsx +++ b/common-front/lib/components/MessagesContainer/Messages/MessageView/ProtectionMessageView/ProtectionMessageView.tsx @@ -13,7 +13,7 @@ export const ProtectionMessageView = ({ message, className }: Props) => {
- {message.protection.kind} + {message.payload.kind}
{ name={message.name} />
- +
); }; diff --git a/common-front/lib/components/MessagesContainer/Messages/Messages.tsx b/common-front/lib/components/MessagesContainer/Messages/Messages.tsx index 98a4c023f..2139ad5d5 100644 --- a/common-front/lib/components/MessagesContainer/Messages/Messages.tsx +++ b/common-front/lib/components/MessagesContainer/Messages/Messages.tsx @@ -1,8 +1,7 @@ -import { Button, Message, messageSlice } from "../../.."; +import { Button, Message, useMessagesStore } from "../../.."; import styles from "./Messages.module.scss"; import { MessageView } from "./MessageView/MessageView"; import { useAutoScroll } from "./useAutoScroll"; -import { useDispatch } from "react-redux"; type Props = { messages: Message[]; @@ -10,7 +9,8 @@ type Props = { export const Messages = ({ messages }: Props) => { const { ref, handleScroll } = useAutoScroll(messages); - const dispatch = useDispatch(); + + const clearMessages = useMessagesStore((state) => state.clearMessages); return (
@@ -41,7 +41,7 @@ export const Messages = ({ messages }: Props) => { className={styles.clearBtn} label="Clear" onClick={() => - dispatch(messageSlice.actions.clearMessages()) + clearMessages() } />
diff --git a/common-front/lib/components/MessagesContainer/useMessages.ts b/common-front/lib/components/MessagesContainer/useMessages.ts index e82e621b1..58a532a1b 100644 --- a/common-front/lib/components/MessagesContainer/useMessages.ts +++ b/common-front/lib/components/MessagesContainer/useMessages.ts @@ -1,13 +1,14 @@ -import { messageSlice } from "../../slices/messagesSlice"; -import { useDispatch, useSelector } from "react-redux"; -import { Message, MessageAdapter, useSubscribe } from "../.."; +import { MessageAdapter, useSubscribe } from "../.."; +import { useMessagesStore } from "../.."; export function useMessages() { - const dispatch = useDispatch(); + + const addMessage = useMessagesStore((state) => state.addMessage); + const messages = useMessagesStore((state) => state.messages); useSubscribe("message/update", (msg: MessageAdapter) => { - dispatch(messageSlice.actions.addMessage(msg)); + addMessage(msg); }); - return useSelector((state: { messages: Message[] }) => state.messages); + return messages; } diff --git a/common-front/lib/components/Orders/BoardOrdersView/OrdersList/Form/Field/Input/Input.tsx b/common-front/lib/components/Orders/BoardOrdersView/OrdersList/Form/Field/Input/Input.tsx index 3f4a3bb17..75a2c2af3 100644 --- a/common-front/lib/components/Orders/BoardOrdersView/OrdersList/Form/Field/Input/Input.tsx +++ b/common-front/lib/components/Orders/BoardOrdersView/OrdersList/Form/Field/Input/Input.tsx @@ -45,7 +45,7 @@ export const Input = ({ input, onChange }: Props) => { {...input} leftColumnName="Position" rightColumnName="Velocity" - onChange={(v) => {}} + onChange={(v) => console.log(v)} /> ); } diff --git a/common-front/lib/components/Orders/useOrders.ts b/common-front/lib/components/Orders/useOrders.ts index 2dd3f5af2..abe7a7bec 100644 --- a/common-front/lib/components/Orders/useOrders.ts +++ b/common-front/lib/components/Orders/useOrders.ts @@ -1,11 +1,9 @@ -import { useDispatch, useSelector } from "react-redux"; import { Field, Form, NumericType, OrderDescription, OrderFieldDescription, - orderSlice, useSubscribe, } from "../.."; import { @@ -13,18 +11,17 @@ import { isNumberValid, isWithinRange, } from "../../numberValidation"; +import { useOrdersStore } from "../.."; export function useOrders() { - const dispatch = useDispatch(); + const updateStateOrders = useOrdersStore((state) => state.updateStateOrders); + const ordersBoards = useOrdersStore((state) => state.vehicleOrders.boards); useSubscribe("order/stateOrders", (msg) => { - dispatch(orderSlice.actions.updateStateOrders(msg)); + updateStateOrders(msg); }); - return useSelector( - (state: { orders: ReturnType }) => - state.orders.boards - ); + return ordersBoards; } export function createFormFromOrder( diff --git a/common-front/lib/form/useForm.ts b/common-front/lib/form/useForm.ts index 2c3e02907..65de0143b 100644 --- a/common-front/lib/form/useForm.ts +++ b/common-front/lib/form/useForm.ts @@ -32,6 +32,9 @@ export function useForm(initialForm: Omit) { isValid: field.validator?.(ev.ev.value) ?? true, }; } else { + console.log( + `field ${field.id} type (${field.type}) and event type (${ev.ev.type}) don't match` + ); } } return field; diff --git a/common-front/lib/hooks/WebRTC/useWebRTC.ts b/common-front/lib/hooks/WebRTC/useWebRTC.ts index 0766de475..9cd44d79c 100644 --- a/common-front/lib/hooks/WebRTC/useWebRTC.ts +++ b/common-front/lib/hooks/WebRTC/useWebRTC.ts @@ -18,6 +18,7 @@ export function useWebRTC(signalUrl: string, configuration?: RTCConfiguration) { function handleTrack(ev: RTCTrackEvent) { + console.log(ev) setMediaStreams(prevStreams => prevStreams ? [...prevStreams, ...ev.streams] : [...ev.streams]); } diff --git a/common-front/lib/hooks/useCameras.ts b/common-front/lib/hooks/useCameras.ts index 9060759de..b6c564318 100644 --- a/common-front/lib/hooks/useCameras.ts +++ b/common-front/lib/hooks/useCameras.ts @@ -6,12 +6,12 @@ export type CameraData = { }; //TODO: use MediaStream id instead -function streamsToCameras(streams: Array): Array { +function streamsToCameras(streams: MediaStream[]): CameraData[] { return streams.map((stream, index) => ({ id: index, stream })); } -export function useCameras(streams: Array) { - const [cameras, setCameras] = useState>( +export function useCameras(streams: MediaStream[]) { + const [cameras, setCameras] = useState( streamsToCameras(streams) ); // When streams change, initialize cameras diff --git a/common-front/lib/index.ts b/common-front/lib/index.ts index e31430a53..294ae0273 100644 --- a/common-front/lib/index.ts +++ b/common-front/lib/index.ts @@ -2,7 +2,7 @@ export * from "./adapters"; export * from "./broker"; export * from "./wsHandler"; export * from "./models"; -export * from "./slices"; +export * from "./store"; export * from "./BackendTypes"; export * from "./components"; export * from "./hooks"; diff --git a/common-front/lib/models/Message.ts b/common-front/lib/models/Message.ts index 1c72455a4..4446524fd 100644 --- a/common-front/lib/models/Message.ts +++ b/common-front/lib/models/Message.ts @@ -12,17 +12,17 @@ type AbstractMessage = { type FaultMessage = AbstractMessage & { kind: "fault"; - protection: Protection; + payload: Protection; }; type WarningMessage = AbstractMessage & { kind: "warning"; - protection: Protection; + payload: Protection; }; export type InfoMessage = AbstractMessage & { kind: "info"; - msg: string; + payload: string; }; export type Protection = diff --git a/common-front/lib/models/PodData/Packet.ts b/common-front/lib/models/PodData/Packet.ts index e338ffbed..b177e2c09 100644 --- a/common-front/lib/models/PodData/Packet.ts +++ b/common-front/lib/models/PodData/Packet.ts @@ -23,6 +23,8 @@ export function updatePacket( packet.measurements, update.measurementUpdates ); + + return packet; } function updateMeasurements( diff --git a/common-front/lib/models/PodData/PodData.ts b/common-front/lib/models/PodData/PodData.ts index a1190d723..d3c157a4f 100644 --- a/common-front/lib/models/PodData/PodData.ts +++ b/common-front/lib/models/PodData/PodData.ts @@ -7,43 +7,3 @@ export type PodData = { packetToBoard: Record; lastUpdates: Record; }; - -export function updatePodData( - podData: PodData, - packetUpdates: { [id: number]: PacketUpdate } -) { - for (const update of Object.values(packetUpdates)) { - const packet = getPacket(podData, update.id); - if (packet) { - const boardIndex = podData.packetToBoard[update.id]; - - if (boardIndex === undefined) { - console.warn( - `packet with id ${update.id} not found in packetToBoard` - ); - continue; - } - - const board = podData.boards[boardIndex]; - - if (!board) { - console.warn(`board with index ${boardIndex} not found`); - continue; - } - - updatePacket(board.name, packet, update); - } else { - console.warn(`packet with id ${update.id} not found`); - } - } -} - -export function getPacket(podData: PodData, id: number): Packet | undefined { - const board = podData.boards[podData.packetToBoard[id]]; - - if (board) { - return board.packets.find((item) => item.id == id); - } - - return undefined; -} diff --git a/common-front/lib/selectors/LCU.ts b/common-front/lib/selectors/LCU.ts index 241694505..1e266872f 100644 --- a/common-front/lib/selectors/LCU.ts +++ b/common-front/lib/selectors/LCU.ts @@ -286,6 +286,6 @@ export function selectLcuMeasurements( ), control_state: - measurements.measurements["LCU_MASTER/lcu_master_control_state"], + measurements["LCU_MASTER/lcu_master_control_state"], } as LcuMeasurements; } diff --git a/common-front/lib/slices/connectionsSlice.ts b/common-front/lib/slices/connectionsSlice.ts deleted file mode 100644 index d8b28928d..000000000 --- a/common-front/lib/slices/connectionsSlice.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { Connection } from ".."; - -export const connectionsSlice = createSlice({ - name: "connections", - initialState: { - websocket: { name: "Backend WebSocket", isConnected: false }, - boards: [] as Connection[], - }, - reducers: { - setWebSocketConnection: ( - connections, - action: PayloadAction - ) => { - connections.websocket.isConnected = action.payload; - - if (!action.payload) { - connections.boards.forEach((board) => { - board.isConnected = false; - }); - } - }, - - updateBoardConnections: ( - connections, - action: PayloadAction> - ) => { - for (const update of action.payload) { - const connIndex = connections.boards.findIndex( - (conn) => conn.name == update.name - ); - - if (connIndex != -1) { - connections.boards[connIndex].isConnected = - update.isConnected; - } else { - connections.boards.push({ - name: update.name, - isConnected: update.isConnected, - }); - } - } - }, - }, -}); diff --git a/common-front/lib/slices/index.ts b/common-front/lib/slices/index.ts deleted file mode 100644 index de85cd2e1..000000000 --- a/common-front/lib/slices/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./podDataSlice"; -export * from "./measurementsSlice"; -export * from "./messagesSlice"; -export * from "./ordersSlice"; -export * from "./connectionsSlice"; diff --git a/common-front/lib/slices/messagesSlice.ts b/common-front/lib/slices/messagesSlice.ts deleted file mode 100644 index 23f259864..000000000 --- a/common-front/lib/slices/messagesSlice.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { nanoid } from "nanoid"; -import { isEqual } from "lodash"; -import { MessageAdapter } from "../adapters"; -import { Message } from "../models"; - -export const messageSlice = createSlice({ - name: "messages", - initialState: [] as Message[], - reducers: { - addMessage: { - reducer(messages: Message[], action: PayloadAction) { - const newMessages = [...messages]; - - if ( - messages.length > 0 && - areMessagesEqual( - messages[messages.length - 1], - action.payload - ) - ) { - newMessages[newMessages.length - 1] = { - ...messages[messages.length - 1], - id: action.payload.id, - count: messages[messages.length - 1].count + 1, - }; - } else { - newMessages.push(action.payload); - } - - return newMessages; - }, - prepare(message: MessageAdapter) { - return { - payload: { - id: nanoid(), - count: 1, - ...message, - } as Message, - }; - }, - }, - clearMessages: () => { - return []; - }, - }, -}); - -function areMessagesEqual(message: Message, adapter: MessageAdapter): boolean { - //TODO: this could break easily, i'm not covering the case where they are not the same kind - if ( - message.board == adapter.board && - message.kind == adapter.kind && - message.name == adapter.name - ) { - if (message.kind == "info" && adapter.kind == "info") { - return message.msg == adapter.msg; - } else if (message.kind != "info" && adapter.kind != "info") { - return isEqual(message.protection, adapter.protection); - } else { - return false; - } - } - - return false; -} diff --git a/common-front/lib/slices/ordersSlice.ts b/common-front/lib/slices/ordersSlice.ts deleted file mode 100644 index 5b481a728..000000000 --- a/common-front/lib/slices/ordersSlice.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { StateOrdersUpdate, VehicleOrders } from ".."; -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; - -export const orderSlice = createSlice({ - name: "orders", - initialState: { - boards: [], - } as VehicleOrders, - reducers: { - setOrders: (_, action: PayloadAction) => { - return action.payload; - }, - updateStateOrders: ( - state, - action: PayloadAction - ) => { - Object.entries(action.payload).forEach(([name, ids]) => { - const index = state.boards.findIndex( - (board) => board.name == name - ); - - if (index == -1) { - return; - } - - state.boards[index].stateOrders = state.boards[ - index - ].stateOrders.map((item) => { - return { - ...item, - enabled: ids.includes(item.id), - }; - }); - }); - }, - }, -}); diff --git a/common-front/lib/slices/podDataSlice.ts b/common-front/lib/slices/podDataSlice.ts deleted file mode 100644 index 183348a45..000000000 --- a/common-front/lib/slices/podDataSlice.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { - createPodDataFromAdapter, - PacketUpdate, - PodDataAdapter, -} from "../adapters"; -import { PodData, updatePodData as updatePackets, Board } from "../models"; - -export const podDataSlice = createSlice({ - name: "podData", - initialState: { - boards: [] as Board[], - packetToBoard: {} as Record, - lastUpdates: {} as Record, - } as PodData, - reducers: { - initPodData: (_: PodData, action: PayloadAction) => { - return createPodDataFromAdapter(action.payload); - }, - updatePodData: ( - state: PodData, - action: PayloadAction> - ) => { - state.lastUpdates = action.payload; - updatePackets(state, action.payload); - }, - }, -}); diff --git a/common-front/lib/store/connectionsStore.ts b/common-front/lib/store/connectionsStore.ts new file mode 100644 index 000000000..9265caccf --- /dev/null +++ b/common-front/lib/store/connectionsStore.ts @@ -0,0 +1,51 @@ +import { Connection } from ".."; +import { StateCreator, StoreApi, create } from "zustand"; + +export interface ConnectionsStore { + connections: { + backend: Connection; + boards: Connection[]; + } + setBackendConnection: (isConnected: boolean) => void; + setConnections: (connections: Connection[]) => void; +} + +export const useConnectionsStore = create((set) => ({ + connections: { + backend: { name: "Backend WebSocket", isConnected: false }, + boards: [] as Connection[], + }, + + /** + * Reducer that sets the state of the websocket connection to isConnected param. + * @param {boolean} isConnected + */ + setBackendConnection: (isConnected: boolean) => { + set(state => ({ + ...state, + connections: { + ...state.connections, + backend: { + ...state.connections.backend, + isConnected: isConnected + }, + boards: state.connections.boards.map(board => ({ + ...board, + isConnected: isConnected && board.isConnected + })) + } + })) + }, + + /** + * Update the board connections in the state. + * When a board connection state changes, it updates all the connections. + * @param {Connection[]} connections + */ + setConnections: (connections: Connection[]) => { + set(state => ({ + ...state, + boards: connections + })) + } +})) \ No newline at end of file diff --git a/common-front/lib/store/index.ts b/common-front/lib/store/index.ts new file mode 100644 index 000000000..ba441b2ce --- /dev/null +++ b/common-front/lib/store/index.ts @@ -0,0 +1,5 @@ +export * from "./podDataStore"; +export * from "./measurementsStore"; +export * from "./messagesStore"; +export * from "./ordersStore"; +export * from "./connectionsStore"; diff --git a/common-front/lib/slices/measurementsSlice.ts b/common-front/lib/store/measurementsStore.ts similarity index 52% rename from common-front/lib/slices/measurementsSlice.ts rename to common-front/lib/store/measurementsStore.ts index 6f659d133..3e51f951c 100644 --- a/common-front/lib/slices/measurementsSlice.ts +++ b/common-front/lib/store/measurementsStore.ts @@ -1,5 +1,4 @@ import { Measurement } from "../models"; -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { getBooleanMeasurement, getEnumMeasurement, @@ -8,52 +7,66 @@ import { PodDataAdapter, PacketUpdate, } from "../adapters"; +import { create } from "zustand"; -export type Measurements = { - measurements: Record; +export type Measurements = Record + +export interface MeasurementsStore { + measurements: Measurements; packetIdToBoard: Record; -}; - -export const measurementsSlice = createSlice({ - name: "measurements", - initialState: { measurements: {}, packetIdToBoard: {} } as Measurements, - reducers: { - initMeasurements: ( - _: Measurements, - action: PayloadAction - ) => { - return { - measurements: createMeasurementsFromPodDataAdapter( - action.payload - ), - packetIdToBoard: getPacketIdToBoard(action.payload), - }; - }, - updateMeasurements: ( - state: Measurements, - action: PayloadAction> - ) => { - for (const update of Object.values(action.payload)) { - for (const [id, mUpdate] of Object.entries( - update.measurementUpdates - )) { - const boardName = state.packetIdToBoard[update.id]; - - if (!boardName) { - continue; - } - - const measId = `${boardName}/${id}`; - state.measurements[measId].value = mUpdate; + initMeasurements: (podDataAdapter: PodDataAdapter) => void; + updateMeasurements: (measurements: Record) => void +} + +export const useMeasurementsStore = create((set, get) => ({ + measurements: {}, + packetIdToBoard: {}, + + /** + * Reducer that receives a PodDataAdapter and initializes the measurements + * and packetIdToBoard map in state. + * @param {PodDataAdapter} podDataAdapter + * @returns {Measurements} + */ + initMeasurements: (podDataAdapter: PodDataAdapter) => { + set(state => ({ + ...state, + measurements: createMeasurementsFromPodDataAdapter(podDataAdapter), + packetIdToBoard: getPacketIdToBoard(podDataAdapter), + })) + }, + + /** + * Reducer that updates the measurements in the state. + * It receives a measurements map with PacketUpdates, extract the measurements + * from each of them and updates the measurements. + * @param {Record} measurements + */ + updateMeasurements: (measurements: Record) => { + + const measurementsDraft = get().measurements; + + for(const update of Object.values(measurements)) { + for (const [id, mUpdate] of Object.entries(update.measurementUpdates)) { + const boardName = get().packetIdToBoard[update.id]; + if (!boardName) { + continue; } + + const measurementId = `${boardName}/${id}`; + measurementsDraft[measurementId].value = mUpdate; } - }, - }, -}); + } + set(state => ({ + ...state, + measurements: measurementsDraft + })) + } +})) function createMeasurementsFromPodDataAdapter( podDataAdapter: PodDataAdapter -): Record { +): Measurements { const measurements: Record = {}; for (const board of Object.values(podDataAdapter.boards)) { @@ -90,7 +103,7 @@ export function getMeasurement( measurements: Measurements, id: string ): Measurement | undefined { - const meas = measurements.measurements[id]; + const meas = measurements[id]; if (!meas) { console.trace(`measurement ${id} not found in store`); diff --git a/common-front/lib/store/messagesStore.ts b/common-front/lib/store/messagesStore.ts new file mode 100644 index 000000000..50b1e7e65 --- /dev/null +++ b/common-front/lib/store/messagesStore.ts @@ -0,0 +1,74 @@ +import { nanoid } from "nanoid"; +import { isEqual } from "lodash"; +import { MessageAdapter } from "../adapters"; +import { Message } from "../models"; +import { StateCreator, StoreApi, UseBoundStore, create } from "zustand"; + +export interface MessagesStore { + messages: Message[] + addMessage: (message: MessageAdapter) => void + clearMessages: () => void +} + +export const useMessagesStore = create((set, get) => ({ + messages: [] as Message[], + + /** + * Reducer that adds to messages the message resulted of processing the MessageAdapter + * @param {MessageAdapter} message + * @returns {Message[]} + */ + addMessage: (message: MessageAdapter) => { + + const preparedMessage = { + id: nanoid(), + count: 1, + ...message + } as Message + + const stateMessages = get().messages; + const lastMessage = stateMessages[stateMessages.length - 1]; + + const resultMessagesState = updateMessagesArray(stateMessages, preparedMessage, lastMessage); + + set(state => ({ + ...state, + messages: resultMessagesState + })) + + }, + + clearMessages: () => { + set(state => ({ + ...state, + messages: [] + })) + } +})) + +function areMessagesEqual(message: Message, adapter: MessageAdapter): boolean { + if ( + message.board == adapter.board && + message.kind == adapter.kind && + message.name == adapter.name + ) { + return message.payload == adapter.payload; + } + + return false; +} + +function updateMessagesArray(stateMessages: Message[], preparedMessage: Message, lastMessage: Message): Message[] { + if(stateMessages.length > 0 && areMessagesEqual(lastMessage, preparedMessage)) { + return [ + ...stateMessages.slice(0, stateMessages.length - 1), + { + ...lastMessage, + id: preparedMessage.id, + count: lastMessage.count + 1 + } + ] as Message[] + } else { + return [...stateMessages, preparedMessage] as Message[] + } +} diff --git a/common-front/lib/store/ordersStore.ts b/common-front/lib/store/ordersStore.ts new file mode 100644 index 000000000..89d5cbf3c --- /dev/null +++ b/common-front/lib/store/ordersStore.ts @@ -0,0 +1,46 @@ +import { StateCreator, StoreApi, UseBoundStore, create } from "zustand"; +import { BoardOrders, StateOrdersUpdate, VehicleOrders } from ".."; + +export interface OrdersStore { + vehicleOrders: VehicleOrders + setOrders: (vehicleOrders: VehicleOrders) => void + updateStateOrders: (stateOrdersUpdate: StateOrdersUpdate) => void +} + +export const useOrdersStore= create((set, get) => ({ + vehicleOrders: { boards: [] as BoardOrders[] }, + + /** + * Reducer that sets the vehicleOrders to the vehicleOrders param + * @param {VehicleOrders} vehicleOrders + */ + setOrders: (vehicleOrders: VehicleOrders) => { + set(state => ({ + ...state, + vehicleOrders: vehicleOrders + })) + }, + + /** + * Reducer that updates orders to stateOrdersUpdate param. + * It checks if the board vinculated with each order of stateOrdersUpdate exists. + * If so, it is updated. If not exists, it ignores that order. + * @param {StateOrdersUpdate} stateOrdersUpdate + */ + updateStateOrders: (stateOrdersUpdate: StateOrdersUpdate) => { + const vehicleOrdersDraft = get().vehicleOrders; + Object.entries(stateOrdersUpdate).forEach(([name, ids]) => { + const index = get().vehicleOrders.boards.findIndex( (board) => board.name == name ); + if (index == -1) return + + vehicleOrdersDraft.boards[index].stateOrders.map(item => { + item.enabled = ids.includes(item.id); + }) + }) + set(state => ({ + ...state, + vehicleOrders: vehicleOrdersDraft + })) + + }, +})) diff --git a/common-front/lib/store/podDataStore.ts b/common-front/lib/store/podDataStore.ts new file mode 100644 index 000000000..2d3015172 --- /dev/null +++ b/common-front/lib/store/podDataStore.ts @@ -0,0 +1,114 @@ +import { + createPodDataFromAdapter, + PacketUpdate, + PodDataAdapter, + getPacketToBoard, + getMeasurementToPacket, + getPackets +} from "../adapters"; +import { PodData, updatePacket, Board, Packet } from "../models"; +import { create, StateCreator, StoreApi, UseBoundStore } from "zustand"; + +export interface PodDataStore { + podData: PodData + initPodData: (podDataAdapter: PodDataAdapter) => void + updatePodData: (packetUpdates: Record) => void +} + +export const usePodDataStore = create((set, get) => ({ + podData: { + boards: [] as Board[], + packetToBoard: {} as Record, + lastUpdates: {} as Record, + }, + + /** + * Reducer that initializes the state based on podDataAdapter. + * It uses a helper function createPodDataFromAdapter to do it. + * @param {PodDataAdapter} podDataAdapter + */ + initPodData: (podDataAdapter: PodDataAdapter) => { + + const boards: Board[] = Object.values(podDataAdapter.boards).map( + (boardAdapter) => { + const packets = getPackets(boardAdapter.name, boardAdapter.packets); + const measurementToPacket = getMeasurementToPacket( + boardAdapter.packets + ); + + return { + name: boardAdapter.name, + packets, + measurementToPacket, + }; + } + ); + + const packetToBoard = getPacketToBoard(podDataAdapter.boards); + + const podDataResult = { boards, packetToBoard, lastUpdates: {} }; + + set(state => ({ + ...state, + podData: podDataResult + })) + }, + + /** + * Reducer that updates the state based on packetUpdates. + * @param {Record} packetUpdates + */ + updatePodData: (packetUpdates: Record) => { + const podData = get().podData; + const updatedBoards = [...podData.boards]; + + for (const update of Object.values(packetUpdates)) { + const packet = getPacket(podData, update.id); + if (packet) { + const boardIndex = podData.packetToBoard[update.id]; + + if (boardIndex == undefined) { + console.warn(`packet with id ${update.id} not found in packetToBoard`); + continue; + } + + const board = podData.boards[boardIndex]; + + if (board == undefined) { + console.warn(`board with index ${boardIndex} not found`); + continue; + } + + const packetIndexInBoard = board.packets.findIndex(p => p.id == packet.id) + + const updatedBoard = {...board} + updatedBoard.packets[packetIndexInBoard] = updatePacket(board.name, packet, update) + + updatedBoards[boardIndex] = updatedBoard; + + } else { + console.warn(`packet with id ${update.id} not found`); + } + + } + + set(state => ({ + ...state, + podData: { + ...state.podData, + boards: updatedBoards, + lastUpdates: packetUpdates + } + })) + }, +})) + +export function getPacket(podData: PodData, id: number): Packet | undefined { + const board = podData.boards[podData.packetToBoard[id]]; + + if (board) { + return board.packets.find((item) => item.id == id); + } + + return undefined; +} \ No newline at end of file diff --git a/common-front/lib/wsHandler/useSubscribe.ts b/common-front/lib/wsHandler/useSubscribe.ts index 15319f2f8..9c76e3adc 100644 --- a/common-front/lib/wsHandler/useSubscribe.ts +++ b/common-front/lib/wsHandler/useSubscribe.ts @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react"; import { HandlerMessages } from "./HandlerMessages"; import { SubscriptionTopic } from "./types"; import { useWsHandler } from "."; -import { nanoid } from "@reduxjs/toolkit"; +import { nanoid } from "nanoid"; export function useSubscribe( topic: T, diff --git a/common-front/package-lock.json b/common-front/package-lock.json index 977652b28..798b9f08a 100644 --- a/common-front/package-lock.json +++ b/common-front/package-lock.json @@ -33,7 +33,8 @@ "typescript": "^5.0.2", "vite": "^4.3.2", "vite-plugin-svgr": "^3.2.0", - "vite-tsconfig-paths": "^4.2.0" + "vite-tsconfig-paths": "^4.2.0", + "zustand": "^4.4.6" } }, "node_modules/@ampproject/remapping": { @@ -3719,6 +3720,33 @@ "engines": { "node": ">=6" } + }, + "node_modules/zustand": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz", + "integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } }, "dependencies": { @@ -6068,6 +6096,14 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "peer": true + }, + "zustand": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz", + "integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==", + "requires": { + "use-sync-external-store": "1.2.0" + } } } } diff --git a/common-front/package.json b/common-front/package.json index b2f6b0bd8..60efe3223 100644 --- a/common-front/package.json +++ b/common-front/package.json @@ -13,31 +13,32 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.2.0", - "react-icons": "^4.9.0", - "react-use-websocket": "^3.0.0", "@react-spring/web": "^9.7.2", - "react-redux": "^8.0.5", "@reduxjs/toolkit": "^1.9.5", + "@rollup/plugin-typescript": "^11.1.0", "@types/lodash": "^4.14.194", "@types/node": "^18.16.1", "@types/react": "^18.2.0", - "@zerollup/ts-transform-paths": "^1.7.18", - "rollup-plugin-postcss": "^4.0.2", - "sass": "^1.62.1", - "ttypescript": "^1.5.15", - "typescript": "^5.0.2", - "vite": "^4.3.2", - "@rollup/plugin-typescript": "^11.1.0", "@vitejs/plugin-react": "^4.0.0", + "@zerollup/ts-transform-paths": "^1.7.18", "lodash": "^4.17.21", "math": "^0.0.3", + "react": "^18.2.0", + "react-icons": "^4.9.0", + "react-redux": "^8.0.5", + "react-use-websocket": "^3.0.0", "rollup-plugin-import-map": "^3.0.0", "rollup-plugin-includepaths": "^0.2.4", + "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript-paths": "^1.4.0", "rollup-plugin-typescript2": "^0.34.1", + "sass": "^1.62.1", "tslib": "^2.5.0", + "ttypescript": "^1.5.15", + "typescript": "^5.0.2", + "vite": "^4.3.2", "vite-plugin-svgr": "^3.2.0", - "vite-tsconfig-paths": "^4.2.0" + "vite-tsconfig-paths": "^4.2.0", + "zustand": "^4.4.6" } } diff --git a/ethernet-view/package-lock.json b/ethernet-view/package-lock.json index b1de08ff1..2c79b93b0 100644 --- a/ethernet-view/package-lock.json +++ b/ethernet-view/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend-h8", "version": "0.0.0", "dependencies": { + "@canvasjs/react-charts": "^1.0.0", "@react-spring/web": "^9.7.2", "@reduxjs/toolkit": "^1.9.0", "axios": "^1.1.3", @@ -15,7 +16,7 @@ "dotenv": "^16.0.3", "events": "^3.3.0", "lodash": "^4.17.21", - "nanoid": "^4.0.0", + "nanoid": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.6.0", @@ -24,7 +25,8 @@ "react-virtualized-auto-sizer": "^1.0.7", "react-window": "^1.8.8", "vite-plugin-svgr": "^2.4.0", - "vite-tsconfig-paths": "^4.0.5" + "vite-tsconfig-paths": "^4.0.5", + "zustand": "^4.4.6" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", @@ -74,7 +76,8 @@ "typescript": "^5.0.2", "vite": "^4.3.2", "vite-plugin-svgr": "^3.2.0", - "vite-tsconfig-paths": "^4.2.0" + "vite-tsconfig-paths": "^4.2.0", + "zustand": "^4.4.6" } }, "../h9-common-front": { @@ -637,6 +640,21 @@ "node": ">=6.9.0" } }, + "node_modules/@canvasjs/charts": { + "version": "3.7.26", + "resolved": "https://registry.npmjs.org/@canvasjs/charts/-/charts-3.7.26.tgz", + "integrity": "sha512-4BcaB0ko64NH+A4OHlKjONBEMaPDRt2V7XYTJ+lFcn0MlzzkzhWhNXGYdPL3wN9uIEkBw/1yaNZuU55cadPVpw==", + "peer": true + }, + "node_modules/@canvasjs/react-charts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@canvasjs/react-charts/-/react-charts-1.0.0.tgz", + "integrity": "sha512-wGI675kz65owAwXZWzJULK9GXKCrPV59+CCawXP801f4rmYGmPskdtGrczgJhA8ThFEgIvq3K4qrkpLDPSmxCQ==", + "peerDependencies": { + "@canvasjs/charts": "^3.7.5", + "react": ">=16.0.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", @@ -1532,9 +1550,9 @@ } }, "node_modules/axios": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", - "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -4719,6 +4737,33 @@ "engines": { "node": ">= 6" } + }, + "node_modules/zustand": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz", + "integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } }, "dependencies": { @@ -5108,6 +5153,18 @@ "to-fast-properties": "^2.0.0" } }, + "@canvasjs/charts": { + "version": "3.7.26", + "resolved": "https://registry.npmjs.org/@canvasjs/charts/-/charts-3.7.26.tgz", + "integrity": "sha512-4BcaB0ko64NH+A4OHlKjONBEMaPDRt2V7XYTJ+lFcn0MlzzkzhWhNXGYdPL3wN9uIEkBw/1yaNZuU55cadPVpw==", + "peer": true + }, + "@canvasjs/react-charts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@canvasjs/react-charts/-/react-charts-1.0.0.tgz", + "integrity": "sha512-wGI675kz65owAwXZWzJULK9GXKCrPV59+CCawXP801f4rmYGmPskdtGrczgJhA8ThFEgIvq3K4qrkpLDPSmxCQ==", + "requires": {} + }, "@esbuild/android-arm": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", @@ -5749,9 +5806,9 @@ "dev": true }, "axios": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", - "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -5913,7 +5970,8 @@ "typescript": "^5.0.2", "vite": "^4.3.2", "vite-plugin-svgr": "^3.2.0", - "vite-tsconfig-paths": "^4.2.0" + "vite-tsconfig-paths": "^4.2.0", + "zustand": "^4.4.6" } }, "convert-source-map": { @@ -7892,6 +7950,14 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "zustand": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz", + "integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==", + "requires": { + "use-sync-external-store": "1.2.0" + } } } } diff --git a/ethernet-view/package.json b/ethernet-view/package.json index 6920bf26a..6f0b035f3 100644 --- a/ethernet-view/package.json +++ b/ethernet-view/package.json @@ -10,6 +10,7 @@ "preview": "npm run build && vite preview" }, "dependencies": { + "@canvasjs/react-charts": "^1.0.0", "@react-spring/web": "^9.7.2", "@reduxjs/toolkit": "^1.9.0", "axios": "^1.1.3", @@ -17,7 +18,7 @@ "dotenv": "^16.0.3", "events": "^3.3.0", "lodash": "^4.17.21", - "nanoid": "^4.0.0", + "nanoid": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.6.0", @@ -26,7 +27,8 @@ "react-virtualized-auto-sizer": "^1.0.7", "react-window": "^1.8.8", "vite-plugin-svgr": "^2.4.0", - "vite-tsconfig-paths": "^4.0.5" + "vite-tsconfig-paths": "^4.0.5", + "zustand": "^4.4.6" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", diff --git a/ethernet-view/src/App.tsx b/ethernet-view/src/App.tsx index b113624b3..3a8c5293c 100644 --- a/ethernet-view/src/App.tsx +++ b/ethernet-view/src/App.tsx @@ -5,22 +5,24 @@ import { useConfig, useFetchBack, Loader, + useMeasurementsStore, + usePodDataStore, + useConnectionsStore } from "common"; import { HomePage } from "pages/HomePage/HomePage"; -import { useDispatch } from "react-redux"; -import { initMeasurements } from "slices/measurementsSlice"; -import { initPodData } from "slices/podDataSlice"; import { SplashScreen } from "components/SplashScreen/SplashScreen"; -import { setWebSocketConnection } from "slices/connectionsSlice"; function App() { - const dispatch = useDispatch(); const config = useConfig(); const podDataDescriptionPromise = useFetchBack( import.meta.env.PROD, config.paths.podDataDescription ); + const initMeasurements = useMeasurementsStore((state) => state.initMeasurements); + const initPodData = usePodDataStore((state) => state.initPodData); + const setBackendConnection = useConnectionsStore((state) => state.setBackendConnection); + const SERVER_URL = import.meta.env.PROD ? `${config.prodServer.ip}:${config.prodServer.port}/${config.paths.websocket}` : `${config.devServer.ip}:${config.devServer.port}/${config.paths.websocket}`; @@ -32,12 +34,12 @@ function App() { createWsHandler( SERVER_URL, true, - () => dispatch(setWebSocketConnection(true)), - () => dispatch(setWebSocketConnection(false)) + () => setBackendConnection(true), + () => setBackendConnection(false), ), podDataDescriptionPromise.then((adapter) => { - dispatch(initPodData(adapter)); - dispatch(initMeasurements(adapter)); + initPodData(adapter); + initMeasurements(adapter); }), ]} LoadingView={} diff --git a/ethernet-view/src/components/ChartMenu/ChartList/ChartWithLegend/ChartWithLegend.tsx b/ethernet-view/src/components/ChartMenu/ChartList/ChartWithLegend/ChartWithLegend.tsx index 3a0c2d7ae..311a00d16 100644 --- a/ethernet-view/src/components/ChartMenu/ChartList/ChartWithLegend/ChartWithLegend.tsx +++ b/ethernet-view/src/components/ChartMenu/ChartList/ChartWithLegend/ChartWithLegend.tsx @@ -3,8 +3,7 @@ import Legend from "./Legend/Legend"; import { ChartElement } from "components/ChartMenu/ChartElement"; import { DragEvent } from "react"; import { MdClose } from "react-icons/md"; -import { LinesChart, NumericMeasurement, getMeasurement } from "common"; -import { store } from "store"; +import { LinesChart, NumericMeasurement, getMeasurement, useMeasurementsStore } from "common"; type Props = { chartElement: ChartElement; @@ -25,6 +24,8 @@ export const ChartWithLegend = ({ handleDropOnChart(chartElement.id, id); } + const measurements = useMeasurementsStore((state) => state.measurements); + return (
{ const meas = getMeasurement( - store.getState().measurements, + measurements, id ) as NumericMeasurement; diff --git a/ethernet-view/src/components/ChartMenu/ChartMenu.tsx b/ethernet-view/src/components/ChartMenu/ChartMenu.tsx index 2c1cc7908..96f191e27 100644 --- a/ethernet-view/src/components/ChartMenu/ChartMenu.tsx +++ b/ethernet-view/src/components/ChartMenu/ChartMenu.tsx @@ -1,7 +1,7 @@ import styles from "components/ChartMenu/ChartMenu.module.scss"; import Sidebar from "components/ChartMenu/Sidebar/Sidebar"; import { ChartList } from "components/ChartMenu/ChartList/ChartList"; -import { store } from "store"; +import { useMeasurementsStore } from "common"; import { Section } from "./Sidebar/Section/Section"; import { NumericMeasurement, getMeasurement } from "common"; @@ -18,6 +18,9 @@ type Props = { }; export const ChartMenu = ({ sidebarSections }: Props) => { + + const measurements = useMeasurementsStore((state) => state.measurements); + if (sidebarSections.length == 0) { return (
@@ -33,7 +36,7 @@ export const ChartMenu = ({ sidebarSections }: Props) => { { const meas = getMeasurement( - store.getState().measurements, + measurements, id ) as NumericMeasurement; @@ -45,7 +48,7 @@ export const ChartMenu = ({ sidebarSections }: Props) => { getUpdate: () => { //TODO: change to getNumericMeasurement and return undefined if its not numeric (or doesnt exist) const meas = getMeasurement( - store.getState().measurements, + measurements, id ) as NumericMeasurement; diff --git a/ethernet-view/src/components/ChartMenu/useMeasurements.ts b/ethernet-view/src/components/ChartMenu/useMeasurements.ts index fa716a9ac..0b80670b5 100644 --- a/ethernet-view/src/components/ChartMenu/useMeasurements.ts +++ b/ethernet-view/src/components/ChartMenu/useMeasurements.ts @@ -1,11 +1,10 @@ -import { updateMeasurements } from "slices/measurementsSlice"; -import { useDispatch } from "react-redux"; +import { useMeasurementsStore } from "common"; import { useSubscribe } from "common"; export function useMeasurements() { - const dispatch = useDispatch(); + const updateMeasurements = useMeasurementsStore((state) => state.updateMeasurements); useSubscribe("podData/update", (msg) => { - dispatch(updateMeasurements(msg)); + updateMeasurements(msg); }); } diff --git a/ethernet-view/src/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.tsx b/ethernet-view/src/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.tsx index 22e354f12..8170a2efa 100644 --- a/ethernet-view/src/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.tsx +++ b/ethernet-view/src/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.tsx @@ -11,7 +11,7 @@ export const InfoMessageView = ({ message, className }: Props) => { return (
{message.board}
-
{message.msg}
+
{message.payload}
); }; diff --git a/ethernet-view/src/components/MessagesContainer/Messages/MessageView/ProtectionMessageView/ProtectionMessageView.tsx b/ethernet-view/src/components/MessagesContainer/Messages/MessageView/ProtectionMessageView/ProtectionMessageView.tsx index d65a1c26e..0e615b9a6 100644 --- a/ethernet-view/src/components/MessagesContainer/Messages/MessageView/ProtectionMessageView/ProtectionMessageView.tsx +++ b/ethernet-view/src/components/MessagesContainer/Messages/MessageView/ProtectionMessageView/ProtectionMessageView.tsx @@ -13,7 +13,7 @@ export const ProtectionMessageView = ({ message, className }: Props) => {
- {message.protection.kind} + {message.payload.kind}
{ name={message.name} />
- +
); }; diff --git a/ethernet-view/src/components/MessagesContainer/Messages/Messages.tsx b/ethernet-view/src/components/MessagesContainer/Messages/Messages.tsx index a3c06f45b..6eb01af64 100644 --- a/ethernet-view/src/components/MessagesContainer/Messages/Messages.tsx +++ b/ethernet-view/src/components/MessagesContainer/Messages/Messages.tsx @@ -3,16 +3,15 @@ import styles from "./Messages.module.scss"; import { MessageView } from "./MessageView/MessageView"; import { useAutoScroll } from "./useAutoScroll"; import { Button } from "components/FormComponents/Button/Button"; -import { useDispatch } from "react-redux"; -import { clearMessages } from "slices/messagesSlice"; +import { useMessagesStore } from "common"; type Props = { - messages: Array; + messages: Message[]; }; export const Messages = ({ messages }: Props) => { const { ref, handleScroll } = useAutoScroll(messages); - const dispatch = useDispatch(); + const clearMessages = useMessagesStore((state) => state.clearMessages); return (
@@ -21,14 +20,13 @@ export const Messages = ({ messages }: Props) => { onScroll={handleScroll} className={styles.messages} > - {messages.map((message) => { - return ( + + { messages.map((message) => ( - ); - })} + )) }
diff --git a/ethernet-view/src/components/MessagesContainer/useMessages.ts b/ethernet-view/src/components/MessagesContainer/useMessages.ts index e36f55b3a..0f350a15b 100644 --- a/ethernet-view/src/components/MessagesContainer/useMessages.ts +++ b/ethernet-view/src/components/MessagesContainer/useMessages.ts @@ -1,14 +1,10 @@ import { MessageAdapter, useSubscribe } from "common"; -import { addMessage } from "slices/messagesSlice"; -import { useDispatch, useSelector } from "react-redux"; -import { RootState } from "store"; +import { useMessagesStore } from "common"; export function useMessages() { - const dispatch = useDispatch(); + const { messages, addMessage } = useMessagesStore(state => ({messages: state.messages, addMessage: state.addMessage})); - useSubscribe("message/update", (msg: MessageAdapter) => { - dispatch(addMessage(msg)); - }); + useSubscribe("message/update", (msg: MessageAdapter) => addMessage(msg)); - return useSelector((state: RootState) => state.messages); + return messages; } diff --git a/ethernet-view/src/components/OrdersContainer/OrdersContainer.tsx b/ethernet-view/src/components/OrdersContainer/OrdersContainer.tsx index ac3b6d7a6..78ab56754 100644 --- a/ethernet-view/src/components/OrdersContainer/OrdersContainer.tsx +++ b/ethernet-view/src/components/OrdersContainer/OrdersContainer.tsx @@ -1,22 +1,20 @@ import styles from "./OrdersContainer.module.scss"; import { Orders } from "./Orders/Orders"; -import { orderSlice, useConfig, useFetchBack } from "common"; +import { useConfig, useFetchBack } from "common"; import { useEffect } from "react"; -import { useDispatch } from "react-redux"; import { useOrders } from "common"; +import { useOrdersStore } from "common"; export const OrdersContainer = () => { - const dispatch = useDispatch(); const config = useConfig(); + const setOrders = useOrdersStore((state) => state.setOrders); const orderDescriptionPromise = useFetchBack( import.meta.env.PROD, config.paths.orderDescription ); useEffect(() => { - orderDescriptionPromise.then((desc) => { - dispatch(orderSlice.actions.setOrders(desc)); - }); + orderDescriptionPromise.then((desc) => setOrders(desc)); }, []); const orders = useOrders(); diff --git a/ethernet-view/src/components/ReceiveTable/BoardView/PacketView/PacketView.tsx b/ethernet-view/src/components/ReceiveTable/BoardView/PacketView/PacketView.tsx index c2e93c208..e5475357b 100644 --- a/ethernet-view/src/components/ReceiveTable/BoardView/PacketView/PacketView.tsx +++ b/ethernet-view/src/components/ReceiveTable/BoardView/PacketView/PacketView.tsx @@ -1,34 +1,33 @@ import { Packet } from "common"; import styles from "./PacketView.module.scss"; import { MeasurementView } from "./MeasurementView/MeasurementView"; -import { useSelector } from "react-redux"; -import { RootState } from "store"; import { memo } from "react"; import { useUpdater } from "./useUpdater"; +import { useColumnsStore } from "store/columnsStore"; type Props = { packet: Packet; }; export const PacketView = memo(({ packet }: Props) => { - const columns = useSelector((state: RootState) => state.columns); + const columnSizes = useColumnsStore((state) => state.columnSizes); const { countRef, cycleTimeRef } = useUpdater(packet); return (
-
{packet.id}
-
{packet.name}
+
{packet.id}
+
{packet.name}
{Object.keys(packet.measurements).length > 0 && ( diff --git a/ethernet-view/src/components/ReceiveTable/Header/Header.tsx b/ethernet-view/src/components/ReceiveTable/Header/Header.tsx index 8efd6f9bc..efb2045bd 100644 --- a/ethernet-view/src/components/ReceiveTable/Header/Header.tsx +++ b/ethernet-view/src/components/ReceiveTable/Header/Header.tsx @@ -1,11 +1,9 @@ -import { useDispatch, useSelector } from "react-redux"; import styles from "./Header.module.scss"; -import { RootState } from "store"; import { useSplit } from "hooks/useSplit/useSplit"; import { Orientation } from "hooks/useSplit/Orientation"; import { Separator } from "./Separator/Separator"; import { useEffect } from "react"; -import { setColumnSizes } from "slices/columnsSlice"; +import { useColumnsStore } from "store/columnsStore"; type Props = { items: string[]; @@ -14,21 +12,16 @@ type Props = { const MINIMUM_ITEM_SIZE = 0.05; export const Header = ({ items }: Props) => { - const columns = useSelector((state: RootState) => state.columns); + const columnSizes = useColumnsStore((state) => state.columnSizes); + const setColumnSizes = useColumnsStore((state) => state.setColumnSizes); const [splitElements, handleMouseDown] = useSplit( new Array(items.length).fill(MINIMUM_ITEM_SIZE), Orientation.HORIZONTAL ); - const dispatch = useDispatch(); - useEffect(() => { - dispatch( - setColumnSizes( - splitElements.map((element) => `${element.length * 100}%`) - ) - ); + setColumnSizes(splitElements.map((element) => `${element.length * 100}%`)); }, [splitElements]); return ( @@ -39,7 +32,7 @@ export const Header = ({ items }: Props) => { handleMouseDown(index, ev)} /> ); @@ -49,7 +42,7 @@ export const Header = ({ items }: Props) => { ); })} diff --git a/ethernet-view/src/components/ReceiveTable/TableUpdater.tsx b/ethernet-view/src/components/ReceiveTable/TableUpdater.tsx index e2a7ba869..26cd73e84 100644 --- a/ethernet-view/src/components/ReceiveTable/TableUpdater.tsx +++ b/ethernet-view/src/components/ReceiveTable/TableUpdater.tsx @@ -3,9 +3,10 @@ import { getPacket, isNumericMeasurement, useGlobalTicker, + useMeasurementsStore, + usePodDataStore, } from "common"; import { createContext, useRef } from "react"; -import { store } from "store"; export type PacketElement = { count: Text; @@ -36,10 +37,11 @@ export const TableUpdater = ({ children }: Props) => { const packetElements = useRef>({}); const measurementElements = useRef([]); - useGlobalTicker(() => { - const state = store.getState(); - const podData = state.podData; + const podData = usePodDataStore(state => state.podData) + const measurements = useMeasurementsStore(state => state.measurements) + useGlobalTicker(() => { + for (const id in packetElements.current) { const packet = getPacket(podData, Number.parseInt(id)); const element = packetElements.current[id]; @@ -52,7 +54,7 @@ export const TableUpdater = ({ children }: Props) => { } for (const item of measurementElements.current) { - const measurement = getMeasurement(state.measurements, item.id); + const measurement = getMeasurement(measurements, item.id); if (!measurement) { console.warn(`measurement ${item.id} not found`); return; @@ -98,4 +100,4 @@ export const TableUpdater = ({ children }: Props) => { {children} ); -}; +}; \ No newline at end of file diff --git a/ethernet-view/src/main.tsx b/ethernet-view/src/main.tsx index 8aaa0ecff..a63191a97 100644 --- a/ethernet-view/src/main.tsx +++ b/ethernet-view/src/main.tsx @@ -4,21 +4,17 @@ import "common/dist/style.css"; import App from "./App"; import "styles/fonts.scss"; import "./index.scss"; -import { store } from "./store"; -import { Provider } from "react-redux"; import { ConfigProvider, GlobalTicker } from "common"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - - - + + + + + -); +); \ No newline at end of file diff --git a/ethernet-view/src/pages/HomePage/ReceiveColumn/ReceiveColumn.tsx b/ethernet-view/src/pages/HomePage/ReceiveColumn/ReceiveColumn.tsx index f8fb639b5..f3147c296 100644 --- a/ethernet-view/src/pages/HomePage/ReceiveColumn/ReceiveColumn.tsx +++ b/ethernet-view/src/pages/HomePage/ReceiveColumn/ReceiveColumn.tsx @@ -2,35 +2,31 @@ import { TabLayout } from "layouts/TabLayout/TabLayout"; import { ChartMenu } from "components/ChartMenu/ChartMenu"; import { ReactComponent as IncomingMessage } from "assets/svg/incoming-message.svg"; import { ReactComponent as Chart } from "assets/svg/chart.svg"; -import { store } from "store"; import { ReceiveTable } from "components/ReceiveTable/ReceiveTable"; import { useMemo } from "react"; -import { useSubscribe } from "common"; -import { useDispatch } from "react-redux"; -import { updateMeasurements } from "slices/measurementsSlice"; -import { updatePodData } from "slices/podDataSlice"; +import { useMeasurementsStore, usePodDataStore, useSubscribe } from "common"; import { createSidebarSections } from "components/ChartMenu/sidebar"; export const ReceiveColumn = () => { - const dispatch = useDispatch(); + const {podData, updatePodData} = usePodDataStore(state => ({podData: state.podData, updatePodData: state.updatePodData})) + const updateMeasurements = useMeasurementsStore(state => state.updateMeasurements) useSubscribe("podData/update", (update) => { - dispatch(updatePodData(update)); - dispatch(updateMeasurements(update)); + updatePodData(update); + updateMeasurements(update); }); const sections = useMemo(() => { - return createSidebarSections(store.getState().podData); + return createSidebarSections(podData); }, []); - const receiveColumnTabItems = useMemo( - () => [ + const receiveColumnTabItems = [ { id: "receiveTable", name: "Packets", icon: , component: ( - + ), }, { @@ -39,9 +35,7 @@ export const ReceiveColumn = () => { icon: , component: , }, - ], - [store] - ); + ] return ; -}; +}; \ No newline at end of file diff --git a/ethernet-view/src/slices/columnsSlice.ts b/ethernet-view/src/slices/columnsSlice.ts deleted file mode 100644 index e56079def..000000000 --- a/ethernet-view/src/slices/columnsSlice.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -const columnSlice = createSlice({ - name: "columns", - initialState: ["30%", "10%", "20%", "20%", "20%"] as Array, - reducers: { - setColumnSizes: (_, action: PayloadAction>) => { - return action.payload; - }, - }, -}); - -export const { setColumnSizes } = columnSlice.actions; - -export default columnSlice.reducer; diff --git a/ethernet-view/src/slices/connectionsSlice.ts b/ethernet-view/src/slices/connectionsSlice.ts deleted file mode 100644 index df3be1d50..000000000 --- a/ethernet-view/src/slices/connectionsSlice.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { connectionsSlice } from "common"; - -export const connectionsReducer = connectionsSlice.reducer; - -export const { setWebSocketConnection, updateBoardConnections } = - connectionsSlice.actions; diff --git a/ethernet-view/src/slices/measurementsSlice.ts b/ethernet-view/src/slices/measurementsSlice.ts deleted file mode 100644 index a07188daa..000000000 --- a/ethernet-view/src/slices/measurementsSlice.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { measurementsSlice } from "common"; - -export const { initMeasurements, updateMeasurements } = - measurementsSlice.actions; - -export const measurementsReducer = measurementsSlice.reducer; diff --git a/ethernet-view/src/slices/messagesSlice.ts b/ethernet-view/src/slices/messagesSlice.ts deleted file mode 100644 index 2ce3e4bf4..000000000 --- a/ethernet-view/src/slices/messagesSlice.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { messageSlice } from "common"; - -export const { addMessage, clearMessages } = messageSlice.actions; - -export default messageSlice.reducer; diff --git a/ethernet-view/src/slices/ordersSlice.ts b/ethernet-view/src/slices/ordersSlice.ts deleted file mode 100644 index 6b96126f1..000000000 --- a/ethernet-view/src/slices/ordersSlice.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { orderSlice } from "common"; - -export const { setOrders, updateStateOrders } = orderSlice.actions; - -export default orderSlice.reducer; diff --git a/ethernet-view/src/slices/podDataSlice.ts b/ethernet-view/src/slices/podDataSlice.ts deleted file mode 100644 index e5d666d33..000000000 --- a/ethernet-view/src/slices/podDataSlice.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { podDataSlice } from "common"; - -export const { initPodData, updatePodData } = podDataSlice.actions; - -export default podDataSlice.reducer; diff --git a/ethernet-view/src/store.ts b/ethernet-view/src/store.ts deleted file mode 100644 index 843a568b0..000000000 --- a/ethernet-view/src/store.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { configureStore } from "@reduxjs/toolkit"; -import podDataReducer from "slices/podDataSlice"; -import orderReducer from "slices/ordersSlice"; -import { connectionsReducer } from "slices/connectionsSlice"; -import messagesReducer from "slices/messagesSlice"; -import { measurementsReducer } from "slices/measurementsSlice"; -import columnsSlice from "slices/columnsSlice"; - -export const store = configureStore({ - reducer: { - podData: podDataReducer, - measurements: measurementsReducer, - orders: orderReducer, - connections: connectionsReducer, - messages: messagesReducer, - columns: columnsSlice, - }, - middleware: [], -}); - -export type RootState = ReturnType; diff --git a/ethernet-view/src/store/columnsStore.ts b/ethernet-view/src/store/columnsStore.ts new file mode 100644 index 000000000..0aa1fc3ed --- /dev/null +++ b/ethernet-view/src/store/columnsStore.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +export interface ColumnsStore { + columnSizes: string[]; + setColumnSizes: (setColumnSizes: string[]) => void; +}; + +// Zustand store for keeping track of column sizes. +// It is useful to the layout of the application. +export const useColumnsStore = create((set) => ({ + columnSizes: ["30%", "10%", "20%", "20%", "20%"] as string[], + setColumnSizes: (columnSizes: string[]) => set({ columnSizes }), +})); \ No newline at end of file diff --git a/go.work.sum b/go.work.sum index 3a3188e77..d57edcb32 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,2 +1,3 @@ cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/load-secret.sh b/load-secret.sh deleted file mode 100755 index 83a55592f..000000000 --- a/load-secret.sh +++ /dev/null @@ -1,9 +0,0 @@ -if [ $# -ne 1 ]; then - echo "expected at least one argument"; - exit 1; -fi - -echo "$1" > "./backend/internal/excel/secret.json" -echo "$1" > "./backend/internal/excel_adapter/internals/secret.json" -echo "$1" > "./backend/pkg/excel/secret.json" -echo "$1" > "./backend/pkg/excel_adapter/internals/secret.json" \ No newline at end of file diff --git a/packet-sender/.gitignore b/packet-sender/.gitignore index a24e2a051..50cbea71a 100644 --- a/packet-sender/.gitignore +++ b/packet-sender/.gitignore @@ -5,6 +5,7 @@ *.so *.dylib packet-sender +packet_sender # Test binary, built with `go test -c` *.test @@ -20,4 +21,4 @@ secret.json excel.xlsx .vscode -*.xlsx \ No newline at end of file +*.xlsx diff --git a/packet-sender/PacketGenerator.go b/packet-sender/PacketGenerator.go index ce9b554fb..5b21e4b74 100644 --- a/packet-sender/PacketGenerator.go +++ b/packet-sender/PacketGenerator.go @@ -21,7 +21,7 @@ type PacketGenerator struct { func New() PacketGenerator { excelAdapter := excelAdapter.New(excelAdapter.ExcelAdapterConfig{ Download: internals.DownloadConfig{ - Id: "1b_nOrWqjMLOSEFIV9dMUObnJ15J7ypmF-KVJ4qztAtw", + Id: "1NyNaAOw_6iWtnCpEg73AtSSFx1fMdhPRmmdOhjgjCZI", Path: ".", Name: "ade.xlsx", }, diff --git a/packet-sender/main.go b/packet-sender/main.go index e30110eb8..0766e6d67 100644 --- a/packet-sender/main.go +++ b/packet-sender/main.go @@ -35,11 +35,13 @@ func main() { count := make(chan struct{}, 10000) ticker := time.NewTicker(time.Millisecond * 1) start := time.Now() + prev := time.Now() go func() { for range ticker.C { packet := packetGenerator.CreateRandomPacket() // packet := []byte{10, 0, 20, 0, 20, 0, 20, 0, 20, 0} - fmt.Println(packet) + fmt.Println(time.Since(prev)) + prev = time.Now() if packet == nil { continue