Skip to content

Commit 687213f

Browse files
authored
Merge pull request #377 from HyperloopUPV-H8/demonstration
Demonstration
2 parents 600bb45 + 6a092d1 commit 687213f

29 files changed

Lines changed: 548 additions & 142 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

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

9+
<img width="1728" height="997" alt="control_station_mock" src="https://github.com/user-attachments/assets/a77b3411-c466-4100-8277-48d957290a46" />
10+
911
## Quick Start
1012

1113
### For Users

common-front/lib/selectors/BCU.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export const BcuMeasurements = {
99
averageCurrentV: 'BCU/average_current_v',
1010
averageCurrentW: 'BCU/average_current_w',
1111
generalState: 'BCU/bcu_general_state',
12-
operationalState: 'BCU/bcu_operational_state'
12+
operationalState: 'BCU/bcu_operational_state',
13+
nestedState: 'BCU/bcu_nested_state'
1314
};

common-front/lib/store/measurementsStore.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface MeasurementsStore {
6262
getMeasurementFallback: (id: MeasurementId) => Measurement;
6363
clearMeasurements: (board: string) => void;
6464
setLogAll: (log: boolean) => void;
65+
setShowAllLatest: (showLatest: boolean) => void;
6566
getLogVariables: () => string[];
6667
}
6768

@@ -254,6 +255,20 @@ export const useMeasurementsStore = create<MeasurementsStore>((set, get) => ({
254255
}));
255256
},
256257

