diff --git a/src/renderer/components/BatteryStatus/BatteryStatus.tsx b/src/renderer/components/BatteryStatus/BatteryStatus.tsx index 6623f48b..d7e79f35 100644 --- a/src/renderer/components/BatteryStatus/BatteryStatus.tsx +++ b/src/renderer/components/BatteryStatus/BatteryStatus.tsx @@ -1,7 +1,10 @@ import BatteryGauge from 'react-battery-gauge'; import React, { memo } from 'react'; import { styled } from '@/renderer/globalStyles/styled'; -import Popup from 'reactjs-popup'; +import { + StyledPopup, + StyledPopupContainer, +} from '@/renderer/components/styles'; import BatteryDetailsPopup from './BatteryDetailsPopup'; import { defaultTheme } from '@/renderer/globalStyles/themes/defaultTheme'; import useBatteryInfo from '@/renderer/hooks/useBatteryInfo'; @@ -36,7 +39,7 @@ const BatteryStatus = ({ name, topicName }: BatteryStatusProps) => { return ( + {name} {batteryInfo.percentage.toFixed(0)}% { aspectRatio={0.42} customization={customization} /> - + } on="click" position="bottom center" @@ -61,43 +64,10 @@ const BatteryStatus = ({ name, topicName }: BatteryStatusProps) => { ); }; -const Container = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - &:hover { - cursor: pointer; - } -`; - const PercentageText = styled.div` font-size: 12px; margin-right: 5px; font-weight: bold; `; -const StyledPopup = styled(Popup)` - @keyframes anvil { - 0% { - transform: scale(1) translateY(0px); - opacity: 0; - box-shadow: 0 0 0 rgba(241, 241, 241, 0); - } - 1% { - transform: scale(0.96) translateY(10px); - opacity: 0; - box-shadow: 0 0 0 rgba(241, 241, 241, 0); - } - 100% { - transform: scale(1) translateY(0px); - opacity: 1; - box-shadow: 0 0 500px rgba(241, 241, 241, 0); - } - } - &-content { - -webkit-animation: anvil 0.2s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards; - } -`; - export default memo(BatteryStatus); diff --git a/src/renderer/components/CountdownStatus/CountdownStatus.tsx b/src/renderer/components/CountdownStatus/CountdownStatus.tsx new file mode 100644 index 00000000..36a3a86f --- /dev/null +++ b/src/renderer/components/CountdownStatus/CountdownStatus.tsx @@ -0,0 +1,19 @@ +import { styled } from '@/renderer/globalStyles/styled'; +import { IoMdStopwatch } from 'react-icons/io'; +import { Countdown } from '@/renderer/components/common/Countdown'; +import React, { FC } from 'react'; + +export const CountdownStatus: FC = () => { + return ( + } + labelPopup={'scenario'} + durationDefault={35} + /> + ); +}; + +const StyledIoMdStopwatch = styled(IoMdStopwatch)` + height: 1.25em; + width: 1.25em; +`; diff --git a/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx b/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx new file mode 100644 index 00000000..19b3d3e3 --- /dev/null +++ b/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx @@ -0,0 +1,35 @@ +import { rosClient } from '@/renderer/utils/ros/rosClient'; +import React, { FC } from 'react'; +import { TopicOptions } from '@/renderer/utils/ros/roslib-ts-client/@types'; +import { styled } from '@/renderer/globalStyles/styled'; +import { FaStop } from 'react-icons/fa'; +import { toast } from 'react-toastify'; + +const stopNavigationTopic: TopicOptions = { + name: '/move_base/cancel', + messageType: 'actionlib_msgs/GoalID', +}; + +interface CancelNavigation { + isCancelNavigationProps: () => void; +} + +export const ExplorationCancelNavigation: FC = ({ + isCancelNavigationProps, +}) => { + const onClick = () => { + rosClient.publish(stopNavigationTopic, {}); + isCancelNavigationProps(); + toast.info('Navigation stop'); + }; + + return ; +}; + +export const StyledFaStop = styled(FaStop)` + margin-top: 0.25em; + color: red; + &:hover { + cursor: pointer; + } +`; diff --git a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx new file mode 100644 index 00000000..f5f7c3c3 --- /dev/null +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -0,0 +1,62 @@ +import { styled } from '@/renderer/globalStyles/styled'; +import { ExplorationCancelNavigation } from './ExplorationCancelNavigation'; +import { GoTelescope } from 'react-icons/go'; +import { Countdown } from '@/renderer/components/common/Countdown'; +import { rosClient } from '@/renderer/utils/ros/rosClient'; +import React, { FC, useState } from 'react'; +import { log } from '@/renderer/logger'; + +export const ExplorationStatus: FC = () => { + const [isNowStopCountdownTimer, setIsNowStopCountdownTimer] = useState(false); + + const startTimer = (duration: number) => { + setIsNowStopCountdownTimer(false); + startRosExplorationTimer(duration); + }; + + const stopTimer = () => { + setIsNowStopCountdownTimer(true); + stopRosExplorationTimer(); + }; + + const startRosExplorationTimer = (duration: number) => { + rosClient + .callService( + { + name: `/start_exploration`, + }, + { timeout: duration * 60 } + ) + .catch(log.error); + }; + + const stopRosExplorationTimer = () => { + rosClient + .callService( + { + name: `/stop_exploration`, + }, + {} + ) + .catch(log.error); + }; + + return ( + } + labelPopup={'exploration'} + durationDefault={2} + onStartClick={startTimer} + onStopClick={stopTimer} + sideElement={ + + } + isNowStopCountdownTimer={isNowStopCountdownTimer} + /> + ); +}; + +const StyledGoTelescope = styled(GoTelescope)` + height: 1.25em; + width: 1.25em; +`; diff --git a/src/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPin.tsx b/src/renderer/components/GpioPinsStatus/GpioPin.tsx similarity index 84% rename from src/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPin.tsx rename to src/renderer/components/GpioPinsStatus/GpioPin.tsx index 0149cf10..4d3e369f 100644 --- a/src/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPin.tsx +++ b/src/renderer/components/GpioPinsStatus/GpioPin.tsx @@ -11,19 +11,6 @@ interface GpioPinProps { gpioPin: GpioPinState; } -const Card = styled.div` - background-color: ${({ theme }) => theme.colors.darkerBackground}; - border-radius: 4px; - padding: 16px; - margin: 8px; - min-width: 150px; - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: flex-start; - box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); -`; - export const GpioPin = ({ gpioPin }: GpioPinProps) => { const dispatch = useDispatch(); @@ -43,7 +30,7 @@ export const GpioPin = ({ gpioPin }: GpioPinProps) => { return ( -

{gpioPin.name}

+ {gpioPin.name} } title={gpioPin.isOn ? 'Power Off' : 'Power On'} @@ -57,3 +44,16 @@ export const GpioPin = ({ gpioPin }: GpioPinProps) => {
); }; + +const Card = styled.div` + padding: 8px; + margin: 4px; + min-width: 150px; + display: flex; + flex-direction: column; + justify-content: flex-start; +`; + +const SytledP = styled.p` + margin-bottom: 0.25em; +`; diff --git a/src/renderer/components/GpioPinsStatus/GpioPinsStatus.tsx b/src/renderer/components/GpioPinsStatus/GpioPinsStatus.tsx new file mode 100644 index 00000000..52566644 --- /dev/null +++ b/src/renderer/components/GpioPinsStatus/GpioPinsStatus.tsx @@ -0,0 +1,64 @@ +import React, { memo } from 'react'; +import { StyledPopup } from '@/renderer/components/styles'; +import { useSelector } from 'react-redux'; +import { selectAllGpioPins } from '@/renderer/store/modules/gpioPins'; +import { GpioPin } from './GpioPin'; +import { styled } from '@/renderer/globalStyles/styled'; +import { BsLightbulb } from 'react-icons/bs'; +import { BsLightbulbOff } from 'react-icons/bs'; + +const GpioPinsStatus = () => { + const gpioPins = useSelector(selectAllGpioPins); + const isAGpioPinOn = gpioPins.find((gpioPin) => gpioPin.isOn)?.isOn; + return ( + + {isAGpioPinOn ? : } + + } + on="click" + position="bottom center" + arrow={false} + repositionOnResize={true} + > + + {gpioPins.map((gpioPin) => ( + + ))} + + + ); +}; + +const Container = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + &:hover { + cursor: pointer; + } +`; + +const StyledDiv = styled.div` + display: flex; + flex-direction: row; + align-items: flex-start; + background-color: ${({ theme }) => theme.colors.darkerBackground}; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); + border-radius: 4px; + min-width: 150px; +`; + +const StyledBsLightbulb = styled(BsLightbulb)` + height: 1.25em; + width: 1.25em; +`; + +const StyledBsLightbulbOff = styled(BsLightbulbOff)` + height: 1.25em; + width: 1.25em; +`; + +export default memo(GpioPinsStatus); diff --git a/src/renderer/components/Header.tsx b/src/renderer/components/Header.tsx index e7bf6138..614562ef 100644 --- a/src/renderer/components/Header.tsx +++ b/src/renderer/components/Header.tsx @@ -4,6 +4,9 @@ import { NavLink } from 'react-router-dom'; import { selectDebugTabVisible } from '@/renderer/store/modules/debugTab'; import { useSelector } from 'react-redux'; import BatteryStatus from './BatteryStatus/BatteryStatus'; +import GpioPinsStatus from './GpioPinsStatus/GpioPinsStatus'; +import { ExplorationStatus } from './ExplorationStatus/ExplorationStatus'; +import { CountdownStatus } from './CountdownStatus/CountdownStatus'; interface NavLinkDefinition { to: string; @@ -47,6 +50,9 @@ export const Header: FC = () => { ))} + + + @@ -57,7 +63,7 @@ export const Header: FC = () => { const HeaderGrid = styled.div` display: grid; - grid-template-columns: 1fr 400px; + grid-template-columns: 1fr 500px; box-shadow: 0 3px 2px rgba(0, 0, 0, 0.25); `; diff --git a/src/renderer/components/StatusBar.tsx b/src/renderer/components/StatusBar.tsx index 694fa776..029e8330 100644 --- a/src/renderer/components/StatusBar.tsx +++ b/src/renderer/components/StatusBar.tsx @@ -85,6 +85,7 @@ const ModeInfo = () => { {flipper.matches('rl') && (isReverse ? 'FRONT RIGHT' : 'REAR LEFT')} {flipper.matches('rr') && (isReverse ? 'FRONT LEFT' : 'REAR RIGHT')} {flipper.matches('all') && 'ALL'} + {flipper.matches('none') && 'NONE'} {flipper.matches('rear') && (isReverse ? 'FRONT' : 'REAR')} ); diff --git a/src/renderer/components/common/Countdown.tsx b/src/renderer/components/common/Countdown.tsx new file mode 100644 index 00000000..3341b884 --- /dev/null +++ b/src/renderer/components/common/Countdown.tsx @@ -0,0 +1,214 @@ +import { styled } from '@/renderer/globalStyles/styled'; +import { + StyledPopup, + StyledPopupContent, + StyledPopupContainer, +} from '@/renderer/components/styles'; +import { Button } from '@/renderer/components/common/Button'; +import React, { useEffect, FC, useState, ChangeEvent, useRef } from 'react'; +import { LabeledInput } from '@/renderer/components/common/LabeledInput'; + +const INTERVAL_MS = 1000; + +interface CountdownProps { + icon: JSX.Element; + labelPopup: string; + durationDefault: number; + onStartClick?: (duration: number) => void; + onStopClick?: () => void; + sideElement?: JSX.Element; + /** + * If true, the timer will be stopped. + */ + isNowStopCountdownTimer?: boolean; +} + +export const Countdown: FC = ({ + icon, + labelPopup, + durationDefault, + onStartClick, + onStopClick, + sideElement, + isNowStopCountdownTimer, +}) => { + const countDownDate = useRef(0); + const intervalRef = useRef>(); + + const [duration, setDuration] = useState(durationDefault); + const [timerDisplay, setTimerDisplay] = useState('00:00'); + + const getTimeRemaining = () => { + return countDownDate.current - Date.now(); + }; + + useEffect(() => { + if (isNowStopCountdownTimer) { + stopTimer(); + updateTimerDisplay(); + } + }, [isNowStopCountdownTimer]); + + /** + * Parses a time in milliseconds to a string in the format mm:ss + * + * @param timeRemain - time in milliseconds + */ + const formatTime = (timeRemain: number): string => { + const minutes = Math.floor((timeRemain % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((timeRemain % (1000 * 60)) / 1000); + + const minutesDiplay = minutes.toString().padStart(2, '0'); + const secondsDiplay = seconds.toString().padStart(2, '0'); + + return `${minutesDiplay}:${secondsDiplay}`; + }; + + const startTimer = () => { + if (intervalRef.current !== undefined) { + stopTimer(); + } + countDownDate.current = Date.now() + duration * 60 * 1000; + intervalRef.current = setInterval(handleTimerTick, INTERVAL_MS); + if (onStartClick !== undefined) { + onStartClick(duration); + } + }; + + const stopTimer = () => { + clearInterval(intervalRef.current); + intervalRef.current = undefined; + countDownDate.current = 0; + if (onStopClick !== undefined) { + onStopClick(); + } + }; + + /** + * Updates the timer display. + * If the timer is negative, it will be set to 0. + */ + const updateTimerDisplay = () => { + let time = getTimeRemaining(); + if (time < 0) { + time = 0; + } + const timeDisplay = formatTime(time); + setTimerDisplay(timeDisplay); + }; + + const isTimerActive = () => { + return countDownDate.current > Date.now(); + }; + + /** + * This function is called when the user clicks the start button. + */ + const handleStartButtonClick = () => { + startTimer(); + handleTimerTick(); + }; + + /** + * This function is called when the user clicks the stop button. + */ + const handleStopButtonClick = () => { + stopTimer(); + updateTimerDisplay(); + }; + + /** + * This function is called when the user changes the duration. + * @param e - the event + */ + const handleDurationChange = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + /** + * This function is called every second by the timer. + */ + const handleTimerTick = () => { + const time = getTimeRemaining(); + if (time <= 0) { + countDownDate.current = 0; + stopTimer(); + } + + updateTimerDisplay(); + }; + + return ( + + {sideElement} + + {isTimerActive() && timerDisplay} + {icon} + + } + on="click" + position="bottom center" + arrow={false} + repositionOnResize={true} + > + + +
+ + + + + +
+ + Time left +

{timerDisplay}

+ {sideElement} +
+
+
+
+
+ ); +}; + +const StyledDiv = styled.div` + display: flex; + column-gap: 5px; +`; + +const StyledDivTimer = styled.div` + margin: 1em; +`; + +const StyledDivDuration = styled.div` + display: flex; + column-gap: 20px; +`; + +const StyledDivInfo = styled(StyledDivDuration)` + margin: 8px; +`; + +const StyledP = styled.p` + margin-right: 20px; +`; diff --git a/src/renderer/components/pages/Config/ConfigPage.tsx b/src/renderer/components/pages/Config/ConfigPage.tsx index 5a22c656..6bd6c183 100644 --- a/src/renderer/components/pages/Config/ConfigPage.tsx +++ b/src/renderer/components/pages/Config/ConfigPage.tsx @@ -8,7 +8,6 @@ import { GraphConfig } from '@/renderer/components/pages/Config/pages/GraphConfi import { styled } from '@/renderer/globalStyles/styled'; import { LaunchConfig } from '@/renderer/components/pages/Config/pages/LaunchConfig/LaunchConfig'; import ArmPresetsConfig from '@/renderer/components/pages/Config/pages/ArmPresetsConfig/ArmPresetsConfig'; -import { GpioPinsConfig } from '@/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPinsConfig'; export const ConfigPage: FC = () => { return ( @@ -23,7 +22,6 @@ export const ConfigPage: FC = () => { } /> } /> } /> - } /> } /> } /> diff --git a/src/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPinsConfig.tsx b/src/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPinsConfig.tsx deleted file mode 100644 index ec09e118..00000000 --- a/src/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPinsConfig.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { SectionTitle } from '../../styles'; -import { useSelector } from 'react-redux'; -import { selectAllGpioPins } from '@/renderer/store/modules/gpioPins'; -import { GpioPin } from './GpioPin'; -import { styled } from '@/renderer/globalStyles/styled'; - -const Container = styled.div` - display: flex; - flex-direction: row; - align-items: flex-start; -`; - -export const GpioPinsConfig = () => { - const gpioPins = useSelector(selectAllGpioPins); - return ( - <> - GPIO Control - - {gpioPins.map((gpioPin) => ( - - ))} - - - ); -}; diff --git a/src/renderer/components/styles.ts b/src/renderer/components/styles.ts new file mode 100644 index 00000000..61057b63 --- /dev/null +++ b/src/renderer/components/styles.ts @@ -0,0 +1,45 @@ +import Popup from 'reactjs-popup'; +import { styled } from '@/renderer/globalStyles/styled'; + +export const StyledPopup = styled(Popup)` + @keyframes anvil { + 0% { + transform: scale(1) translateY(0px); + opacity: 0; + box-shadow: 0 0 0 rgba(241, 241, 241, 0); + } + 1% { + transform: scale(0.96) translateY(10px); + opacity: 0; + box-shadow: 0 0 0 rgba(241, 241, 241, 0); + } + 100% { + transform: scale(1) translateY(0px); + opacity: 1; + box-shadow: 0 0 500px rgba(241, 241, 241, 0); + } + } + &-content { + -webkit-animation: anvil 0.2s cubic-bezier(0.38, 0.1, 0.36, 0.9) forwards; + } +`; + +export const StyledPopupContent = styled.div` + display: flex; + flex-direction: row; + align-items: flex-start; + background-color: ${({ theme }) => theme.colors.darkerBackground}; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); + border-radius: 4px; + min-width: 150px; +`; + +export const StyledPopupContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + &:hover { + cursor: pointer; + } +`; diff --git a/src/renderer/inputSystem/index.ts b/src/renderer/inputSystem/index.ts index 56612010..7eaf1b6f 100644 --- a/src/renderer/inputSystem/index.ts +++ b/src/renderer/inputSystem/index.ts @@ -6,7 +6,7 @@ import { controlService } from '@/renderer/state/control'; import { log } from '@/renderer/logger'; import { armModeActions } from './armModeActions'; import { flipperModeActions } from './flipperModeActions'; -import { TopicOptions } from '../utils/ros/roslib-ts-client/@types'; +import { TopicOptions } from '@/renderer/utils/ros/roslib-ts-client/@types'; import { store } from '@/renderer/store/store'; import { selectRobotNameState } from '@/renderer/store/modules/input'; @@ -15,6 +15,11 @@ const gripperTopic: TopicOptions = { messageType: 'ovis_robotiq_gripper/OvisGripperPosition', }; +const stopNavigationTopic: TopicOptions = { + name: '/move_base/cancel', + messageType: 'actionlib_msgs/GoalID', +}; + const defaultActions: Action[] = [ { name: 'estop', @@ -57,14 +62,11 @@ const defaultActions: Action[] = [ }); }, }, - { - name: 'wristlights', + name: 'stopNavigation', bindings: [{ type: 'gamepadBtnDown', button: buttonMappings.X }], perform: () => { - rosClient - .callService({ name: '/capra/wrist_light_toggle' }) - .catch(log.error); + rosClient.publish(stopNavigationTopic, {}); }, }, ]; diff --git a/src/renderer/state/flipper.ts b/src/renderer/state/flipper.ts index c09ec425..a1b7be50 100644 --- a/src/renderer/state/flipper.ts +++ b/src/renderer/state/flipper.ts @@ -16,6 +16,7 @@ interface FlipperStateSchema { rl: Record; rr: Record; all: Record; + none: Record; }; } @@ -32,44 +33,44 @@ export const flipperMachine = Machine< >( { id: 'flipper', - initial: 'all', + initial: 'none', context: {}, states: { front: { on: { MODE_LEFT: { target: 'fl', actions: 'set_mode_fl' }, MODE_RIGHT: { target: 'fr', actions: 'set_mode_fr' }, - MODE_REAR: { target: 'all', actions: 'set_mode_all' }, + MODE_REAR: { target: 'none', actions: 'set_mode_none' }, }, }, fl: { on: { MODE_RIGHT: { target: 'front', actions: 'set_mode_front' }, - MODE_REAR: { target: 'all', actions: 'set_mode_all' }, + MODE_REAR: { target: 'none', actions: 'set_mode_none' }, }, }, fr: { on: { MODE_LEFT: { target: 'front', actions: 'set_mode_front' }, - MODE_REAR: { target: 'all', actions: 'set_mode_all' }, + MODE_REAR: { target: 'none', actions: 'set_mode_none' }, }, }, rear: { on: { - MODE_FRONT: { target: 'all', actions: 'set_mode_all' }, + MODE_FRONT: { target: 'none', actions: 'set_mode_none' }, MODE_LEFT: { target: 'rl', actions: 'set_mode_rl' }, MODE_RIGHT: { target: 'rr', actions: 'set_mode_rr' }, }, }, rl: { on: { - MODE_FRONT: { target: 'all', actions: 'set_mode_all' }, + MODE_FRONT: { target: 'none', actions: 'set_mode_none' }, MODE_RIGHT: { target: 'rear', actions: 'set_mode_rear' }, }, }, rr: { on: { - MODE_FRONT: { target: 'all', actions: 'set_mode_none' }, + MODE_FRONT: { target: 'none', actions: 'set_mode_none' }, MODE_LEFT: { target: 'rear', actions: 'set_mode_rear' }, }, }, @@ -77,12 +78,24 @@ export const flipperMachine = Machine< on: { MODE_FRONT: { target: 'front', actions: 'set_mode_front' }, MODE_REAR: { target: 'rear', actions: 'set_mode_rear' }, + MODE_LEFT: { target: 'none', actions: 'set_mode_none' }, + }, + }, + none: { + on: { + MODE_FRONT: { target: 'front', actions: 'set_mode_front' }, + MODE_REAR: { target: 'rear', actions: 'set_mode_rear' }, + MODE_RIGHT: { target: 'all', actions: 'set_mode_all' }, }, }, }, }, { actions: { + set_mode_none: () => { + void sendFlipperMode('front_disable'); + void sendFlipperMode('rear_disable'); + }, set_mode_all: () => { void sendFlipperMode('front_enable'); void sendFlipperMode('rear_enable'); diff --git a/src/renderer/store/modules/launchFiles.ts b/src/renderer/store/modules/launchFiles.ts index 670a5dca..3f6d3402 100644 --- a/src/renderer/store/modules/launchFiles.ts +++ b/src/renderer/store/modules/launchFiles.ts @@ -27,6 +27,42 @@ export const initialState: LaunchFilesState[] = [ fileName: 'ovis_arm.launch', isLaunched: false, }, + { + name: 'Navigation', + packageName: 'markhor_navigation', + fileName: 'markhor_nav.launch', + isLaunched: false, + }, + { + name: 'Slam Real', + packageName: 'markhor_slam', + fileName: 'markhor_slam_real.launch', + isLaunched: false, + }, + { + name: 'Slam Real RGB', + packageName: 'markhor_slam', + fileName: 'markhor_slam_real.launch camera:=true', + isLaunched: false, + }, + { + name: 'Slam Simulation', + packageName: 'markhor_slam', + fileName: 'markhor_slam.launch', + isLaunched: false, + }, + { + name: 'Slam 2D', + packageName: 'markhor_slam', + fileName: 'markhor_slam_2d.launch', + isLaunched: false, + }, + { + name: 'Radiation Sensor Mapping', + packageName: 'capra_sensor_mapping', + fileName: 'sensor_mapping.launch', + isLaunched: false, + }, ]; export const launchFilesSlice = createSlice({