diff --git a/.gitignore b/.gitignore index c54617ecd..52551fad2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,7 @@ backend/cmd/cmd CLAUDE* .claude +*.exe + # Rust build artifacts packet-sender/target/ diff --git a/backend/.gitignore b/backend/.gitignore index dc6dbb27a..fa1a04f8d 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -23,3 +23,4 @@ downloads audience_static cmd/adj/ +cmd/config.toml diff --git a/common-front/lib/components/Connections/useConnections.ts b/common-front/lib/components/Connections/useConnections.ts index 42432c416..4fafb9be5 100644 --- a/common-front/lib/components/Connections/useConnections.ts +++ b/common-front/lib/components/Connections/useConnections.ts @@ -1,4 +1,4 @@ -import { useConnectionsStore, useMeasurementsStore, usePodDataStore, useSubscribe } from "../.."; +import { useConnectionsStore, useMeasurementsStore, usePodDataStore, useSubscribe } from "../../"; export function useConnections() { diff --git a/common-front/lib/components/MessagesContainer/Messages/MessageView/Counter/Counter.module.scss b/common-front/lib/components/MessagesContainer/Messages/MessageView/Counter/Counter.module.scss index 0f5af02fe..72e89f6af 100644 --- a/common-front/lib/components/MessagesContainer/Messages/MessageView/Counter/Counter.module.scss +++ b/common-front/lib/components/MessagesContainer/Messages/MessageView/Counter/Counter.module.scss @@ -1,11 +1,11 @@ .counter { width: fit-content; height: fit-content; - border-radius: 4rem; - padding: 0.25rem 0.35rem; + border-radius: 3rem; + padding: 0.15rem 0.25rem; font-family: Consolas; - font-size: small; + font-size: 0.7rem; line-height: 90%; color: white; diff --git a/common-front/lib/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.module.scss b/common-front/lib/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.module.scss index aa0d25bb6..0c7712778 100644 --- a/common-front/lib/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.module.scss +++ b/common-front/lib/components/MessagesContainer/Messages/MessageView/InfoMessageView/InfoMessageView.module.scss @@ -1,17 +1,18 @@ .infoMessageView { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.2rem; min-width: 0; } .board { color: var(--main-color); font-weight: bold; + font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; } .payload { - font-size: 0.8rem; + font-size: 0.7rem; } diff --git a/common-front/lib/components/MessagesContainer/Messages/MessageView/MessageView.module.scss b/common-front/lib/components/MessagesContainer/Messages/MessageView/MessageView.module.scss index e8667afad..d0b4c10e8 100644 --- a/common-front/lib/components/MessagesContainer/Messages/MessageView/MessageView.module.scss +++ b/common-front/lib/components/MessagesContainer/Messages/MessageView/MessageView.module.scss @@ -24,12 +24,13 @@ "icon content counter" auto "icon timestamp timestamp" auto / auto 1fr auto; - gap: 0.4rem 0.8rem; + gap: 0.2rem 0.4rem; - padding: 0.6rem; + padding: 0.3rem; background-color: var(--background-color); - border-radius: 1rem; + border-radius: 0.5rem; filter: (--shadow); + font-size: 0.8rem; } .icon { @@ -48,7 +49,7 @@ .idAndCounter { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.3rem; } .timestamp { diff --git a/common-front/lib/components/MessagesContainer/Messages/MessageView/MessageView.tsx b/common-front/lib/components/MessagesContainer/Messages/MessageView/MessageView.tsx index 3b11687e6..a37ffdaf1 100644 --- a/common-front/lib/components/MessagesContainer/Messages/MessageView/MessageView.tsx +++ b/common-front/lib/components/MessagesContainer/Messages/MessageView/MessageView.tsx @@ -48,7 +48,7 @@ export const MessageView = React.memo(({ message }: Props) => { return (
- + {Message} :not(:last-child)::after { content: ''; border-bottom: 1px solid rgba(165, 131, 101, 0.368); - margin-bottom: 0.5rem; + margin-bottom: 0.2rem; } diff --git a/common-front/lib/components/Orders/Orders.tsx b/common-front/lib/components/Orders/Orders.tsx index 85d94d683..86267f674 100644 --- a/common-front/lib/components/Orders/Orders.tsx +++ b/common-front/lib/components/Orders/Orders.tsx @@ -17,10 +17,11 @@ export const Orders = ({ boards }: Props) => {
Always show state orders:{' '} - {alwaysShowStateOrders ? 'true' : 'false'}
); diff --git a/control-station/src/components/BatteriesModules/BatteriesModule.module.scss b/control-station/src/components/BatteriesModules/BatteriesModule.module.scss new file mode 100644 index 000000000..8242820a7 --- /dev/null +++ b/control-station/src/components/BatteriesModules/BatteriesModule.module.scss @@ -0,0 +1,203 @@ +.boxContainer1 { + width: 100%; + max-width: 220px; + min-width: 200px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 20px; + margin: 5px; + position: relative; + } + + .boxContainer2 { + border: 2.5px solid #FFE7CF; + width: 98%; + height: 115px; + border-radius: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + position: relative; + background-color: white; + box-sizing: border-box; + padding: 8px 8px 8px 8px; + padding-top: 38px; + } + + .h2Module { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + width: 100%; + height: 28px; + background-color: #FFE7CF; + text-align: center; + border-radius: 20px 20px 0 0; + font-weight: bold; + font-size: 0.85rem; + margin: 0; + padding: 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0; + left: 0; + right: 0; + } + + .voltageContainer { + width: 100%; + padding: 0 8px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + box-sizing: border-box; + } + + .totalRow { + display: flex; + justify-content: center; + width: 100%; + margin-bottom: 0px; + } + + .totalStyle { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.7rem; + display: flex; + flex-direction: row; + align-items: center; + font-weight: bold; + } + + .totalLabel { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.7rem; + margin: 0px; + margin-right: 8px; + font-weight: bold; + } + + .totalValue { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.7rem; + margin: 0px; + padding: 3px 16px; + background-color: #FFE7CF; + border-radius: 12px; + font-weight: bold; + min-width: 85px; + text-align: center; + } + + .dataRow { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + margin-bottom: -2px; + } + + .titleDecorationModule { + text-align: center; + border-top-left-radius: 20px; + border-top-right-radius: 20px; + } + + .dataStyle { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.65rem; + display: flex; + flex-direction: row; + align-items: center; + flex: 1; + margin-right: 8px; + } + + .p { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.65rem; + margin-top: 1px; + max-height: 1.4rem; + overflow: hidden; + margin-bottom: 1px; + padding: 3px 14px; + background-color: #FFE7CF; + border-radius: 12px; + width: fit-content; + align-items: center; + box-sizing: border-box; + margin-left: auto; + min-width: 70px; + text-align: center; + } + + .flexCells { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 8px; + justify-content: center; + align-items: center; + width: 100%; + margin: 15px auto -5px auto; + border-radius: 0; + border: none; + padding: 6px; + background-color: transparent; + transform: none; + } + + .cell { + width: 52px; + height: 28px; + border: 1px solid orange; + border-radius: 6px; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: bold; + } + + .red { + background-color: red; + } + + .green { + background-color: rgb(33, 240, 33); + } + + .yellow { + background-color: yellow; + } + + .lightOrange1 { + background-color: #FFA500; + } + + .lightOrange2 { + background-color: #FFCC80; + } + + .moduleInfoLabel { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.65rem; + width: 45%; + text-align: left; + margin: 0px; + white-space: nowrap; + } \ No newline at end of file diff --git a/control-station/src/components/BatteriesModules/BatteriesModule.tsx b/control-station/src/components/BatteriesModules/BatteriesModule.tsx new file mode 100644 index 000000000..bd5954212 --- /dev/null +++ b/control-station/src/components/BatteriesModules/BatteriesModule.tsx @@ -0,0 +1,57 @@ +import React, { useState } from "react"; +import styles from "./BatteriesModule.module.scss"; +import { useGlobalTicker, useMeasurementsStore } from "common"; + +interface CellProps { + value: number; + min: number; +} + +const BatteriesModule: React.FC<{ id: string | number }> = ({ id }) => { + const minThresholdCellVoltage = 3.73; + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + const [cellValues, setCellValues] = useState(Array(6).fill(0)); + + useGlobalTicker(() => { + setCellValues( + Array.from({ length: 6 }, (_, i) => { + const variableName = `HVSCU/battery${id}_cell${i + 1}`; + return getNumericMeasurementInfo(variableName)?.getUpdate() ?? 0; + }) + ); + }); + + const getColorFromValue = (value: number, min: number) => { + if (value < min) return styles.yellow; + return styles.green; + }; + + const Cell: React.FC = ({ value, min }) => { + const colorClass = getColorFromValue(value, min); + return ( +
+ ); + }; + + return ( +
+
+
+

Module {id}

+
+ +
+ {cellValues.map((value, index) => ( + + ))} +
+
+
+ ); +}; + +export default BatteriesModule; + diff --git a/control-station/src/components/BatteriesModules/LowVoltageModule.tsx b/control-station/src/components/BatteriesModules/LowVoltageModule.tsx new file mode 100644 index 000000000..29379ce1e --- /dev/null +++ b/control-station/src/components/BatteriesModules/LowVoltageModule.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import styles from "./BatteriesModule.module.scss"; +import { useGlobalTicker, useMeasurementsStore } from "common"; + +interface CellProps { + value: number; + min: number; +} + +const LowVoltageModule: React.FC = () => { + const minThresholdCellVoltage = 3.67; + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + const [cellValues, setCellValues] = useState(Array(6).fill(0)); + + useGlobalTicker(() => { + setCellValues( + Array.from({ length: 6 }, (_, i) => { + const variableName = `BMSL/cell_${i + 1}`; + return getNumericMeasurementInfo(variableName)?.getUpdate() ?? 0; + }) + ); + }); + + const getColorFromValue = (value: number, min: number) => { + if (value < min) return styles.yellow; + return styles.green; + }; + + const Cell: React.FC = ({ value, min }) => { + const colorClass = getColorFromValue(value, min); + return ( +
+ ); + }; + return ( +
+
+
+

Low Voltage

+
+ +
+ {cellValues.map((value, index) => ( + + ))} +
+
+
+ ); +}; + +export default LowVoltageModule; diff --git a/control-station/src/components/BatteryIndicator/BatteryIndicator.module.scss b/control-station/src/components/BatteryIndicator/BatteryIndicator.module.scss new file mode 100644 index 000000000..1e6a9be8b --- /dev/null +++ b/control-station/src/components/BatteryIndicator/BatteryIndicator.module.scss @@ -0,0 +1,111 @@ +@use 'src/styles/fonts'; + +.battery_indicator { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + padding: 0.7rem; + width: 4.2rem; + background-color: transparent; +} + +.battery_container { + position: relative; + width: 2rem; + height: 4rem; + background-color: #DFFFD4; + border: 0.0625rem solid #9CBDD7; + border-radius: 0.35rem; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: -0.35rem; + left: 50%; + transform: translateX(-50%); + width: 1rem; + height: 0.35rem; + background-color: #9CBDD7; + border-radius: 0.175rem 0.175rem 0 0; + } +} + +.battery_fill { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background-color: currentColor; + transition: height 0.3s ease-in-out; + border-radius: 0 0 0.1875rem 0.1875rem; +} + +.battery_level_text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 0.5rem; + font-weight: bold; + color: #333; + text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.8); + z-index: 10; + display: flex; + align-items: baseline; + gap: 0.0625rem; + + .value { + margin: 0; + font-size: 0.6rem; + font-weight: 600; + } + + .units { + margin: 0; + font-size: 0.45rem; + font-weight: 400; + } +} + +.info_container { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.1875rem; +} + +.name_container { + display: flex; + align-items: center; + gap: 0.1875rem; +} + +.icon { + width: 0.875rem; + height: 0.875rem; + opacity: 0.8; +} + +.name { + margin: 0; + font-size: 0.7rem; + font-weight: 500; + color: #333; + text-align: center; +} + +.percentage_container { + display: flex; + align-items: center; + justify-content: center; +} + +.percentage { + margin: 0; + font-size: 0.8rem; + font-weight: 600; + color: #333; +} + diff --git a/control-station/src/components/BatteryIndicator/BatteryIndicator.tsx b/control-station/src/components/BatteryIndicator/BatteryIndicator.tsx new file mode 100644 index 000000000..9711d8a08 --- /dev/null +++ b/control-station/src/components/BatteryIndicator/BatteryIndicator.tsx @@ -0,0 +1,99 @@ +import { useGlobalTicker } from 'common'; +import styles from './BatteryIndicator.module.scss'; +import { + getPercentageFromRange, + getStateFromRange, + stateToColor, + stateToColorBackground, +} from 'state'; +import { memo, useContext, useEffect, useRef, useState } from 'react'; +import { LostConnectionContext } from 'services/connections'; + +interface Props { + icon?: string; + getValue: () => number; + getValueSOC: () => number; + safeRangeMin: number; + warningRangeMin: number; + safeRangeMax: number; + warningRangeMax: number; + units?: string; + color?: string; + backgroundColor?: string; + className?: string; +} + +export const BatteryIndicator = memo( + ({ + icon, + getValue, + getValueSOC, + safeRangeMin, + warningRangeMin, + safeRangeMax, + warningRangeMax, + units, + color, + backgroundColor, + className, + }: Props) => { + const [valueState, setValueState] = useState(0); + const [valueStateSOC, setValueStateSOC] = useState(0); + const lostConnection = useContext(LostConnectionContext); + + const state = lostConnection + ? 'fault' + : getStateFromRange( + valueState, + safeRangeMin, + safeRangeMax, + warningRangeMin, + warningRangeMax + ); + + useGlobalTicker(() => { + setValueState(getValue()); + setValueStateSOC(getValueSOC) + }); + + return ( +
+
+
+
+ + {lostConnection ? '-.-' : valueState?.toFixed(1)} + + {units && {units}} +
+
+ +
+
+ {icon && } +
+ +
+ + {lostConnection ? '-.--' : valueStateSOC ? Math.round(valueStateSOC) : '-.--'}% + +
+
+
+ ); + } +); diff --git a/control-station/src/components/BigOrderButton.module.scss b/control-station/src/components/BigOrderButton.module.scss index 1f6506b4c..6f1f350d9 100644 --- a/control-station/src/components/BigOrderButton.module.scss +++ b/control-station/src/components/BigOrderButton.module.scss @@ -1,6 +1,18 @@ .big_order_button { + display: flex; flex-flow: column; + align-items: center; + justify-content: center; + background-color: #FBD15B; + border-width: 4px; + border-color: black; + font-size: 1.5rem; + font-family: IBM Plex Mono, monospace; + font-weight: bold; + border-radius: 0.5rem; gap: 0.7rem; padding: 0.5rem; + height: 100%; + width: 100%; } diff --git a/control-station/src/components/GuiModules/Module.module.scss b/control-station/src/components/BoosterModules/BoosterModule.module.scss similarity index 94% rename from control-station/src/components/GuiModules/Module.module.scss rename to control-station/src/components/BoosterModules/BoosterModule.module.scss index 77ec4d4db..a7bf0b7b9 100644 --- a/control-station/src/components/GuiModules/Module.module.scss +++ b/control-station/src/components/BoosterModules/BoosterModule.module.scss @@ -10,7 +10,7 @@ .boxContainer2 { border: 2.5px solid #FFE7CF; width: 65%; - height: 130px; + height: 10.5rem; border-radius: 20px; display: flex; flex-direction: column; @@ -39,13 +39,14 @@ box-sizing: border-box; } - .voltajeContainer { + .voltageContainer { width: 100%; padding: 0 10px; display: flex; flex-direction: column; justify-content: center; - height: 190px; + margin-top: 5%; + height: 8rem; box-sizing: border-box; } @@ -74,6 +75,8 @@ font-family: 'IBM Plex Mono', monospace; font-size: 1rem; margin-top: 1px; + max-height: 1.6rem; + overflow: hidden; margin-bottom: 1px; padding: 5px 10px; background-color: #FFE7CF; @@ -158,4 +161,8 @@ width: 50%; text-align: left; margin: 0px; + } + + .yellow { + background-color: yellow; } \ No newline at end of file diff --git a/control-station/src/components/BoosterModules/BoosterModule.tsx b/control-station/src/components/BoosterModules/BoosterModule.tsx new file mode 100644 index 000000000..2fb047034 --- /dev/null +++ b/control-station/src/components/BoosterModules/BoosterModule.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from "react"; +import styles from "./BoosterModule.module.scss"; + +import { useMeasurementsStore, useGlobalTicker } from "common"; + +interface CellProps { + value: number; +} + +const BoosterModule: React.FC<{ id: string | number }> = ({ id }) => { + const minThresholdCellVoltage = 3.73; + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + + // Estados para todos los valores + const [moduleMinCell, setModuleMinCell] = useState(0); + const [moduleMaxCell, setModuleMaxCell] = useState(0); + const [moduleTotalVoltage, setModuleTotalVoltage] = useState(0); + const [moduleMaxTemp, setModuleMaxTemp] = useState(0); + const [moduleMinTemp, setModuleMinTemp] = useState(0); + const [cellValues, setCellValues] = useState(Array(48).fill(0)); + + useGlobalTicker(() => { + // Actualizar valores del módulo + setModuleMinCell(getNumericMeasurementInfo(`HVSCU-Cabinet/HVSCU-Cabinet_module_${id}_min_cell`)?.getUpdate() ?? 0); + setModuleMaxCell(getNumericMeasurementInfo(`HVSCU-Cabinet/HVSCU-Cabinet_module_${id}_max_cell`)?.getUpdate() ?? 0); + setModuleTotalVoltage(getNumericMeasurementInfo(`HVSCU-Cabinet/HVSCU-Cabinet_module_${id}_voltage`)?.getUpdate() ?? 0); + setModuleMaxTemp(getNumericMeasurementInfo(`HVSCU-Cabinet/HVSCU-Cabinet_module_${id}_max_temp`)?.getUpdate() ?? 0); + setModuleMinTemp(getNumericMeasurementInfo(`HVSCU-Cabinet/HVSCU-Cabinet_module_${id}_min_temp`)?.getUpdate() ?? 0); + + // Actualizar valores de las celdas + setCellValues( + Array.from({ length: 48 }, (_, i) => { + const variableName = `HVSCU-Cabinet/HVSCU-Cabinet_module_${id}_cell_${i + 1}_voltage`; + return getNumericMeasurementInfo(variableName)?.getUpdate() ?? 0; + }) + ); + }); + + const Cell: React.FC = ({ value }) => { + return ( +
+ ); + }; + + return ( +
+
+
+

Module {id}

+
+ +
+
+

Total V:

+

{`${moduleTotalVoltage.toFixed(2)} V`}

+
+
+

V max:

+

{`${moduleMaxCell.toFixed(2)} V`}

+
+
+

V min:

+

{`${moduleMinCell.toFixed(2)} V`}

+
+
+

Max Temp:

+

{`${moduleMaxTemp.toFixed(2)} °C`}

+
+
+

Min Temp:

+

{`${moduleMinTemp.toFixed(2)} °C`}

+
+
+
+
+ {cellValues.map((value, index) => ( + + ))} +
+
+ ); +}; + +export default BoosterModule; + diff --git a/control-station/src/components/EnumIndicator/EnumIndicator.module.scss b/control-station/src/components/EnumIndicator/EnumIndicator.module.scss new file mode 100644 index 000000000..667daf954 --- /dev/null +++ b/control-station/src/components/EnumIndicator/EnumIndicator.module.scss @@ -0,0 +1,46 @@ +.enum_indicator { + display: flex; + align-items: center; + width: 100%; + min-height: 2.5rem; + margin-left: auto; + padding: 0; + box-sizing: border-box; + margin-bottom: 0.6rem; + border-radius: 0.5rem; +} + +.enum_indicator:last-child { + margin-bottom: 0; +} + +.title { + font-family: Roboto; + font-size: 20px; + font-style: normal; + text-align: center; + font-weight: 400; + opacity: 0.8; + margin: 0; + flex: 1 1 auto; +} + +.icon { + opacity: 0.6; + width: 1.1rem; + height: 1.1rem; + display: flex; + align-items: center; + justify-content: center; + margin: 0; +} +.enum_indicator > .icon:first-child { + justify-self: start; + margin-left: 0.2rem; + margin-right: 0.5rem; +} +.enum_indicator > .icon:last-child { + justify-self: end; + margin-left: 0.5rem; + margin-right: 0.2rem; +} \ No newline at end of file diff --git a/control-station/src/components/EnumIndicator/EnumIndicator.tsx b/control-station/src/components/EnumIndicator/EnumIndicator.tsx new file mode 100644 index 000000000..08c446b13 --- /dev/null +++ b/control-station/src/components/EnumIndicator/EnumIndicator.tsx @@ -0,0 +1,63 @@ +import { useGlobalTicker, useMeasurementsStore } from 'common'; +import styles from './EnumIndicator.module.scss'; +import { memo, useContext, useState } from 'react'; +import { LostConnectionContext } from 'services/connections'; + +interface Props { + measurementId: string; + icon: string; +} + +export const EnumIndicator = memo(({ measurementId, icon }: Props) => { + const getValue = useMeasurementsStore( + (state) => state.getEnumMeasurementInfo(measurementId).getUpdate + ); + + const lostConnection = useContext(LostConnectionContext); + + const [variant, setVariant] = useState(getValue()); + const state = lostConnection + ? 'DISCONNECTED' + : variant; + + useGlobalTicker(() => { + setVariant(getValue()); + }); + + return ( +
+ + +

+ {lostConnection ? 'DISCONNECTED' : state.toUpperCase()} +

+ + +
+ ); +}); + +const enumToColor: { [key: string]: string } = { + 'DISCONNECTED' : '#cccccc', + + 'NOT_CHARGING' : '#EBF6FF', + 'CHARGING' : '#F583F8', + 'CHARGED' : '#FBD15B', + + 'HV OPEN' : '#83C0F8', + 'HV CLOSED' : '#ACF293', + + 'OPEN' : '#83C0F8', + 'PRECHARGE' : '#BB83F8', + 'CLOSED' : '#ACF293', + 'CLOSE' : '#ACF293', + + 'DISENGAGED' : '#FBD15B', + 'ENGAGED' : '#ACF293', + + default : '#EBF6FF' + +} diff --git a/control-station/src/components/EnumIndicator/VehicleState.tsx b/control-station/src/components/EnumIndicator/VehicleState.tsx new file mode 100644 index 000000000..626506f5f --- /dev/null +++ b/control-station/src/components/EnumIndicator/VehicleState.tsx @@ -0,0 +1,58 @@ +import { useGlobalTicker, useMeasurementsStore, VcuMeasurements } from 'common'; +import styles from './EnumIndicator.module.scss'; +import { useContext, useState } from 'react'; +import { LostConnectionContext } from 'services/connections'; +import teamLogo from 'assets/svg/team_logo.svg' + +export const VehicleState = () => { + const generalStateMeasurement = useMeasurementsStore( + (state) => state.getEnumMeasurementInfo(VcuMeasurements.generalState).getUpdate + ); + const operationalStateMeasurement = useMeasurementsStore( + (state) => state.getEnumMeasurementInfo(VcuMeasurements.operationalState).getUpdate + ); + + const lostConnection = useContext(LostConnectionContext); + + const [generalState, setGeneralState] = useState(generalStateMeasurement); + const [operationalState, setOperationalState] = useState(operationalStateMeasurement); + const state = lostConnection + ? 'DISCONNECTED' + : (generalState == 'OPERATIONAL') ? operationalState : generalState; + + useGlobalTicker(() => { + setGeneralState(generalStateMeasurement); + setOperationalState(operationalStateMeasurement); + }); + + return ( +
+ + +

+ {enumToText[state] ?? state} +

+ + +
+ ); +} + +const enumToColor: { [key: string]: string } = { + 'DISCONNECTED' : '#cccccc', + "FAULT" : '#EF9A87', + "END_OF_RUN" : '#BB83F8', + "ENERGIZED" : '#FBD15B', + "READY" : '#B2CFD6', + "DEMONSTRATION" : '#ACF293', + default : '#EBF6FF' + +} + +const enumToText: { [key:string]: string } = { + 'FAULT' : 'EMERGENCY', + "END_OF_RUN" : 'END OF RUN' +} diff --git a/control-station/src/components/GaugeTag/Gauge/Gauge.module.scss b/control-station/src/components/GaugeTag/Gauge/Gauge.module.scss index 889526d30..f8b97e81a 100644 --- a/control-station/src/components/GaugeTag/Gauge/Gauge.module.scss +++ b/control-station/src/components/GaugeTag/Gauge/Gauge.module.scss @@ -22,3 +22,39 @@ #F3785C ); } + +.gaugeContainer { + width: 100%; + max-width: 4em; + aspect-ratio: 1 / 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: visible; +} + +svg { + width: 100%; + height: auto; + max-width: 100%; +} + +.gaugeLabel { + fill: #3a5a6a; + font-size: 1.2rem; + font-style: italic; + opacity: 0.7; +} + +.gaugeValue { + fill: #18404b; + font-size: 1rem; + font-weight: 300; +} + +.gaugeUnits { + fill: #18404b; + font-size: 0.9rem; + opacity: 0.8; +} diff --git a/control-station/src/components/GaugeTag/Gauge/Gauge.tsx b/control-station/src/components/GaugeTag/Gauge/Gauge.tsx index a7128cea8..d4a11626b 100644 --- a/control-station/src/components/GaugeTag/Gauge/Gauge.tsx +++ b/control-station/src/components/GaugeTag/Gauge/Gauge.tsx @@ -44,4 +44,4 @@ export const Gauge = ({ > ); -}; +}; \ No newline at end of file diff --git a/control-station/src/components/GaugeTag/GaugeTag.module.scss b/control-station/src/components/GaugeTag/GaugeTag.module.scss index 047ff13bd..b5a15f6b9 100644 --- a/control-station/src/components/GaugeTag/GaugeTag.module.scss +++ b/control-station/src/components/GaugeTag/GaugeTag.module.scss @@ -1,16 +1,29 @@ @use "../../styles/colors"; .gaugeTagWrapper { - display: grid; - justify-items: center; + position: relative; + display: flex; + justify-content: center; align-items: center; background-color: map-get($map: colors.$key-colors, $key: lightskyblue); - padding: .3rem; - border-radius: 100%; + padding: .2rem; + border-radius: 50%; + + width: 4.5rem; + height: 4.5rem; + + flex-shrink: 0; + min-width: 4.5rem; + min-height: 4.5rem; + transform: translateY(-0.9rem); } .gauge { - grid-row-start: 1; - grid-column-start: 1; - font-size: 6rem; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 4.5rem; + height: 4.5rem; + z-index: 1; } diff --git a/control-station/src/components/GaugeTag/TextData/TextData.module.scss b/control-station/src/components/GaugeTag/TextData/TextData.module.scss index fc0d01029..13ad437d3 100644 --- a/control-station/src/components/GaugeTag/TextData/TextData.module.scss +++ b/control-station/src/components/GaugeTag/TextData/TextData.module.scss @@ -1,15 +1,16 @@ @use '../../../styles/fonts.scss'; .textData { + position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; - padding-top: 1rem; + padding-top: 2rem; - grid-row-start: 1; - grid-column-start: 1; - font-size: inherit; + z-index: 2; + max-width: 80%; + text-align: center; .name { font-weight: inherit; @@ -23,7 +24,7 @@ .value { font-family: map-get($map: fonts.$font-families, $key: roboto); font-weight: map-get($map: fonts.$font-weights, $key: bold); - font-size: 1.2rem; + font-size: 0.9rem; } .units { diff --git a/control-station/src/components/GuiModules/Module.tsx b/control-station/src/components/GuiModules/Module.tsx deleted file mode 100644 index f610eec50..000000000 --- a/control-station/src/components/GuiModules/Module.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useEffect, useState } from "react"; -import styles from "./Module.module.scss"; - -import { useMeasurementsStore } from "common"; - -interface CellProps { - value: number; - min: number; - max: number; -} - -const Module: React.FC<{ id: string | number }> = ({ id }) => { - const moduleMinCell = useMeasurementsStore( - (state) => (state.getNumericMeasurementInfo(`HVSCU-Cabinet/module_${id}_min_cell`)?.getUpdate() ?? 0) - ); - const moduleMaxCell = useMeasurementsStore( - (state) => (state.getNumericMeasurementInfo(`HVSCU-Cabinet/module_${id}_max_cell`)?.getUpdate() ?? 0) - ); - - const moduleTotalVoltage = useMeasurementsStore( - (state) => (state.getNumericMeasurementInfo(`HVSCU-Cabinet/module_${id}_voltage`)?.getUpdate() ?? 0) - ); - - // Estado para las celdas - const [cellValues, setCellValues] = useState(Array(48).fill(0)); // Define el tipo correctamente - - useEffect(() => { - const intervalId = setInterval(() => { - setCellValues(() => - Array.from({ length: 48 }, (_, i) => { - const variableName = `HVSCU-Cabinet/module_${id}_cell_${i + 1}_voltage`; - return useMeasurementsStore.getState().getNumericMeasurementInfo(variableName)?.getUpdate() ?? 0; - }) - ); - }, 100); - - return () => clearInterval(intervalId); - }, [id]); - - const getColorFromValue = (value: number, min: number, max: number) => { - if (value < min) return styles.red; - if (value > max) return styles.red; - if (value >= min && value <= max) return styles.green; - return styles.yellow; - }; - - const Cell: React.FC = ({ value, min, max }) => { - const colorClass = getColorFromValue(value, min, max); - return ( -
- ); - }; - - return ( -
-
-
-

Module {id}

-
- -
-
-

max:

-

{`${moduleMaxCell} V`}

-
-
-

min:

-

{`${moduleMinCell} V`}

-
-
-

total:

-

{`${moduleTotalVoltage} V`}

-
-
-
-
- {cellValues.map((value, index) => ( - - ))} -
-
- ); -}; - -export default Module; - diff --git a/control-station/src/components/ImdIndicator/ImdIndicator.module.scss b/control-station/src/components/ImdIndicator/ImdIndicator.module.scss new file mode 100644 index 000000000..5cee50be7 --- /dev/null +++ b/control-station/src/components/ImdIndicator/ImdIndicator.module.scss @@ -0,0 +1,46 @@ +.state_indicator { + display: flex; + align-items: center; + width: 100%; + min-height: 2.5rem; + margin-left: auto; + padding: 0; + box-sizing: border-box; + margin-bottom: 0.6rem; + border-radius: 0.5rem; +} + +.state_indicator:last-child { + margin-bottom: 0; +} + +.title { + font-family: Roboto; + font-size: 20px; + font-style: normal; + text-align: center; + font-weight: 400; + opacity: 0.8; + margin: 0; + flex: 1 1 auto; +} + +.icon { + opacity: 0.6; + width: 1.1rem; + height: 1.1rem; + display: flex; + align-items: center; + justify-content: center; + margin: 0; +} +.state_indicator > .icon:first-child { + justify-self: start; + margin-left: 0.2rem; + margin-right: 0.5rem; +} +.state_indicator > .icon:last-child { + justify-self: end; + margin-left: 0.5rem; + margin-right: 0.2rem; +} diff --git a/control-station/src/components/ImdIndicator/ImdIndicator.tsx b/control-station/src/components/ImdIndicator/ImdIndicator.tsx new file mode 100644 index 000000000..43d68a965 --- /dev/null +++ b/control-station/src/components/ImdIndicator/ImdIndicator.tsx @@ -0,0 +1,39 @@ +import { HvscuMeasurements, useGlobalTicker, useMeasurementsStore } from 'common'; +import styles from './ImdIndicator.module.scss'; +import thunderIcon from 'assets/svg/thunder-filled.svg' +import { memo, useContext, useState } from 'react'; +import { LostConnectionContext } from 'services/connections'; + +interface Props { + measurementId: string; + icon: string; +} + +export const ImdIndicator = () => { + const getValue = useMeasurementsStore( + (state) => state.getBooleanMeasurementInfo(HvscuMeasurements.IsImdOk).getUpdate + ); + + const lostConnection = useContext(LostConnectionContext); + + const [IsImdOk, setVariant] = useState(getValue()); + + useGlobalTicker(() => { + setVariant(getValue()); + }); + + return ( +
+ + +

+ {lostConnection ? 'DISCONNECTED' : IsImdOk ? 'ISOLATED' : 'ISOLATION FAULT'} +

+ + +
+ ); +}; \ No newline at end of file diff --git a/control-station/src/components/LevitationUnit/CurrentIndicator/CurrentIndicator.module.scss b/control-station/src/components/LevitationUnit/CurrentIndicator/CurrentIndicator.module.scss new file mode 100644 index 000000000..f79a2bab6 --- /dev/null +++ b/control-station/src/components/LevitationUnit/CurrentIndicator/CurrentIndicator.module.scss @@ -0,0 +1,106 @@ +@use 'src/styles/fonts'; + +.bar_indicator { + position: relative; + + min-height: fit-content; + width: 100%; + overflow: hidden; + + padding: 2px 8px; + + display: flex; + flex-flow: row; + align-items: center; + justify-content: space-between; + gap: 2px; +} + +.range_bar { + position: absolute; + top: 0; + bottom: 0; + left: 0; + + height: 100%; + + box-shadow: 2px 0 1rem 1rem currentColor; + background-color: currentColor; + + transition: width 0.1s ease-in-out; +} + +.name_display, +.value_display { + display: flex; + flex-flow: row; + align-items: center; + gap: 4px; + width: 90px; + min-width: 90px; + max-width: 90px; +} + +.name_display { + width: 100%; +} + +.icon { + max-width: 10px; + + opacity: 0.8; +} + +.name { + width: 100%; + + margin: 0; + + opacity: 0.8; + + font-size: 14px; + font-style: italic; + font-weight: 300; + text-overflow: ellipsis; +} + +.value, +.units { + opacity: 0.8; + margin: 0; + font-size: 20px; + font-weight: 400; + width: 40px; + min-width: 40px; + max-width: 40px; + text-align: left; + white-space: nowrap; +} + +.value { + opacity: 0.8; + margin: 0; + font-size: 20px; + font-weight: 400; + width: 70px; + min-width: 70px; + max-width: 70px; + text-align: left; + white-space: nowrap; +} + +.min_max { + display: flex; + flex-flow: column; + gap: 0.1rem; + margin-left: 0.2rem; +} + +.min, +.max { + opacity: 0.8; + margin: 0; + font-size: 12px; + font-weight: 300; + white-space: nowrap; +} diff --git a/control-station/src/components/LevitationUnit/CurrentIndicator/CurrentIndicator.tsx b/control-station/src/components/LevitationUnit/CurrentIndicator/CurrentIndicator.tsx new file mode 100644 index 000000000..936e9a688 --- /dev/null +++ b/control-station/src/components/LevitationUnit/CurrentIndicator/CurrentIndicator.tsx @@ -0,0 +1,92 @@ +import { useGlobalTicker } from 'common'; +import styles from './CurrentIndicator.module.scss'; +import { + getPercentageFromRange, + getStateFromRange, + stateToColor, + stateToColorBackground, +} from 'state'; +import { memo, useContext, useEffect, useRef, useState } from 'react'; +import { LostConnectionContext } from 'services/connections'; + +interface Props { + icon?: string; + getValue: () => number; + safeRangeMin: number; + warningRangeMin: number; + safeRangeMax: number; + warningRangeMax: number; + units?: string; + color?: string; + backgroundColor?: string; + className?: string; +} + +export const CurrentIndicator = memo( + ({ + icon, + getValue, + safeRangeMin, + warningRangeMin, + safeRangeMax, + warningRangeMax, + units, + color, + backgroundColor, + className, + }: Props) => { + const [valueState, setValueState] = useState(0); + const lostConnection = useContext(LostConnectionContext); + + const percentage = lostConnection + ? 100 + : getPercentageFromRange( + valueState, + warningRangeMin, + warningRangeMax + ); + const state = lostConnection + ? 'fault' + : getStateFromRange( + valueState, + safeRangeMin, + safeRangeMax, + warningRangeMin, + warningRangeMax + ); + + useGlobalTicker(() => { + setValueState(getValue()); + }); + + return ( +
+
+ +
+ +
+
+

+ {lostConnection ? '-.--' : valueState?.toFixed(2)} +

+

{units}

+
+
+ ); + } +); diff --git a/control-station/src/components/LevitationUnit/LevitationUnit.module.scss b/control-station/src/components/LevitationUnit/LevitationUnit.module.scss index f37876192..b950b019b 100644 --- a/control-station/src/components/LevitationUnit/LevitationUnit.module.scss +++ b/control-station/src/components/LevitationUnit/LevitationUnit.module.scss @@ -2,9 +2,8 @@ width: 100%; display: flex; gap: 1rem; - > * { - flex: 1; + flex: 1rem; } } @@ -13,7 +12,6 @@ display: flex; justify-content: center; align-items: center; - > img { width: 60%; } diff --git a/control-station/src/components/LevitationUnit/LevitationUnit.tsx b/control-station/src/components/LevitationUnit/LevitationUnit.tsx index 35e7f8148..3e7e81a2c 100644 --- a/control-station/src/components/LevitationUnit/LevitationUnit.tsx +++ b/control-station/src/components/LevitationUnit/LevitationUnit.tsx @@ -1,16 +1,14 @@ import { LcuMeasurements } from 'common'; import styles from './LevitationUnit.module.scss'; import { IndicatorStack } from 'components/IndicatorStack/IndicatorStack'; -import { BarIndicator } from 'components/BarIndicator/BarIndicator'; -import batteryFilled from 'assets/svg/battery-filled.svg'; -import thermometerFilled from 'assets/svg/thermometer-filled.svg'; -import airgapIcon from 'assets/svg/z-index.svg'; +import thunder from 'assets/svg/thunder-filled.svg'; import { useMeasurementsStore } from 'common'; import EMSRepresentation from './EMSRepresentation/EMSRepresentation'; import HEMSRepresentation from './HEMSRepresentation/HEMSRepresentation'; +import { CurrentIndicator } from './CurrentIndicator/CurrentIndicator'; export interface Props { unitIndex: number; - imageSide: 'left' | 'right'; + imageSide: 'left' | 'right' | 'none'; kind: 'ems' | 'hems'; } @@ -59,9 +57,7 @@ export const LevitationUnit = ({ unitIndex, kind, imageSide }: Props) => { ); const current = getNumericMeasurementInfo(currentMeasurements[unitIndex]); - const temperature = getNumericMeasurementInfo( - temperatureMeasurements[unitIndex] - ); + const airgap = getNumericMeasurementInfo(airgapMeasurements[unitIndex]); const airgap2 = unitIndex == 6 || unitIndex == 7 @@ -100,9 +96,8 @@ export const LevitationUnit = ({ unitIndex, kind, imageSide }: Props) => { /> ))} - { warningRangeMin={current.warningRange[0]!!} warningRangeMax={current.warningRange[1]!!} /> - { ? null!! : airgap.warningRange[1]!! } - /> + /> */} {imageSide === 'right' && (kind == 'ems' ? ( diff --git a/control-station/src/components/Logger/Logger.module.scss b/control-station/src/components/Logger/Logger.module.scss new file mode 100644 index 000000000..66df204d3 --- /dev/null +++ b/control-station/src/components/Logger/Logger.module.scss @@ -0,0 +1,42 @@ +@use "src/styles/styles"; + +.logger { + height: min-content; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0.5rem; + height: 2rem; +} + +.state { + flex: 0 0 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 300; + font-size: 0.8rem; + margin-right: auto; +} + +.buttons { + flex: 0 0 auto; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.8rem; + width: 10rem; + + > * { + min-width: 0; + min-height: 0; + height: 100%; + overflow: hidden; + display: flex; + align-items: center; + cursor: pointer; + padding: 0.1rem 0.5rem; + } +} diff --git a/control-station/src/components/Logger/Logger.tsx b/control-station/src/components/Logger/Logger.tsx new file mode 100644 index 000000000..3c07c5cd4 --- /dev/null +++ b/control-station/src/components/Logger/Logger.tsx @@ -0,0 +1,35 @@ +import styles from "components/Logger/Logger.module.scss"; +import { useLogger } from "./useLogger"; +import { Window } from "components/Window/Window"; +import { Button } from "common"; +import { useConfig } from "common"; + +export const Logger = () => { + + const [state, startLogging, stopLogging] = useLogger(); + const config = useConfig(); + + return ( + +
+ Logging: {`${state}`} +
+ + +
+
+
+ ); +}; diff --git a/control-station/src/components/Logger/useLogger.ts b/control-station/src/components/Logger/useLogger.ts new file mode 100644 index 000000000..c4fc57df9 --- /dev/null +++ b/control-station/src/components/Logger/useLogger.ts @@ -0,0 +1,29 @@ +import { useSubscribe, useWsHandler } from "common"; +import { useState } from "react"; +import { useMeasurementsStore } from "common"; + +export function useLogger() { + const [state, setState] = useState(false); + + const handler = useWsHandler(); + + function getLoggedVariableIds() { + return useMeasurementsStore.getState().getLogVariables(); + } + + function startLogging() { + const variables = getLoggedVariableIds(); + handler.post("logger/variables", variables); + handler.post("logger/enable", true); + } + + function stopLogging() { + handler.post("logger/enable", false); + } + + useSubscribe("logger/response", (result) => { + setState(result); + }); + + return [state, startLogging, stopLogging] as const; +} diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/BoardOrders.module.scss b/control-station/src/components/OrdersContainer/Orders/BoardOrders/BoardOrders.module.scss new file mode 100644 index 000000000..9f46f5d51 --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/BoardOrders.module.scss @@ -0,0 +1,28 @@ +@use "src/styles/styles"; +.boardOrders { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.name { + display: flex; + gap: 0.3rem; + font-family: Inter; + font-weight: 500; + font-size: 0.8rem; + color: hsl(29, 88%, 70%); + cursor: pointer; +} +.orders, +.stateOrders { + display: flex; + flex-direction: column; + gap: 0.2rem; + + .title { + color: rgb(145, 145, 145); + font-weight: 500; + font-size: 0.7rem; + } +} \ No newline at end of file diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/BoardOrders.tsx b/control-station/src/components/OrdersContainer/Orders/BoardOrders/BoardOrders.tsx new file mode 100644 index 000000000..d628f74eb --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/BoardOrders.tsx @@ -0,0 +1,60 @@ +import { BoardOrders } from "common"; +import styles from "./BoardOrders.module.scss"; +import { OrderForm } from "./OrderForm/OrderForm"; +import { useState } from "react"; +import { Caret } from "common"; + +type Props = { + boardOrders: BoardOrders; + alwaysShowStateOrders: boolean; +}; + +export const BoardOrdersView = ({ + boardOrders, + alwaysShowStateOrders, +}: Props) => { + + const [isOpen, setIsOpen] = useState(true); + + return ( +
+
setIsOpen((prev) => !prev)} > + + {boardOrders.name} +
+ +
+ Permanent orders + {boardOrders.orders.map((desc) => { + return ( + + ); + })} +
+ + {(boardOrders.stateOrders.length > 0 && + (alwaysShowStateOrders || boardOrders.stateOrders.some((item) => item.enabled))) && ( +
+ State orders + {boardOrders.stateOrders.map((desc) => { + if (alwaysShowStateOrders || desc.enabled) { + return ( + + ); + } else { + return false; + } + })} +
+ )} +
+ ); +}; diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/Field.module.scss b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/Field.module.scss new file mode 100644 index 000000000..2d6f177a0 --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/Field.module.scss @@ -0,0 +1,25 @@ +@use "src/styles/styles"; + +.fieldWrapper { + width: 100%; + display: grid; + grid-template-columns: minmax(10rem, 1fr) 1fr auto; //TODO: show tooltip in names (to account for overflow) + gap: 1rem; + align-items: center; + gap: 0.2rem; +} + +.input { + display: flex; + flex-flow: row; + gap: 0.2rem; +} + +.name { + text-overflow: ellipsis; + overflow: hidden; + font-size: 0.8rem; +} +.disabled { + color: rgb(176, 176, 176); +} diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/Field.tsx b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/Field.tsx new file mode 100644 index 000000000..f70e81d96 --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/Field.tsx @@ -0,0 +1,72 @@ +import styles from "./Field.module.scss"; +import { NumericType } from "common"; +import { isNumberValid } from "./validation"; +import { NumericInput, CheckBox, Dropdown } from "common"; +import { FormField } from "../../form"; + +type Props = { + name: string; + field: FormField; + onChange: (newValue: boolean | string | number, isValid: boolean) => void; + changeEnabled: (isEnabled: boolean) => void; +}; + +export const Field = ({ name, field, onChange, changeEnabled }: Props) => { + function handleTextInputChange( + value: string, + type: NumericType, + range: [number | null, number | null] + ) { + const isValid = isNumberValid(value, type, range); + onChange(Number.parseFloat(value), isValid); + } + + return ( +
+
{name}
+ {field.kind == "numeric" ? ( + + handleTextInputChange( + value, + field.type, + field.safeRange + ) + } + /> + ) : field.kind == "boolean" ? ( + { + onChange(value, true); + }} + /> + ) : ( + { + onChange(newValue, true); + }} + /> + )} + + +
+ ); +}; diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/validation.ts b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/validation.ts new file mode 100644 index 000000000..0c356c401 --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Field/validation.ts @@ -0,0 +1,111 @@ +import { + NumericType, + isSignedIntegerType, + isUnsignedIntegerType, +} from "common"; + +export function isNumberValid( + valueStr: string, + numberType: NumericType, + range: [number | null, number | null] +): boolean { + if (stringIsNumber(valueStr, numberType)) { + if (isUnsignedIntegerType(numberType)) { + let isValid = true; + if (range[0]) { + isValid &&= Number.parseInt(valueStr) >= range[0]; + } + + if (range[1]) { + isValid &&= Number.parseInt(valueStr) <= range[1]; + } + + return ( + isValid && + checkUnsignedIntegerOverflow( + Number.parseInt(valueStr), + getBits(numberType) + ) + ); + } else if (isSignedIntegerType(numberType)) { + let isValid = true; + if (range[0]) { + isValid &&= Number.parseInt(valueStr) >= range[0]; + } + + if (range[1]) { + isValid &&= Number.parseInt(valueStr) <= range[1]; + } + + return ( + isValid && + checkSignedIntegerOverflow( + Number.parseInt(valueStr), + getBits(numberType) + ) + ); + } else { + let isValid = true; + if (range[0]) { + isValid &&= Number.parseFloat(valueStr) >= range[0]; + } + + if (range[1]) { + isValid &&= Number.parseFloat(valueStr) <= range[1]; + } + + return isValid && checkFloatOverflow(Number.parseFloat(valueStr)); + } + } else { + return false; + } +} + +function stringIsNumber(valueStr: string, numberType: NumericType): boolean { + if (isUnsignedIntegerType(numberType)) { + return /^\d+$/.test(valueStr); + } else if (isSignedIntegerType(numberType)) { + return /^-?\d+$/.test(valueStr); + } else { + return /^-?\d+(?:\.\d+)?$/.test(valueStr); + } +} + +function checkUnsignedIntegerOverflow(value: number, bits: number): boolean { + return value >= 0 && value <= Math.pow(2, bits) - 1; +} + +function checkSignedIntegerOverflow(value: number, bits: number): boolean { + const min = -Math.pow(2, bits - 1); + const max = Math.pow(2, bits - 1) - 1; + return value >= min && value <= max; +} + +function checkFloatOverflow(value: number): boolean { + return !Number.isNaN(value); +} + +function getBits(type: NumericType): number { + switch (type) { + case "uint8": + return 8; + case "uint16": + return 16; + case "uint32": + return 32; + case "uint64": + return 64; + case "int8": + return 8; + case "int16": + return 16; + case "int32": + return 32; + case "int64": + return 64; + case "float32": + return 32; + case "float64": + return 64; + } +} diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Fields.module.scss b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Fields.module.scss new file mode 100644 index 000000000..302176cff --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Fields.module.scss @@ -0,0 +1,13 @@ +@use "src/styles/styles"; + +.fieldsWrapper { + width: 100%; + padding: 0.4rem; + border-top: 1px solid styles.$orange; + + display: flex; + flex-direction: column; + gap: 0.2rem; + + overflow-x: auto; +} diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Fields.tsx b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Fields.tsx new file mode 100644 index 000000000..e2a53d5fe --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Fields/Fields.tsx @@ -0,0 +1,36 @@ +import styles from "./Fields.module.scss"; +import { Field } from "./Field/Field"; +import { FormField } from "../form"; + +type Props = { + fields: FormField[]; + updateField: ( + id: string, + value: boolean | string | number, + isValid: boolean + ) => void; + changeEnable: (id: string, isEnabled: boolean) => void; +}; + +export const Fields = ({ fields, updateField, changeEnable }: Props) => { + return ( +
+ {fields.map((field) => { + return ( + { + updateField(field.id, newValue, isValid); + }} + changeEnabled={(value) => changeEnable(field.id, value)} + /> + ); + })} +
+ ); +}; diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Header/Header.module.scss b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Header/Header.module.scss new file mode 100644 index 000000000..90611249e --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Header/Header.module.scss @@ -0,0 +1,75 @@ +@use "src/styles/styles"; +$header-color: #ffe4cc; + +.headerWrapper { + flex: 0 0 0; + padding: 0.25rem 0.3rem; + display: grid; + grid-template: + "caret name target button" auto + / auto 1fr auto 4rem; + align-items: center; + gap: 0.3rem; + background-color: $header-color; + cursor: pointer; +} + +.caret { + grid-area: caret; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.caret > * { + color: styles.$orange; +} + +.visible { + visibility: visible; +} + +.hidden { + visibility: hidden; +} + +.name { + grid-area: name; + color: styles.$orange; + font-weight: 500; + font-size: 0.8rem; + overflow: hidden; + text-overflow: ellipsis; +} + +.target { + grid-area: target; + color: red; + font-size: 0.7rem; + margin: 0 0.3rem; + opacity: 0; + cursor: pointer; + transition: opacity 0.07s linear; +} + +.targetVisible { + opacity: 1; +} + +.headerWrapper:hover { + .target:not(.targetVisible) { + opacity: 0.5; + } +} + +.sendBtn { + grid-area: button; + height: 100%; + button { + height: 100%; + font-size: 0.7rem !important; + padding: 0.2rem !important; + } +} diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Header/Header.tsx b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Header/Header.tsx new file mode 100644 index 000000000..10ee5800b --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/Header/Header.tsx @@ -0,0 +1,81 @@ +import styles from './Header.module.scss'; +import { useState, useEffect } from 'react'; +import { Button, Caret } from 'common'; +import { SpringValue, animated } from '@react-spring/web'; +import { ReactComponent as Target } from 'assets/svg/target.svg'; + +export type HeaderInfo = ToggableHeader | FixedHeader; + +type ToggableHeader = { + type: 'toggable'; + isOpen: boolean; + toggleDropdown: () => void; +}; + +type FixedHeader = { + type: 'fixed'; +}; + +type Props = { + name: string; + disabled: boolean; + info: HeaderInfo; + springs: Record; + onTargetClick: (state: boolean) => void; + onButtonClick: () => void; +}; + +export const Header = ({ + name, + disabled, + info, + springs, + onTargetClick, + onButtonClick, +}: Props) => { + const [targetOn, setTargetOn] = useState(false); + + useEffect(() => { + onTargetClick(targetOn); + }, [targetOn]); + + return ( + {}} + style={{ + ...springs, + cursor: info.type == 'toggable' ? 'pointer' : 'auto', + }} + > + +
{name}
+ { + ev.stopPropagation(); + setTargetOn((prev) => { + return !prev; + }); + }} + /> +
+
+
+ ); +}; diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/OrderForm.module.scss b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/OrderForm.module.scss new file mode 100644 index 000000000..49b6d61e4 --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/OrderForm.module.scss @@ -0,0 +1,17 @@ +@use "src/styles/styles"; + +.orderFormWrapper { + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + @include styles.code-text; + font-size: 0.7rem; + border: 1px solid styles.$orange; + border-radius: 0.3rem; + overflow: hidden; + box-sizing: border-box; + + @include styles.shadow; +} diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/OrderForm.tsx b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/OrderForm.tsx new file mode 100644 index 000000000..227c75a5d --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/OrderForm.tsx @@ -0,0 +1,87 @@ +import styles from './OrderForm.module.scss'; +import { OrderDescription } from 'common'; +import { Header, HeaderInfo } from './Header/Header'; +import { Fields } from './Fields/Fields'; +import { useContext, useState } from 'react'; +import { Order } from 'common'; +import { useForm } from './useForm'; +import { useSpring } from '@react-spring/web'; +import { useListenKey } from './useListenKey'; +import { OrderContext } from '../../OrderContext'; +import { FormField } from './form'; + +type Props = { + description: OrderDescription; +}; + +function createOrder(id: number, fields: FormField[]): Order { + return { + id: id, + fields: Object.fromEntries( + fields.map((field) => { + return [ + field.id, + { + value: field.value, + isEnabled: field.isEnabled, + type: field.type, + }, + ]; + }) + ), + }; +} + +export const OrderForm = ({ description }: Props) => { + const sendOrder = useContext(OrderContext); + const { form, updateField, changeEnable } = useForm(description.fields); + const [isOpen, setIsOpen] = useState(false); + const [springs, api] = useSpring(() => ({ + from: { filter: 'brightness(1)' }, + config: { + tension: 600, + }, + })); + + const trySendOrder = () => { + if (form.isValid) { + api.start({ + from: { filter: 'brightness(1.2)' }, + to: { filter: 'brightness(1)' }, + }); + + sendOrder(createOrder(description.id, form.fields)); + } + }; + + const listen = useListenKey(' ', trySendOrder); + + const headerInfo: HeaderInfo = + form.fields.length > 0 + ? { + type: 'toggable', + isOpen: isOpen, + toggleDropdown: () => setIsOpen((prevValue) => !prevValue), + } + : { type: 'fixed' }; + + return ( +
+
+ {isOpen && ( + + )} +
+ ); +}; diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/form.ts b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/form.ts new file mode 100644 index 000000000..3c2181f29 --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/form.ts @@ -0,0 +1,102 @@ +import { + BooleanDescription, + EnumDescription, + NumericDescription, + OrderFieldDescription, +} from "common"; + +export type FormField = NumericField | BooleanField | EnumField; + +type AbstractFormField = { + id: string; + isValid: boolean; + isEnabled: boolean; +}; + +export type NumericField = AbstractFormField & + NumericDescription & { + value: number; + }; + +export type BooleanField = AbstractFormField & + BooleanDescription & { + value: boolean; + }; + +export type EnumField = AbstractFormField & + EnumDescription & { + value: string; + }; + +export type Form = { + fields: FormField[]; + isValid: boolean; +}; + +export function areFieldsValid(fields: Array): boolean { + return fields.reduce((prevValid, currentField) => { + return ( + prevValid && + ((currentField.isEnabled && currentField.isValid) || + !currentField.isEnabled) + ); + }, true); +} + +export function createForm( + descriptions: Record +): Form { + const fields = Object.entries(descriptions).map(([_, fieldDescription]) => { + const field = getFormField(fieldDescription); + return field; + }); + + return { fields, isValid: areFieldsValid(fields) }; +} + +function getFormField(desc: OrderFieldDescription): FormField { + if (desc.kind == "numeric") { + return getNumericFormField(desc); + } else if (desc.kind == "boolean") { + return getBooleanFormField(desc); + } else { + return getEnumFormField(desc); + } +} + +function getNumericFormField(desc: NumericDescription): NumericField { + return { + id: desc.id, + name: desc.name, + kind: desc.kind, + type: desc.type, + safeRange: desc.safeRange, + warningRange: desc.warningRange, + value: 0, + isValid: false, + isEnabled: true, + }; +} +function getBooleanFormField(desc: BooleanDescription): BooleanField { + return { + id: desc.id, + name: desc.name, + kind: desc.kind, + type: desc.type, + value: false, + isValid: true, + isEnabled: true, + }; +} +function getEnumFormField(desc: EnumDescription): EnumField { + return { + id: desc.id, + name: desc.name, + kind: desc.kind, + type: desc.type, + options: desc.options, + value: desc.options[0], + isValid: true, + isEnabled: true, + }; +} diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/useForm.ts b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/useForm.ts new file mode 100644 index 000000000..df9edeb4f --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/useForm.ts @@ -0,0 +1,78 @@ +import { OrderFieldDescription } from "common"; +import { useReducer } from "react"; +import { Form, areFieldsValid, createForm, FormField } from "./form"; + +type Action = UpdateField | ChangeEnable; + +type UpdateField = { + type: "update_field"; + payload: { + id: string; + isValid: boolean; + value: number | string | boolean; + }; +}; + +type ChangeEnable = { + type: "change_enable"; + payload: { + id: string; + enable: boolean; + }; +}; + +function reducer(state: Form, action: Action): Form { + switch (action.type) { + case "update_field": { + const fields: FormField[] = state.fields.map((field) => { + if (field.id == action.payload.id) { + return { + ...field, + value: action.payload.value, + isValid: action.payload.isValid, + }; + } + return field; + }) as FormField[]; + + return { + fields, + isValid: areFieldsValid(fields), + }; + } + + case "change_enable": { + const fields = state.fields.map((field) => + field.id == action.payload.id + ? { ...field, isEnabled: action.payload.enable } + : field + ); + return { + fields, + isValid: areFieldsValid(fields), + }; + } + } +} + +export function useForm(descriptions: Record) { + const [form, dispatch] = useReducer(reducer, descriptions, createForm); + + const updateField = ( + id: string, + value: string | boolean | number, + isValid: boolean + ) => + dispatch({ + type: "update_field", + payload: { id, value, isValid }, + }); + + const changeEnable = (id: string, enable: boolean) => + dispatch({ + type: "change_enable", + payload: { id, enable }, + }); + + return { form, updateField, changeEnable } as const; +} diff --git a/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/useListenKey.ts b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/useListenKey.ts new file mode 100644 index 000000000..d4150974f --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/BoardOrders/OrderForm/useListenKey.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from "react"; + +export function useListenKey(key: string, callback: () => unknown) { + const [listen, setListen] = useState(false); + + const listener = (ev: KeyboardEvent) => { + if (ev.key == key) { + ev.preventDefault(); + callback(); + } + }; + + useEffect(() => { + if (listen) { + document.addEventListener("keydown", listener); + } + + return () => { + document.removeEventListener("keydown", listener); + }; + }, [listen, key, listener, callback]); + + return (value: boolean) => { + setListen(value); + }; +} diff --git a/control-station/src/components/OrdersContainer/Orders/OrderContext.ts b/control-station/src/components/OrdersContainer/Orders/OrderContext.ts new file mode 100644 index 000000000..4e67398ed --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/OrderContext.ts @@ -0,0 +1,4 @@ +import { Order } from "common"; +import { createContext } from "react"; + +export const OrderContext = createContext<(order: Order) => void>(() => {}); diff --git a/control-station/src/components/OrdersContainer/Orders/Orders.module.scss b/control-station/src/components/OrdersContainer/Orders/Orders.module.scss new file mode 100644 index 000000000..fb774b6fe --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/Orders.module.scss @@ -0,0 +1,41 @@ +.ordersWrapper { + width: 100%; + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + overflow-y: auto; + gap: 0.2rem; + box-sizing: border-box; +} + +.stateOrdersToggle { + top: 0px; + position: sticky; + display: flex; + flex-direction: row; + gap: 0.2rem; + z-index: 1; + font-size: 0.7rem; + background-color: white; + align-items: center; +} + +.stateOrdersToggleButton { + font-size: 0.6rem; + padding: 0.15rem 0.3rem; + height: auto; + min-height: auto; +} + +.boardOrderList { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.boardOrderList > :not(:last-child)::after { + content: ""; + border-bottom: 1px solid rgba(165, 131, 101, 0.368); + margin-bottom: 0.2rem; +} diff --git a/control-station/src/components/OrdersContainer/Orders/Orders.tsx b/control-station/src/components/OrdersContainer/Orders/Orders.tsx new file mode 100644 index 000000000..0073c4268 --- /dev/null +++ b/control-station/src/components/OrdersContainer/Orders/Orders.tsx @@ -0,0 +1,50 @@ +import styles from './Orders.module.scss'; +import { BoardOrders, Button } from 'common'; +import { OrderContext } from './OrderContext'; +import { useSendOrder } from '../useSendOrder'; +import { BoardOrdersView } from './BoardOrders/BoardOrders'; +import { useState } from 'react'; + +type Props = { + boards: BoardOrders[]; +}; + +export const Orders = ({ boards }: Props) => { + const sendOrder = useSendOrder(); + const [alwaysShowStateOrders, setAlwaysShowStateOrders] = useState(false); + + return ( + +
+
+ Always show state orders:{' '} +
+
+ {boards.map((board) => { + return ( + (board.orders.length > 0 || + board.stateOrders.length > 0) && ( + + ) + ); + })} +
+
+
+ ); +}; diff --git a/control-station/src/components/OrdersContainer/OrdersContainer.module.scss b/control-station/src/components/OrdersContainer/OrdersContainer.module.scss new file mode 100644 index 000000000..946612df1 --- /dev/null +++ b/control-station/src/components/OrdersContainer/OrdersContainer.module.scss @@ -0,0 +1,13 @@ +.orderTableWrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.emptyAlert { + margin-top: 2rem; + text-align: center; + color: rgb(156, 156, 156); +} diff --git a/control-station/src/components/OrdersContainer/OrdersContainer.tsx b/control-station/src/components/OrdersContainer/OrdersContainer.tsx new file mode 100644 index 000000000..720a25258 --- /dev/null +++ b/control-station/src/components/OrdersContainer/OrdersContainer.tsx @@ -0,0 +1,47 @@ +import styles from "./OrdersContainer.module.scss"; +import { Orders } from "./Orders/Orders"; +import { useConfig, useFetchBack, BoardOrders } from "common"; +import { useEffect } from "react"; +import { useOrders } from "common"; +import { useOrdersStore } from "common"; + +interface Props { + boardFilter?: string[]; // Array de nombres de placas a mostrar + boardOrdersFilter?: (boardOrders: BoardOrders[]) => BoardOrders[]; // Función personalizada para filtrar BoardOrders +} + +export const OrdersContainer = ({ boardFilter, boardOrdersFilter }: Props) => { + const config = useConfig(); + const setOrders = useOrdersStore((state) => state.setOrders); + + const orderDescriptionPromise = useFetchBack( + import.meta.env.PROD, + config.paths.orderDescription + ); + useEffect(() => { + orderDescriptionPromise.then((desc) => setOrders(desc)); + }, []); + + const orders = useOrders(); + + let filteredOrders = orders; + + if (boardFilter) { + filteredOrders = filteredOrders.filter(board => boardFilter.includes(board.name)); + } + if (boardOrdersFilter) { + filteredOrders = boardOrdersFilter(filteredOrders); + } + + return ( +
+ {filteredOrders.length == 0 ? ( + + Orders added to ADJ will appear here + + ) : ( + + )} +
+ ); +}; diff --git a/control-station/src/components/OrdersContainer/useSendOrder.ts b/control-station/src/components/OrdersContainer/useSendOrder.ts new file mode 100644 index 000000000..5b510bad0 --- /dev/null +++ b/control-station/src/components/OrdersContainer/useSendOrder.ts @@ -0,0 +1,9 @@ +import { Order, useWsHandler } from "common"; + +export function useSendOrder() { + const handler = useWsHandler(); + + return (order: Order) => { + handler.post("order/send", order); + }; +} diff --git a/control-station/src/components/OrientationIndicator/OrientationIndicator.module.scss b/control-station/src/components/OrientationIndicator/OrientationIndicator.module.scss new file mode 100644 index 000000000..fd3d43b9d --- /dev/null +++ b/control-station/src/components/OrientationIndicator/OrientationIndicator.module.scss @@ -0,0 +1,61 @@ +@use 'src/styles/fonts'; + +.bar_indicator { + position: relative; + + min-height: fit-content; + width: 141px; + max-width: 141px; + min-width: 141px; + background-color: #C2E9D0; + + padding: 8px; + + display: flex; + flex-flow: row; + align-items: center; + justify-content: space-between; + gap: 1px; +} + + +.name_display, +.value_display { + display: flex; + flex-flow: row; + align-items: center; + gap: 4px; +} + +.name_display { + width: 100%; +} + +.icon { + max-height: 23px; + opacity: 0.8; +} + +.name { + width: 100%; + margin: 2px 2px; + opacity: 0.8; + font-size: 14px; + font-style: italic; + font-weight: 300; + text-overflow: ellipsis; +} + +.value, +.units { + opacity: 0.8; + margin: 0; + font-size: 12px; + font-weight: 400; + max-width: 70px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; +} + diff --git a/control-station/src/components/OrientationIndicator/OrientationIndicator.tsx b/control-station/src/components/OrientationIndicator/OrientationIndicator.tsx new file mode 100644 index 000000000..8f00b277f --- /dev/null +++ b/control-station/src/components/OrientationIndicator/OrientationIndicator.tsx @@ -0,0 +1,48 @@ +import { useGlobalTicker } from 'common'; +import styles from './OrientationIndicator.module.scss'; +import { memo, useContext, useState } from 'react'; +import { LostConnectionContext } from 'services/connections'; + +interface Props { + icon?: string; + name: string; + getValue: () => number; + units?: string; + color?: string; + backgroundColor?: string; + className?: string; +} + +export const OrientationIndicator = memo( + ({ + icon, + name, + getValue, + units, + className, + }: Props) => { + const [valueState, setValueState] = useState(0); + const lostConnection = useContext(LostConnectionContext); + + useGlobalTicker(() => { + setValueState(getValue()); + }); + + return ( +
+
+ +

{name}

+
+
+

+ {lostConnection ? '-.--' : valueState?.toFixed(2)} +

+

{units}

+
+
+ ); + } +); diff --git a/control-station/src/components/StateIndicator/StateIndicator.tsx b/control-station/src/components/StateIndicator/StateIndicator.tsx index 6dc03b992..a63b8ea10 100644 --- a/control-station/src/components/StateIndicator/StateIndicator.tsx +++ b/control-station/src/components/StateIndicator/StateIndicator.tsx @@ -33,7 +33,7 @@ export const StateIndicator = memo(({ measurementId, icon }: Props) => {

- {lostConnection ? 'DISCONNECTED' : variant} + {lostConnection ? 'DISCONNECTED' : state}

diff --git a/control-station/src/components/Window/Window2.module.scss b/control-station/src/components/Window/Window2.module.scss new file mode 100644 index 000000000..f8c007233 --- /dev/null +++ b/control-station/src/components/Window/Window2.module.scss @@ -0,0 +1,33 @@ +@use 'src/styles/colors'; +@use 'src/styles/fonts'; + +.window { + display: flex; + flex-flow: column; + border-radius: 0.8rem; + filter: var(--shadow); + overflow: hidden; + width: auto; + flex: 1 0 auto; +} + +.header { + background-color: #D1E3F1; + color: #5894A7; + font-weight: bold; + padding: 0.3rem 0.6rem; + font-size: map-get($map: fonts.$font-sizes, $key: x-small); +} + +.content { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 1.5rem; + padding: 0.6rem; + background-color: colors.getColor('primary', 99); + overflow: scroll; + height: 100%; + width: 100%; +} \ No newline at end of file diff --git a/control-station/src/components/Window/Window2.tsx b/control-station/src/components/Window/Window2.tsx new file mode 100644 index 000000000..b2908c192 --- /dev/null +++ b/control-station/src/components/Window/Window2.tsx @@ -0,0 +1,16 @@ +import styles from 'components/Window/Window2.module.scss'; + +type Props = { + title: string; + children?: React.ReactNode; + className?: string; +}; + +export const Window2 = ({ title, children, className }: Props) => { + return ( +
+
{title}
+
{children}
+
+ ); +}; \ No newline at end of file diff --git a/control-station/src/hooks/useConnectionContext.ts b/control-station/src/hooks/useConnectionContext.ts new file mode 100644 index 000000000..22a717f0c --- /dev/null +++ b/control-station/src/hooks/useConnectionContext.ts @@ -0,0 +1,28 @@ +import { Connection, useConnections } from 'common'; + +export function useConnectionContext() { + const connections = useConnections(); + + function isDisconnected(connection: Connection): boolean { + return !connection.isConnected; + } + + function any(data: T[], condition: (value: T) => boolean): boolean { + for (const value of data) { + if (condition(value)) { + return true; + } + } + return false; + } + + const lostConnection = any( + [...connections.boards, connections.backend], + isDisconnected + ); + + return { + connections, + lostConnection + }; +} diff --git a/control-station/src/hooks/useEmergencyOrders.ts b/control-station/src/hooks/useEmergencyOrders.ts index b4087a070..860946423 100644 --- a/control-station/src/hooks/useEmergencyOrders.ts +++ b/control-station/src/hooks/useEmergencyOrders.ts @@ -1,5 +1,5 @@ import { Order, useListenKey, useSendOrder } from 'common'; -import { emergencyStopOrders } from 'pages/VehiclePage/Data2Page/FixedOrders'; +import { emergencyStopOrders } from 'pages/VehiclePage/BatteriesPage/FixedOrders'; export function useEmergencyOrders( shortcut: string = ' ', diff --git a/control-station/src/main.tsx b/control-station/src/main.tsx index 1966b4ce7..2209aa666 100644 --- a/control-station/src/main.tsx +++ b/control-station/src/main.tsx @@ -9,10 +9,10 @@ import { } from 'react-router-dom'; import { App } from './App'; import './index.css'; -import { vehicleRoute } from 'pages/VehiclePage/vehicleRoute'; +import { mainPageRoute } from 'pages/VehiclePage/MainPage/mainPageRoute'; import { camerasRoute } from 'pages/CamerasPage/camerasRoute'; -import { tubeRoute } from 'pages/TubePage/tubeRoute'; -import { guiRoute } from 'pages/VehiclePage/GuiBoosterPage/guiRoute'; +import { batteriesRoute } from 'pages/VehiclePage/BatteriesPage/batteriesRoute' +import { boosterRoute } from 'pages/VehiclePage/BoosterPage/boosterRoute'; import { ConfigProvider, GlobalTicker } from 'common'; const router = createBrowserRouter([ @@ -21,10 +21,10 @@ const router = createBrowserRouter([ element: , children: [ { path: '', element: }, - vehicleRoute, - camerasRoute, - tubeRoute, - guiRoute, + mainPageRoute, + boosterRoute, + batteriesRoute, + camerasRoute ], }, ]); diff --git a/control-station/src/pages/PageWrapper/PageWrapper.module.scss b/control-station/src/pages/PageWrapper/PageWrapper.module.scss index ba565d0b0..0d8c211ec 100644 --- a/control-station/src/pages/PageWrapper/PageWrapper.module.scss +++ b/control-station/src/pages/PageWrapper/PageWrapper.module.scss @@ -4,7 +4,7 @@ .page { display: flex; flex-flow: column; - gap: 0.6rem; + gap: 0.4rem; width: 100%; diff --git a/control-station/src/pages/VehiclePage/BatteriesPage/BatteriesPage.module.scss b/control-station/src/pages/VehiclePage/BatteriesPage/BatteriesPage.module.scss new file mode 100644 index 000000000..7415662c7 --- /dev/null +++ b/control-station/src/pages/VehiclePage/BatteriesPage/BatteriesPage.module.scss @@ -0,0 +1,277 @@ +.header{ + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.batteriesMainContainer{ + display: flex; + justify-content: center; + align-items: flex-start; + width: 100%; + min-height: 100vh; + padding: 5px; + box-sizing: border-box; +} + +.batteriesContainer { + width: 100%; + max-width: none; + background-color: white; + border-radius: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + overflow-y: auto; + box-sizing: border-box; + padding: 10px; + margin: 0; +} + +.sectionContainer { + width: 100%; + margin-bottom: 20px; +} + +.sectionTitle { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 1.4rem; + text-align: center; + margin-bottom: 15px; + font-weight: bold; +} + +.statusContainer { + margin: 10px auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + max-width: 95%; +} + +.statusRow1, .statusRow2 { + background-color: #FFE7CF; + border-radius: 20px; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + width: 100%; + margin-bottom: 5px; /* Espacio entre filas */ +} + +.statusItem{ + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; +} + +.value{ + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + border: 1.5px solid #EF7E30; + border-radius: 1.2rem; + font-size: 16px; + height: 40px; + width: 40%; + margin-left: 5px; + display: flex; + justify-content: center; + align-items: center; +} + + +.statusFirstLabel { + width: 95%; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + border: 2.5px solid #FFE7CF; + border-radius: 20px; + text-align: center; + background-color: #FFF7EC; + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + margin-bottom: 10px; +} + +.statusSecondLabel { + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + border: 2.5px solid #FFE7CF; + border-radius: 20px; + width: 95%; + text-align: center; + background-color: #FFF7EC; + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; +} +.value { + border: 1.5px solid #EF7E30; + border-radius: 1.2rem; + width: 40%; + height: 40px; + font-size: 25px; + margin-left: 5px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; +} + +.modulesContainer { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 10px; + width: 100%; + padding: 0 10px; + justify-items: center; + margin: 0 auto; +} + +.lowVoltageContainer { + display: flex; + justify-content: center; + width: 100%; + padding: 0 20px; +} + +h1 { + padding-left: 20px; + padding-top: 20px; +} + +.batteriesDecorationLine { + flex: 2; + border-bottom: 2.5px solid #C6DCE9; + margin-top: 20px; + margin-left: 40px; +} + +h2 { + font-size: 25px; +} + +h3 { + font-size: 20px; +} + +p { + font-size: 15px; +} + +.main { + display: flex; + flex-direction: row; +} + +.messagesAndOrders { + margin-top: 20px; + flex-grow: 1; + width: 100%; + max-width: 25vw; +} + +.orders { + margin-top: 20px; + width: 100%; + height: 100%; + flex: 1; + overflow: auto; + height: 42vh; + max-height: 42vh; + min-height: 42vh; +} + +.messages { + flex: 1; + overflow: auto; + height: 20rem; +} + +.order_column { + display: flex; + flex-flow: column; + gap: 1rem; + width: 100%; +} + +.voltageValueContainer { + display: inline-block; + padding: 5px 10px; + border: 1px solid #EF7E30; + border-radius: 15px; +} + +.voltageValue { + font-size: 1.2rem; + font-weight: bold; +} + +/* Voltage Bar Styles */ +.voltageBar { + width: 100%; + background-color: #FFE7CF; + border-radius: 10px; + padding: 10px 15px; + margin-bottom: 15px; + border: 2px solid #EF7E30; +} + +.voltageBarTitle { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 1rem; + font-weight: bold; + text-align: center; + margin: 0 0 10px 0; +} + +.voltageBarContent { + display: flex; + justify-content: space-around; + align-items: center; + flex-wrap: wrap; + gap: 10px; +} + +.voltageBarItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; +} + +.voltageBarLabel { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.7rem; + font-weight: bold; + text-align: center; +} + +.voltageBarValue { + color: #EF7E30; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.85rem; + font-weight: bold; + background-color: white; + padding: 3px 8px; + border-radius: 8px; + border: 1px solid #EF7E30; + text-align: center; + min-width: 50px; +} diff --git a/control-station/src/pages/VehiclePage/BatteriesPage/BatteriesPage.tsx b/control-station/src/pages/VehiclePage/BatteriesPage/BatteriesPage.tsx new file mode 100644 index 000000000..64d6b2734 --- /dev/null +++ b/control-station/src/pages/VehiclePage/BatteriesPage/BatteriesPage.tsx @@ -0,0 +1,225 @@ +import { useState } from "react"; +import styles from "./BatteriesPage.module.scss"; +import BatteriesModule from "components/BatteriesModules/BatteriesModule"; +import LowVoltageModule from "components/BatteriesModules/LowVoltageModule"; +import { useMeasurementsStore, HvscuCabinetMeasurements, useGlobalTicker, BcuMeasurements, HvscuMeasurements, BmslMeasurements } from "common"; +import { usePodDataUpdate } from 'hooks/usePodDataUpdate'; +import { Connection, useConnections } from 'common'; +import { LostConnectionContext } from 'services/connections'; + +interface ModuleData { + id: number | string; + name: string; +} + +const highVoltageGroup1: ModuleData[] = [ + { id: 1, name: "Module 1" }, + { id: 2, name: "Module 2" }, + { id: 3, name: "Module 3" }, + { id: 4, name: "Module 4" }, + { id: 5, name: "Module 5" }, + { id: 6, name: "Module 6" }, +]; + +const highVoltageGroup2: ModuleData[] = [ + { id: 7, name: "Module 7" }, + { id: 8, name: "Module 8" }, + { id: 9, name: "Module 9" }, + { id: 10, name: "Module 10" }, + { id: 11, name: "Module 11" }, + { id: 12, name: "Module 12" }, +]; + +const highVoltageGroup3: ModuleData[] = [ + { id: 13, name: "Module 13" }, + { id: 14, name: "Module 14" }, + { id: 15, name: "Module 15" }, + { id: 16, name: "Module 16" }, + { id: 17, name: "Module 17" }, + { id: 18, name: "Module 18" }, +]; + +export function BatteriesPage() { + usePodDataUpdate(); + + const connections = useConnections(); + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + + const totalVoltageHighMeasurement = getNumericMeasurementInfo(HvscuMeasurements.BatteriesVoltage) + const maxVoltageHighMeasurement = getNumericMeasurementInfo(HvscuMeasurements.VoltageMax) + const minVoltageHighMeasurement = getNumericMeasurementInfo(HvscuMeasurements.VoltageMin) + const maxTempHighMeasurement = getNumericMeasurementInfo(HvscuMeasurements.TempMax) + const minTempHighMeasurement = getNumericMeasurementInfo(HvscuMeasurements.TempMin) + + const totalVoltageLowMeasurement = getNumericMeasurementInfo(BmslMeasurements.totalVoltage) + const maxVoltageLowMeasurement = getNumericMeasurementInfo(BmslMeasurements.voltageMax) + const minVoltageLowMeasurement = getNumericMeasurementInfo(BmslMeasurements.voltageMin) + const maxTempLowMeasurement = getNumericMeasurementInfo(BmslMeasurements.tempMax) + const minTempLowMeasurement = getNumericMeasurementInfo(BmslMeasurements.tempMin) + + const [voltageTotal, setVoltageTotal] = useState(null); + const [maxVoltageHigh, setMaxVoltageHigh] = useState(null); + const [minVoltageHigh, setMinVoltageHigh] = useState(null); + const [maxTempHigh, setMaxTempHigh] = useState(null); + const [minTempHigh, setMinTempHigh] = useState(null); + + const [voltageTotalLow, setVoltageTotalLow] = useState(null); + const [maxVoltageLow, setMaxVoltageLow] = useState(null); + const [minVoltageLow, setMinVoltageLow] = useState(null); + const [maxTempLow, setMaxTempLow] = useState(null); + const [minTempLow, setMinTempLow] = useState(null); + + useGlobalTicker(() => { + setVoltageTotal(totalVoltageHighMeasurement.getUpdate); + setMaxVoltageHigh(maxVoltageHighMeasurement.getUpdate); + setMinVoltageHigh(minVoltageHighMeasurement.getUpdate); + setMaxTempHigh(maxTempHighMeasurement.getUpdate); + setMinTempHigh(minTempHighMeasurement.getUpdate); + + setVoltageTotalLow(totalVoltageLowMeasurement.getUpdate); + setMaxVoltageLow(maxVoltageLowMeasurement.getUpdate); + setMinVoltageLow(minVoltageLowMeasurement.getUpdate); + setMaxTempLow(maxTempLowMeasurement.getUpdate); + setMinTempLow(minTempLowMeasurement.getUpdate); + }); + + return ( + +
+
+ +

High Voltage Batteries

+
+
+
+

Total Voltage:

+
+ {voltageTotal?.toFixed(1)}V +
+
+
+

Max V:

+
+ {maxVoltageHigh?.toFixed(2)}V +
+
+
+

Min V:

+
+ {minVoltageHigh?.toFixed(2)}V +
+
+
+

Max Temp:

+
+ {maxTempHigh?.toFixed(1)}°C +
+
+
+

Min Temp:

+
+ {minTempHigh?.toFixed(1)}°C +
+
+
+
+ +
+
+ {highVoltageGroup1.map((module) => ( + + ))} +
+
+ +
+
+ {highVoltageGroup2.map((module) => ( + + ))} +
+
+ +
+
+ {highVoltageGroup3.map((module) => ( + + ))} +
+
+ +
+

Low Voltage Battery

+ + {/* Low Voltage Bar */} +
+
+
+

Total Voltage:

+
+ {voltageTotalLow?.toFixed(1) ?? "-"}V +
+
+
+

Max V:

+
+ {maxVoltageLow?.toFixed(2) ?? "-"}V +
+
+
+

Min V:

+
+ {minVoltageLow?.toFixed(2) ?? "-"}V +
+
+
+

Max Temp:

+
+ {maxTempLow?.toFixed(1) ?? "-"}°C +
+
+
+

Min Temp:

+
+ {minTempLow?.toFixed(1) ?? "-"}°C +
+
+
+
+ +
+ +
+
+
+
+
+ ); +} + +function isDisconnected(connection: Connection): boolean { + return !connection.isConnected; +} + +function all(data: T[], condition: (value: T) => boolean): boolean { + for (const value of data) { + if (!condition(value)) { + return false; + } + } + return true; +} + +function any(data: T[], condition: (value: T) => boolean): boolean { + for (const value of data) { + if (condition(value)) { + return true; + } + } + return false; +} diff --git a/control-station/src/pages/VehiclePage/Data2Page/FixedOrders.module.scss b/control-station/src/pages/VehiclePage/BatteriesPage/FixedOrders.module.scss similarity index 100% rename from control-station/src/pages/VehiclePage/Data2Page/FixedOrders.module.scss rename to control-station/src/pages/VehiclePage/BatteriesPage/FixedOrders.module.scss diff --git a/control-station/src/pages/VehiclePage/Data2Page/FixedOrders.tsx b/control-station/src/pages/VehiclePage/BatteriesPage/FixedOrders.tsx similarity index 88% rename from control-station/src/pages/VehiclePage/Data2Page/FixedOrders.tsx rename to control-station/src/pages/VehiclePage/BatteriesPage/FixedOrders.tsx index 810ffb1ad..b9f986fc8 100644 --- a/control-station/src/pages/VehiclePage/Data2Page/FixedOrders.tsx +++ b/control-station/src/pages/VehiclePage/BatteriesPage/FixedOrders.tsx @@ -45,8 +45,18 @@ export const openContactorsOrders: Order[] = [ ]; export const emergencyStopOrders: Order[] = [ - ...brakeOrders, - ...openContactorsOrders, + { + id: 55, + fields: {}, + }, + { + id: 1799, + fields: {}, + }, + { + id: 1698, + fields: {}, + }, { id: 0, fields: {}, @@ -54,8 +64,8 @@ export const emergencyStopOrders: Order[] = [ ]; export const desiredOrders = [ - 0, 902, 903, 216, 210, 355, 356, 357, 360, 609, 619, 614, 615, 363, 364, - 293, 294, 645, 646, + 53, 44, 52, 43, 37, 46, 38, 47, 62, 55, 1799, 1795, 1792, 1791, + 1694, 1695, 1697, 1698, 1693, 1699 ]; export function getHardcodedOrders(boardOrders: BoardOrders[]): BoardOrders[] { diff --git a/control-station/src/pages/VehiclePage/BatteriesPage/batteriesRoute.tsx b/control-station/src/pages/VehiclePage/BatteriesPage/batteriesRoute.tsx new file mode 100644 index 000000000..da8c73120 --- /dev/null +++ b/control-station/src/pages/VehiclePage/BatteriesPage/batteriesRoute.tsx @@ -0,0 +1,6 @@ +import { BatteriesPage } from "./BatteriesPage"; + +export const batteriesRoute = { + path: "/batteries", + element: +}; \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/Boards/BMSL/BMSL.tsx b/control-station/src/pages/VehiclePage/Boards/BMSL/BMSL.tsx index 33ce650c2..aceae5762 100644 --- a/control-station/src/pages/VehiclePage/Boards/BMSL/BMSL.tsx +++ b/control-station/src/pages/VehiclePage/Boards/BMSL/BMSL.tsx @@ -1,4 +1,4 @@ -import { Window } from 'components/Window/Window'; +/* import { Window } from 'components/Window/Window'; import { GaugeTag } from 'components/GaugeTag/GaugeTag'; import { BmslMeasurements, useMeasurementsStore } from 'common'; import { IndicatorStack } from 'components/IndicatorStack/IndicatorStack'; @@ -175,3 +175,6 @@ export const BMSL = () => { ); }; + */ + +export {} \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/Boards/HVSCU/HVSCU.module.scss b/control-station/src/pages/VehiclePage/Boards/HVSCU/HVSCU.module.scss new file mode 100644 index 000000000..97ccf9111 --- /dev/null +++ b/control-station/src/pages/VehiclePage/Boards/HVSCU/HVSCU.module.scss @@ -0,0 +1,79 @@ +.HVSCU{ + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding: 0rem; + margin: 0; + border-radius: 0.75rem; +} + +.subtitle { + font-size: 1.4rem; + font-weight: 300; + text-align: center; + color: #5894A7; + font-style: italic; +} + +.levitationUnitsWrapper { + display: flex; + flex-direction: row; + justify-content: stretch; + align-items: stretch; + width: 100%; + height: 100%; + gap: 0.5rem; +} + +.levitationUnitsColumn { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: space-evenly; + flex: 1 1 0; + width: 100%; + height: 100%; +} + +.currentBar { + display: flex; + align-items: right; + justify-content: space-between; + border-radius: 0.5rem; + padding: 0.3rem 0.8rem; + font-size: 0.9rem; + color: #2e7d32; + font-weight: 500; + + &::before { + content: ''; + display: inline-block; + background-color: #66bb6a; + height: 0.6rem; + border-radius: 0.3rem; + margin-right: 0.5rem; + width: 80%; + } +} + +.text { + display: flex; + align-items: center; + margin: 0.8rem 0.5rem; + gap: 0.6rem; + flex: 1; + min-height: 3rem; +} + +.text > :first-child { + min-width: 9rem; + max-width: 9rem; + font-size: 1.1rem; +} + +.text > :last-child { + flex: 1 1 0; + min-width: 14rem; + min-height: 2.5rem; +} \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/Boards/HVSCU/HVSCU.tsx b/control-station/src/pages/VehiclePage/Boards/HVSCU/HVSCU.tsx new file mode 100644 index 000000000..24a5a568e --- /dev/null +++ b/control-station/src/pages/VehiclePage/Boards/HVSCU/HVSCU.tsx @@ -0,0 +1,25 @@ +import { Window } from 'components/Window/Window'; +import styles from './HVSCU.module.scss'; +import { ImdIndicator } from 'components/ImdIndicator/ImdIndicator'; +import { EnumIndicator } from 'components/EnumIndicator/EnumIndicator'; +import Battery from 'assets/svg/battery-filled.svg' +import Contactors from 'assets/svg/open-contactors-icon.svg' +import { HvscuCabinetMeasurements, HvscuMeasurements } from 'common'; + +export const HVSCU = () => { +// TODO: Get correct measurements from ADJ and correct icons + return ( + +
+
+
+
IMD
+
Vehicle Contactors
+
Cabinet Contactors
+
SDC
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/Boards/LCU/LCU.module.scss b/control-station/src/pages/VehiclePage/Boards/LCU/LCU.module.scss index bc93d358a..6231cfcb3 100644 --- a/control-station/src/pages/VehiclePage/Boards/LCU/LCU.module.scss +++ b/control-station/src/pages/VehiclePage/Boards/LCU/LCU.module.scss @@ -1,29 +1,89 @@ .LCUWrapper { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - width: 100%; + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + height: 8%; + padding: 0rem; + border-radius: 0.75rem; +} + +.subtitle { + font-size: 1rem; + text-align: center; + color: #5894A7; + font-style: italic; + margin-top: 2px; + margin-bottom: 2px; + padding-top: 0; + padding-bottom: 0; } .levitationUnitsWrapper { - display: flex; - gap: 1rem; - width: 100%; + display: flex; + justify-content: space-between; + width: 100%; + gap: 1rem; } .levitationUnitsColumn { - display: flex; - flex-direction: column; - gap: 1rem; - width: 100%; + display: flex; + flex-direction: column; + gap: 0.5rem; + flex: 1; +} + +.currentBar { + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 0.5rem; + padding: 0.3rem 0.8rem; + font-size: 0.9rem; + color: #2e7d32; + font-weight: 500; + + &::before { + content: ''; + display: inline-block; + background-color: #66bb6a; + height: 0.6rem; + border-radius: 0.3rem; + margin-right: 0.5rem; + width: 80%; + } } .rotationIndicatorsWrapper { - max-width: 40rem; - display: flex; - justify-content: center; - gap: 0.8rem; - flex-wrap: wrap; + margin-top: 0.2rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + border-radius: 0.5rem; + padding: 0; +} + +.rotationRow { + display: flex; + flex-direction: row; + gap: 1rem; + justify-content: center; + width: 100%; +} + +.positionRow { + display: flex; + flex-direction: row; + gap: 1rem; + justify-content: center; + width: 100%; +} + +.rotationIndicator { + font-size: 0.9rem; + font-weight: 500; + display: flex; + flex-direction: column; + align-items: center; } diff --git a/control-station/src/pages/VehiclePage/Boards/LCU/LCU.tsx b/control-station/src/pages/VehiclePage/Boards/LCU/LCU.tsx index c4e2b938f..d84b35823 100644 --- a/control-station/src/pages/VehiclePage/Boards/LCU/LCU.tsx +++ b/control-station/src/pages/VehiclePage/Boards/LCU/LCU.tsx @@ -1,6 +1,5 @@ import { Window } from 'components/Window/Window'; import styles from './LCU.module.scss'; -import { BarIndicator } from 'components/BarIndicator/BarIndicator'; import pitchRotation from 'assets/svg/pitch-rotation.svg'; import yawRotation from 'assets/svg/yaw-rotation.svg'; import rollRotation from 'assets/svg/roll-rotation.svg'; @@ -9,6 +8,7 @@ import yIndex from 'assets/svg/y-index.svg'; import { IndicatorStack } from 'components/IndicatorStack/IndicatorStack'; import { LevitationUnit } from 'components/LevitationUnit/LevitationUnit'; import { LcuMeasurements, useMeasurementsStore } from 'common'; +import { OrientationIndicator } from 'components/OrientationIndicator/OrientationIndicator'; export const LCU = () => { const getNumericMeasurementInfo = useMeasurementsStore( @@ -22,129 +22,77 @@ export const LCU = () => { const positionZ = getNumericMeasurementInfo(LcuMeasurements.positionZ); return ( - +
- - - - - +
Left side
+ + + + +
- - - - - +
Right side
+ + + + +
+
Orientation
- - - - - - - - - - - - - - - +
+ + + + + + + + + +
+
+ + + + + + +
); }; + diff --git a/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.module.scss b/control-station/src/pages/VehiclePage/BoosterPage/BoosterPage.module.scss similarity index 89% rename from control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.module.scss rename to control-station/src/pages/VehiclePage/BoosterPage/BoosterPage.module.scss index 7d75863c6..a434ddf94 100644 --- a/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.module.scss +++ b/control-station/src/pages/VehiclePage/BoosterPage/BoosterPage.module.scss @@ -22,6 +22,7 @@ overflow: hidden; box-sizing: border-box; margin: 20px; + padding-bottom: 2vh; } .statusContainer { @@ -63,6 +64,7 @@ font-size: 25px; height: 100px; width: 100%; + min-width: 150px; display: flex; justify-content: center; align-items: center; @@ -100,7 +102,8 @@ .value { border: 1.5px solid #EF7E30; border-radius: 1.2rem; - width: 40%; + width: 60%; + min-width: 150px; height: 40px; font-size: 25px; margin-left: 5px; @@ -149,19 +152,33 @@ p { .messagesAndOrders { margin-top: 20px; - overflow: hidden; flex-grow: 1; width: 100%; max-width: 25vw; } .orders { - text-align: center; - border-radius: 20px; - padding-top: 20px; margin-top: 20px; width: 100%; - height: 80px; + height: 100%; + flex: 1; + overflow: auto; + height: 42vh; + max-height: 42vh; + min-height: 42vh; +} + +.messages { + flex: 1; + overflow: auto; + height: 20rem; +} + +.order_column { + display: flex; + flex-flow: column; + gap: 1rem; + width: 100%; } .voltageValueContainer { diff --git a/control-station/src/pages/VehiclePage/BoosterPage/BoosterPage.tsx b/control-station/src/pages/VehiclePage/BoosterPage/BoosterPage.tsx new file mode 100644 index 000000000..834b8708d --- /dev/null +++ b/control-station/src/pages/VehiclePage/BoosterPage/BoosterPage.tsx @@ -0,0 +1,147 @@ +import { useContext, useState } from "react"; +import styles from "./BoosterPage.module.scss"; +import BoosterModule from "components/BoosterModules/BoosterModule"; +import { useMeasurementsStore, HvscuCabinetMeasurements, getBooleanMeasurement, GlobalTicker, useGlobalTicker, useOrders, BoardOrders, MessagesContainer, BcuMeasurements } from "common"; +import { OrdersContainer } from "components/OrdersContainer/OrdersContainer"; +import { Window } from "components/Window/Window"; +import { getHardcodedOrders } from "../BatteriesPage/FixedOrders"; +import { usePodDataUpdate } from 'hooks/usePodDataUpdate'; +import { Connection, useConnections } from 'common'; +import { LostConnectionContext } from 'services/connections'; + +interface ModuleData { + id: number | string; + name: string; +} + +const modules: ModuleData[] = [ + { id: 1, name: "Module 1" }, + { id: 2, name: "Module 2" }, + { id: 3, name: "Module 3" }, +]; + +export function BoosterPage() { + usePodDataUpdate(); + + const connections = useConnections(); + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + + const boardOrders = useOrders(); + + // Medidas + const totalSupercapsVoltageInfo = getNumericMeasurementInfo(HvscuCabinetMeasurements.TotalVoltage) + + const currentMeasurementInfo = getNumericMeasurementInfo(HvscuCabinetMeasurements.CurrentOutput) + + const temperatureMeasurementInfo = getNumericMeasurementInfo(HvscuCabinetMeasurements.Temperature) + + // Enums + const contactorsStateMeasurement = useMeasurementsStore( + (state) => state.getEnumMeasurementInfo(HvscuCabinetMeasurements.ContactorsState).getUpdate + ); + const bcuGeneralStateMeasurement = useMeasurementsStore( + (state) => state.getEnumMeasurementInfo(BcuMeasurements.generalState).getUpdate + ); + const bcuOperationalStateMeasurement = useMeasurementsStore( + (state) => state.getEnumMeasurementInfo(BcuMeasurements.operationalState).getUpdate + ); + + // Estados + const [voltageTotal, setVoltageTotal] = useState(null); + const [current, setCurrent] = useState(null); + const [temperature, setTemperature] = useState(null); + const [contactorsState, setContactorsState] = useState(contactorsStateMeasurement); + const [generalState, setGeneralState] = useState(bcuGeneralStateMeasurement); + const [operationalState, setOperationalState] = useState(bcuOperationalStateMeasurement); + + useGlobalTicker(() => { + setVoltageTotal(totalSupercapsVoltageInfo.getUpdate); + setCurrent(currentMeasurementInfo.getUpdate); + setTemperature(temperatureMeasurementInfo.getUpdate); + setContactorsState(contactorsStateMeasurement); + setGeneralState(bcuGeneralStateMeasurement); + setOperationalState(bcuOperationalStateMeasurement); + }); + + const bcuState = (generalState == 'Operational') ? operationalState : generalState; + + return ( + +
+
+
+
+
+

Total Voltage:

+
+ {voltageTotal?.toFixed(2) ?? "-"} V +
+
+
+

Current:

+
+ {current?.toFixed(2) ?? "-"} A +
+
+
+

Contactors status:

+
+ {contactorsState ?? "-"} +
+
+
+

Charge:

+
+ - % +
+
+
+
+ +
+ {modules.map((module) => ( + + ))} +
+
+
+ + + + +
+ +
+
+
+
+
+ ); +} + +function isDisconnected(connection: Connection): boolean { + return !connection.isConnected; +} + +function all(data: T[], condition: (value: T) => boolean): boolean { + for (const value of data) { + if (!condition(value)) { + return false; + } + } + return true; +} + +function any(data: T[], condition: (value: T) => boolean): boolean { + for (const value of data) { + if (condition(value)) { + return true; + } + } + return false; +} diff --git a/control-station/src/pages/VehiclePage/BoosterPage/boosterRoute.tsx b/control-station/src/pages/VehiclePage/BoosterPage/boosterRoute.tsx new file mode 100644 index 000000000..065c097a5 --- /dev/null +++ b/control-station/src/pages/VehiclePage/BoosterPage/boosterRoute.tsx @@ -0,0 +1,6 @@ +import { BoosterPage } from "./BoosterPage"; + +export const boosterRoute = { + path: "/booster", + element: +}; \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/Data1Page/Data1Page.module.scss b/control-station/src/pages/VehiclePage/Data1Page/Data1Page.module.scss deleted file mode 100644 index 3546f1f24..000000000 --- a/control-station/src/pages/VehiclePage/Data1Page/Data1Page.module.scss +++ /dev/null @@ -1,31 +0,0 @@ -.data1_page { - display: flex; - flex-flow: row wrap; - gap: 1rem; -} - -.column { - display: flex; - flex-flow: column; - gap: 1rem; - - &:nth-child(1) { - width: 27vw; - } - - &:nth-child(2) { - width: 12vw; - } - - &:nth-child(3) { - width: 12vw; - } - - &:nth-child(4) { - width: 16vw; - } - - &:nth-child(5) { - width: 26vw; - } -} diff --git a/control-station/src/pages/VehiclePage/Data1Page/Data1Page.tsx b/control-station/src/pages/VehiclePage/Data1Page/Data1Page.tsx deleted file mode 100644 index 7327cc032..000000000 --- a/control-station/src/pages/VehiclePage/Data1Page/Data1Page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import styles from './Data1Page.module.scss'; -import { OBCCUBatteries } from '../Boards/OBCCU/OBCCUBatteries'; -import { OBCCUGeneralInfo } from '../Boards/OBCCU/OBCCUGeneralInfo'; -import { VCUBrakesInfo } from '../Boards/VCU/VCUBrakesInfo'; -import { VCUPositionInfo } from '../Boards/VCU/VCUPositionInfo'; -import { BCU } from '../Boards/BCU/BCU'; -import { BMSL } from '../Boards/BMSL/BMSL'; -import { useEmergencyOrders } from 'hooks/useEmergencyOrders'; -import Connections from '../Windows/Connections'; -import { Connection, useConnections } from 'common'; -import { PCU } from '../Boards/PCU/PCU'; - -export const Data1Page = () => { - useEmergencyOrders(); - - return ( -
- {/*
- -
- -
- - -
- -
- -
- -
- -
- -
- - -
*/} -
- ); -}; diff --git a/control-station/src/pages/VehiclePage/Data2Page/Data2Page.module.scss b/control-station/src/pages/VehiclePage/Data2Page/Data2Page.module.scss deleted file mode 100644 index 112216e09..000000000 --- a/control-station/src/pages/VehiclePage/Data2Page/Data2Page.module.scss +++ /dev/null @@ -1,38 +0,0 @@ -.data2_page { - display: flex; - flex-flow: row wrap; - gap: 1rem; -} - -.column { - display: flex; - flex-flow: column; - gap: 1rem; - height: fit-content; - - &:nth-child(1) { - width: 34vw; - } - - &:nth-child(2) { - width: 34vw; - } -} - -.orders { - max-height: 90vh; - width: 18vw; - max-width: 18vw; -} - -.order_column { - display: flex; - flex-flow: column; - gap: 1rem; -} - -.messages { - height: 70vh; - max-height: 70vh; - width: 100%; -} diff --git a/control-station/src/pages/VehiclePage/Data2Page/Data2Page.tsx b/control-station/src/pages/VehiclePage/Data2Page/Data2Page.tsx deleted file mode 100644 index eed01389d..000000000 --- a/control-station/src/pages/VehiclePage/Data2Page/Data2Page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import styles from './Data2Page.module.scss'; -import { LCU } from '../Boards/LCU/LCU'; -import { PCU } from '../Boards/PCU/PCU'; -import { Orders, useOrders } from 'common'; -import { Connections, Logger, MessagesContainer } from 'common'; -import { Window } from 'components/Window/Window'; -import FixedOrders, { getHardcodedOrders } from './FixedOrders'; - -export const Data2Page = () => { - const boardOrders = useOrders(); - - return ( -
-
- -
- -
- - - - - - - - - - - -
- -
- -
- - -
-
-
-
- ); -}; diff --git a/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.tsx b/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.tsx deleted file mode 100644 index b84b32462..000000000 --- a/control-station/src/pages/VehiclePage/GuiBoosterPage/GuiPage.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { useEffect, useState } from "react"; -import styles from "./GuiPage.module.scss"; -import Module from "../../../components/GuiModules/Module"; -import { Messages } from "../Messages/Messages"; -import { Orders, useMeasurementsStore, NumericMeasurementInfo } from "common"; - -interface ModuleData { - id: number | string; - name: string; -} - -const modules: ModuleData[] = [ - { id: 1, name: "Module 1" }, - { id: 2, name: "Module 2" }, - { id: 3, name: "Module 3" }, -]; - -export function GuiPage() { - // Medidas - const totalSupercapsVoltageInfo = useMeasurementsStore((state) => - state.getNumericMeasurementInfo("HVSCU-Cabinet/total_supercaps_voltage") - ); - const currentMeasurementInfo = useMeasurementsStore((state) => - state.getNumericMeasurementInfo("HVSCU-Cabinet/output_current") - ); - const temperatureMeasurementInfo = useMeasurementsStore((state) => - state.getNumericMeasurementInfo("HVSCU-Cabinet/temperature_total") - ); - - // Enums - const contactorsStateInfo = useMeasurementsStore((state) => - state.getMeasurement("HVSCU-Cabinet/contactors_state") - ); - const bcuGeneralStateInfo = useMeasurementsStore((state) => - state.getMeasurement("HVSCU-Cabinet/BCU_state_master_nested") - ); - - // Estados - const [voltageTotal, setVoltageTotal] = useState(null); - const [current, setCurrent] = useState(null); - const [temperature, setTemperature] = useState(null); - const [contactorsState, setContactorsState] = useState(null); - const [bcuState, setBcuState] = useState(null); - - // Efectos - useEffect(() => { - setVoltageTotal(totalSupercapsVoltageInfo?.getUpdate() ?? null); // Si `getUpdate()` es el método adecuado - }, [totalSupercapsVoltageInfo]); - - useEffect(() => { - setCurrent(currentMeasurementInfo?.getUpdate() ?? null); // Similar para otras mediciones - }, [currentMeasurementInfo]); - - useEffect(() => { - setTemperature(temperatureMeasurementInfo?.getUpdate() ?? null); - }, [temperatureMeasurementInfo]); - - - useEffect(() => { - const value = contactorsStateInfo?.value; - setContactorsState(typeof value === "string" ? value : null); - }, [contactorsStateInfo]); - - useEffect(() => { - const value = bcuGeneralStateInfo?.value; - setBcuState(typeof value === "string" ? value : null); - }, [bcuGeneralStateInfo]); - - return ( -
-
-
-
-
-
-

V total:

-
- {voltageTotal ?? "-"} V -
-
-
-

Current:

-
- {current ?? "-"} A -
-
-
-

Contactors status:

-
- {contactorsState ?? "-"} -
-
-
-
-
-

BCU status:

-
- {bcuState ?? "-"} -
-
-
-

Temperature total:

-
- {temperature ?? "-"} ºC -
-
-
-

Charge:

-
- - % -
-
-
-
- -
- {modules.map((module) => ( - - ))} -
-
- -
-
- -
-
- -
-
-
-
- ); -} diff --git a/control-station/src/pages/VehiclePage/GuiBoosterPage/guiRoute.tsx b/control-station/src/pages/VehiclePage/GuiBoosterPage/guiRoute.tsx deleted file mode 100644 index 5efff228c..000000000 --- a/control-station/src/pages/VehiclePage/GuiBoosterPage/guiRoute.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GuiPage } from "./GuiPage"; -export const guiRoute = { - path: "/guiBooster", - element: , -}; diff --git a/control-station/src/pages/VehiclePage/MainPage/MainPage.module.scss b/control-station/src/pages/VehiclePage/MainPage/MainPage.module.scss new file mode 100644 index 000000000..9735565e8 --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/MainPage.module.scss @@ -0,0 +1,228 @@ +.current_chart { + width: 100%; + height: 100%; + max-height: 10em; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + padding: 0.3em; + margin: 0; +} + +.chart { + width: 100%; + height: 8em; + max-height: 8em; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +.chart > div, +.chart svg { + width: 100% !important; + height: 100% !important; + max-width: 100% !important; + max-height: 100% !important; +} + +.title { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.data1_page { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + box-sizing: border-box; + padding: 0.5rem; + gap: 0.4rem; + margin: 0; + max-width: 100%; + width: 100%; + overflow-x: hidden; + overflow-y: auto; +} + +.leds { + display: flex; + justify-content: center; + align-items: center; + padding: 0.5rem; + border-radius: 0.5rem; + font-weight: bold; + width: 100%; + height: 1rem; + min-height: 1rem; + max-height: 1rem; + border: 2px solid #000; +} + +.pod { + display: flex; + position: relative; + justify-content: center; + align-items: center; + padding: 0.5rem; + font-weight: bold; + width: 100%; + height: 2rem; + min-height: 2rem; + max-height: 2rem; + border: 2px solid #000; +} + +.break_state { + display: flex; + justify-content: center; + align-items: center; + padding: 0.5rem; + border-radius: 0.5rem; + font-weight: bold; + width: 100%; + height: 40px; + min-height: 40px; + max-height: 40px; +} + +.leds { + background-color: #faebd7; +} + +.break_state { + background-color: #f3785c; + color: white; +} + +.main { + display: grid; + grid-template-columns: 1fr 2.3fr 1fr; + gap: 0.5rem; + width: 100%; + min-width: 0; + max-width: 100%; + overflow-x: hidden; + overflow-y: visible; +} + +.column { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + min-width: 0; + max-width: 100%; +} + +.column_center { + display: flex; + flex-direction: column; + gap: 0.4rem; + width: 100%; + min-width: 0; + max-width: 100%; +} + +.row { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 1rem; + width: 100%; + height: auto; + min-height: 0; + max-height: 100%; +} + +.messages { + flex: 1; + overflow: auto; +} + +.orders { + flex: 1; + overflow: auto; + height: 42vh; + min-height: 42vh; + max-height: 42vh; +} + +.order_column { + display: flex; + flex-flow: column; + gap: 1rem; + width: 100%; +} + +.emergency_wrapper { + display: flex; + justify-content: center; + height: 4.2rem; +} +.compact { + height: 200px; + overflow: visible; +} + +.podEdit { + object-fit: contain; + display: block; + transform: rotate(90deg); + transform-origin: center; + max-width: 100%; + height: auto; +} + +.LCUandMaster{ + display: flex; + flex-direction: column; + gap: 1rem; +} + +.batteriesRow { + display: flex; + flex-direction: row; + gap: 0.5rem; + height: auto; + min-height: 0; + max-height: 100%; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; + overflow-x: hidden; + overflow-y: visible; + padding: 0; + & > * { + width: auto; + flex: 1; + min-width: 0; + margin: 0; + } +} + +.pneumatic{ + display: flex; + flex-direction: row; + gap: 0; + + & > * { + border: 1px solid black; + } + + & > *:first-child { + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; + } + + & > *:last-child { + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; + } +} \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/MainPage/MainPage.tsx b/control-station/src/pages/VehiclePage/MainPage/MainPage.tsx new file mode 100644 index 000000000..c9dc15227 --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/MainPage.tsx @@ -0,0 +1,107 @@ +import styles from './MainPage.module.scss'; +import { HVSCU } from '../Boards/HVSCU/HVSCU'; +import { MessagesContainer, VcuMeasurements } from 'common'; +import { Window } from 'components/Window/Window'; +import { emergencyStopOrders, getHardcodedOrders } from '../BatteriesPage/FixedOrders'; +import { BigOrderButton } from 'components/BigOrderButton'; +import { ChartDLIM, ChartLSM } from './MainPageModules/MainCharts'; +import { Batteries } from './MainPageModules/MainBatteries'; +import { LEDS } from './MainPageModules/Leds'; +import { BrakeState } from './MainPageModules/BrakeState'; +import { PodPosition } from './MainPageModules/PodPosition'; +import { OrdersContainer } from 'components/OrdersContainer/OrdersContainer'; +import { usePodDataUpdate } from 'hooks/usePodDataUpdate'; +import { Connection, useConnections } from 'common'; +import { LostConnectionContext } from 'services/connections'; +import { LCU } from '../Boards/LCU/LCU'; +import { Logger } from 'components/Logger/Logger'; +import { VehicleState } from 'components/EnumIndicator/VehicleState'; +import { Pneumatic } from './MainPageModules/Pneunmatic'; + +export const MainPage = () => { + usePodDataUpdate(); + + const connections = useConnections(); + + return ( + +
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ + +
+ + + + +
+ +
+
+ +
+ + + + + + + +
+
+
+ +
+ ); +}; + +function isDisconnected(connection: Connection): boolean { + return !connection.isConnected; +} + +function all(data: T[], condition: (value: T) => boolean): boolean { + for (const value of data) { + if (!condition(value)) { + return false; + } + } + return true; +} + +function any(data: T[], condition: (value: T) => boolean): boolean { + for (const value of data) { + if (condition(value)) { + return true; + } + } + return false; +} + +// H10: Joan Física @JFisica, Marc Sanchís @msanlli +// @JFisica: My frist time doing frontend was 4 days ago so sorry for probably breaking everything for next year \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/MainPage/MainPageModules/BrakeState.tsx b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/BrakeState.tsx new file mode 100644 index 000000000..bf2d61850 --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/BrakeState.tsx @@ -0,0 +1,26 @@ +import { VcuMeasurements, useGlobalTicker, useMeasurementsStore } from "common"; +import { useContext, useState } from "react"; +import { LostConnectionContext } from "services/connections"; +import styles from '../MainPage.module.scss'; + +export const BrakeState = () => { + const getValue = useMeasurementsStore( + (state) => state.getBooleanMeasurementInfo(VcuMeasurements.allReeds).getUpdate + ); + + const lostConnection = useContext(LostConnectionContext); + + const [ReedsState, setVariant] = useState(getValue()); + + useGlobalTicker(() => { + setVariant(getValue()); + }); + + return ( +
+
+ {lostConnection ? 'DISCONNECTED' : ReedsState ? 'BRAKED' : 'UNBRAKED'} +
+
+ ); +} \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/MainPage/MainPageModules/Leds.tsx b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/Leds.tsx new file mode 100644 index 000000000..496b2b7cd --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/Leds.tsx @@ -0,0 +1,38 @@ +import { HvscuMeasurements, useGlobalTicker, useMeasurementsStore } from "common"; +import { useContext, useState } from "react"; +import { LostConnectionContext } from "services/connections"; +import styles from '../MainPage.module.scss'; +import { useEffect } from 'react'; + +export const LEDS = () => { + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + const lostConnection = useContext(LostConnectionContext); + const voltage = getNumericMeasurementInfo( + HvscuMeasurements.VoltageReading + ); + + const [VoltageValue, setValueState] = useState(0); + useGlobalTicker(() => { + setValueState(voltage.getUpdate); + }); + + + const [blink, setBlink] = useState(true); + useEffect(() => { + if (lostConnection) return; + const interval = setInterval(() => { + setBlink((b) => !b); + }, 500); + return () => clearInterval(interval); + }, [lostConnection]); + + const bgColor = lostConnection + ? '#cccccc' + : VoltageValue < 60 + ? ('#9BF37C') + : (blink ? 'red' : 'white'); + + return ( +
+ ); +} \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/MainPage/MainPageModules/MainBatteries.tsx b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/MainBatteries.tsx new file mode 100644 index 000000000..111f26b90 --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/MainBatteries.tsx @@ -0,0 +1,52 @@ +import { Window2 } from 'components/Window/Window2'; +import styles from '../MainPage.module.scss'; +import { BatteryIndicator } from 'components/BatteryIndicator/BatteryIndicator'; +import { BmslMeasurements, useMeasurementsStore, HvscuMeasurements, HvscuCabinetMeasurements } from "common"; +import { GaugeTag } from 'components/GaugeTag/GaugeTag'; + +export const Batteries = () => { + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + + const CurrentHV = getNumericMeasurementInfo(HvscuMeasurements.CurrentReading); + const CurrentLV = getNumericMeasurementInfo(BmslMeasurements.current); + const CurrentCabinet = getNumericMeasurementInfo(HvscuCabinetMeasurements.CurrentOutput); + + const SocHigh = getNumericMeasurementInfo(HvscuMeasurements.MinimumSoc); + const SocLow = getNumericMeasurementInfo(BmslMeasurements.stateOfCharge); + const SocCabinet = getNumericMeasurementInfo(HvscuCabinetMeasurements.Soc); + + const TotalVoltageHigh = getNumericMeasurementInfo(HvscuMeasurements.BatteriesVoltage); + const TotalVoltageLow = getNumericMeasurementInfo(BmslMeasurements.totalVoltage); + const TotalVoltageCabinet = getNumericMeasurementInfo(HvscuCabinetMeasurements.TotalVoltage); + + const BusHV = getNumericMeasurementInfo(HvscuMeasurements.BusVoltage); + const BusCabinet = getNumericMeasurementInfo(HvscuCabinetMeasurements.BusVoltage); + + return ( +
+ + + + + + + + + + + + + + + + +
+ ); + +}; \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/MainPage/MainPageModules/MainCharts.tsx b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/MainCharts.tsx new file mode 100644 index 000000000..ff25e9107 --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/MainCharts.tsx @@ -0,0 +1,101 @@ +import { BcuMeasurements, ColorfulChart, PcuMeasurements, useMeasurementsStore } from "common"; +import { useContext } from "react"; +import { LostConnectionContext } from "services/connections"; +import styles from '../MainPage.module.scss'; +import { Window } from 'components/Window/Window'; + + +export const ChartDLIM = () => { + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + const lostConnection = useContext(LostConnectionContext); + const DLIMcurrentU = getNumericMeasurementInfo( + PcuMeasurements.motorACurrentU + ); + const DLIMcurrentV = getNumericMeasurementInfo( + PcuMeasurements.motorACurrentV + ); + const DLIMcurrentW = getNumericMeasurementInfo( + PcuMeasurements.motorACurrentW + ); + + return ( + +
+ 0, + } + : DLIMcurrentU, + lostConnection + ? { + ...DLIMcurrentV, + getUpdate: () => 0, + } + : DLIMcurrentV, + lostConnection + ? { + ...DLIMcurrentW, + getUpdate: () => 0, + } + : DLIMcurrentW, + ]} + /> +
+ +
+ ); + + +}; + +export const ChartLSM = () => { + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + const lostConnection = useContext(LostConnectionContext); + const LSMaverageCurrentU = getNumericMeasurementInfo( + BcuMeasurements.averageCurrentU + ); + const LSMaverageCurrentV = getNumericMeasurementInfo( + BcuMeasurements.averageCurrentV + ); + const LSMaverageCurrentW = getNumericMeasurementInfo( + BcuMeasurements.averageCurrentW + ); + + + return ( + +
+ 0, + } + : LSMaverageCurrentU, + lostConnection + ? { + ...LSMaverageCurrentV, + getUpdate: () => 0, + } + : LSMaverageCurrentV, + lostConnection + ? { + ...LSMaverageCurrentW, + getUpdate: () => 0, + } + : LSMaverageCurrentW, + ]} + /> +
+ +
+ ); +}; \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/MainPage/MainPageModules/Pneunmatic.tsx b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/Pneunmatic.tsx new file mode 100644 index 000000000..01d9cb737 --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/Pneunmatic.tsx @@ -0,0 +1,73 @@ +import { BarIndicator } from "components/BarIndicator/BarIndicator" +import { Window } from "components/Window/Window"; +import pressure from 'assets/svg/pressure-filled.svg' +import thermometer from 'assets/svg/thermometer-field.svg' +import { useMeasurementsStore, VcuMeasurements } from "common"; +import styles from '../MainPage.module.scss' + +export const Pneumatic = () => { + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + + const HighPressure = getNumericMeasurementInfo(VcuMeasurements.pressureHigh); + const BrakesPressure = getNumericMeasurementInfo(VcuMeasurements.pressureBrakes); + const CapsulePressure = getNumericMeasurementInfo(VcuMeasurements.pressureCapsule); + const CoolingEM = getNumericMeasurementInfo(''); + const CoolingPCB = getNumericMeasurementInfo(''); + + return ( + +
+ + + + + +
+
+ ) +} \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/MainPage/MainPageModules/PodPosition.tsx b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/PodPosition.tsx new file mode 100644 index 000000000..cbcf11101 --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/MainPageModules/PodPosition.tsx @@ -0,0 +1,49 @@ +import { PcuMeasurements, useGlobalTicker, useMeasurementsStore } from "common"; +import { useContext, useState } from "react"; +import { LostConnectionContext } from "services/connections"; +import levion from 'assets/svg/levion.svg' +import styles from '../MainPage.module.scss'; +import { getPercentageFromRange } from "state"; + +export const PodPosition = () => { + const getNumericMeasurementInfo = useMeasurementsStore((state) => state.getNumericMeasurementInfo); + const lostConnection = useContext(LostConnectionContext); + const position = getNumericMeasurementInfo( + PcuMeasurements.podPosition + ); + + const [positionValue, setValueState] = useState(0); + useGlobalTicker(() => { + setValueState(position.getUpdate); + }); + + const percent = getPercentageFromRange(positionValue, 0, 53.4); + + return ( +
+ Pod +
+ ); +} \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/MainPage/mainPageRoute.tsx b/control-station/src/pages/VehiclePage/MainPage/mainPageRoute.tsx new file mode 100644 index 000000000..c1f22943d --- /dev/null +++ b/control-station/src/pages/VehiclePage/MainPage/mainPageRoute.tsx @@ -0,0 +1,6 @@ +import { MainPage } from "./MainPage"; + +export const mainPageRoute = { + path: "/vehicle", + element: +}; \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/VehiclePage.module.scss b/control-station/src/pages/VehiclePage/VehiclePage.module.scss index 1078cbe8a..d601d8d45 100644 --- a/control-station/src/pages/VehiclePage/VehiclePage.module.scss +++ b/control-station/src/pages/VehiclePage/VehiclePage.module.scss @@ -2,6 +2,5 @@ position: fixed; right: 50%; bottom: 1rem; - width: fit-content; } diff --git a/control-station/src/pages/VehiclePage/VehiclePage.tsx b/control-station/src/pages/VehiclePage/VehiclePage.tsx index 27cea023a..42e9283f0 100644 --- a/control-station/src/pages/VehiclePage/VehiclePage.tsx +++ b/control-station/src/pages/VehiclePage/VehiclePage.tsx @@ -1,22 +1,23 @@ -import styles from './VehiclePage.module.scss'; +/* import styles from './VehiclePage.module.scss'; import { Pagination } from 'components/Pagination/Pagination'; import { PageWrapper } from 'pages/PageWrapper/PageWrapper'; import { Outlet } from 'react-router-dom'; import { usePodDataUpdate } from 'hooks/usePodDataUpdate'; import { Connection, useConnections } from 'common'; import { LostConnectionContext } from 'services/connections'; +import { OBCCUGeneralInfo } from './Boards/OBCCU/OBCCUGeneralInfo'; export const VehiclePage = () => { usePodDataUpdate(); - + const connections = useConnections(); return ( @@ -24,7 +25,9 @@ export const VehiclePage = () => {
+ + ); }; @@ -49,3 +52,5 @@ function any(data: T[], condition: (value: T) => boolean): boolean { } return false; } + */ +export{} \ No newline at end of file diff --git a/control-station/src/pages/VehiclePage/Windows/Connections.tsx b/control-station/src/pages/VehiclePage/Windows/Connections.tsx index 2a635b983..e4e050e36 100644 --- a/control-station/src/pages/VehiclePage/Windows/Connections.tsx +++ b/control-station/src/pages/VehiclePage/Windows/Connections.tsx @@ -26,7 +26,7 @@ export default function Connections(_: Props) { />
diff --git a/control-station/src/pages/VehiclePage/vehicleRoute.tsx b/control-station/src/pages/VehiclePage/vehicleRoute.tsx index cd7851c97..c3ea6843a 100644 --- a/control-station/src/pages/VehiclePage/vehicleRoute.tsx +++ b/control-station/src/pages/VehiclePage/vehicleRoute.tsx @@ -1,4 +1,4 @@ -import { Data1Page } from './Data1Page/Data1Page'; +/* import { Data1Page } from './MainPage/MainPage'; import { Data2Page } from './Data2Page/Data2Page'; import { VehiclePage } from './VehiclePage'; import { Navigate } from 'react-router-dom'; @@ -7,9 +7,11 @@ export const vehicleRoute = { path: '/vehicle', element: , children: [ - { path: '', element: }, + { path: '', element: }, { path: 'data-1', element: }, { path: 'data-2', element: }, { path: 'guiBooster', element: }, ], }; + */ +export{} \ No newline at end of file diff --git a/control-station/src/styles/styles.scss b/control-station/src/styles/styles.scss new file mode 100644 index 000000000..35d1e950a --- /dev/null +++ b/control-station/src/styles/styles.scss @@ -0,0 +1,122 @@ +@use "sass:color"; + +// COLORS +$background-color: #dce3eb; +$title-color: #45677d; +$normal-text-color: #373c43; +$alternate-text-color: #505868; +$orange: #ee7623; +$blue: #317ae7; +$base-color: hsl(212, 27%, 55%); + +$dark-normal-text-color: #bec6d2; + +@function getColor($name, $lightness) { + @return var(--color-#{$name}-#{$lightness}); +} + +// FONTS +$sans-font: Inter; +$code-font: Consolas, Cascadia Code, Cascadia Mono, Monospace; +$alternate-code-font: Consolas, Monospace; + +// FONT-SIZE +$title-font-size: 1.9rem; +$normal-font-size: 1rem; +$small-font-size: x-small; + +// FONT-WEIGHT +$bold-font-weight: 700; +$normal-font-weight: 400; + +// PADDING +$large-padding: 2rem; +$normal-padding: 1.2rem; + +// BORDER-RADIUS +$large-border-radius: 1rem; +$normal-border-radius: 0.2rem; + +// BORDER-WIDTH +$normal-border-width: 1px; + +// TRANSITIONS +$normal-transition-time: 0.08s; +$opacity-transition: opacity $normal-transition-time linear; +$background-color-transition: background-color $normal-transition-time linear; + +// MIXINS +@mixin code-text { + font-family: $code-font; + font-size: $normal-font-size; + color: $normal-text-color; +} + +@mixin alternate-code-text { + font-family: $alternate-code-font; + font-size: $normal-font-size; + color: $alternate-text-color; +} + +@mixin title-text { + font-family: $sans-font; + font-size: $title-font-size; + color: $title-color; + font-weight: $bold-font-weight; +} + +@mixin normal-text { + font-family: $sans-font; + font-size: $normal-font-size; + color: $alternate-text-color; + font-weight: $normal-font-weight; +} + +@mixin subtitle-text { + font-family: $code-font; + font-size: $normal-font-size; + font-style: italic; + color: #a9adb6; +} + +@mixin tab-text { + font-family: $sans-font; + font-size: $normal-font-size; + font-weight: 500; + color: $title-color; +} + +@mixin inherit-text { + font-family: inherit; + font-size: inherit; + color: inherit; + font-weight: inherit; +} + +@mixin undraggable { + user-drag: none; + user-select: none; + -moz-user-select: none; + -webkit-user-drag: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +@mixin shadow { + box-shadow: 0px 4px 5px -4px rgba(0, 0, 0, 0.1); +} + +// CLASSES + +// FUNCTIONS +@function transparency($color, $transparency) { + @return color.adjust($color, $alpha: $transparency); +} + +@function lightness($color, $lightness) { + @return color.adjust($color, $lightness: $lightness); +} + +@function saturation($color, $saturation) { + @return color.adjust($color, $saturation: $saturation); +} diff --git a/control-station/vite.config.ts b/control-station/vite.config.ts index cbc46daf8..ea2f8d22e 100644 --- a/control-station/vite.config.ts +++ b/control-station/vite.config.ts @@ -17,3 +17,4 @@ export default defineConfig({ }, }, }); + diff --git a/ethernet-view/package-lock.json b/ethernet-view/package-lock.json index 32e2ea76f..07db7b5e7 100644 --- a/ethernet-view/package-lock.json +++ b/ethernet-view/package-lock.json @@ -131,21 +131,20 @@ } }, "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", - "license": "MIT", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -161,13 +160,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", - "license": "MIT", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -219,14 +217,13 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", - "license": "MIT", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -273,25 +270,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", - "license": "MIT", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", - "license": "MIT", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -408,16 +403,15 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", - "license": "MIT", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -426,10 +420,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "license": "MIT", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" diff --git a/ethernet-view/src/components/OrdersContainer/Orders/Orders.module.scss b/ethernet-view/src/components/OrdersContainer/Orders/Orders.module.scss index 01fab23f7..40e73ba21 100644 --- a/ethernet-view/src/components/OrdersContainer/Orders/Orders.module.scss +++ b/ethernet-view/src/components/OrdersContainer/Orders/Orders.module.scss @@ -12,10 +12,18 @@ top: 0px; position: sticky; display: flex; - flex-direction: column; - gap: 0.8rem; + flex-direction: row; + gap: 0.2rem; z-index: 1; background-color: white; + align-items: center; +} + +.stateOrdersToggleButton { + font-size: 0.6rem; + padding: 0.15rem 0.3rem; + height: auto; + min-height: auto; } .boardOrderList { diff --git a/ethernet-view/src/components/OrdersContainer/Orders/Orders.tsx b/ethernet-view/src/components/OrdersContainer/Orders/Orders.tsx index 6261e9dda..0073c4268 100644 --- a/ethernet-view/src/components/OrdersContainer/Orders/Orders.tsx +++ b/ethernet-view/src/components/OrdersContainer/Orders/Orders.tsx @@ -18,9 +18,11 @@ export const Orders = ({ boards }: Props) => {
Always show state orders:{' '} - {alwaysShowStateOrders ? 'true' : 'false'}