diff --git a/README.md b/README.md index 91ec66e9c..13a336e89 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,6 @@ The main project file is inside `backend/cmd`. Ensure you have the proper `confi See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways to contribute to the Control Station. -### Authors - -- [Juan Martinez Alonso](https://github.com/jmaralo) -- [Marc Sanchis Llinares](https://github.com/msanlli) -- [Sergio Moreno Suay](https://github.com/smorsua) -- [Felipe Zaballa Martinez](https://github.com/lipezaballa) -- [Andrés de la Torre Mora](https://github.com/andresdlt03) -- [Alejandro Losa](https://github.com/Losina24) - ### About HyperloopUPV is a student team based at Universitat Politècnica de València (Spain), which works every year to develop the transport of the future, the hyperloop. Check out [our website](https://hyperloopupv.com/#/) diff --git a/backend/cmd/VERSION.md b/backend/cmd/VERSION.md new file mode 100644 index 000000000..eb430cbb1 --- /dev/null +++ b/backend/cmd/VERSION.md @@ -0,0 +1 @@ +2.2.8 \ No newline at end of file diff --git a/backend/cmd/adj b/backend/cmd/adj new file mode 160000 index 000000000..fd87ad3fa --- /dev/null +++ b/backend/cmd/adj @@ -0,0 +1 @@ +Subproject commit fd87ad3fac5aa62e7b274b5ae00412caa544c0a0 diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 4c1bac83d..01ba862a8 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -7,18 +7,24 @@ import ( "flag" "fmt" + "encoding/json" "log" "net" "net/http" _ "net/http/pprof" "os" + "os/exec" "os/signal" "path" + "path/filepath" "runtime" "runtime/pprof" + "strings" "time" + "github.com/hashicorp/go-version" + adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj" "github.com/HyperloopUPV-H8/h9-backend/internal/common" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" @@ -76,9 +82,25 @@ var enableSNTP = flag.Bool("sntp", false, "enables a simple SNTP server on port var networkDevice = flag.Int("dev", -1, "index of the network device to use, overrides device prompt") var blockprofile = flag.Int("blockprofile", 0, "number of block profiles to include") var playbackFile = flag.String("playback", "", "") +var currentVersion string func main() { + + versionFile := "VERSION.md" + versionData, err := os.ReadFile(versionFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading version file (%s): %v\n", versionFile, err) + os.Exit(1) + } + currentVersion = strings.TrimSpace(string(versionData)) + + versionFlag := flag.Bool("version", false, "Show the backend version") flag.Parse() + if *versionFlag { + fmt.Println("Hyperloop UPV Backend Version:", currentVersion) + os.Exit(0) + } + traceFile := initTrace(*traceLevel, *traceFile) defer traceFile.Close() @@ -99,6 +121,89 @@ func main() { runtime.SetBlockProfileRate(*blockprofile) config := getConfig("./config.toml") + latestVersionStr, err := getLatestVersionFromGitHub() + if err != nil { + fmt.Println("Warning:", err) + fmt.Println("Skipping version check. Proceeding with the current version:", currentVersion) + } else { + current, err := version.NewVersion(currentVersion) + if err != nil { + fmt.Println("Invalid current version:", err) + return + } + + latest, err := version.NewVersion(latestVersionStr) + if err != nil { + fmt.Println("Invalid latest version:", err) + return + } + + if latest.GreaterThan(current) { + fmt.Printf("There is a new version available: %s (current version: %s)\n", latest, current) + fmt.Print("Do you want to update? (y/n): ") + + var response string + fmt.Scanln(&response) + + if strings.ToLower(response) == "y" { + fmt.Println("Launching updater to update the backend...") + + // Get the directory of the current executable + execPath, err := os.Executable() + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting executable path: %v\n", err) + os.Exit(1) + } + execDir := filepath.Dir(execPath) + + backendPath := filepath.Join(execDir, "..", "..", "backend") + + if _, err := os.Stat(backendPath); err == nil { + + fmt.Println("Backend folder detected. Building and launching updater...") + + updaterPath := filepath.Join(execDir, "..", "..", "updater") + + cmd := exec.Command("go", "build", "-o", filepath.Join(updaterPath, "updater.exe"), updaterPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error building updater: %v\n", err) + os.Exit(1) + } + + updaterExe := filepath.Join(updaterPath, "updater.exe") + cmd = exec.Command(updaterExe) + cmd.Dir = updaterPath + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error launching updater: %v\n", err) + os.Exit(1) + } + } else { + + fmt.Println("Backend folder not detected. Launching existing updater...") + + updaterExe := filepath.Join(execDir, "updater.exe") + cmd := exec.Command(updaterExe) + cmd.Dir = filepath.Dir(updaterExe) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error launching updater: %v\n", err) + os.Exit(1) + } + } + + os.Exit(0) + } else { + fmt.Println("Skipping update. Proceeding with the current version.") + } + } else { + fmt.Printf("You are using the latest version: %s\n", current) + } + } // <--- ADJ ---> @@ -590,3 +695,23 @@ func getUDPFilter(addrs []net.IP, backendAddr net.IP, port uint16) string { return fmt.Sprintf("(%s) and (%s) and (%s or (dst host %s))", udpPort, srcUdpAddrsStr, dstUdpAddrsStr, backendAddr) } + +type GitHubRelease struct { + TagName string `json:"tag_name"` +} + +func getLatestVersionFromGitHub() (string, error) { + resp, err := http.Get("https://api.github.com/repos/HyperloopUPV-H8/software/releases/latest") + if err != nil { + return "", fmt.Errorf("unable to connect to the internet: %w", err) + } + defer resp.Body.Close() + + var release GitHubRelease + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return "", fmt.Errorf("error decoding GitHub response: %w", err) + } + + version := strings.TrimPrefix(release.TagName, "v") + return version, nil +} diff --git a/backend/go.mod b/backend/go.mod index 90df5f1e6..6eb92f224 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,9 +7,11 @@ require ( github.com/google/gopacket v1.1.19 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 + github.com/hashicorp/go-version v1.7.0 github.com/jmaralo/sntp v0.0.0-20240116111937-45a0a3419272 github.com/pelletier/go-toml/v2 v2.0.7 github.com/pin/tftp/v3 v3.0.0 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/rs/zerolog v1.29.0 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 ) @@ -29,7 +31,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect diff --git a/backend/go.sum b/backend/go.sum index 038f251fe..16dca18a7 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -46,6 +46,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jmaralo/sntp v0.0.0-20240116111937-45a0a3419272 h1:dtQzdBn2P781UxyDPZd1tv4QE29ffpnmJ15qBkXHypY= diff --git a/backend/internal/adj/git.go b/backend/internal/adj/git.go index 5a6ee2687..8eb3a11b8 100644 --- a/backend/internal/adj/git.go +++ b/backend/internal/adj/git.go @@ -2,6 +2,7 @@ package adj import ( "os" + "path/filepath" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" @@ -21,6 +22,27 @@ func updateRepo(AdjBranch string) error { SingleBranch: true, Depth: 1, } + + // Try to clone the ADJ to a temp directory to check for accessibility to the repo (also checks internet connection) + tempPath := filepath.Join(os.TempDir(), "temp_adj") + + // Remove previous failed cloning attempts + if err = os.RemoveAll(tempPath); err != nil { + return err + } + + // Try to import the ADJ to the temp directory + _, err = git.PlainClone(tempPath, false, cloneOptions) + if err != nil { + // If the clone fails, work with the local ADJ + return nil + } + + // If the clone is succesful, delete the temp files + if err = os.RemoveAll(tempPath); err != nil { + return err + } + if _, err = os.Stat(RepoPath); os.IsNotExist(err) { _, err = git.PlainClone(RepoPath, false, cloneOptions) if err != nil { diff --git a/backend/pkg/adj/git.go b/backend/pkg/adj/git.go index 5a6ee2687..8eb3a11b8 100644 --- a/backend/pkg/adj/git.go +++ b/backend/pkg/adj/git.go @@ -2,6 +2,7 @@ package adj import ( "os" + "path/filepath" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" @@ -21,6 +22,27 @@ func updateRepo(AdjBranch string) error { SingleBranch: true, Depth: 1, } + + // Try to clone the ADJ to a temp directory to check for accessibility to the repo (also checks internet connection) + tempPath := filepath.Join(os.TempDir(), "temp_adj") + + // Remove previous failed cloning attempts + if err = os.RemoveAll(tempPath); err != nil { + return err + } + + // Try to import the ADJ to the temp directory + _, err = git.PlainClone(tempPath, false, cloneOptions) + if err != nil { + // If the clone fails, work with the local ADJ + return nil + } + + // If the clone is succesful, delete the temp files + if err = os.RemoveAll(tempPath); err != nil { + return err + } + if _, err = os.Stat(RepoPath); os.IsNotExist(err) { _, err = git.PlainClone(RepoPath, false, cloneOptions) if err != nil { diff --git a/backend/pkg/logger/data/logger.go b/backend/pkg/logger/data/logger.go index 566a9dcbc..4198b18c7 100644 --- a/backend/pkg/logger/data/logger.go +++ b/backend/pkg/logger/data/logger.go @@ -137,8 +137,9 @@ func (sublogger *Logger) getFile(valueName data.ValueName) (*file.CSV, error) { func (sublogger *Logger) createFile(valueName data.ValueName) (*os.File, error) { filename := path.Join( - "logger", "data", + "logger", loggerHandler.Timestamp.Format(loggerHandler.TimestampFormat), + "data", fmt.Sprintf("%s.csv", valueName), ) diff --git a/backend/pkg/logger/order/logger.go b/backend/pkg/logger/order/logger.go index 1a58cfeea..07828ffcd 100644 --- a/backend/pkg/logger/order/logger.go +++ b/backend/pkg/logger/order/logger.go @@ -59,8 +59,9 @@ func (sublogger *Logger) Start() error { func (sublogger *Logger) createFile() (*os.File, error) { filename := path.Join( - "logger", "order", + "logger", logger.Timestamp.Format(logger.TimestampFormat), + "order", "order.csv", ) diff --git a/backend/pkg/logger/protection/logger.go b/backend/pkg/logger/protection/logger.go index ae1d1c2e7..bd93f62d8 100644 --- a/backend/pkg/logger/protection/logger.go +++ b/backend/pkg/logger/protection/logger.go @@ -118,8 +118,9 @@ func (sublogger *Logger) createFile(boardId abstraction.BoardId) (*os.File, erro } filename := path.Join( - "logger", "protections", + "logger", logger.Timestamp.Format(logger.TimestampFormat), + "protections", fmt.Sprintf("%s.csv", boardName), ) diff --git a/backend/pkg/logger/state/logger.go b/backend/pkg/logger/state/logger.go index cc04030fe..a4459c0b8 100644 --- a/backend/pkg/logger/state/logger.go +++ b/backend/pkg/logger/state/logger.go @@ -94,8 +94,9 @@ func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error { func (sublogger *Logger) createFile(timestamp time.Time) (*file.CSV, error) { filename := path.Join( - "logger", "state", + "logger", logger.Timestamp.Format(logger.TimestampFormat), + "state", fmt.Sprintf("%s.csv", timestamp.Format(logger.TimestampFormat)), ) diff --git a/backend/pkg/vehicle/notification.go b/backend/pkg/vehicle/notification.go index 578463c8b..7fcbd016b 100644 --- a/backend/pkg/vehicle/notification.go +++ b/backend/pkg/vehicle/notification.go @@ -43,6 +43,8 @@ func (vehicle *Vehicle) Notification(notification abstraction.TransportNotificat } func (vehicle *Vehicle) handlePacketNotification(notification transport.PacketNotification) error { + var from string + var to string switch p := notification.Packet.(type) { case *data.Packet: @@ -53,10 +55,25 @@ func (vehicle *Vehicle) handlePacketNotification(notification transport.PacketNo return errors.Join(fmt.Errorf("update data to frontend (data with id %d from %s to %s)", p.Id(), notification.From, notification.To), err) } + from_ip := strings.Split(notification.From, ":")[0] + to_ip := strings.Split(notification.To, ":")[0] + + if from_ip == "192.168.0.9" { + from = "backend" + } else { + from = vehicle.idToBoardName[uint16(vehicle.ipToBoardId[from_ip])] + } + + if to_ip == "192.168.0.9" { + to = "backend" + } else { + to = vehicle.idToBoardName[uint16(vehicle.ipToBoardId[to_ip])] + } + err = vehicle.logger.PushRecord(&data_logger.Record{ Packet: p, - From: notification.From, - To: notification.To, + From: from, + To: to, Timestamp: notification.Timestamp, }) diff --git a/control-station/src/App.tsx b/control-station/src/App.tsx index b5c6182e3..1091eb699 100644 --- a/control-station/src/App.tsx +++ b/control-station/src/App.tsx @@ -21,6 +21,7 @@ export const App = () => { items={[ { path: '/vehicle', icon: }, { path: '/cameras', icon: }, + { path: '/guiBooster', icon: } ]} /> diff --git a/control-station/src/components/GuiModules/Module.module.scss b/control-station/src/components/GuiModules/Module.module.scss index 86cd7a6b2..77ec4d4db 100644 --- a/control-station/src/components/GuiModules/Module.module.scss +++ b/control-station/src/components/GuiModules/Module.module.scss @@ -1,100 +1,106 @@ .boxContainer1 { - width: 90%; + width: 40%; display: flex; flex-direction: column; justify-content: center; align-items: center; - border-radius: 20px; - margin-bottom: 20px; -} - -.boxContainer2 { + border-radius: 20px; + } + + .boxContainer2 { border: 2.5px solid #FFE7CF; - width: 80%; - height: 150px; + width: 65%; + height: 130px; border-radius: 20px; display: flex; flex-direction: column; - align-items: center; + align-items: stretch; position: relative; background-color: white; -} - -.boxContainer3 { - display: flex; - flex-direction: row; - width: 100%; - justify-content:center; -} - -.voltajeContainer { - border-right: #FFE7CF solid 2.5px; - width: 50%; - padding: 10px; - display: flex; - flex-direction: column; - justify-content: center; -} - -.intensityContainer { - width: 50%; - padding: 10px; - display: flex; - flex-direction: column; - justify-content: left; -} + box-sizing: border-box; + padding-top: 20px; -.h2Module { + } + + .h2Module { color: #EF7E30; font-family: 'IBM Plex Mono', monospace; width: 100%; + height: 30px; background-color: #FFE7CF; text-align: center; border-top-left-radius: 20px; border-top-right-radius: 20px; font-weight: bold; position: absolute; - transform: translateY(-100%); -} + transform: translateY(-165%); + padding-top: 1px; + padding-bottom: 10px; + box-sizing: border-box; + } -.titleDecorationModule { - text-align: center; + .voltajeContainer { width: 100%; + padding: 0 10px; + display: flex; + flex-direction: column; + justify-content: center; + height: 190px; + box-sizing: border-box; + } + + .titleDecorationModule { + text-align: center; border-top-left-radius: 20px; border-top-right-radius:20px ; - height: 20px; -} - -.h3 { + } + + .h3 { color: #EF7E30; font-family: 'IBM Plex Mono', monospace; - margin: 0; -} - -.p { + margin: 2px 0; /* Reducido el margen vertical */ + } + + .dataStyle { color: #EF7E30; font-family: 'IBM Plex Mono', monospace; + font-size: 0.9rem; + display: flex; + flex-direction: row; + align-items: center; + } + .p{ + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 1rem; + margin-top: 1px; + margin-bottom: 1px; + padding: 5px 10px; background-color: #FFE7CF; - border-radius: 10px; - margin-top: 2.5px; - margin-bottom: 2.5px; - padding: 0px 0px 5px 6px; - width: 85%; -} - -.flexCells { + border-radius: 20px; + width: fit-content; + align-items: center; + box-sizing: border-box; + margin-left: auto; + } + + .flexCells { display: grid; grid-template-columns: repeat(4, 1fr); - gap: 5px; - width: 90%; + gap: 9px; + justify-content: center; + align-items: center; + width: fit-content; + margin: 5 auto; border-radius: 20px; border: 2.5px solid #FFE7CF; padding: 10px; margin-top: 10px; background-color: white; -} - -.cell { + } + + + .cell { width: 65px; height: 30px; border: 1px solid orange; @@ -109,11 +115,47 @@ background-color: red; } - .yellow { - background-color: rgb(255, 255, 0); - } - .green { background-color: rgb(33, 240, 33); } - \ No newline at end of file + + .lightOrange1 { + background-color: #FFA500; + } + + .lightOrange2 { + background-color: #FFCC80; + } + + .voltageTotal { + display: inline-block; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + text-align: center; + border-radius: 15px; + background-color: #FFE7CF; + font-size: 0.8rem; + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + box-sizing: border-box; + } + + .value { + height: 50px; + font-size: 25px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + } + + .moduleInfoLabel { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.9rem; + width: 50%; + text-align: left; + margin: 0px; + } \ No newline at end of file diff --git a/control-station/src/components/GuiModules/Module.tsx b/control-station/src/components/GuiModules/Module.tsx index 73152f0c5..eb7f449b6 100644 --- a/control-station/src/components/GuiModules/Module.tsx +++ b/control-station/src/components/GuiModules/Module.tsx @@ -1,68 +1,89 @@ import React, { useEffect, useState } from "react"; import styles from "./Module.module.scss"; + import { useMeasurementsStore } from "common"; interface CellProps { - value: number; + value: number; + min: number; + max: number; } const Module: React.FC<{ id: string | number }> = ({ id }) => { - const numericInfo = useMeasurementsStore( - (state) => state.getNumericMeasurementInfo(`module/${id}`) - ); + const moduleMinCell = useMeasurementsStore( + (state) => (state.getNumericMeasurementInfo(`module_${id}_min_cell`)?.getUpdate() ?? 0) + ); + const moduleMaxCell = useMeasurementsStore( + (state) => (state.getNumericMeasurementInfo(`module_${id}_max_cell`)?.getUpdate() ?? 0) + ); - const [cellValues, setCellValues] = useState(Array(48).fill(0)); + const moduleTotalVoltage = useMeasurementsStore( + (state) => (state.getNumericMeasurementInfo(`module_${id}_voltage`)?.getUpdate() ?? 0) + ); - useEffect(() => { - const intervalId = setInterval(() => { - const newValue = numericInfo.getUpdate(); - setCellValues((prev) => prev.map(() => newValue)); - }, 1); + // Estado para las celdas + const [cellValues, setCellValues] = useState(Array(48).fill(0)); // Define el tipo correctamente - return () => clearInterval(intervalId); - }, [numericInfo]); + useEffect(() => { + const intervalId = setInterval(() => { + setCellValues(() => + Array.from({ length: 48 }, (_, i) => { + const variableName = `module_${id}_cell_${i + 1}_voltage`; + return useMeasurementsStore.getState().getNumericMeasurementInfo(variableName)?.getUpdate() ?? 0; + }) + ); + }, 100); - const getColorFromValue = (value: number, min: number | null, max: number | null) => { - if (min !== null && max !== null) { - if (value < min) return styles.red; - if (value > max) return styles.red; - if (value >= min && value <= max) return styles.green; - } - return styles.yellow; - }; + return () => clearInterval(intervalId); + }, [id]); - const Cell: React.FC = ({ value }) => { - const colorClass = getColorFromValue(value, numericInfo.range[0], numericInfo.range[1]); - return
; - }; + const getColorFromValue = (value: number, min: number, max: number) => { + if (value < min) return styles.red; + if (value > max) return styles.red; + if (value >= min && value <= max) return styles.green; + return styles.yellow; + }; + const Cell: React.FC = ({ value, min, max }) => { + const colorClass = getColorFromValue(value, min, max); return ( -
-
-
-

Module {id}

-
-
-
-

Voltage

-

max: {numericInfo.range[1]} V

-

min: {numericInfo.range[0]} V

-

mean: {cellValues.reduce((a, b) => a + b, 0) / cellValues.length}

-
-
-

Intensity

-

max: {numericInfo.range[1]} A

-

min: {numericInfo.range[0]} A

-
-
-
-
- {cellValues.map((value, index) => ( - - ))} -
-
+
); + }; + + return ( +
+
+
+

Module {id}

+
+ +
+
+

max:

+

{`${moduleMaxCell} V`}

+
+
+

min:

+

{`${moduleMinCell} V`}

+
+
+

total:

+

{`${moduleTotalVoltage} V`}

+
+
+
+
+ {cellValues.map((value, index) => ( + + ))} +
+
+ ); }; export default Module; + diff --git a/control-station/src/components/Window/Window.module.scss b/control-station/src/components/Window/Window.module.scss index 1d7448470..8df759328 100644 --- a/control-station/src/components/Window/Window.module.scss +++ b/control-station/src/components/Window/Window.module.scss @@ -7,6 +7,7 @@ border-radius: 0.8rem; filter: var(--shadow); overflow: hidden; + width: 100%; } .header { @@ -24,4 +25,5 @@ background-color: colors.getColor('primary', 99); overflow: scroll; height: 100%; + width: 100%; } diff --git a/control-station/src/main.tsx b/control-station/src/main.tsx index be3d31fc0..1966b4ce7 100644 --- a/control-station/src/main.tsx +++ b/control-station/src/main.tsx @@ -25,7 +25,6 @@ const router = createBrowserRouter([ camerasRoute, tubeRoute, guiRoute, - ], }, ]); diff --git a/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.module.scss b/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.module.scss index 4a8d211d5..7d75863c6 100644 --- a/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.module.scss +++ b/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.module.scss @@ -7,7 +7,7 @@ .boosterMainContainer{ display: flex; - justify-content: start; + justify-content: start; align-items: start; width: 100%; } @@ -17,42 +17,95 @@ background-color: white; border-radius: 20px; display: flex; - flex-direction: column; - align-items: center; + flex-direction: column; + align-items: center; overflow: hidden; box-sizing: border-box; margin: 20px; } .statusContainer { - width: 100%; + margin: 20px; display: flex; + flex-direction: column; /* Cambiado a columna */ justify-content: center; - margin: 20px; + align-items: center; + width: 90%; +} + +.statusRow1, .statusRow2 { + background-color: #FFE7CF; + border-radius: 20px; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + width: 100%; + margin-bottom: 5px; /* Espacio entre filas */ +} + +.statusItem{ + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; +} + +.value{ + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + border: 1.5px solid #EF7E30; + border-radius: 1.2rem; + font-size: 25px; + height: 100px; + width: 100%; + display: flex; + justify-content: center; + align-items: center; } -.status { + +.statusFirstLabel { + width: 95%; display: flex; flex-direction: row; justify-content: space-around; align-items: center; border: 2.5px solid #FFE7CF; border-radius: 20px; - width: 95%; text-align: center; background-color: #FFF7EC; color: #EF7E30; font-family: 'IBM Plex Mono', monospace; + margin-bottom: 10px; } +.statusSecondLabel { + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + border: 2.5px solid #FFE7CF; + border-radius: 20px; + width: 95%; + text-align: center; + background-color: #FFF7EC; + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; +} .value { border: 1.5px solid #EF7E30; border-radius: 1.2rem; - width: 12%; - height: 50px; + width: 40%; + height: 40px; font-size: 25px; + margin-left: 5px; display: flex; - justify-content: center; + justify-content: center; align-items: center; text-align: center; } @@ -61,7 +114,7 @@ display: flex; justify-content: center; align-items: center; - gap: 20px; + gap: 20px; width: 100%; } @@ -96,6 +149,10 @@ p { .messagesAndOrders { margin-top: 20px; + overflow: hidden; + flex-grow: 1; + width: 100%; + max-width: 25vw; } .orders { @@ -110,8 +167,8 @@ p { .voltageValueContainer { display: inline-block; padding: 5px 10px; - border: 1px solid #EF7E30; - border-radius: 15px; + border: 1px solid #EF7E30; + border-radius: 15px; } .voltageValue { diff --git a/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.tsx b/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.tsx index 5f8b263fa..525f6c2af 100644 --- a/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.tsx +++ b/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.tsx @@ -2,92 +2,133 @@ import { useEffect, useState } from "react"; import styles from "./GuiPage.module.scss"; import Module from "../../../components/GuiModules/Module"; import { Messages } from "../Messages/Messages"; -import { Orders, useMeasurementsStore } from "common"; +import { Orders, useMeasurementsStore, NumericMeasurementInfo } from "common"; interface ModuleData { - id: number | string; - name: string; -}; + id: number | string; + name: string; +} const modules: ModuleData[] = [ - { id: 1, name: "Module 1" }, - { id: 2, name: "Module 2" }, - { id: 3, name: "Module 3" }, + { id: 1, name: "Module 1" }, + { id: 2, name: "Module 2" }, + { id: 3, name: "Module 3" }, ]; export function GuiPage() { + // Medidas + const totalSupercapsVoltageInfo = useMeasurementsStore((state) => + state.getNumericMeasurementInfo("total_supercaps_voltage") + ); + const currentMeasurementInfo = useMeasurementsStore((state) => + state.getNumericMeasurementInfo("output_current") + ); + const temperatureMeasurementInfo = useMeasurementsStore((state) => + state.getNumericMeasurementInfo("temperature_total") + ); - const voltageTotalMeasurement = useMeasurementsStore((state) => - state.getNumericMeasurementInfo("total_voltage_high") - ); - - const bcuVoltageMeasurement = useMeasurementsStore((state) => - state.getNumericMeasurementInfo("vdc") - ); + // Enums + const contactorsStateInfo = useMeasurementsStore((state) => + state.getMeasurement("contactors_state") + ); + const bcuGeneralStateInfo = useMeasurementsStore((state) => + state.getMeasurement("bcu_general_state") + ); - const constStatusMeasurement = useMeasurementsStore((state) => - state.getBooleanMeasurementInfo("const_status") - ); + // Estados + const [voltageTotal, setVoltageTotal] = useState(null); + const [current, setCurrent] = useState(null); + const [temperature, setTemperature] = useState(null); + const [contactorsState, setContactorsState] = useState(null); + const [bcuState, setBcuState] = useState(null); - const [voltageTotal, setVoltageTotal] = useState(null); - const [bcuVoltage, setBcuVoltage] = useState(null); - const [constStatus, setConstStatus] = useState(false); + // Efectos + useEffect(() => { + setVoltageTotal(totalSupercapsVoltageInfo?.getUpdate() ?? null); // Si `getUpdate()` es el método adecuado + }, [totalSupercapsVoltageInfo]); + + useEffect(() => { + setCurrent(currentMeasurementInfo?.getUpdate() ?? null); // Similar para otras mediciones + }, [currentMeasurementInfo]); + + useEffect(() => { + setTemperature(temperatureMeasurementInfo?.getUpdate() ?? null); + }, [temperatureMeasurementInfo]); + - useEffect(() => { - // (VER) - setVoltageTotal(voltageTotalMeasurement?.getUpdate() || null); - setConstStatus(constStatusMeasurement.getUpdate()); - }, [voltageTotalMeasurement, constStatusMeasurement]); + useEffect(() => { + const value = contactorsStateInfo?.value; + setContactorsState(typeof value === "string" ? value : null); + }, [contactorsStateInfo]); - useEffect(() => { - if (bcuVoltageMeasurement?.getUpdate) { - const newValue = bcuVoltageMeasurement.getUpdate(); - console.log("Nuevo valor BCU Voltage:", newValue); - setBcuVoltage(newValue); - } - }, [bcuVoltageMeasurement]); + useEffect(() => { + const value = bcuGeneralStateInfo?.value; + setBcuState(typeof value === "string" ? value : null); + }, [bcuGeneralStateInfo]); - return ( -
-
-

Booster GUI

-
-
-
-
-
-
-

V total:

-
- {voltageTotal} V -
- -

BCU status:

-
- {bcuVoltage} V -
- -

CONST status:

-
- {constStatus ? "On" : "Off"} {/* Muestra On/Off para el estado de los contactores */} -
-
-
-
- {modules.map((module) => ( - - ))} -
+ return ( +
+
+
+
+
+
+

V total:

+
+ {voltageTotal ?? "-"} V
-
-
- -
- -
- +
+
+

Current:

+
+ {current ?? "-"} A +
+
+
+

Contactors status:

+
+ {contactorsState ?? "-"} +
+
+
+
+
+

BCU status:

+
+ {bcuState ?? "-"}
-
+
+
+

Temperature total:

+
+ {temperature ?? "-"} ºC +
+
+
+

Charge:

+
+ - % +
+
+
+
+ +
+ {modules.map((module) => ( + + ))} +
+ + +
+
+ +
+
+ +
- ); -} \ No newline at end of file + + + ); +} diff --git a/control-station/vite.config.ts b/control-station/vite.config.ts index 0475a2528..cbc46daf8 100644 --- a/control-station/vite.config.ts +++ b/control-station/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "vite"; +import path from "path" import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; import svgr from "vite-plugin-svgr"; @@ -10,4 +11,9 @@ export default defineConfig({ outDir: "./static", minify: false, }, + resolve: { + alias: { + common: path.resolve(__dirname, '../common-front'), + }, + }, }); diff --git a/ethernet-view/vite.config.ts b/ethernet-view/vite.config.ts index 0cc8e8b54..0eb072bbc 100644 --- a/ethernet-view/vite.config.ts +++ b/ethernet-view/vite.config.ts @@ -1,5 +1,6 @@ /// import { defineConfig } from "vite"; +import path from "path"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; import svgr from "vite-plugin-svgr"; @@ -17,4 +18,9 @@ export default defineConfig({ environment: "jsdom", setupFiles: "./src/tests/setup.ts", }, + resolve: { + alias: { + common: path.resolve(__dirname, '../common-front'), + }, + }, }); diff --git a/go.work b/go.work index aa827f367..934d10dca 100644 --- a/go.work +++ b/go.work @@ -1,7 +1,9 @@ -go 1.21.3 +go 1.23.1 use ( ./backend - ./state-order-tester ./scripts + ./state-order-tester + ./updater + ./packet-sender ) diff --git a/packet-sender/PacketGenerator.go b/packet-sender/PacketGenerator.go index f826051c4..3cb0b8ae8 100644 --- a/packet-sender/PacketGenerator.go +++ b/packet-sender/PacketGenerator.go @@ -18,7 +18,7 @@ type PacketGenerator struct { } func New() PacketGenerator { - adj, err := adj.NewADJ("main", true) + adj, err := adj.NewADJ("HVSCU-Cabinet", false) if err != nil { log.Fatalf("Failed to load ADJ: %v\n", err) } @@ -55,30 +55,65 @@ func New() PacketGenerator { } func (pg *PacketGenerator) CreateRandomPacket() []byte { + if len(pg.packets) == 0 { + + return nil + } + randomIndex := rand.Int63n(int64(len(pg.packets))) randomPacket := pg.packets[randomIndex] - buff := bytes.NewBuffer(make([]byte, 0)) + if len(randomPacket.Measurements) == 0 { + log.Printf("The packet with ID %d has no measurements\n", randomPacket.ID) + return nil + } + + buff := bytes.NewBuffer(make([]byte, 0)) binary.Write(buff, binary.LittleEndian, randomPacket.ID) for _, measurement := range randomPacket.Measurements { if strings.Contains(measurement.Type, "enum") { - binary.Write(buff, binary.LittleEndian, uint8(rand.Int63n(int64(len(strings.Split(strings.ReplaceAll(strings.TrimSuffix(strings.TrimPrefix(measurement.Type, "enum("), ")"), " ", ""), ",")))))) + + list := strings.Split(strings.ReplaceAll(strings.TrimSuffix(strings.TrimPrefix(measurement.Type, "enum("), ")"), " ", ""), ",") + if len(list) == 0 { + + log.Printf("Empty list for enum: %v\n", measurement.Type) + continue + } + + randomIndex := rand.Int63n(int64(len(list))) + if randomIndex >= 0 && randomIndex < int64(len(list)) { + binary.Write(buff, binary.LittleEndian, uint8(randomIndex)) + } else { + + log.Printf("Index out of range for enum: %v, index: %d, length: %d\n", measurement.Type, randomIndex, len(list)) + continue + } } else if measurement.Type == "bool" { + binary.Write(buff, binary.LittleEndian, rand.Int31n(2) == 1) } else if measurement.Type != "string" { + var number float64 - if len(measurement.WarningRange) == 0 { - number = mapNumberToRange(rand.Float64(), measurement.WarningRange, measurement.Type) - } else { - number = mapNumberToRange(rand.Float64(), []float64{measurement.WarningRange[0] * 0.8, measurement.WarningRange[1] * 1.2}, measurement.Type) + if len(measurement.WarningRange) < 2 { + + continue } + + number = mapNumberToRange( + rand.Float64(), + []float64{ + measurement.WarningRange[0] * 0.8, + measurement.WarningRange[1] * 1.2, + }, + measurement.Type, + ) writeNumberAsBytes(number, measurement.Type, buff) } else { - return nil - } + continue + } } return buff.Bytes() diff --git a/packet-sender/adj b/packet-sender/adj new file mode 160000 index 000000000..e51cccf45 --- /dev/null +++ b/packet-sender/adj @@ -0,0 +1 @@ +Subproject commit e51cccf4527fc0fa623c3f0487a6d5741502067c diff --git a/packet-sender/packet_sender.exe b/packet-sender/packet_sender.exe new file mode 100644 index 000000000..070f12c58 Binary files /dev/null and b/packet-sender/packet_sender.exe differ diff --git a/updater/go.mod b/updater/go.mod new file mode 100644 index 000000000..69925d276 --- /dev/null +++ b/updater/go.mod @@ -0,0 +1,3 @@ +module updater + +go 1.23.1 diff --git a/updater/main.go b/updater/main.go new file mode 100644 index 000000000..7469e4b99 --- /dev/null +++ b/updater/main.go @@ -0,0 +1,275 @@ +package main + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +const ( + repoOwner = "HyperloopUPV-H8" + repoName = "software" +) + +func main() { + // Detect the operating system + osType := detectOS() + + // Check if the `../backend/` folder exists + if _, err := os.Stat("../backend"); err == nil { + fmt.Println("Directory '../backend' found. Updating from the repository...") + updateFromGit() + } else { + fmt.Println("Directory '../backend' not found. Checking binaries...") + updateFromBinaries(osType) + } +} + +func detectOS() string { + switch runtime.GOOS { + case "windows": + return "backend-windows-64.exe" // Incluye la extensión .exe para Windows + case "darwin": + if strings.Contains(runtime.GOARCH, "arm") { + return "backend-macos-m1-64" + } + return "backend-macos-64" + case "linux": + return "backend-linux-64" + default: + fmt.Fprintf(os.Stderr, "Unsupported operating system: %s\n", runtime.GOOS) + os.Exit(1) + return "" + } +} + +func updateFromGit() { + // Run `git pull` in the `../backend` folder + cmd := exec.Command("git", "-C", "../backend", "pull") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error running git pull: %v\n", err) + os.Exit(1) + } + + // Run `go mod tidy` to update dependencies + cmd = exec.Command("go", "mod", "tidy") + cmd.Dir = "../backend" + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error running go mod tidy: %v\n", err) + os.Exit(1) + } + + // Run `go build` in the `../backend` folder + cmd = exec.Command("go", "build", "-o", "../backend/cmd/cmd.exe", "../backend/cmd/...") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error running go build: %v\n", err) + os.Exit(1) + } + + // Launch the compiled executable + launchExecutable("../backend/cmd/cmd") +} + +// Check if a process is running by its name +func isProcessRunning(processName string) (bool, error) { + cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s", processName)) + output, err := cmd.Output() + if err != nil { + return false, err + } + return strings.Contains(string(output), processName), nil +} + +func stopProcess(processName string) error { + cmd := exec.Command("taskkill", "/IM", processName, "/F") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("taskkill failed: %v - output: %s", err, output) + } + fmt.Printf("Process %s stopped successfully.\n", processName) + return nil +} + +func updateFromBinaries(osType string) { + binaries := []string{"backend-windows-64.exe", "backend-linux-64", "backend-macos-64", "backend-macos-m1-64"} + for _, binary := range binaries { + if _, err := os.Stat("./" + binary); err == nil { + // Check if the backend process is running + isRunning, err := isProcessRunning(binary) + if err != nil { + fmt.Fprintf(os.Stderr, "Error checking if process is running: %v\n", err) + os.Exit(1) + } + + // Stop the process if it's running + if isRunning { + fmt.Printf("Process %s is running. Stopping it...\n", binary) + if err := stopProcess(binary); err != nil { + fmt.Fprintf(os.Stderr, "Error stopping process %s: %v\n", binary, err) + os.Exit(1) + } + time.Sleep(500 * time.Millisecond) // waits 1/2 second to ensure the process is stopped + } + + fmt.Printf("Deleting old binary: %s\n", binary) + deleted := false + for i := 0; i < 5; i++ { + if err := os.Remove("./" + binary); err == nil { + deleted = true + break + } else { + fmt.Printf("Retrying delete (%d/5)...\n", i+1) + time.Sleep(300 * time.Millisecond) + } + } + if !deleted { + fmt.Fprintf(os.Stderr, "Error deleting old binary after multiple attempts.\n") + os.Exit(1) + } + } + } + + // Get the latest version from GitHub + latestVersion, err := getLatestVersion() + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting the latest version: %v\n", err) + os.Exit(1) + } + + // Construct the ZIP file URL + zipFileName := fmt.Sprintf("control-station-v%s.zip", strings.ReplaceAll(latestVersion, ".", "-")) + url := fmt.Sprintf("https://github.com/%s/%s/releases/download/v%s/%s", repoOwner, repoName, latestVersion, zipFileName) + fmt.Printf("Downloading ZIP from: %s\n", url) + + // Download the ZIP file + err = downloadFile("./"+zipFileName, url) + if err != nil { + fmt.Fprintf(os.Stderr, "Error downloading the ZIP file: %v\n", err) + os.Exit(1) + } + + // Extract the binary from the ZIP file + binaryPath, err := extractBinaryFromZip("./"+zipFileName, osType) + if err != nil { + fmt.Fprintf(os.Stderr, "Error extracting the binary: %v\n", err) + os.Exit(1) + } + + // Launch the extracted binary + launchExecutable(binaryPath) +} + +func downloadFile(filepath string, url string) error { + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + // Get the data + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // Write the body to file + _, err = io.Copy(out, resp.Body) + return err +} + +func extractBinaryFromZip(zipPath, binaryName string) (string, error) { + // Open the ZIP file + r, err := zip.OpenReader(zipPath) + if err != nil { + return "", err + } + defer r.Close() + + // Iterate through the files in the ZIP + for _, f := range r.File { + if f.Name == binaryName { + // Open the file inside the ZIP + rc, err := f.Open() + if err != nil { + return "", err + } + defer rc.Close() + + // Create the output file + outPath := "./" + binaryName + outFile, err := os.Create(outPath) + if err != nil { + return "", err + } + defer outFile.Close() + + // Copy the contents of the file + _, err = io.Copy(outFile, rc) + if err != nil { + return "", err + } + + // Return the path to the extracted binary + return outPath, nil + } + } + + return "", fmt.Errorf("binary %s not found in ZIP", binaryName) +} + +func getLatestVersion() (string, error) { + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName) + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var release struct { + TagName string `json:"tag_name"` + } + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return "", err + } + + return strings.TrimPrefix(release.TagName, "v"), nil +} + +func launchExecutable(path string) { + if runtime.GOOS == "windows" && !strings.HasSuffix(path, ".exe") { + path += ".exe" + } + + absPath, err := filepath.Abs(path) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting the absolute path: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Launching executable: %s\n", absPath) + + cmd := exec.Command(absPath) + cmd.Dir = filepath.Dir(absPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error launching the executable: %v\n", err) + os.Exit(1) + } +}