diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 2564f79db..516ddbdc7 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -53,11 +53,13 @@ jobs:
competition-view: ${{ steps.filter.outputs.competition-view == 'true' || github.event.inputs.rebuild-competition-view == 'true' || inputs.build-competition-view == true }}
steps:
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- uses: dorny/paths-filter@v3
id: filter
with:
- ref: "production"
+ base: ${{ github.event.before }}
filters: |
backend:
- 'backend/**/*'
@@ -113,7 +115,7 @@ jobs:
with:
workflow: build.yaml
branch: production
- workflow_conclusion: success
+ workflow_conclusion: completed
name: backend-${{ matrix.platform }}
path: backend/cmd
diff --git a/.github/workflows/frontend-tests.yaml b/.github/workflows/frontend-tests.yaml
index 149d955df..0599a5097 100644
--- a/.github/workflows/frontend-tests.yaml
+++ b/.github/workflows/frontend-tests.yaml
@@ -41,5 +41,8 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile --filter=testing-view --filter=ui --filter=core
+ - name: Build frontend
+ run: pnpm build --filter="./frontend/**"
+
- name: Run tests
run: pnpm test --filter="./frontend/**"
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index abcc3c225..01f375565 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -86,6 +86,12 @@ jobs:
echo "Updated version to:"
cat package.json | grep version
+ - name: Install Linux build dependencies
+ if: runner.os == 'Linux'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y rpm libarchive-tools
+
# Download ONLY the appropriate backend for this platform
- name: Download Linux backend
if: runner.os == 'Linux'
@@ -182,6 +188,8 @@ jobs:
electron-app/dist/*.exe
electron-app/dist/*.AppImage
electron-app/dist/*.deb
+ electron-app/dist/*.rpm
+ electron-app/dist/*.pacman
electron-app/dist/*.dmg
electron-app/dist/*.zip
electron-app/dist/*.yml
diff --git a/backend/cmd/main.go b/backend/cmd/main.go
index addc5c879..b31a17d63 100644
--- a/backend/cmd/main.go
+++ b/backend/cmd/main.go
@@ -5,12 +5,12 @@ import (
"os"
"os/signal"
- adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj"
"github.com/HyperloopUPV-H8/h9-backend/internal/config"
"github.com/HyperloopUPV-H8/h9-backend/internal/flags"
"github.com/HyperloopUPV-H8/h9-backend/internal/pod_data"
"github.com/HyperloopUPV-H8/h9-backend/internal/update_factory"
vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models"
+ adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj"
"github.com/HyperloopUPV-H8/h9-backend/pkg/transport"
"github.com/HyperloopUPV-H8/h9-backend/pkg/websocket"
trace "github.com/rs/zerolog/log"
diff --git a/backend/cmd/orchestrator.go b/backend/cmd/orchestrator.go
index 180b26c8d..6d34b064a 100644
--- a/backend/cmd/orchestrator.go
+++ b/backend/cmd/orchestrator.go
@@ -7,11 +7,11 @@ import (
"runtime/pprof"
"strings"
- adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj"
"github.com/HyperloopUPV-H8/h9-backend/internal/config"
"github.com/HyperloopUPV-H8/h9-backend/internal/flags"
"github.com/HyperloopUPV-H8/h9-backend/internal/pod_data"
"github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction"
+ adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj"
"github.com/HyperloopUPV-H8/h9-backend/pkg/logger"
data_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/data"
order_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/order"
diff --git a/backend/cmd/setup_transport.go b/backend/cmd/setup_transport.go
index c86f9270e..9c9a5b628 100644
--- a/backend/cmd/setup_transport.go
+++ b/backend/cmd/setup_transport.go
@@ -7,12 +7,12 @@ import (
"net"
"time"
- adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj"
"github.com/HyperloopUPV-H8/h9-backend/internal/common"
"github.com/HyperloopUPV-H8/h9-backend/internal/config"
"github.com/HyperloopUPV-H8/h9-backend/internal/pod_data"
"github.com/HyperloopUPV-H8/h9-backend/internal/utils"
"github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction"
+ adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj"
"github.com/HyperloopUPV-H8/h9-backend/pkg/transport"
"github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/tcp"
"github.com/HyperloopUPV-H8/h9-backend/pkg/transport/network/udp"
diff --git a/backend/cmd/setup_vehicle.go b/backend/cmd/setup_vehicle.go
index b8a7b62e0..8fe8999e2 100644
--- a/backend/cmd/setup_vehicle.go
+++ b/backend/cmd/setup_vehicle.go
@@ -12,12 +12,12 @@ import (
h "github.com/HyperloopUPV-H8/h9-backend/pkg/http"
"github.com/HyperloopUPV-H8/h9-backend/pkg/websocket"
- adj_module "github.com/HyperloopUPV-H8/h9-backend/internal/adj"
"github.com/HyperloopUPV-H8/h9-backend/internal/common"
"github.com/HyperloopUPV-H8/h9-backend/internal/config"
"github.com/HyperloopUPV-H8/h9-backend/internal/pod_data"
"github.com/HyperloopUPV-H8/h9-backend/internal/update_factory"
"github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction"
+ adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj"
"github.com/HyperloopUPV-H8/h9-backend/pkg/broker"
connection_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/connection"
data_topic "github.com/HyperloopUPV-H8/h9-backend/pkg/broker/topics/data"
diff --git a/backend/internal/pod_data/measurement.go b/backend/internal/pod_data/measurement.go
index 446b88611..b2581c4e3 100644
--- a/backend/internal/pod_data/measurement.go
+++ b/backend/internal/pod_data/measurement.go
@@ -4,9 +4,9 @@ import (
"fmt"
"strings"
- "github.com/HyperloopUPV-H8/h9-backend/internal/adj"
"github.com/HyperloopUPV-H8/h9-backend/internal/common"
"github.com/HyperloopUPV-H8/h9-backend/internal/utils"
+ "github.com/HyperloopUPV-H8/h9-backend/pkg/adj"
)
const EnumType = "enum"
diff --git a/backend/internal/pod_data/pod_data.go b/backend/internal/pod_data/pod_data.go
index 7cafa1411..3f091aaaa 100644
--- a/backend/internal/pod_data/pod_data.go
+++ b/backend/internal/pod_data/pod_data.go
@@ -3,8 +3,8 @@ package pod_data
import (
"github.com/HyperloopUPV-H8/h9-backend/internal/utils"
- "github.com/HyperloopUPV-H8/h9-backend/internal/adj"
"github.com/HyperloopUPV-H8/h9-backend/internal/common"
+ "github.com/HyperloopUPV-H8/h9-backend/pkg/adj"
)
func NewPodData(adjBoards map[string]adj.Board, globalUnits map[string]utils.Operations) (PodData, error) {
diff --git a/backend/internal/adj/adj.go b/backend/pkg/adj/adj.go
similarity index 100%
rename from backend/internal/adj/adj.go
rename to backend/pkg/adj/adj.go
diff --git a/backend/internal/adj/boards.go b/backend/pkg/adj/boards.go
similarity index 100%
rename from backend/internal/adj/boards.go
rename to backend/pkg/adj/boards.go
diff --git a/backend/internal/adj/git.go b/backend/pkg/adj/git.go
similarity index 100%
rename from backend/internal/adj/git.go
rename to backend/pkg/adj/git.go
diff --git a/backend/internal/adj/models.go b/backend/pkg/adj/models.go
similarity index 100%
rename from backend/internal/adj/models.go
rename to backend/pkg/adj/models.go
diff --git a/electron-app/README.md b/electron-app/README.md
index 854930745..7034c163e 100644
--- a/electron-app/README.md
+++ b/electron-app/README.md
@@ -79,7 +79,7 @@ pnpm run dist:linux # Linux
On macOS, the backend requires the loopback address `127.0.0.9` to be configured. If you encounter a "can't assign requested address" error when starting the backend, run:
```
-sudo ipconfig set en0 INFORM 127.0.0.9
+sudo ifconfig lo0 alias 127.0.0.9 up
```
## Available Scripts
@@ -89,6 +89,7 @@ sudo ipconfig set en0 INFORM 127.0.0.9
- `pnpm start` - Run application in development mode
- `pnpm run dist` - Build production executable
- `pnpm test` - Run tests
+- `pnpm build-icons` - build icon from the icon.png file in the `/electron-app` folder
...and many custom variations (see package.json)
# Only works and makes sense after running `pnpm run dist`
diff --git a/electron-app/build.mjs b/electron-app/build.mjs
index f83964a19..b4dff1753 100644
--- a/electron-app/build.mjs
+++ b/electron-app/build.mjs
@@ -5,7 +5,7 @@
*/
import { execSync } from "child_process";
-import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync } from "fs";
+import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { logger } from "./src/utils/logger.js";
@@ -20,6 +20,7 @@ const CONFIG = {
type: "go",
path: join(ROOT, "backend"), // Root of backend (where package.json is)
output: join(__dirname, "binaries"),
+ entry: "./cmd",
commands: ["pnpm run build:ci"],
platforms: [
{
@@ -52,18 +53,43 @@ const CONFIG = {
},
],
},
- "packet-sender": {
- type: "rust",
- path: join(ROOT, "packet-sender"),
- output: join(__dirname, "binaries"),
- commands: ["pnpm run build"],
- binaryPath: "target/release/packet-sender",
- platforms: [
- { id: "win64", ext: ".exe", tags: ["win", "windows"] },
- { id: "linux64", ext: "", tags: ["linux"] },
- { id: "mac64", ext: "", tags: ["mac", "macos"] },
- ],
- },
+ // "packet-sender": {
+ // type: "go",
+ // path: join(ROOT, "packet-sender"),
+ // output: join(__dirname, "binaries"),
+ // entry: ".",
+ // commands: ["pnpm run build:ci"],
+ // platforms: [
+ // {
+ // id: "win64",
+ // goos: "windows",
+ // goarch: "amd64",
+ // ext: ".exe",
+ // tags: ["win", "windows"],
+ // },
+ // {
+ // id: "linux64",
+ // goos: "linux",
+ // goarch: "amd64",
+ // ext: "",
+ // tags: ["linux"],
+ // },
+ // {
+ // id: "mac64",
+ // goos: "darwin",
+ // goarch: "amd64",
+ // ext: "",
+ // tags: ["mac", "macos"],
+ // },
+ // {
+ // id: "macArm",
+ // goos: "darwin",
+ // goarch: "arm64",
+ // ext: "",
+ // tags: ["mac", "macos"],
+ // },
+ // ],
+ // },
"testing-view": {
type: "frontend",
path: join(ROOT, "frontend/testing-view"),
@@ -98,8 +124,8 @@ const run = (cmd, cwd, env = {}) => {
}
};
-const buildBackend = (config, requestedPlatforms, extraArgs = "") => {
- logger.info("Building Backend (Go)...");
+const buildGo = (name, config, requestedPlatforms, extraArgs = "") => {
+ logger.info(`Building ${name} (Go)...`);
mkdirSync(config.output, { recursive: true });
const targets = config.platforms.filter((p) => {
@@ -112,22 +138,15 @@ const buildBackend = (config, requestedPlatforms, extraArgs = "") => {
return p.tags.some((tag) => requestedPlatforms.includes(tag));
});
- if (targets.length === 0) {
- logger.error(
- `No matching platforms found for: ${requestedPlatforms.join(", ")}`
- );
- return false;
- }
-
let success = true;
for (const p of targets) {
- const filename = `backend-${p.goos}-${p.goarch}${p.ext}`;
+ const filename = `${name}-${p.goos}-${p.goarch}${p.ext}`;
logger.step(`Building ${p.goos}/${p.goarch}...`);
+ const entryPath = config.entry || ".";
+
for (const cmd of config.commands) {
- // cmd is like "pnpm run build:ci --"
- // We append the output flag and target directory
- const buildCmd = `${cmd} -o "${join(config.output, filename)}" ${extraArgs} ./cmd`;
+ const buildCmd = `${cmd} -o "${join(config.output, filename)}" ${extraArgs} ${entryPath}`;
const result = run(buildCmd, config.path, {
GOOS: p.goos,
@@ -145,37 +164,6 @@ const buildBackend = (config, requestedPlatforms, extraArgs = "") => {
return success;
};
-const buildRust = (name, config, requestedPlatforms, extraArgs = "") => {
- logger.info(`Building ${name} (Rust)...`);
- mkdirSync(config.output, { recursive: true });
-
- for (const cmd of config.commands) {
- // Only append extra args to build commands
- const finalCmd = cmd.includes("build") ? `${cmd} ${extraArgs}` : cmd;
- if (!run(finalCmd, config.path)) return false;
- }
-
- const isWin =
- process.platform === "win32" ||
- (requestedPlatforms && requestedPlatforms.includes("win"));
- const ext = isWin ? ".exe" : "";
-
- // Check for source binary
- const sourceBin = join(config.path, config.binaryPath + ext);
- const destName = `packet-sender${ext}`;
- const destPath = join(config.output, destName);
-
- logger.step(`Copying binary to ${destPath}...`);
-
- if (existsSync(sourceBin)) {
- copyFileSync(sourceBin, destPath);
- return true;
- } else {
- logger.error(`Rust binary not found at ${sourceBin}`);
- return false;
- }
-};
-
const buildFrontend = (name, config, extraArgs = "") => {
if (config.optional && !existsSync(join(config.path, "package.json"))) {
logger.warning(`Skipping ${name} (not initialized)`);
@@ -252,9 +240,7 @@ logger.header("Hyperloop Control Station Build");
let success = true;
if (config.type === "go") {
- success = buildBackend(config, requestedPlatforms, extraArgs);
- } else if (config.type === "rust") {
- success = buildRust(key, config, requestedPlatforms, extraArgs);
+ success = buildGo(key, config, requestedPlatforms, extraArgs);
} else if (config.type === "frontend") {
success = buildFrontend(key, config, extraArgs);
if (success && !config.optional) frontendBuilt = true;
diff --git a/electron-app/main.js b/electron-app/main.js
index ef5841b58..fd7a0a40d 100644
--- a/electron-app/main.js
+++ b/electron-app/main.js
@@ -15,6 +15,22 @@ import { createWindow } from "./src/windows/mainWindow.js";
const { autoUpdater } = pkg;
+// Disable sandbox for Linux
+if (process.platform === "linux") {
+ try {
+ const userns = fs
+ .readFileSync("/proc/sys/kernel/unprivileged_userns_clone", "utf8")
+ .trim();
+ if (userns === "0") {
+ app.commandLine.appendSwitch("no-sandbox");
+ }
+ } catch (e) {}
+
+ if (process.getuid && process.getuid() === 0) {
+ app.commandLine.appendSwitch("no-sandbox");
+ }
+}
+
// Setup IPC handlers for renderer process communication
setupIpcHandlers();
diff --git a/electron-app/package.json b/electron-app/package.json
index a37d20f36..30344aa16 100644
--- a/electron-app/package.json
+++ b/electron-app/package.json
@@ -106,7 +106,9 @@
"linux": {
"target": [
"AppImage",
- "deb"
+ "deb",
+ "rpm",
+ "pacman"
],
"icon": "icons/512x512.png",
"category": "Utility",
diff --git a/electron-app/src/menu/menu.js b/electron-app/src/menu/menu.js
index c0436ab79..64f776da9 100644
--- a/electron-app/src/menu/menu.js
+++ b/electron-app/src/menu/menu.js
@@ -83,7 +83,7 @@ function createMenu(mainWindow) {
}
const packetSenderProcess = getPacketSenderProcess();
if (!packetSenderProcess || packetSenderProcess.killed) {
- startPacketSender(["random"]);
+ startPacketSender();
}
},
},
diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js
index 1e215619d..747336e3d 100644
--- a/electron-app/src/processes/backend.js
+++ b/electron-app/src/processes/backend.js
@@ -86,7 +86,7 @@ function startBackend() {
// If the backend didn't fail in this period of time, resolve the promise
setTimeout(() => {
resolve(backendProcess);
- }, 1000);
+ }, 4000);
// Handle process exit
backendProcess.on("close", (code) => {
diff --git a/electron-app/src/processes/packetSender.js b/electron-app/src/processes/packetSender.js
index e6efc5cf9..74b653fa1 100644
--- a/electron-app/src/processes/packetSender.js
+++ b/electron-app/src/processes/packetSender.js
@@ -12,16 +12,18 @@ import { getBinaryPath } from "../utils/paths.js";
// Store the packet sender process instance
let packetSenderProcess = null;
+// Default arguments for packet sender
+const DEFAULT_ARGS = ["1", "1"]; // Send mode, Random type
+
/**
* Starts the packet sender process by spawning the packet-sender binary with optional arguments.
* Sets up event handlers for stdout and stderr with appropriate logging.
* @param {string[]} [args=[]] - Optional array of command-line arguments to pass to the packet-sender binary.
* @returns {import("child_process").ChildProcessWithoutNullStreams | null} The spawned ChildProcess object, or null if the binary is not found.
* @example
- * const process = startPacketSender(["--port", "8080"]);
* startPacketSender();
*/
-function startPacketSender(args = []) {
+function startPacketSender(args = DEFAULT_ARGS) {
// Get the path to the packet-sender binary
const packetSenderBin = getBinaryPath("packet-sender");
@@ -44,6 +46,14 @@ function startPacketSender(args = []) {
// Log stdout output from packet sender
process.stdout.on("data", (data) => {
logger.packetSender.info(`${data.toString().trim()}`);
+
+ if (data.toString().includes("1) Send packets")) {
+ process.stdin.write("1\n");
+ }
+
+ if (data.toString().includes("1) Random")) {
+ process.stdin.write("1\n");
+ }
});
// Log stderr output as errors
@@ -90,7 +100,7 @@ function restartPacketSender() {
// Wait before starting new process to ensure cleanup
setTimeout(() => {
// Start with help arguments
- startPacketSender(["random"]);
+ startPacketSender();
}, 500);
}
}
diff --git a/electron-app/src/utils/paths.js b/electron-app/src/utils/paths.js
index f7bf70033..1d44ceb39 100644
--- a/electron-app/src/utils/paths.js
+++ b/electron-app/src/utils/paths.js
@@ -41,13 +41,6 @@ function getBinaryPath(name) {
const arch = process.arch;
const ext = platform === "win32" ? ".exe" : "";
- if (name === "packet-sender") {
- if (!app.isPackaged) {
- return path.join(getAppPath(), "binaries", `${name}${ext}`);
- }
- return path.join(process.resourcesPath, "binaries", `${name}${ext}`);
- }
-
const goosMap = {
win32: "windows",
darwin: "darwin",
diff --git a/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx b/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx
index eb3980a2f..8ece18567 100644
--- a/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx
+++ b/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx
@@ -20,21 +20,27 @@ export const MultiCheckboxField = ({
- {field.options?.map((opt) => (
-
- handleToggle(opt, !!checked)}
- />
-
-
- ))}
+ {!field.options || field.options.length === 0 ? (
+
+ No boards detected. Connect to the backend to see available options.
+
+ ) : (
+ field.options?.map((opt) => (
+
+ handleToggle(opt, !!checked)}
+ />
+
+
+ ))
+ )}
);
diff --git a/frontend/testing-view/src/components/settings/SettingsForm.tsx b/frontend/testing-view/src/components/settings/SettingsForm.tsx
index 96357b57e..c69512c55 100644
--- a/frontend/testing-view/src/components/settings/SettingsForm.tsx
+++ b/frontend/testing-view/src/components/settings/SettingsForm.tsx
@@ -1,5 +1,7 @@
import { get, set } from "lodash";
-import { SETTINGS_SCHEMA } from "../../constants/settingsSchema";
+import { useMemo } from "react";
+import { getSettingsSchema } from "../../constants/settingsSchema";
+import { useStore } from "../../store/store";
import type { ConfigData } from "../../types/common/config";
import type { SettingField } from "../../types/common/settings";
import { BooleanField } from "./BooleanField";
@@ -23,6 +25,9 @@ export const SettingsForm = ({ config, onChange }: SettingsFormProps) => {
onChange(nextConfig);
};
+ const boards = useStore((s) => s.boards);
+ const schema = useMemo(() => getSettingsSchema(boards), [boards]);
+
const renderField = (field: SettingField) => {
const currentValue = get(config, field.path);
@@ -94,9 +99,9 @@ export const SettingsForm = ({ config, onChange }: SettingsFormProps) => {
return (
- {SETTINGS_SCHEMA.map((section) => (
+ {schema.map((section) => (
-
+
{section.title}
diff --git a/frontend/testing-view/src/constants/boards.ts b/frontend/testing-view/src/constants/boards.ts
deleted file mode 100644
index 68a8a4127..000000000
--- a/frontend/testing-view/src/constants/boards.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/** List of names of available boards. */
-export const BOARD_NAMES: readonly string[] = [
- "BCU", // Battery Control Unit
- "PCU", // Propulsion Control Unit
- "LCU", // Levitation Control Unit
- "HVSCU", // High Voltage System Control Unit
- "BMSL", // Battery Management System Level
- "VCU", // Vehicle Control Unit
- "HVSCU-Cabinet", // High Voltage System Control Unit Cabinet
-];
diff --git a/frontend/testing-view/src/constants/settingsSchema.ts b/frontend/testing-view/src/constants/settingsSchema.ts
index eecc75f85..28561ea8c 100644
--- a/frontend/testing-view/src/constants/settingsSchema.ts
+++ b/frontend/testing-view/src/constants/settingsSchema.ts
@@ -1,8 +1,8 @@
import type { SettingsSection } from "../types/common/settings";
-import { BOARD_NAMES } from "./boards";
+import type { BoardName } from "../types/data/board";
/** Settings form is generated from this schema. */
-export const SETTINGS_SCHEMA: SettingsSection[] = [
+export const getSettingsSchema = (boards: BoardName[]): SettingsSection[] => [
{
title: "Vehicle Configuration",
fields: [
@@ -10,7 +10,7 @@ export const SETTINGS_SCHEMA: SettingsSection[] = [
label: "Boards",
path: "vehicle.boards",
type: "multi-checkbox",
- options: BOARD_NAMES as string[],
+ options: boards,
},
],
},
diff --git a/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx b/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx
index 747f80aff..05f2a2467 100644
--- a/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx
+++ b/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx
@@ -22,7 +22,7 @@ export const FilterCategoryItem = ({ category }: FilterCategoryItemProps) => {
const toggleCategoryFilter = useStore((s) => s.toggleCategoryFilter);
const toggleItemFilter = useStore((s) => s.toggleItemFilter);
- const items = useStore((s) => s.getCatalog(scope)[category]);
+ const items = useStore((s) => s.getCatalog(scope)[category]) || [];
const totalItems = items.length;
const selectedIds = useStore(
@@ -61,7 +61,7 @@ export const FilterCategoryItem = ({ category }: FilterCategoryItemProps) => {
- {items.map((item) => (
+ {items?.map((item) => (
{
const { isOpen, scope } = useStore((s) => s.filterDialog);
const close = useStore((s) => s.closeFilterDialog);
+ const boards = useStore((s) => s.boards);
+ const activeFilters = useStore(useShallow((s) => s.getActiveFilters(scope)));
+
const clearFilters = useStore((s) => s.clearFilters);
const selectAllFilters = useStore((s) => s.selectAllFilters);
if (!scope) return null;
+ const extraBoards = detectExtraBoards(activeFilters, boards);
+
return (
{
onClose={close}
onClearAll={() => clearFilters(scope)}
onSelectAll={() => selectAllFilters(scope)}
- categories={BOARD_NAMES}
+ categories={boards}
+ extraCategories={extraBoards}
FilterCategoryComponent={FilterCategoryItem}
/>
);
diff --git a/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx b/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx
index 6c72ba88c..f211b2b33 100644
--- a/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx
+++ b/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx
@@ -6,6 +6,7 @@ import {
DialogHeader,
DialogTitle,
} from "@workspace/ui";
+import { AlertTriangle } from "@workspace/ui/icons";
import { type ComponentType } from "react";
import type { BoardName } from "../../../types/data/board";
@@ -17,6 +18,7 @@ interface FilterDialogProps {
onClearAll: () => void;
onSelectAll: () => void;
categories: readonly BoardName[];
+ extraCategories: readonly BoardName[];
FilterCategoryComponent: ComponentType<{ category: BoardName }>;
}
@@ -28,11 +30,13 @@ export const FilterDialog = ({
onClearAll,
onSelectAll,
categories,
+ extraCategories,
FilterCategoryComponent,
}: FilterDialogProps) => {
+ console.log(extraCategories);
return (
+ {extraCategories.length > 0 && (
+
+
+
+ Stale filters detected
+
+
+ The following boards are in your saved filters but not in the
+ current configuration:{" "}
+
+ {extraCategories.join(", ")}
+
+
+
+
+ )}
+
{categories.map((category) => (
diff --git a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts
index 1bcc4cc6d..58cefaccc 100644
--- a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts
+++ b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts
@@ -38,7 +38,7 @@ export interface FilteringSlice {
workspaceFilters: Record
;
initializeWorkspaceFilters: () => void;
updateFilters: (scope: FilterScope, filters: TabFilter) => void;
- getActiveFilters: (scope: FilterScope) => TabFilter | undefined;
+ getActiveFilters: (scope: FilterScope | null) => TabFilter | undefined;
/** Filter Actions */
selectAllFilters: (scope: FilterScope) => void;
@@ -135,7 +135,7 @@ export const createFilteringSlice: StateCreator<
const currentWorkspaceFilters = get().workspaceFilters[workspaceId] || {};
const currentTabFilter =
- currentWorkspaceFilters[scope] || createEmptyFilter();
+ currentWorkspaceFilters[scope] || createEmptyFilter(get().boards);
const currentCategoryIds = currentTabFilter[category] || [];
@@ -157,13 +157,13 @@ export const createFilteringSlice: StateCreator<
const items = get().getCatalog(scope);
- const fullFilter = createFullFilter(items);
+ const fullFilter = createFullFilter(items, get().boards);
get().updateFilters(scope, fullFilter);
},
clearFilters: (scope) => {
const workspaceId = get().getActiveWorkspaceId();
if (!workspaceId) return;
- const emptyFilter = createEmptyFilter();
+ const emptyFilter = createEmptyFilter(get().boards);
get().updateFilters(scope, emptyFilter);
},
toggleCategoryFilter: (scope, category, checked) => {
@@ -173,7 +173,8 @@ export const createFilteringSlice: StateCreator<
const catalog = get().getCatalog(scope);
const currentFilters =
- get().workspaceFilters[workspaceId]?.[scope] || createEmptyFilter();
+ get().workspaceFilters[workspaceId]?.[scope] ||
+ createEmptyFilter(get().boards);
const newItems = checked
? catalog?.[category]?.map((item) => item.id) || []
@@ -196,9 +197,9 @@ export const createFilteringSlice: StateCreator<
if (Object.keys(currentFilters).length === 0) {
set({
workspaceFilters: generateInitialFilters({
- commands: createFullFilter(commands),
- telemetry: createFullFilter(telemetry),
- logs: createFullFilter(telemetry),
+ commands: createFullFilter(commands, get().boards),
+ telemetry: createFullFilter(telemetry, get().boards),
+ logs: createFullFilter(telemetry, get().boards),
}),
});
}
@@ -228,6 +229,7 @@ export const createFilteringSlice: StateCreator<
// Helper getters
getActiveFilters: (scope) => {
const id = get().getActiveWorkspaceId();
+ if (!scope) return {};
return id ? get().workspaceFilters[id]?.[scope] : undefined;
},
diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx
index 60b1a0876..59b3dbaff 100644
--- a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx
+++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/CommandsSection.tsx
@@ -1,15 +1,19 @@
-import { BOARD_NAMES } from "../../../../../constants/boards";
+import { useStore } from "../../../../../store/store";
import type { CommandCatalogItem } from "../../../../../types/data/commandCatalogItem";
import { CommandItem } from "../tabs/commands/CommandItem";
import { Tab } from "../tabs/Tab";
-export const CommandsSection = () => (
- (
-
- )}
- />
-);
+export const CommandsSection = () => {
+ const boards = useStore((s) => s.boards);
+
+ return (
+ (
+
+ )}
+ />
+ );
+};
diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx
index 4798da54d..3cb19cfce 100644
--- a/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx
+++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TelemetrySection.tsx
@@ -1,16 +1,20 @@
-import { BOARD_NAMES } from "../../../../../constants/boards";
+import { useStore } from "../../../../../store/store";
import type { TelemetryCatalogItem } from "../../../../../types/data/telemetryCatalogItem";
import { Tab } from "../tabs/Tab";
import { TelemetryItem } from "../tabs/telemetry/TelemetryItem";
-export const TelemetrySection = () => (
- (
-
- )}
- virtualized
- />
-);
+export const TelemetrySection = () => {
+ const boards = useStore((s) => s.boards);
+
+ return (
+ (
+
+ )}
+ virtualized
+ />
+ );
+};
diff --git a/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx
index 08730d14c..e93c10110 100644
--- a/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx
+++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx
@@ -1,5 +1,7 @@
import { Button } from "@workspace/ui";
-import { ListFilterPlus } from "@workspace/ui/icons";
+import { AlertTriangle, ListFilterPlus } from "@workspace/ui/icons";
+import { useShallow } from "zustand/shallow";
+import { detectExtraBoards } from "../../../../../lib/utils";
import { useStore } from "../../../../../store/store";
import type { SidebarTab } from "../../../types/sidebar";
@@ -13,23 +15,40 @@ export const TabHeader = ({ title, scope }: TabHeaderProps) => {
const totalCount = useStore((state) => state.getTotalCount(scope));
const filteredCount = useStore((state) => state.getFilteredCount(scope));
+ const boards = useStore((s) => s.boards);
+ const activeFilters = useStore(useShallow((s) => s.getActiveFilters(scope)));
+ const extraBoards = detectExtraBoards(activeFilters, boards);
+
return (
-
-
- {title}
-
- {filteredCount} / {totalCount}
-
-
-
+
+
+
+ {title}
+
+ {filteredCount} / {totalCount}
+
+
+
+
+
+ {/* Warning for stale boards */}
+ {extraBoards.length > 0 && (
+
+
+
{extraBoards.length} stale board(s) affecting counts
+
+ )}
);
};
diff --git a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts
index ea09a6844..28a1be861 100644
--- a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts
+++ b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts
@@ -63,9 +63,9 @@ export const createWorkspacesSlice: StateCreator<
const newWorkspaceFilters = {
...state.workspaceFilters,
[newWorkspaceId]: {
- commands: createFullFilter(commands),
- telemetry: createFullFilter(telemetry),
- logs: createFullFilter(telemetry),
+ commands: createFullFilter(commands, get().boards),
+ telemetry: createFullFilter(telemetry, get().boards),
+ logs: createFullFilter(telemetry, get().boards),
},
};
diff --git a/frontend/testing-view/src/hooks/useBoardData.ts b/frontend/testing-view/src/hooks/useBoardData.ts
index 22ff3d8f9..3f40f4cfc 100644
--- a/frontend/testing-view/src/hooks/useBoardData.ts
+++ b/frontend/testing-view/src/hooks/useBoardData.ts
@@ -90,6 +90,8 @@ export function useBoardData(
logger.testingView.log("[useBoardData] Commands data processed");
+ console.log("availableBoards", availableBoards);
+
return {
telemetryCatalog: telemetryCatalogResult,
commandsCatalog: commandsCatalogResult,
diff --git a/frontend/testing-view/src/hooks/useTransformedBoards.ts b/frontend/testing-view/src/hooks/useTransformedBoards.ts
index c8fc04c40..aa7cda62b 100644
--- a/frontend/testing-view/src/hooks/useTransformedBoards.ts
+++ b/frontend/testing-view/src/hooks/useTransformedBoards.ts
@@ -12,6 +12,7 @@ export function useTransformedBoards(
const setTelemetryCatalog = useStore((s) => s.setTelemetryCatalog);
const setCommandsCatalog = useStore((s) => s.setCommandsCatalog);
+ const setBoards = useStore((s) => s.setBoards);
const initializeWorkspaceFilters = useStore(
(s) => s.initializeWorkspaceFilters,
);
@@ -25,7 +26,14 @@ export function useTransformedBoards(
setTelemetryCatalog(transformedBoards.telemetryCatalog);
setCommandsCatalog(transformedBoards.commandsCatalog);
- initializeWorkspaceFilters();
+ setBoards(Array.from(transformedBoards.boards));
+
+ const hasTelemetryData =
+ Object.keys(transformedBoards.telemetryCatalog).length > 0;
+ const hasCommandsData =
+ Object.keys(transformedBoards.commandsCatalog).length > 0;
+
+ if (hasTelemetryData && hasCommandsData) initializeWorkspaceFilters();
}, [
transformedBoards,
setTelemetryCatalog,
diff --git a/frontend/testing-view/src/lib/utils.test.ts b/frontend/testing-view/src/lib/utils.test.ts
index 947b6c5c7..fa667af7a 100644
--- a/frontend/testing-view/src/lib/utils.test.ts
+++ b/frontend/testing-view/src/lib/utils.test.ts
@@ -105,7 +105,17 @@ describe("getTypeBadgeClass", () => {
describe("emptyFilter", () => {
it("should return the correct empty filter", () => {
- expect(createEmptyFilter()).toStrictEqual({
+ const boards = [
+ "BCU",
+ "PCU",
+ "LCU",
+ "HVSCU",
+ "HVSCU-Cabinet",
+ "BMSL",
+ "VCU",
+ ];
+
+ expect(createEmptyFilter(boards)).toStrictEqual({
BCU: [],
PCU: [],
LCU: [],
@@ -133,7 +143,17 @@ describe("fullFilter", () => {
VCU: [],
};
- expect(createFullFilter(testDataSource)).toStrictEqual({
+ const boards = [
+ "BCU",
+ "PCU",
+ "LCU",
+ "HVSCU",
+ "HVSCU-Cabinet",
+ "BMSL",
+ "VCU",
+ ];
+
+ expect(createFullFilter(testDataSource, boards)).toStrictEqual({
BCU: [1],
PCU: [2],
LCU: [3],
diff --git a/frontend/testing-view/src/lib/utils.ts b/frontend/testing-view/src/lib/utils.ts
index fe0306338..0df30e9c3 100644
--- a/frontend/testing-view/src/lib/utils.ts
+++ b/frontend/testing-view/src/lib/utils.ts
@@ -1,5 +1,4 @@
import { ACRONYMS } from "../constants/acronyms";
-import { BOARD_NAMES } from "../constants/boards";
import { variablesBadgeClasses } from "../constants/variablesBadgeClasses";
import type {
FilterScope,
@@ -29,8 +28,8 @@ export const generateInitialFilters = (
);
};
-export const createEmptyFilter = (): TabFilter => {
- return BOARD_NAMES.reduce((acc, category) => {
+export const createEmptyFilter = (boards: BoardName[]): TabFilter => {
+ return boards.reduce((acc, category) => {
acc[category] = [];
return acc;
}, {} as TabFilter);
@@ -38,8 +37,9 @@ export const createEmptyFilter = (): TabFilter => {
export const createFullFilter = (
dataSource: Record
,
+ boards: BoardName[],
): TabFilter => {
- return BOARD_NAMES.reduce((acc, category) => {
+ return boards.reduce((acc, category) => {
acc[category] = dataSource[category]?.map((item) => item.id) || [];
return acc;
}, {} as TabFilter);
@@ -119,3 +119,11 @@ export const formatTimestamp = (ts: MessageTimestamp) => {
if (!ts) return "00:00:00";
return `${ts.hour.toString().padStart(2, "0")}:${ts.minute.toString().padStart(2, "0")}:${ts.second.toString().padStart(2, "0")}`;
};
+
+export const detectExtraBoards = (
+ activeFilters: TabFilter | undefined,
+ boards: BoardName[],
+) =>
+ Object.keys(activeFilters || {}).filter(
+ (key) => !boards.includes(key),
+ ) as BoardName[];
diff --git a/frontend/testing-view/src/store/slices/catalogSlice.ts b/frontend/testing-view/src/store/slices/catalogSlice.ts
index e2e60017f..dcd365534 100644
--- a/frontend/testing-view/src/store/slices/catalogSlice.ts
+++ b/frontend/testing-view/src/store/slices/catalogSlice.ts
@@ -15,6 +15,10 @@ export interface CatalogSlice {
setTelemetryCatalog: (
telemetryCatalog: Record,
) => void;
+
+ // Boards
+ boards: BoardName[];
+ setBoards: (boards: BoardName[]) => void;
}
export const createCatalogSlice: StateCreator = (
@@ -24,4 +28,6 @@ export const createCatalogSlice: StateCreator = (
telemetryCatalog: {} as Record,
setCommandsCatalog: (commandsCatalog) => set({ commandsCatalog }),
setTelemetryCatalog: (telemetryCatalog) => set({ telemetryCatalog }),
+ boards: [] as BoardName[],
+ setBoards: (boards) => set({ boards }),
});
diff --git a/go.work b/go.work
index 521811c76..957518330 100644
--- a/go.work
+++ b/go.work
@@ -2,4 +2,5 @@ go 1.23.1
use (
./backend
+ ./packet-sender
)
diff --git a/packet-sender/main.go b/packet-sender/main.go
index 9a52ccb99..f748519f3 100644
--- a/packet-sender/main.go
+++ b/packet-sender/main.go
@@ -7,8 +7,6 @@ import (
boardpkg "packet_sender/pkg/board"
"packet_sender/pkg/listener"
"packet_sender/pkg/sender"
- "path"
- "path/filepath"
"strings"
adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj"
@@ -66,11 +64,13 @@ func getConn(lip string, lport uint16, rip string, rport uint16) *net.UDPConn {
// getADJ loads the same ADJ used by backend directly from backend/cmd/adj.
func getADJ() adj_module.ADJ {
- adjPath, err := filepath.Abs(path.Join("..", "backend", "cmd", "adj"))
- if err != nil {
- log.Fatalf("Failed to resolve ADJ path: %v", err)
- }
- adj_module.RepoPath = adjPath + string(filepath.Separator)
+ // adjPath, err := filepath.Abs(path.Join("..", "backend", "cmd", "adj"))
+ // if err != nil {
+ // log.Fatalf("Failed to resolve ADJ path: %v", err)
+ // }
+ // adj_module.RepoPath = adjPath + string(filepath.Separator)
+
+ // Uses the same ADJ RepoPath as the backend by default
adj, err := adj_module.NewADJ("")
if err != nil {
diff --git a/packet-sender/package.json b/packet-sender/package.json
new file mode 100644
index 000000000..d96551e08
--- /dev/null
+++ b/packet-sender/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "packet-sender",
+ "version": "1.0.0",
+ "private": true,
+ "author": "Hyperloop UPV Team",
+ "license": "MIT",
+ "scripts": {
+ "build": "go build -o packet-sender main.go",
+ "build:ci": "go build",
+ "test": "go test ./..."
+ }
+}