258+
setShowAllLatest: (showLatest: boolean) => {
259+
const measurementsDraft = get().measurements;
260+
for (const id in measurementsDraft) {
261+
const m = measurementsDraft[id];
262+
if (isNumericType(m.type) && typeof m.value === 'object' && 'showLatest' in m.value) {
263+
(m.value as NumericValue).showLatest = showLatest;
264+
}
265+
}
266+
set((state) => ({
267+
...state,
268+
measurements: measurementsDraft,
269+
}));
270+
},
271+
257272
getLogVariables: () => {
258273
const measurements = get().measurements;
259274
return Object.values(measurements)

control-station/src/components/BatteriesModules/BatteriesModule.module.scss

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
.boxContainer2 {
1717
border: 2.5px solid colors.getThemeColor('border');
1818
width: 98%;
19-
height: 115px;
19+
height: 155px;
2020
border-radius: 20px;
2121
display: flex;
2222
flex-direction: column;
@@ -194,8 +194,8 @@
194194
background-color: yellow;
195195
}
196196

197-
.red {
198-
background-color: red;
197+
.disconnected {
198+
background-color: #cccccc;
199199
}
200200

201201
.lightOrange1 {
@@ -214,4 +214,118 @@
214214
text-align: left;
215215
margin: 0px;
216216
white-space: nowrap;
217+
}
218+
219+
.temperatureContainer {
220+
display: flex;
221+
justify-content: space-between;
222+
width: 100%;
223+
margin-top: 8px;
224+
padding: 0 8px;
225+
gap: 8px;
226+
}
227+
228+
.temperatureIndicator {
229+
display: flex;
230+
flex-direction: column;
231+
align-items: center;
232+
flex: 1;
233+
}
234+
235+
.temperatureLabel {
236+
color: colors.getThemeColor('text-secondary');
237+
font-family: 'IBM Plex Mono', monospace;
238+
font-size: 0.6rem;
239+
font-weight: bold;
240+
margin-bottom: 4px;
241+
}
242+
243+
.temperatureValue {
244+
width: 100%;
245+
height: 24px;
246+
border: 1px solid colors.getThemeColor('border');
247+
border-radius: 6px;
248+
display: flex;
249+
align-items: center;
250+
justify-content: center;
251+
min-width: 60px;
252+
}
253+
254+
.temperatureText {
255+
font-size: 0.55rem;
256+
font-family: 'IBM Plex Mono', monospace;
257+
font-weight: bold;
258+
line-height: 1;
259+
white-space: nowrap;
260+
}
261+
262+
.lowVoltageBoxContainer1 {
263+
width: 100%;
264+
max-width: 400px;
265+
min-width: 350px;
266+
display: flex;
267+
flex-direction: column;
268+
justify-content: center;
269+
align-items: center;
270+
border-radius: 20px;
271+
margin: 5px;
272+
position: relative;
273+
}
274+
275+
.lowVoltageBoxContainer2 {
276+
border: 2.5px solid colors.getThemeColor('border');
277+
width: 98%;
278+
height: 155px;
279+
border-radius: 20px;
280+
display: flex;
281+
flex-direction: column;
282+
align-items: center;
283+
justify-content: flex-end;
284+
position: relative;
285+
background-color: colors.getThemeColor('surface-variant');
286+
box-sizing: border-box;
287+
padding: 8px 8px 8px 8px;
288+
padding-top: 38px;
289+
}
290+
291+
.lowVoltageTemperatureContainer {
292+
display: flex;
293+
justify-content: space-between;
294+
width: 100%;
295+
margin-top: 8px;
296+
padding: 0 8px;
297+
gap: 6px;
298+
}
299+
300+
.lowVoltageBoxContainer2 .flexCells {
301+
display: grid;
302+
grid-template-columns: repeat(3, 1fr);
303+
grid-template-rows: repeat(2, 1fr);
304+
gap: 8px;
305+
justify-content: center;
306+
align-items: center;
307+
justify-items: center;
308+
width: 100%;
309+
margin: 15px auto -5px auto;
310+
border-radius: 0;
311+
border: none;
312+
padding: 6px;
313+
background-color: transparent;
314+
transform: none;
315+
}
316+
317+
.lowVoltageBoxContainer2 .cell {
318+
width: 80px;
319+
height: 28px;
320+
border: 1px solid colors.getThemeColor('border');
321+
border-radius: 6px;
322+
text-align: center;
323+
display: flex;
324+
align-items: center;
325+
justify-content: center;
326+
position: relative;
327+
font-family: 'IBM Plex Mono', monospace;
328+
font-size: 0.75rem;
329+
font-weight: bold;
330+
box-sizing: border-box;
217331
}

control-station/src/components/BatteriesModules/BatteriesModule.tsx

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,89 @@
1-
import React, { useState } from "react";
1+
import React, { useState, useContext } from "react";
22
import styles from "./BatteriesModule.module.scss";
3-
import { useGlobalTicker, useMeasurementsStore } from "common";
3+
import { useGlobalTicker, useMeasurementsStore, usePodDataStore } from "common";
4+
import { LostConnectionContext } from "services/connections";
45

56
interface CellProps {
67
value: number;
78
min: number;
89
max: number;
910
}
1011

12+
interface TemperatureProps {
13+
value: number;
14+
min: number;
15+
max: number;
16+
label: string;
17+
}
18+
1119
const BatteriesModule: React.FC<{ id: string | number }> = ({ id }) => {
1220
const minThresholdCellVoltage = 3.73;
1321
const maxThresholdCellVoltage = 4.2;
22+
const minThresholdTemperature = 0;
23+
const maxThresholdTemperature = 60;
24+
const temp1VariableName = `HVSCU/battery${id}_temperature1`;
25+
const temp2VariableName = `HVSCU/battery${id}_temperature2`;
26+
1427
const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo);
28+
const podData = usePodDataStore((state) => state.podData);
29+
const lostConnection = useContext(LostConnectionContext);
30+
1531
const [cellValues, setCellValues] = useState<number[]>(Array(6).fill(0));
32+
const [temperature1, setTemperature1] = useState<number>(0);
33+
const [temperature2, setTemperature2] = useState<number>(0);
34+
const [hasReceivedData, setHasReceivedData] = useState(false);
1635

1736
useGlobalTicker(() => {
37+
const boardName = 'HVSCU';
38+
const board = podData.boards.find(b => b.name === boardName);
39+
const hasReceivedPackets = board?.packets.some(packet => packet.count > 0) || false;
40+
41+
if (hasReceivedPackets && !hasReceivedData) {
42+
setHasReceivedData(true);
43+
}
44+
1845
setCellValues(
1946
Array.from({ length: 6 }, (_, i) => {
2047
const variableName = `HVSCU/battery${id}_cell${i + 1}`;
2148
return getNumericMeasurementInfo(variableName)?.getUpdate() ?? 0;
2249
})
2350
);
51+
52+
setTemperature1(getNumericMeasurementInfo(temp1VariableName)?.getUpdate() ?? 0);
53+
setTemperature2(getNumericMeasurementInfo(temp2VariableName)?.getUpdate() ?? 0);
2454
});
2555

26-
const getColorFromValue = (value: number, min: number, max: number) => {
56+
const getColorFromValue = (value: number, min: number, max: number, showDisconnected: boolean) => {
57+
if (showDisconnected) return styles.disconnected;
2758
if (value < min) return styles.yellow;
2859
if (value > max) return styles.red;
2960
return styles.green;
3061
};
3162

63+
const showDisconnected = lostConnection || !hasReceivedData;
64+
3265
const Cell: React.FC<CellProps> = ({ value, min, max }) => {
33-
const colorClass = getColorFromValue(value, min, max);
34-
const formattedValue = Math.max(0, Math.min(9.999, value)).toFixed(3);
66+
const colorClass = getColorFromValue(value, min, max, showDisconnected);
67+
const formattedValue = Math.max(0, Math.min(9.999, value)).toFixed(3);
3568
return (
3669
<div
3770
className={`${styles.cell} ${colorClass}`}
71+
title={showDisconnected ? "DISCONNECTED" : `${value.toFixed(3)} V`}
3872
>
39-
<span className={styles.cellText}>{formattedValue}V</span>
73+
<span className={styles.cellText}>{formattedValue}{showDisconnected ? "" : "V"}</span>
74+
</div>
75+
);
76+
};
77+
78+
const TemperatureIndicator: React.FC<TemperatureProps> = ({ value, min, max, label }) => {
79+
const colorClass = getColorFromValue(value, min, max, showDisconnected);
80+
const formattedValue = value.toFixed(1);
81+
return (
82+
<div className={styles.temperatureIndicator}>
83+
<span className={styles.temperatureLabel}>{label}</span>
84+
<div className={`${styles.temperatureValue} ${colorClass}`}>
85+
<span className={styles.temperatureText}>{formattedValue}{showDisconnected ? "" : "°C"}</span>
86+
</div>
4087
</div>
4188
);
4289
};
@@ -45,14 +92,29 @@ const BatteriesModule: React.FC<{ id: string | number }> = ({ id }) => {
4592
<div className={styles.boxContainer1}>
4693
<div className={styles.boxContainer2}>
4794
<article className={styles.titleDecorationModule}>
48-
<h2 className={styles.h2Module}>Module {id}</h2>
95+
<h2 className={styles.h2Module}>Segment {id}</h2>
4996
</article>
5097

5198
<div className={styles.flexCells}>
5299
{cellValues.map((value, index) => (
53100
<Cell key={index} value={value} min={minThresholdCellVoltage} max={maxThresholdCellVoltage}/>
54101
))}
55102
</div>
103+
104+
<div className={styles.temperatureContainer}>
105+
<TemperatureIndicator
106+
value={temperature1}
107+
min={minThresholdTemperature}
108+
max={maxThresholdTemperature}
109+
label="T1"
110+
/>
111+
<TemperatureIndicator
112+
value={temperature2}
113+
min={minThresholdTemperature}
114+
max={maxThresholdTemperature}
115+
label="T2"
116+
/>
117+
</div>
56118
</div>
57119
</div>
58120
);

0 commit comments

Comments
 (0)