Skip to content
Merged
6 changes: 6 additions & 0 deletions electron-app/src/app/initialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
* @description Application initialization: config setup and cleanup.
*/

import { Menu } from "electron";
import { getConfigManager } from "../config/configInstance.js";
import { setupIpcHandlers } from "../ipc/handlers.js";
import { createMenu } from "../menu/menu.js";
import { logger } from "../utils/logger.js";
import { cleanupLeftoverBackendProcesses } from "./cleanup.js";

/**
* Initializes the application:
* - Sets the application menu shared by all windows
* - Sets up IPC handlers
* - Initializes configuration
* - Cleans up leftover processes
* @returns {Promise<void>}
*/
async function initializeApp() {
// Application-wide menu so shortcuts (F11 fullscreen, F12 DevTools, ...) work in every window
Menu.setApplicationMenu(createMenu());

// Setup IPC handlers for renderer process communication
setupIpcHandlers();

Expand Down
6 changes: 4 additions & 2 deletions electron-app/src/ipc/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ function setupIpcHandlers() {
* @event setup-kernel
* @async
* @description Configures kernel parameters needed by the backend
* (disables the TCP invalid-packet rate limit). Linux only; on other
* platforms it shows a warning dialog.
* (disables the TCP invalid-packet rate limit and lowers the TCP
* retransmission limit). Linux only; on other platforms it shows a
* warning dialog.
* @returns {Promise<{success: boolean, message: string}>}
*/
ipcMain.handle("setup-kernel", async () => {
Expand All @@ -88,6 +89,7 @@ function setupIpcHandlers() {
"sysctl",
"-w",
"net.ipv4.tcp_invalid_ratelimit=0",
"net.ipv4.tcp_retries2=3",
]);
logger.electron.info(`Kernel setup completed: ${stdout.trim()}`);
return { success: true, message: "Kernel set up successfully" };
Expand Down
36 changes: 15 additions & 21 deletions electron-app/src/menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Application menu system for the Electron application. Provides the native menu b

## Overview

Creates and manages the native application menu bar with view switching, window controls, developer tools, and utility applications.
Creates the native application menu bar shared by all windows. Menu actions operate on the currently focused window, so shortcuts like fullscreen and DevTools work in the main window, the mode selector, and the log window alike.

## Files

Expand All @@ -14,48 +14,42 @@ Creates and manages the native application menu bar with view switching, window

### File Menu

- **Reload** (`CmdOrCtrl+R`) - Reloads the current window
- **Reload** (`CmdOrCtrl+R`) - Reloads the focused window
- **Return to Selector** (`CmdOrCtrl+Shift+S`) - Stops services and returns to the mode selector
- **Exit** (`CmdOrCtrl+Q`) - Quits the application

### View Menu
### Tools Menu

- **Control Station** (`CmdOrCtrl+1`) - Switches to Competition View
- **Ethernet View** (`CmdOrCtrl+2`) - Switches to Testing View
- **Toggle DevTools** (`F12`) - Opens/closes Chrome DevTools

### Tools Menu (Developing)

- **Start Packet Sender** - Launches packet sender utility (validates binary exists)
- **Stop Packet Sender** - Stops the running packet sender process
- **Zoom In** (`CmdOrCtrl+Plus`) - Increases zoom of the focused window
- **Zoom Out** (`CmdOrCtrl+-`) - Decreases zoom of the focused window
- **Reset Zoom** (`CmdOrCtrl+0`) - Restores the focused window to 100% zoom
- **Toggle Full Screen** (`F11`) - Toggles fullscreen on the focused window
- **Toggle DevTools** (`F12`) - Opens/closes Chrome DevTools for the focused window

### Help Menu

- **About** - Displays application information dialog

## Functions

### `createMenu(mainWindow)`
### `createMenu()`

Creates and sets the application menu bar. Takes the main window instance as parameter.
Builds and returns the application menu. Set it globally with `Menu.setApplicationMenu(createMenu())` so it applies to every window.

## Dependencies

- `../utils/paths.js` - For resolving binary paths
- `electron` - For Menu, dialog, and app APIs
- `electron` - For Menu, BrowserWindow, dialog, and app APIs

## Used By

- **`windows/mainWindow.js`** - Creates the menu when initializing the main window
- **`app/initialization.js`** - Sets the application-wide menu during app initialization

## Notes

- Keyboard shortcuts automatically use `Cmd` on macOS and `Ctrl` on Windows/Linux
- Menu appears in system menu bar on macOS, in window on Windows/Linux
- Packet sender binary existence is validated before starting
- View switching requires `loadView` to be imported from windows module
- Menu items resolve their target window at click time (focused window), so no window reference is needed when building the menu

## See Also

- [../windows/README.md](../windows/README.md) - Window management (used for view switching)
- [../processes/README.md](../processes/README.md) - Process management (packet sender)
- [../utils/README.md](../utils/README.md) - Utility functions (path resolution)
- [../windows/README.md](../windows/README.md) - Window management
30 changes: 22 additions & 8 deletions electron-app/src/menu/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
* Defines menu structure with File, Tools, and Help sections with keyboard shortcuts and actions.
*/

import { Menu, app, dialog } from "electron";
import { BrowserWindow, Menu, app, dialog } from "electron";

/**
* Creates and sets the application menu with File, Tools, and Help sections.
* Includes menu items for reloading, exiting, toggling DevTools, and app information.
* Creates the application menu with File, Tools, and Help sections.
* Includes menu items for reloading, exiting, toggling fullscreen/DevTools, and app information.
* Menu actions operate on the currently focused window, so the menu can be
* shared by all windows via Menu.setApplicationMenu.
* View switching is no longer available since the mode is selected at startup.
* @param {import("electron").BrowserWindow} mainWindow - The main browser window instance to attach menu actions to.
* @returns {void}
* @returns {Menu} The built application menu.
* @example
* createMenu(mainWindow);
* Menu.setApplicationMenu(createMenu());
*/
function createMenu(mainWindow) {
function createMenu() {
const template = [
{
label: "File",
Expand Down Expand Up @@ -45,6 +46,19 @@ function createMenu(mainWindow) {
{
label: "Tools",
submenu: [
{ role: "zoomIn", label: "Zoom In" },
{ role: "zoomOut", label: "Zoom Out" },
{ role: "resetZoom", label: "Reset Zoom" },
{ type: "separator" },
{
label: "Toggle Full Screen",
accelerator: "F11",
click: (_, browserWindow) => {
if (browserWindow) {
browserWindow.setFullScreen(!browserWindow.isFullScreen());
}
},
},
{
label: "Toggle DevTools",
accelerator: "F12",
Expand All @@ -62,7 +76,7 @@ function createMenu(mainWindow) {
{
label: "About",
click: () => {
dialog.showMessageBox(mainWindow, {
dialog.showMessageBox(BrowserWindow.getFocusedWindow() ?? undefined, {
type: "info",
title: "About",
message: "Hyperloop UPV Control Station",
Expand Down
5 changes: 0 additions & 5 deletions electron-app/src/windows/mainWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import { BrowserWindow, app, dialog } from "electron";
import fs from "fs";
import path from "path";
import { createMenu } from "../menu/menu.js";
import { getAppPath } from "../utils/paths.js";

// Get the application root path
Expand Down Expand Up @@ -57,10 +56,6 @@ function createWindow(screenWidth, screenHeight, initialView) {
loadView(currentView);
}

// Create application menu
const menu = createMenu(mainWindow);
mainWindow.setMenu(menu);

// Open DevTools in development mode (skip in test env to keep window order predictable)
if (!app.isPackaged && process.env.NODE_ENV !== "test") {
mainWindow.webContents.openDevTools();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ const StatValue = ({

const DashboardStatusBar = () => {
const state = useMeasurement(BOARDS.VCU, VCU.state);
const brakeRaw = useMeasurement(BOARDS.VCU, VCU.activeBrakes);
const brakeRaw = useMeasurement(BOARDS.VCU, VCU.brakesStatus);
const dcLinkV = useMeasurement(BOARDS.HVBMS, HVBMS.voltageReading) as number | undefined;

const stateStale = useIsStale(BOARDS.VCU, VCU.state);
const brakeStale = useIsStale(BOARDS.VCU, VCU.activeBrakes);
const brakeStale = useIsStale(BOARDS.VCU, VCU.brakesStatus);
const dcLinkStale = useIsStale(BOARDS.HVBMS, HVBMS.voltageReading);

const dcLinkActive = dcLinkV !== undefined && dcLinkV > HVAL_THRESHOLD_V;
Expand All @@ -53,11 +53,11 @@ const DashboardStatusBar = () => {
? "text-muted-foreground"
: dcLinkActive ? "text-red-500" : "text-green-500";

const brakeLabel = brakeRaw === undefined ? "—" : brakeRaw ? "BRAKED" : "UNBRAKED";
const brakeLabel = brakeRaw === undefined ? "—" : String(brakeRaw);
const brakeClass =
brakeStale ? STALE_TEXT_CLASS :
brakeRaw === true ? "text-red-500" :
brakeRaw === false ? "text-blue-500" :
brakeStale ? STALE_TEXT_CLASS :
brakeRaw === "BRAKED" ? "text-red-500" :
brakeRaw === "UNBRAKED" ? "text-blue-500" :
"text-muted-foreground";

return (
Expand Down
9 changes: 8 additions & 1 deletion frontend/competition-view/src/constants/measurements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const VCU = {
lowPressure: "low_pressure",
pressureRegulatorFdbk: "pressure_regulator_feedback",
sdcClosed: "sdc_closed",
activeBrakes: "active_brakes",
// Enum: "BRAKED" | "UNBRAKED"
brakesStatus: "brakes_status",
brakeFault: "brake_fault_detected",
electrovalveEnabled: "electrovalve_enabled",
// Sub-board connectivity as reported by the VCU
Expand Down Expand Up @@ -65,6 +66,12 @@ export const HV_CURRENT_RANGE: readonly [number, number] = [0, 120];
export const LEV_CURRENT_RANGE: readonly [number, number] = [-55, 55];
/** Propulsion phase current interval (A). */
export const PROP_CURRENT_RANGE: readonly [number, number] = [0, 120];
/** Battery temperature safety interval (°C). */
export const TEMP_RANGE: readonly [number, number] = [0, 60];
/** Propulsion velocity safety interval (km/h). */
export const SPEED_RANGE: readonly [number, number] = [0, 50];
/** Vertical airgap safety interval (mm). */
export const VERT_AIRGAP_RANGE: readonly [number, number] = [10, 25];

/** Cell voltage display range and warning thresholds (V), shared by the battery views. */
export const CELL_V_MIN = 3.0;
Expand Down
50 changes: 50 additions & 0 deletions frontend/competition-view/src/constants/orderLimits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { CommandCatalogItem, NumericParameter } from "../types/catalog";

/**
* Per-order overrides for the numeric input limits reported by the backend
* catalog. Keyed by field key (the keys of `CommandCatalogItem.fields`);
* a `min`/`max` set here replaces the corresponding bound of the field's
* safeRange, so the input min/max, placeholder, out-of-range validation and
* error message all pick it up. `null` removes the bound; an omitted key
* keeps the catalog value.
*/
export interface FieldLimitChanges {
min?: number | null;
max?: number | null;
}

export type OrderLimitOverride = [orderId: number, changes: Record<string, FieldLimitChanges>];

export const ORDER_LIMIT_OVERRIDES: readonly OrderLimitOverride[] = [
[100, { propulsion_current_reference: { min: 0, max: 120 } }],
[101, { levitation_target_height: { min: 5, max: 25 } }],
[102, {
propulsion_current_reference: { min: 0, max: 120 },
levitation_target_height: { min: 5, max: 25 },
}],
];

const overridesById = new Map(ORDER_LIMIT_OVERRIDES);

const overrideSafeRange = (param: NumericParameter, changes: FieldLimitChanges): NumericParameter => ({
...param,
safeRange: [
"min" in changes ? (changes.min ?? null) : param.safeRange[0],
"max" in changes ? (changes.max ?? null) : param.safeRange[1],
],
});

/** Returns the item with ORDER_LIMIT_OVERRIDES applied to its numeric fields (the item itself if it has none). */
export const applyOrderLimitOverrides = (item: CommandCatalogItem): CommandCatalogItem => {
const changes = overridesById.get(item.id);
if (!changes) return item;

const fields = Object.fromEntries(
Object.entries(item.fields).map(([key, param]) => [
key,
param.kind === "numeric" && changes[key] ? overrideSafeRange(param, changes[key]) : param,
]),
);

return { ...item, fields };
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CELL_V_WARN_LOW,
HVBMS,
PACK_V_RANGE,
TEMP_RANGE,
} from "../../../constants/measurements";
import { useStaleFlags } from "../../../hooks/useIsStale";
import useMeasurement from "../../../hooks/useMeasurement";
Expand Down Expand Up @@ -87,8 +88,8 @@ const HvBatterySection = () => {
range: CELL_V_RANGE,
valueClass: typeof voltageMin === "number" && voltageMin < CELL_V_WARN_LOW ? "text-red-500" : "",
},
{ label: "T max", value: fmt(tempMax), unit: "°C" },
{ label: "T min", value: fmt(tempMin), unit: "°C" },
{ label: "T max", value: fmt(tempMax), unit: "°C", range: TEMP_RANGE },
{ label: "T min", value: fmt(tempMin), unit: "°C", range: TEMP_RANGE },
].map(({ label, value, unit, valueClass, range }, i) => (
<div key={label} className="bg-card flex min-w-0 flex-col items-center gap-0.5 px-2 py-2">
<span className="text-muted-foreground text-xs">{label}</span>
Expand Down
2 changes: 1 addition & 1 deletion frontend/competition-view/src/pages/Booster/Booster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const Booster = () => (
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
<EnumRow label="SDC Status" board={BOARDS.HVBMS} measurementKey={HVBMS.sdcStatus} goodValues={["ENGAGED"]} />
<StatusRow label="SDC Closed (VCU)" board={BOARDS.VCU} measurementKey={VCU.sdcClosed} trueLabel="CLOSED" falseLabel="OPEN" />
<StatusRow label="Active Brakes" board={BOARDS.VCU} measurementKey={VCU.activeBrakes} trueLabel="ENGAGED" falseLabel="DISENGAGED" trueIsGood={false} />
<EnumRow label="Brakes Status" board={BOARDS.VCU} measurementKey={VCU.brakesStatus} goodValues={["UNBRAKED"]} />
<StatusRow label="Brake Fault" board={BOARDS.VCU} measurementKey={VCU.brakeFault} trueLabel="FAULT" falseLabel="OK" trueIsGood={false} />
<EnumRow label="IMD Status" board={BOARDS.HVBMS} measurementKey={HVBMS.imdStatus} goodValues={["NORMAL"]} />
<StatusRow label="IMD OK" board={BOARDS.HVBMS} measurementKey={HVBMS.imdOk} trueLabel="OK" falseLabel="FAULT" />
Expand Down
7 changes: 3 additions & 4 deletions frontend/competition-view/src/pages/Charts/Charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
MoveVertical,
Zap,
} from "lucide-react";
import { BOARDS, HVBMS, LCU, PCU, VCU } from "../../constants/measurements";
import { BOARDS, HVBMS, LCU, PCU, SPEED_RANGE, VERT_AIRGAP_RANGE } from "../../constants/measurements";
import MultiSeriesChart, { type SeriesConfig } from "./components/MultiSeriesChart";
import TelemetryChart from "./components/TelemetryChart";

Expand Down Expand Up @@ -60,7 +60,7 @@ const Charts = () => (
<div className="flex h-full flex-col gap-4 overflow-y-auto p-4">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{/* Row 1 — Kinematic */}
<TelemetryChart title="Speed" icon={Gauge} board={BOARDS.PCU} measurementKey={PCU.speed} unit="km/h" colorIndex={0} />
<TelemetryChart title="Speed" icon={Gauge} board={BOARDS.PCU} measurementKey={PCU.speed} unit={`[${SPEED_RANGE[0]}, ${SPEED_RANGE[1]}] km/h`} colorIndex={0} />
<TelemetryChart title="Position" icon={MapPin} board={BOARDS.PCU} measurementKey={PCU.position} unit="m" colorIndex={1} />

{/* Row 2 — Electrical */}
Expand All @@ -69,10 +69,9 @@ const Charts = () => (

{/* Row 3 — DLIM motor currents */}
<MultiSeriesChart title="DLIM — Phase Currents" icon={Zap} series={DLIM_SERIES} unit="A" />
<TelemetryChart title="High Pressure" icon={Gauge} board={BOARDS.VCU} measurementKey={VCU.highPressure} unit="bar" colorIndex={4} />

{/* Row 4 — Airgaps */}
<MultiSeriesChart title="Vertical Airgaps" icon={MoveVertical} series={VERT_AIRGAP_SERIES} unit="mm" />
<MultiSeriesChart title="Vertical Airgaps" icon={MoveVertical} series={VERT_AIRGAP_SERIES} unit={`[${VERT_AIRGAP_RANGE[0]}, ${VERT_AIRGAP_RANGE[1]}] mm`} />
<MultiSeriesChart title="Lateral Airgaps" icon={MoveHorizontal} series={LAT_AIRGAP_SERIES} unit="mm" />

{/* Row 5 — Levitation currents */}
Expand Down
Loading
Loading