Skip to content

Commit f6733f5

Browse files
authored
Merge branch 'develop' into control-station/gui-booster
2 parents 7dde7af + 1fcc063 commit f6733f5

30 files changed

Lines changed: 1185 additions & 743 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
build
22
profiles
33
backend/cmd/cmd
4+
backend/cmd/adj/
5+
.ropeproject
46

57
# MacOS Files
68
.DS_Store

.gitmodules

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[submodule "backend/cmd/JSON_ADE"]
2-
path = backend/cmd/JSON_ADE
3-
url = https://github.com/HyperloopUPV-H8/JSON_ADE.git
1+
[submodule "backend/cmd/adj"]
2+
path = backend/cmd/adj
3+
url = https://github.com/HyperloopUPV-H8/adj.git
44
branch = main

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
[![CI](https://github.com/HyperloopUPV-H8/software/actions/workflows/build-ethernet-view.yaml/badge.svg)](https://github.com/HyperloopUPV-H8/software/actions/workflows/build-ethernet-view.yaml)
55
[![CI](https://github.com/HyperloopUPV-H8/software/actions/workflows/build-control-station.yaml/badge.svg)](https://github.com/HyperloopUPV-H8/software/actions/workflows/build-control-station.yaml)
66

7-
Hyperloop UPV's Control STtaion is a unified software solution for monitoring and commanding the pod in real time. It combines a back-end (Go) that ingests and interprets sensor data–defined via the JSON-based "ADJ" specifications–and a front-end (Typescript/React) that displays metrics, logs, and diagnostics to operators. With features like packet parsing, logging, and live dashboards, it acts as the central hub to safely interface the pod, making it easier for team members to oversee performance, detect faults, and send precise orders to the vehicle.
7+
Hyperloop UPV's Control Station is a unified software solution for real-time monitoring and commanding of the pod. It combines a back-end (Go) that ingests and interprets sensor data–defined via the JSON-based "ADJ" specifications–and a front-end (Typescript/React) that displays metrics, logs, and diagnostics to operators. With features like packet parsing, logging, and live dashboards, it acts as the central hub to safely interface the pod, making it easier for team members to oversee performance, detect faults, and send precise orders to the vehicle.
88

99
### Installation
1010

1111
Download the last release, unzip it and leave the executable compatible with your OS in the folder.
1212

1313
### Developing
1414

15-
The main project file is inside `backend/cmd`. Make sure to have the proper `config.toml` configuration and that you are in the `develop` branch. To build the project just run `go build` inside the folder. With everything set up execute the `cmd` executable, then move to `ethernet-view` and run `npm run dev`, then to the `control-station` and do the same.
15+
The main project file is inside `backend/cmd`. Ensure you have the proper `config.toml` configuration and are in the `develop` branch. To build the project, just run `go build` inside the folder. With everything set up, execute the `cmd` executable, then move to `ethernet-view` and run `npm run dev`, then to the `control-station` and do the same.
1616

1717
### Contributing
1818

backend/cmd/JSON_ADE

Lines changed: 0 additions & 1 deletion
This file was deleted.

backend/cmd/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ import (
55
"github.com/HyperloopUPV-H8/h9-backend/internal/vehicle"
66
)
77

8+
type Adj struct {
9+
Branch string
10+
}
11+
12+
type Network struct {
13+
Manual bool
14+
}
15+
816
type Config struct {
917
Vehicle vehicle.Config
1018
Server server.Config
19+
Network Network
20+
Adj Adj
1121
}

backend/cmd/config.toml

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
2-
[server.local]
1+
[server.ethernet-view]
2+
address = "127.0.0.1:4040"
3+
static = "./ethernet-view"
4+
[server.ethernet-view.endpoints]
5+
pod_data = "/podDataStructure"
6+
order_data = "/orderStructures"
7+
programable_boards = "/uploadableBoards"
8+
connections = "/backend"
9+
files = "/"
10+
[server.control-station]
311
address = "127.0.0.1:4000"
4-
static = "./static"
5-
6-
[server.local.endpoints]
12+
static = "./control-station"
13+
[server.control-station.endpoints]
714
pod_data = "/podDataStructure"
815
order_data = "/orderStructures"
916
programable_boards = "/uploadableBoards"
1017
connections = "/backend"
1118
files = "/"
19+
[network]
20+
manual = false
1221

13-
[vehicle]
14-
boardsList = ["VCU"]
22+
[adj]
23+
branch = "sw-bcu" # Leave blank when using ADJ as a submodule (like this: "")

backend/cmd/main.go

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/http"
1313
_ "net/http/pprof"
1414
"os"
15+
"os/exec"
1516
"os/signal"
1617
"path"
1718
"runtime"
@@ -96,15 +97,23 @@ func main() {
9697
defer pprof.StopCPUProfile()
9798
}
9899
runtime.SetBlockProfileRate(*blockprofile)
100+
99101
config := getConfig("./config.toml")
100102

101103
// <--- ADJ --->
102104

103-
adj, err := adj_module.NewADJ()
105+
adj, err := adj_module.NewADJ(config.Adj.Branch)
104106
if err != nil {
105107
trace.Fatal().Err(err).Msg("setting up ADJ")
106108
}
107109

110+
test := exec.Command("python3", "testadj.py")
111+
out, err := test.CombinedOutput()
112+
if err != nil || len(out) != 0 {
113+
fmt.Printf("\nPython test failed:\nError: %v\nOutput: %s\n", err, string(out))
114+
os.Exit(1)
115+
}
116+
108117
podData, err := pod_data.NewPodData(adj.Boards, adj.Info.Units)
109118
if err != nil {
110119
fmt.Println(err)
@@ -120,7 +129,7 @@ func main() {
120129

121130
dev = devs[*networkDevice]
122131
} else {
123-
dev, err = selectDev()
132+
dev, err = selectDev(adj.Info.Addresses, config)
124133
if err != nil {
125134
trace.Fatal().Err(err).Msg("Error selecting device")
126135
}
@@ -345,26 +354,40 @@ func createPid(path string) {
345354
}
346355
}
347356

348-
func selectDev() (pcap.Interface, error) {
357+
func selectDev(adjAddr map[string]string, conf Config) (pcap.Interface, error) {
349358
devs, err := pcap.FindAllDevs()
350359
if err != nil {
351360
return pcap.Interface{}, err
352361
}
353362

354-
cyan := color.New(color.FgCyan)
363+
if conf.Network.Manual {
364+
cyan := color.New(color.FgCyan)
355365

356-
cyan.Print("select a device: ")
357-
fmt.Printf("(0-%d)\n", len(devs)-1)
358-
for i, dev := range devs {
359-
displayDev(i, dev)
360-
}
366+
cyan.Print("select a device: ")
367+
fmt.Printf("(0-%d)\n", len(devs)-1)
368+
for i, dev := range devs {
369+
displayDev(i, dev)
370+
}
361371

362-
dev, err := acceptInput(len(devs))
363-
if err != nil {
364-
return pcap.Interface{}, err
365-
}
372+
dev, err := acceptInput(len(devs))
373+
if err != nil {
374+
return pcap.Interface{}, err
375+
}
376+
377+
return devs[dev], nil
378+
} else {
379+
for _, dev := range devs {
380+
for _, addr := range dev.Addresses {
381+
if addr.IP.String() == adjAddr["backend"] {
382+
println(dev.Name)
383+
return dev, nil
384+
}
385+
}
386+
}
366387

367-
return devs[dev], nil
388+
log.Fatal("backend address not found in any device")
389+
return pcap.Interface{}, nil
390+
}
368391
}
369392

370393
func displayDev(i int, dev pcap.Interface) {
@@ -558,7 +581,7 @@ func getIPIPfilter() string {
558581
return "ip[9] == 4"
559582
}
560583

561-
func getUDPFilter(addrs []net.IP, backendAddr net.IP, port uint16) string {
584+
func getUDPFilter(addrs []net.IP, backendAddr net.IP, _ uint16) string {
562585
udpPort := "udp" // TODO use proper ports for the filter
563586
srcUdpAddrs := common.Map(addrs, func(addr net.IP) string {
564587
return fmt.Sprintf("(src host %s)", addr)

backend/cmd/testadj.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import os
2+
import json
3+
4+
5+
def validate_json_structure(data):
6+
errors = []
7+
8+
if "board_id" in data:
9+
if not isinstance(data["board_id"], int):
10+
errors.append(f"'board_id' debe ser un entero, pero se encontró: {type(data['board_id']).__name__}.")
11+
12+
if "board_ip" in data:
13+
if not isinstance(data["board_ip"], str):
14+
errors.append(f"'board_ip' debe ser una cadena, pero se encontró: {type(data['board_ip']).__name__}.")
15+
16+
17+
if "measurements" in data:
18+
if not isinstance(data["measurements"], list):
19+
errors.append("La clave 'measurements' debe ser una lista.")
20+
else:
21+
for measurement in data["measurements"]:
22+
if isinstance(measurement, dict):
23+
required_keys = ["id", "name", "type", "podUnits", "displayUnits"]
24+
for key in required_keys:
25+
if key not in measurement:
26+
errors.append(f"Falta la clave '{key}' en un objeto de 'measurements'.")
27+
if "id" in measurement and not isinstance(measurement["id"], str):
28+
errors.append(f"El 'id' debe ser una cadena en: {measurement}")
29+
if "name" in measurement and not isinstance(measurement["name"], str):
30+
errors.append(f"El 'name' debe ser una cadena en: {measurement}")
31+
if "type" in measurement and not isinstance(measurement["type"], str):
32+
errors.append(f"El 'type' debe ser una cadena en: {measurement}")
33+
if "safeRange" in measurement and not isinstance(measurement.get("safeRange", []), list):
34+
errors.append(f"'safeRange' debe ser una lista en: {measurement}")
35+
if "warningRange" in measurement and not isinstance(measurement.get("warningRange", []), list):
36+
errors.append(f"'warningRange' debe ser una lista en: {measurement}")
37+
if "displayUnits" in measurement and not isinstance(measurement["displayUnits"], str):
38+
errors.append(f"El 'podUnits' debe ser una cadena en: {measurement}")
39+
if "podUnits" in measurement and not isinstance(measurement["podUnits"], str):
40+
errors.append(f"El 'podUnits' debe ser una cadena en: {measurement}") #esto se puede quitar (json 516)
41+
elif not isinstance(measurement, str):
42+
errors.append("Cada elemento en 'measurements' debe ser un objeto o una cadena (nombre de archivo).")
43+
44+
45+
if "packets" in data:
46+
if not isinstance(data["packets"], list):
47+
errors.append("La clave 'packets' debe ser una lista.")
48+
else:
49+
for packet in data["packets"]:
50+
if isinstance(packet, dict):
51+
required_keys = ["id", "name", "type","variable"]
52+
for key in required_keys:
53+
if key not in packet:
54+
errors.append(f"Falta la clave '{key}' en un objeto de 'packets'.")
55+
if "id" in packet and not isinstance(packet["id"], str):
56+
errors.append(f"El 'id' debe ser una cadena en: {packet}")
57+
if "name" in packet and not isinstance(packet["name"], str):
58+
errors.append(f"El 'name' debe ser una cadena en: {packet}")
59+
if "type" in packet and not isinstance(packet["type"], str):
60+
errors.append(f"El 'type' debe ser una cadena en: {packet}")
61+
if "variables" in packet:
62+
if not isinstance(packet["variables"], list):
63+
errors.append(f"'variables' debe ser una lista en: {packet}")
64+
else:
65+
for variable in packet["variables"]:
66+
if not isinstance(variable, dict):
67+
errors.append(f"Cada elemento en 'variables' debe ser un objeto en: {packet}")
68+
if "name" not in variable:
69+
errors.append(f"Falta la clave 'name' en un objeto de 'variables' en: {packet}")
70+
elif not isinstance(packet, str):
71+
errors.append("Cada elemento en 'packets' debe ser un objeto o una cadena (nombre de archivo).")
72+
73+
return errors
74+
75+
76+
77+
def validate_json_folder(folder_path):
78+
boards_file_path = os.path.join(folder_path, "boards.json")
79+
80+
try:
81+
with open(boards_file_path, 'r') as boards_file:
82+
boards_data = json.load(boards_file)
83+
boards = boards_data.get("boards", {})
84+
85+
86+
board_keys = list(boards.keys())
87+
duplicate_keys = [key for key in board_keys if board_keys.count(key) > 1]
88+
89+
if duplicate_keys:
90+
print(f"Error: El archivo boards.json contiene claves duplicadas: {', '.join(duplicate_keys)}")
91+
return
92+
93+
except json.JSONDecodeError as e:
94+
print(f"Error al decodificar JSON en {boards_file_path}: {e}")
95+
return
96+
except Exception as e:
97+
print(f"Error al procesar el archivo {boards_file_path}: {e}")
98+
return
99+
100+
101+
for board_name, board_file_path in boards.items():
102+
full_path = os.path.join(folder_path, board_file_path)
103+
try:
104+
with open(full_path, 'r') as board_file:
105+
data = json.load(board_file)
106+
errors = validate_json_structure(data)
107+
if errors:
108+
print(f"Errores encontrados en {full_path} para la placa '{board_name}':")
109+
for error in errors:
110+
print(f"- {error}")
111+
112+
except json.JSONDecodeError as e:
113+
print(f"Error al decodificar JSON en {full_path}: {e}")
114+
except Exception as e:
115+
print(f"Error al procesar el archivo {full_path}: {e}")
116+
117+
118+
if os.path.exists('./adj/') == False:
119+
print("La carpeta ./adj/ no existe")
120+
if __name__ == "__main__":
121+
validate_json_folder("./adj/")

backend/go.mod

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,18 @@ module github.com/HyperloopUPV-H8/h9-backend
33
go 1.21.3
44

55
require (
6-
github.com/HyperloopUPV-H8/ade-linter v0.0.0-20230530153315-3379f05a664f
76
github.com/go-git/go-git/v5 v5.12.0
87
github.com/google/gopacket v1.1.19
98
github.com/google/uuid v1.3.0
109
github.com/gorilla/websocket v1.5.0
1110
github.com/jmaralo/sntp v0.0.0-20240116111937-45a0a3419272
1211
github.com/pelletier/go-toml/v2 v2.0.7
1312
github.com/pin/tftp/v3 v3.0.0
14-
github.com/pkg/errors v0.9.1
1513
github.com/rs/zerolog v1.29.0
16-
github.com/xuri/excelize/v2 v2.7.1
1714
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
18-
google.golang.org/api v0.114.0
1915
)
2016

2117
require (
22-
cloud.google.com/go/compute v1.19.1 // indirect
23-
cloud.google.com/go/compute/metadata v0.2.3 // indirect
2418
dario.cat/mergo v1.0.0 // indirect
2519
github.com/Microsoft/go-winio v0.6.1 // indirect
2620
github.com/ProtonMail/go-crypto v1.0.0 // indirect
@@ -30,33 +24,19 @@ require (
3024
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
3125
github.com/go-git/go-billy/v5 v5.5.0 // indirect
3226
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
33-
github.com/golang/protobuf v1.5.3 // indirect
34-
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
35-
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
3627
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
3728
github.com/kevinburke/ssh_config v1.2.0 // indirect
3829
github.com/mattn/go-colorable v0.1.13 // indirect
3930
github.com/mattn/go-isatty v0.0.17 // indirect
40-
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
4131
github.com/pjbgf/sha1cd v0.3.0 // indirect
42-
github.com/richardlehane/mscfb v1.0.4 // indirect
43-
github.com/richardlehane/msoleps v1.0.3 // indirect
32+
github.com/pkg/errors v0.9.1 // indirect
4433
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
4534
github.com/skeema/knownhosts v1.2.2 // indirect
4635
github.com/xanzy/ssh-agent v0.3.3 // indirect
47-
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
48-
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
49-
go.opencensus.io v0.24.0 // indirect
5036
golang.org/x/crypto v0.21.0 // indirect
5137
golang.org/x/mod v0.12.0 // indirect
52-
golang.org/x/oauth2 v0.7.0 // indirect
5338
golang.org/x/sys v0.18.0 // indirect
54-
golang.org/x/text v0.14.0 // indirect
5539
golang.org/x/tools v0.13.0 // indirect
56-
google.golang.org/appengine v1.6.7 // indirect
57-
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
58-
google.golang.org/grpc v1.56.3 // indirect
59-
google.golang.org/protobuf v1.30.0 // indirect
6040
gopkg.in/warnings.v0 v0.1.2 // indirect
6141
)
6242

0 commit comments

Comments
 (0)