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..ea52752c4 100644
--- a/electron-app/main.js
+++ b/electron-app/main.js
@@ -4,17 +4,34 @@
* Handles application lifecycle, initialization, and cleanup of processes and windows.
*/
-import { app, BrowserWindow, dialog } from "electron";
+import { app, BrowserWindow, dialog, screen } from "electron";
import pkg from "electron-updater";
import { getConfigManager } from "./src/config/configInstance.js";
import { setupIpcHandlers } from "./src/ipc/handlers.js";
import { startBackend, stopBackend } from "./src/processes/backend.js";
import { stopPacketSender } from "./src/processes/packetSender.js";
import { logger } from "./src/utils/logger.js";
+import { createLogWindow } from "./src/windows/logWindow.js";
import { createWindow } from "./src/windows/mainWindow.js";
const { autoUpdater } = pkg;
+// 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();
@@ -22,15 +39,22 @@ app.setName("hyperloop-control-station");
// App lifecycle: wait for Electron to be ready
app.whenReady().then(async () => {
+ // Get the screen width and height
+ // Only can be used inside app.whenReady()
+ const { width: screenWidth, height: screenHeight } =
+ screen.getPrimaryDisplay().workAreaSize;
+
// Initialize ConfigManager and ensure config exists BEFORE starting backend
logger.electron.header("Initializing configuration...");
// Get ConfigManager instance (creates config from template if needed)
await getConfigManager();
logger.electron.header("Configuration ready");
+ const logWindow = createLogWindow(screenWidth, screenHeight);
+
// Start backend process
try {
- await startBackend();
+ await startBackend(logWindow);
logger.electron.header("Backend process spawned");
} catch (error) {
// Start backend already shows these errors
@@ -38,7 +62,9 @@ app.whenReady().then(async () => {
}
// Create main application window
- createWindow();
+ const mainWindow = createWindow(screenWidth, screenHeight);
+ mainWindow.maximize();
+
logger.electron.header("Main application window created");
// Updater setup
diff --git a/electron-app/package.json b/electron-app/package.json
index a37d20f36..c5957648d 100644
--- a/electron-app/package.json
+++ b/electron-app/package.json
@@ -38,6 +38,7 @@
},
"dependencies": {
"@iarna/toml": "^2.2.5",
+ "ansi-to-html": "^0.7.2",
"electron-store": "^11.0.2",
"electron-updater": "^6.7.3",
"picocolors": "^1.1.1"
@@ -106,7 +107,9 @@
"linux": {
"target": [
"AppImage",
- "deb"
+ "deb",
+ "rpm",
+ "pacman"
],
"icon": "icons/512x512.png",
"category": "Utility",
diff --git a/electron-app/preload.js b/electron-app/preload.js
index 761fa8dcf..2bc27909c 100644
--- a/electron-app/preload.js
+++ b/electron-app/preload.js
@@ -36,4 +36,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
importConfig: () => ipcRenderer.invoke("import-config"),
// Open folder selection dialog
selectFolder: () => ipcRenderer.invoke("select-folder"),
+ // Receive log message from backend
+ onLog: (callback) =>
+ ipcRenderer.on("log", (_event, value) => callback(value)),
});
diff --git a/electron-app/src/log-viewer/index.html b/electron-app/src/log-viewer/index.html
new file mode 100644
index 000000000..c9496d24a
--- /dev/null
+++ b/electron-app/src/log-viewer/index.html
@@ -0,0 +1,34 @@
+
+
+
+ Backend Logs
+
+
+
+
+
+
+
+
diff --git a/electron-app/src/menu/menu.js b/electron-app/src/menu/menu.js
index c0436ab79..c2000533d 100644
--- a/electron-app/src/menu/menu.js
+++ b/electron-app/src/menu/menu.js
@@ -30,7 +30,11 @@ function createMenu(mainWindow) {
{
label: "Reload",
accelerator: "CmdOrCtrl+R",
- click: () => mainWindow.reload(),
+ click: (_, browserWindow) => {
+ if (browserWindow) {
+ browserWindow.reload();
+ }
+ },
},
{ type: "separator" },
{
@@ -61,7 +65,11 @@ function createMenu(mainWindow) {
{
label: "Toggle DevTools",
accelerator: "F12",
- click: () => mainWindow.webContents.toggleDevTools(),
+ click: (_, browserWindow) => {
+ if (browserWindow) {
+ browserWindow.webContents.toggleDevTools();
+ }
+ },
},
],
},
@@ -83,7 +91,7 @@ function createMenu(mainWindow) {
}
const packetSenderProcess = getPacketSenderProcess();
if (!packetSenderProcess || packetSenderProcess.killed) {
- startPacketSender(["random"]);
+ startPacketSender();
}
},
},
@@ -118,7 +126,7 @@ function createMenu(mainWindow) {
];
const menu = Menu.buildFromTemplate(template);
- Menu.setApplicationMenu(menu);
+ return menu;
}
export { createMenu };
diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js
index 1e215619d..9075f3b45 100644
--- a/electron-app/src/processes/backend.js
+++ b/electron-app/src/processes/backend.js
@@ -4,6 +4,7 @@
* Handles starting, stopping, and restarting the backend process with proper error handling and logging.
*/
+import AnsiToHtml from "ansi-to-html";
import { spawn } from "child_process";
import { app, dialog } from "electron";
import fs from "fs";
@@ -15,6 +16,9 @@ import {
getUserConfigPath,
} from "../utils/paths.js";
+// Create ANSI to HTML converter
+const convert = new AnsiToHtml();
+
// Get the application root path
const appPath = getAppPath();
@@ -30,7 +34,7 @@ let lastBackendError = null;
* @example
* startBackend();
*/
-function startBackend() {
+function startBackend(logWindow = null) {
return new Promise((resolve, reject) => {
// Get paths for binary and config
const backendBin = getBinaryPath("backend");
@@ -63,6 +67,12 @@ function startBackend() {
// Log stdout output from backend
backendProcess.stdout.on("data", (data) => {
logger.backend.info(`${data.toString().trim()}`);
+
+ // Send log message to log window
+ if (logWindow) {
+ const htmlData = convert.toHtml(data.toString().trim());
+ logWindow.webContents.send("log", htmlData);
+ }
});
// Capture stderr output (where Go errors/panics are written)
@@ -71,6 +81,12 @@ function startBackend() {
logger.backend.error(errorMsg);
// Store the last error message
lastBackendError = errorMsg;
+
+ // Send error message to log window
+ if (logWindow) {
+ const htmlError = convert.toHtml(errorMsg);
+ logWindow.webContents.send("log", htmlError);
+ }
});
// Handle spawn errors
@@ -86,7 +102,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) => {
@@ -120,8 +136,20 @@ function stopBackend() {
// Only stop if process exists and is still running
if (backendProcess && !backendProcess.killed) {
logger.backend.info("Stopping backend...");
- // Send termination signal
- backendProcess.kill("SIGTERM");
+
+ backendProcess.stdin.end();
+
+ const fallbackTimer = setTimeout(() => {
+ if (backendProcess && !backendProcess.killed) {
+ logger.backend.warning(
+ "Backend did not exit gracefully, force killing..."
+ );
+ backendProcess.kill("SIGKILL");
+ }
+ }, 2000);
+
+ fallbackTimer.unref();
+
// Clear the process reference
backendProcess = null;
}
diff --git a/electron-app/src/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/electron-app/src/windows/README.md b/electron-app/src/windows/README.md
index bb4e4ec5a..c13435b7a 100644
--- a/electron-app/src/windows/README.md
+++ b/electron-app/src/windows/README.md
@@ -4,24 +4,25 @@ Window management module for the Electron application. Handles creation, configu
## Overview
-Manages the primary Electron `BrowserWindow` instance and provides functionality for switching between different application views (Competition View and Testing View).
+Manages the primary and logs Electron `BrowserWindow` instances and provides functionality for switching between different application views (Competition View and Testing View).
## Files
+- `logWindow.js` - Backend logs and messages
- `mainWindow.js` - Main window creation and management
## Window Configuration
- **Default Size**: 1920x1080 pixels
-- **Minimum Size**: 1280x720 pixels
+- **Minimum Size**: 800x600 pixels
- **Title**: "Hyperloop Control Station"
- **Background Color**: `#1a1a1a` (dark theme)
- **Security**: Context isolation enabled, node integration disabled
## Available Views
-- **Ethernet View** (default) - Testing interface, loads from `renderer/ethernet-view/index.html`
-- **Control Station** - Competition interface, loads from `renderer/control-station/index.html`
+- **Testing View** (default) - Testing interface, loads from `renderer/testing-view/index.html`
+- **Competition View** - Competition interface, loads from `renderer/competition-view/index.html`
## Functions
diff --git a/electron-app/src/windows/logWindow.js b/electron-app/src/windows/logWindow.js
new file mode 100644
index 000000000..4c1c0e27e
--- /dev/null
+++ b/electron-app/src/windows/logWindow.js
@@ -0,0 +1,27 @@
+import { BrowserWindow } from "electron";
+import path from "path";
+
+import { getAppPath } from "../utils/paths.js";
+
+// Get the application root path
+const appPath = getAppPath();
+
+export const createLogWindow = (screenWidth, screenHeight) => {
+ const logWindow = new BrowserWindow({
+ x: Math.floor(screenWidth * 0.65),
+ y: 0,
+ width: Math.floor(screenWidth * 0.35),
+ height: screenHeight,
+ title: "Backend Logs",
+ webPreferences: {
+ preload: path.join(appPath, "preload.js"),
+ contextIsolation: true,
+ nodeIntegration: false,
+ },
+ });
+
+ const logFilePath = path.join(appPath, "src", "log-viewer", "index.html");
+ logWindow.loadFile(logFilePath);
+
+ return logWindow;
+};
diff --git a/electron-app/src/windows/mainWindow.js b/electron-app/src/windows/mainWindow.js
index 259f8dc79..a6b41380c 100644
--- a/electron-app/src/windows/mainWindow.js
+++ b/electron-app/src/windows/mainWindow.js
@@ -24,13 +24,15 @@ let currentView = "testing-view";
* @example
* createWindow();
*/
-function createWindow() {
+function createWindow(screenWidth, screenHeight) {
// Create new browser window with configuration
mainWindow = new BrowserWindow({
- width: 1920,
- height: 1080,
- minWidth: 1280,
- minHeight: 720,
+ x: 0,
+ y: 0,
+ width: screenWidth,
+ height: screenHeight,
+ minWidth: 800,
+ minHeight: 600,
webPreferences: {
// Path to preload script for secure IPC
preload: path.join(appPath, "preload.js"),
@@ -49,7 +51,8 @@ function createWindow() {
loadView(currentView);
// Create application menu
- createMenu(mainWindow);
+ const menu = createMenu(mainWindow);
+ mainWindow.setMenu(menu);
// Open DevTools in development mode
if (!app.isPackaged) {
@@ -60,6 +63,8 @@ function createWindow() {
mainWindow.on("closed", () => {
mainWindow = null;
});
+
+ return mainWindow;
}
/**
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 ./..."
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9698bd788..29d83d227 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -34,6 +34,9 @@ importers:
'@iarna/toml':
specifier: ^2.2.5
version: 2.2.5
+ ansi-to-html:
+ specifier: ^0.7.2
+ version: 0.7.2
electron-store:
specifier: ^11.0.2
version: 11.0.2
@@ -1805,6 +1808,11 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
+ ansi-to-html@0.7.2:
+ resolution: {integrity: sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==}
+ engines: {node: '>=8.0.0'}
+ hasBin: true
+
app-builder-bin@4.0.0:
resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==}
@@ -2392,6 +2400,9 @@ packages:
resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==}
engines: {node: '>=10.13.0'}
+ entities@2.2.0:
+ resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
+
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -6218,6 +6229,10 @@ snapshots:
ansi-styles@6.2.3: {}
+ ansi-to-html@0.7.2:
+ dependencies:
+ entities: 2.2.0
+
app-builder-bin@4.0.0: {}
app-builder-bin@5.0.0-alpha.12: {}
@@ -7045,6 +7060,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
+ entities@2.2.0: {}
+
env-paths@2.2.1: {}
env-paths@3.0.0: {}