From 5fa192cd8446e007a66b94637ce867a828b20cf8 Mon Sep 17 00:00:00 2001 From: GLDuval Date: Sat, 10 Jun 2023 16:19:25 +0200 Subject: [PATCH 01/17] Add navigation files to launch config --- src/renderer/store/modules/launchFiles.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/renderer/store/modules/launchFiles.ts b/src/renderer/store/modules/launchFiles.ts index 670a5dca..c32d7dd5 100644 --- a/src/renderer/store/modules/launchFiles.ts +++ b/src/renderer/store/modules/launchFiles.ts @@ -27,6 +27,18 @@ export const initialState: LaunchFilesState[] = [ fileName: 'ovis_arm.launch', isLaunched: false, }, + { + name: 'Navigation', + packageName: 'markhor_navigation', + fileName: 'markhor_nav.launch', + isLaunched: false, + }, + { + name: 'Slam', + packageName: 'markhor_slam', + fileName: 'markhor_slam_real.launch', + isLaunched: false, + }, ]; export const launchFilesSlice = createSlice({ From ad019fc67468c054593635b26537dae0782e4b15 Mon Sep 17 00:00:00 2001 From: GLDuval Date: Mon, 12 Jun 2023 09:31:58 +0200 Subject: [PATCH 02/17] Add slam launch files --- src/renderer/store/modules/launchFiles.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/renderer/store/modules/launchFiles.ts b/src/renderer/store/modules/launchFiles.ts index c32d7dd5..cbb2b90c 100644 --- a/src/renderer/store/modules/launchFiles.ts +++ b/src/renderer/store/modules/launchFiles.ts @@ -34,11 +34,29 @@ export const initialState: LaunchFilesState[] = [ isLaunched: false, }, { - name: 'Slam', + 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, + }, ]; export const launchFilesSlice = createSlice({ From e490156a79b952952653cc658ea6a725dbc8136d Mon Sep 17 00:00:00 2001 From: GLDuval Date: Mon, 12 Jun 2023 09:42:45 +0200 Subject: [PATCH 03/17] add flipper none mode --- src/renderer/components/StatusBar.tsx | 1 + src/renderer/state/flipper.ts | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) 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/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'); From da5381a33b972d319f155a3192801d450beb3779 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Mon, 12 Jun 2023 11:33:12 -0400 Subject: [PATCH 04/17] Update header bar : gpio and exploration timer config (#179) * add exploration timer * fix lint * re fix lint * move gpio config to header tab * try fix pr comment * try fix pr comment * debug * clean up --- .../BatteryStatus/BatteryStatus.tsx | 42 +----- .../ExplorationStatus/ExplorationStatus.tsx | 46 ++++++ .../ExplorationStatus/ExplorationTimer.tsx | 135 ++++++++++++++++++ .../GpioPin.tsx | 28 ++-- .../GpioPinsStatus/GpioPinsStatus.tsx | 64 +++++++++ src/renderer/components/Header.tsx | 4 + .../components/pages/Config/ConfigPage.tsx | 2 - .../pages/GpioPinsConfig/GpioPinsConfig.tsx | 26 ---- src/renderer/components/styles.ts | 45 ++++++ 9 files changed, 314 insertions(+), 78 deletions(-) create mode 100644 src/renderer/components/ExplorationStatus/ExplorationStatus.tsx create mode 100644 src/renderer/components/ExplorationStatus/ExplorationTimer.tsx rename src/renderer/components/{pages/Config/pages/GpioPinsConfig => GpioPinsStatus}/GpioPin.tsx (84%) create mode 100644 src/renderer/components/GpioPinsStatus/GpioPinsStatus.tsx delete mode 100644 src/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPinsConfig.tsx create mode 100644 src/renderer/components/styles.ts 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/ExplorationStatus/ExplorationStatus.tsx b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx new file mode 100644 index 00000000..b06365fd --- /dev/null +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -0,0 +1,46 @@ +import React, { FC, useState } from 'react'; +import { styled } from '@/renderer/globalStyles/styled'; +import { ExplorationTimer } from './ExplorationTimer'; +import { + StyledPopup, + StyledPopupContent, + StyledPopupContainer, +} from '@/renderer/components/styles'; +import { GoTelescope } from 'react-icons/go'; + +export const ExplorationStatus: FC = () => { + const [timerDisplay, setTimerDisplay] = useState('00:00'); + + const setTimerDisplayProps = (timerToDisplay: string) => { + setTimerDisplay(timerToDisplay); + }; + + const isShowTimerDisplay = () => { + return timerDisplay !== '00:00'; + }; + + return ( + + {isShowTimerDisplay() && timerDisplay} + + + } + on="click" + position="bottom center" + arrow={false} + repositionOnResize={true} + > + + + + + ); +}; + +const StyledGoTelescope = styled(GoTelescope)` + height: 1.25em; + width: 1.25em; + margin-left: 0.5em; +`; diff --git a/src/renderer/components/ExplorationStatus/ExplorationTimer.tsx b/src/renderer/components/ExplorationStatus/ExplorationTimer.tsx new file mode 100644 index 00000000..f1b18b71 --- /dev/null +++ b/src/renderer/components/ExplorationStatus/ExplorationTimer.tsx @@ -0,0 +1,135 @@ +import { Button } from '@/renderer/components/common/Button'; +import { rosClient } from '@/renderer/utils/ros/rosClient'; +import React, { useEffect, FC, useState, ChangeEvent } from 'react'; +import { LabeledInput } from '@/renderer/components/common/LabeledInput'; +import { log } from '@/renderer/logger'; +import { styled } from '@/renderer/globalStyles/styled'; + +interface TimerDisplayProps { + setTimerDisplayProps: (timerDisplay: string) => void; +} + +export const ExplorationTimer: FC = ({ + setTimerDisplayProps, +}) => { + const timeDisplayDefault = '00:00'; + + const [duration, setDuration] = useState(2); + const [timeRemaining, setTimeRemaining] = useState(0); + const [isTimerActive, setIsTimerActive] = useState(false); + const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); + const [countDownDate, setCountDownDate] = useState(Date.now()); + + const updateDuration = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + const startTimer = () => { + setIsTimerActive(false); + setCountDownDate(Date.now() + duration * 60 * 1000); + setIsTimerActive(true); + setRosExplorationTimer(); + }; + + const stopTimer = () => { + setIsTimerActive(false); + setDuration(0); + setRosExplorationTimer(); + }; + + useEffect(() => { + let interval: ReturnType | undefined; + const intervalMs = 1000; + if (isTimerActive) { + interval = setInterval(() => { + setTimerDisplayProps(getTimeRemaining()); + setTimerDisplay(getTimeRemaining()); + setTimeRemaining(timeRemaining - intervalMs); + }, intervalMs); + } else if (!isTimerActive && timeRemaining !== 0) { + if (interval !== undefined) { + clearInterval(interval); + } + setTimerDisplay(timeDisplayDefault); + setTimerDisplayProps(timeDisplayDefault); + setTimerDisplay(timeDisplayDefault); + setTimeRemaining(0); + } + + const getTimeRemaining = () => { + const total = countDownDate - Date.now(); + + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = + minutes < 10 ? '0' + minutes.toString() : minutes.toString(); + const secondsDiplay = + seconds < 10 ? '0' + seconds.toString() : seconds.toString(); + + if (total < 0) { + setIsTimerActive(false); + } + return `${minutesDiplay}:${secondsDiplay}`; + }; + return () => clearInterval(interval); + }, [isTimerActive, countDownDate, timeRemaining, setTimerDisplayProps]); + + const setRosExplorationTimer = () => { + rosClient + .callService( + { + name: `/start_exploration`, + }, + { timeout: duration * 60 } + ) + .catch(log.error); + }; + + return ( + +
+ + + + + +
+ +

Time left

+

{timerDisplay}

+
+
+ ); +}; + +const StyledDiv = styled.div` + margin: 1em; +`; + +const StyledDivDuration = styled.div` + display: flex; + column-gap: 20px; +`; + +const StyledDivInfo = styled(StyledDivDuration)` + margin: 8px; + column-gap: 45px; +`; 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..e87153c0 100644 --- a/src/renderer/components/Header.tsx +++ b/src/renderer/components/Header.tsx @@ -4,6 +4,8 @@ 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'; interface NavLinkDefinition { to: string; @@ -47,6 +49,8 @@ export const Header: FC = () => { ))} + + 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; + } +`; From bf9396d63fe1b178dc65c27386b88fab0708c893 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 04:53:41 -0400 Subject: [PATCH 05/17] add stop navigation btn && fix timer display --- .../ExplorationStatus/ExplorationStatus.tsx | 172 +++++++++++++++--- .../ExplorationStatus/ExplorationTimer.tsx | 135 -------------- src/renderer/inputSystem/index.ts | 13 +- 3 files changed, 159 insertions(+), 161 deletions(-) delete mode 100644 src/renderer/components/ExplorationStatus/ExplorationTimer.tsx diff --git a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx index b06365fd..3b35e68a 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -1,46 +1,176 @@ -import React, { FC, useState } from 'react'; import { styled } from '@/renderer/globalStyles/styled'; -import { ExplorationTimer } from './ExplorationTimer'; +import { ExplorationCancelNavigation } from './ExplorationCancelNavigation'; import { StyledPopup, StyledPopupContent, StyledPopupContainer, } from '@/renderer/components/styles'; import { GoTelescope } from 'react-icons/go'; +import { Button } from '@/renderer/components/common/Button'; +import { rosClient } from '@/renderer/utils/ros/rosClient'; +import React, { useEffect, FC, useState, ChangeEvent } from 'react'; +import { LabeledInput } from '@/renderer/components/common/LabeledInput'; +import { log } from '@/renderer/logger'; export const ExplorationStatus: FC = () => { - const [timerDisplay, setTimerDisplay] = useState('00:00'); + const timeDisplayDefault = '00:00'; - const setTimerDisplayProps = (timerToDisplay: string) => { - setTimerDisplay(timerToDisplay); + const [duration, setDuration] = useState(2); + const [timeRemaining, setTimeRemaining] = useState(0); + const [isTimerActive, setIsTimerActive] = useState(false); + const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); + const [countDownDate, setCountDownDate] = useState(Date.now()); + + const updateDuration = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + const startTimer = () => { + setIsTimerActive(false); + setCountDownDate(Date.now() + duration * 60 * 1000); + setIsTimerActive(true); + setRosExplorationTimer(); + }; + + const stopTimer = () => { + setIsTimerActive(false); + setDuration(0); + setRosExplorationTimer(); }; const isShowTimerDisplay = () => { return timerDisplay !== '00:00'; }; - return ( - - {isShowTimerDisplay() && timerDisplay} - - + useEffect(() => { + let interval: ReturnType | undefined; + const intervalMs = 1000; + if (isTimerActive) { + interval = setInterval(() => { + setTimerDisplay(getTimeRemaining()); + setTimeRemaining(timeRemaining - intervalMs); + }, intervalMs); + } else if (!isTimerActive && timeRemaining !== 0) { + if (interval !== undefined) { + clearInterval(interval); } - on="click" - position="bottom center" - arrow={false} - repositionOnResize={true} - > - - - - + setTimerDisplay(timeDisplayDefault); + setTimerDisplay(timeDisplayDefault); + setTimeRemaining(0); + } + + const getTimeRemaining = () => { + const total = countDownDate - Date.now(); + + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = + minutes < 10 ? '0' + minutes.toString() : minutes.toString(); + const secondsDiplay = + seconds < 10 ? '0' + seconds.toString() : seconds.toString(); + + if (total < 0) { + setIsTimerActive(false); + } + return `${minutesDiplay}:${secondsDiplay}`; + }; + return () => clearInterval(interval); + }, [isTimerActive, countDownDate, timeRemaining]); + + const setRosExplorationTimer = () => { + rosClient + .callService( + { + name: `/start_exploration`, + }, + { timeout: duration * 60 } + ) + .catch(log.error); + }; + + return ( + + {isShowTimerDisplay() && ( + + )} + + {isShowTimerDisplay() && timerDisplay} + + + } + on="click" + position="bottom center" + arrow={false} + repositionOnResize={true} + > + + +
+ + + + + +
+ + Time left +

{timerDisplay}

+ {isTimerActive && ( + + )} +
+
+
+
+
); }; +const StyledDiv = styled.div` + display: flex; + column-gap: 5px; +`; + const StyledGoTelescope = styled(GoTelescope)` height: 1.25em; width: 1.25em; margin-left: 0.5em; `; +const StyledDivTimer = styled.div` + margin: 1em; +`; + +const StyledDivDuration = styled.div` + display: flex; + column-gap: 20px; +`; + +const StyledP = styled.p` + margin-right: 20px; +`; + +const StyledDivInfo = styled(StyledDivDuration)` + margin: 8px; +`; diff --git a/src/renderer/components/ExplorationStatus/ExplorationTimer.tsx b/src/renderer/components/ExplorationStatus/ExplorationTimer.tsx deleted file mode 100644 index f1b18b71..00000000 --- a/src/renderer/components/ExplorationStatus/ExplorationTimer.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { Button } from '@/renderer/components/common/Button'; -import { rosClient } from '@/renderer/utils/ros/rosClient'; -import React, { useEffect, FC, useState, ChangeEvent } from 'react'; -import { LabeledInput } from '@/renderer/components/common/LabeledInput'; -import { log } from '@/renderer/logger'; -import { styled } from '@/renderer/globalStyles/styled'; - -interface TimerDisplayProps { - setTimerDisplayProps: (timerDisplay: string) => void; -} - -export const ExplorationTimer: FC = ({ - setTimerDisplayProps, -}) => { - const timeDisplayDefault = '00:00'; - - const [duration, setDuration] = useState(2); - const [timeRemaining, setTimeRemaining] = useState(0); - const [isTimerActive, setIsTimerActive] = useState(false); - const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); - const [countDownDate, setCountDownDate] = useState(Date.now()); - - const updateDuration = (e: ChangeEvent) => { - setDuration(Number(e.target.value)); - }; - - const startTimer = () => { - setIsTimerActive(false); - setCountDownDate(Date.now() + duration * 60 * 1000); - setIsTimerActive(true); - setRosExplorationTimer(); - }; - - const stopTimer = () => { - setIsTimerActive(false); - setDuration(0); - setRosExplorationTimer(); - }; - - useEffect(() => { - let interval: ReturnType | undefined; - const intervalMs = 1000; - if (isTimerActive) { - interval = setInterval(() => { - setTimerDisplayProps(getTimeRemaining()); - setTimerDisplay(getTimeRemaining()); - setTimeRemaining(timeRemaining - intervalMs); - }, intervalMs); - } else if (!isTimerActive && timeRemaining !== 0) { - if (interval !== undefined) { - clearInterval(interval); - } - setTimerDisplay(timeDisplayDefault); - setTimerDisplayProps(timeDisplayDefault); - setTimerDisplay(timeDisplayDefault); - setTimeRemaining(0); - } - - const getTimeRemaining = () => { - const total = countDownDate - Date.now(); - - const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((total % (1000 * 60)) / 1000); - - const minutesDiplay = - minutes < 10 ? '0' + minutes.toString() : minutes.toString(); - const secondsDiplay = - seconds < 10 ? '0' + seconds.toString() : seconds.toString(); - - if (total < 0) { - setIsTimerActive(false); - } - return `${minutesDiplay}:${secondsDiplay}`; - }; - return () => clearInterval(interval); - }, [isTimerActive, countDownDate, timeRemaining, setTimerDisplayProps]); - - const setRosExplorationTimer = () => { - rosClient - .callService( - { - name: `/start_exploration`, - }, - { timeout: duration * 60 } - ) - .catch(log.error); - }; - - return ( - -
- - - - - -
- -

Time left

-

{timerDisplay}

-
-
- ); -}; - -const StyledDiv = styled.div` - margin: 1em; -`; - -const StyledDivDuration = styled.div` - display: flex; - column-gap: 20px; -`; - -const StyledDivInfo = styled(StyledDivDuration)` - margin: 8px; - column-gap: 45px; -`; diff --git a/src/renderer/inputSystem/index.ts b/src/renderer/inputSystem/index.ts index 56612010..52a96e96 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', @@ -59,12 +64,10 @@ 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, { id: -1 }); }, }, ]; From cce13601142038540b91b38789fe03a8878dd889 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 04:54:06 -0400 Subject: [PATCH 06/17] add missing file --- .../ExplorationCancelNavigation.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx diff --git a/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx b/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx new file mode 100644 index 00000000..c541fd01 --- /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 { MdOutlineCancelScheduleSend } from 'react-icons/md'; +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, { id: -1 }); + isCancelNavigationProps(); + toast.info('Navigation stop'); + }; + + return ; +}; + +export const StyledImCancelCircle = styled(MdOutlineCancelScheduleSend)` + margin: 0.25em 0.25em 0 0; + color: red; + &:hover { + cursor: pointer; + } +`; From de1568dcceb2d6d32519534104497b6bd30c08d7 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 06:15:37 -0400 Subject: [PATCH 07/17] send empty msg for cancel navigation --- .../ExplorationCancelNavigation.tsx | 2 +- .../ExplorationStatus/ExplorationStatus.tsx | 21 +++++++------------ src/renderer/inputSystem/index.ts | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx b/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx index c541fd01..f3188443 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx @@ -18,7 +18,7 @@ export const ExplorationCancelNavigation: FC = ({ isCancelNavigationProps, }) => { const onClick = () => { - rosClient.publish(stopNavigationTopic, { id: -1 }); + rosClient.publish(stopNavigationTopic, {}); isCancelNavigationProps(); toast.info('Navigation stop'); }; diff --git a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx index 3b35e68a..e937298e 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -29,13 +29,12 @@ export const ExplorationStatus: FC = () => { setIsTimerActive(false); setCountDownDate(Date.now() + duration * 60 * 1000); setIsTimerActive(true); - setRosExplorationTimer(); + setRosExplorationTimer(duration); }; const stopTimer = () => { setIsTimerActive(false); - setDuration(0); - setRosExplorationTimer(); + setRosExplorationTimer(0); }; const isShowTimerDisplay = () => { @@ -78,22 +77,20 @@ export const ExplorationStatus: FC = () => { return () => clearInterval(interval); }, [isTimerActive, countDownDate, timeRemaining]); - const setRosExplorationTimer = () => { + const setRosExplorationTimer = (time: number) => { rosClient .callService( { name: `/start_exploration`, }, - { timeout: duration * 60 } + { timeout: time * 60 } ) .catch(log.error); }; return ( - {isShowTimerDisplay() && ( - - )} + @@ -135,11 +132,9 @@ export const ExplorationStatus: FC = () => { Time left

{timerDisplay}

- {isTimerActive && ( - - )} +
diff --git a/src/renderer/inputSystem/index.ts b/src/renderer/inputSystem/index.ts index 52a96e96..1192a168 100644 --- a/src/renderer/inputSystem/index.ts +++ b/src/renderer/inputSystem/index.ts @@ -67,7 +67,7 @@ const defaultActions: Action[] = [ name: 'stopNavigation', bindings: [{ type: 'gamepadBtnDown', button: buttonMappings.X }], perform: () => { - rosClient.publish(stopNavigationTopic, { id: -1 }); + rosClient.publish(stopNavigationTopic, {}); }, }, ]; From 5f2465f26463f36afa48738efa38f3022b3c160f Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 06:26:26 -0400 Subject: [PATCH 08/17] change stop navigation icon --- .../ExplorationStatus/ExplorationCancelNavigation.tsx | 8 ++++---- .../components/ExplorationStatus/ExplorationStatus.tsx | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx b/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx index f3188443..19b3d3e3 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationCancelNavigation.tsx @@ -2,7 +2,7 @@ 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 { MdOutlineCancelScheduleSend } from 'react-icons/md'; +import { FaStop } from 'react-icons/fa'; import { toast } from 'react-toastify'; const stopNavigationTopic: TopicOptions = { @@ -23,11 +23,11 @@ export const ExplorationCancelNavigation: FC = ({ toast.info('Navigation stop'); }; - return ; + return ; }; -export const StyledImCancelCircle = styled(MdOutlineCancelScheduleSend)` - margin: 0.25em 0.25em 0 0; +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 index e937298e..65f46803 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -151,7 +151,6 @@ const StyledDiv = styled.div` const StyledGoTelescope = styled(GoTelescope)` height: 1.25em; width: 1.25em; - margin-left: 0.5em; `; const StyledDivTimer = styled.div` margin: 1em; From 938b9fade86dd68e1edaf6f514faeb044b4b5bc5 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 06:43:37 -0400 Subject: [PATCH 09/17] call service stop_exploration --- .../ExplorationStatus/ExplorationStatus.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx index 65f46803..dfeecbbd 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -29,12 +29,12 @@ export const ExplorationStatus: FC = () => { setIsTimerActive(false); setCountDownDate(Date.now() + duration * 60 * 1000); setIsTimerActive(true); - setRosExplorationTimer(duration); + startRosExplorationTimer(); }; const stopTimer = () => { setIsTimerActive(false); - setRosExplorationTimer(0); + stopRosExplorationTimer(); }; const isShowTimerDisplay = () => { @@ -77,13 +77,24 @@ export const ExplorationStatus: FC = () => { return () => clearInterval(interval); }, [isTimerActive, countDownDate, timeRemaining]); - const setRosExplorationTimer = (time: number) => { + const startRosExplorationTimer = () => { rosClient .callService( { name: `/start_exploration`, }, - { timeout: time * 60 } + { timeout: duration * 60 } + ) + .catch(log.error); + }; + + const stopRosExplorationTimer = () => { + rosClient + .callService( + { + name: `/stop_exploration`, + }, + {} ) .catch(log.error); }; From 676cca478af4191eaeab28300798e4ebcf556b92 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 09:05:16 -0400 Subject: [PATCH 10/17] better code --- .../components/ExplorationStatus/ExplorationStatus.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx index dfeecbbd..bb24df77 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -64,10 +64,8 @@ export const ExplorationStatus: FC = () => { const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((total % (1000 * 60)) / 1000); - const minutesDiplay = - minutes < 10 ? '0' + minutes.toString() : minutes.toString(); - const secondsDiplay = - seconds < 10 ? '0' + seconds.toString() : seconds.toString(); + const minutesDiplay = minutes.toString().padStart(2, '0'); + const secondsDiplay = seconds.toString().padStart(2, '0'); if (total < 0) { setIsTimerActive(false); From 3ec28196d37fd8b4ad6b57569223fcdeaf5eb762 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 08:33:55 -0400 Subject: [PATCH 11/17] add countdown timer --- .../CountdownStatus/CountdownStatus.tsx | 150 ++++++++++++++++++ src/renderer/components/Header.tsx | 4 +- 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/renderer/components/CountdownStatus/CountdownStatus.tsx diff --git a/src/renderer/components/CountdownStatus/CountdownStatus.tsx b/src/renderer/components/CountdownStatus/CountdownStatus.tsx new file mode 100644 index 00000000..2aeadf48 --- /dev/null +++ b/src/renderer/components/CountdownStatus/CountdownStatus.tsx @@ -0,0 +1,150 @@ +import { styled } from '@/renderer/globalStyles/styled'; +import { + StyledPopup, + StyledPopupContent, + StyledPopupContainer, +} from '@/renderer/components/styles'; +import { IoMdStopwatch } from 'react-icons/io'; +import { Button } from '@/renderer/components/common/Button'; +import React, { useEffect, FC, useState, ChangeEvent } from 'react'; +import { LabeledInput } from '@/renderer/components/common/LabeledInput'; + +export const CountdownStatus: FC = () => { + const timeDisplayDefault = '00:00'; + + const [duration, setDuration] = useState(35); + const [timeRemaining, setTimeRemaining] = useState(0); + const [isTimerActive, setIsTimerActive] = useState(false); + const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); + const [countDownDate, setCountDownDate] = useState(Date.now()); + + const updateDuration = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + const startTimer = () => { + setIsTimerActive(false); + setCountDownDate(Date.now() + duration * 60 * 1000); + setIsTimerActive(true); + }; + + const stopTimer = () => { + setIsTimerActive(false); + }; + + const isShowTimerDisplay = () => { + return timerDisplay !== '00:00'; + }; + + useEffect(() => { + let interval: ReturnType | undefined; + const intervalMs = 1000; + if (isTimerActive) { + interval = setInterval(() => { + setTimerDisplay(getTimeRemaining()); + setTimeRemaining(timeRemaining - intervalMs); + }, intervalMs); + } else if (!isTimerActive && timeRemaining !== 0) { + if (interval !== undefined) { + clearInterval(interval); + } + setTimerDisplay(timeDisplayDefault); + setTimerDisplay(timeDisplayDefault); + setTimeRemaining(0); + } + + const getTimeRemaining = () => { + const total = countDownDate - Date.now(); + + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = + minutes < 10 ? '0' + minutes.toString() : minutes.toString(); + const secondsDiplay = + seconds < 10 ? '0' + seconds.toString() : seconds.toString(); + + if (total < 0) { + setIsTimerActive(false); + } + return `${minutesDiplay}:${secondsDiplay}`; + }; + return () => clearInterval(interval); + }, [isTimerActive, countDownDate, timeRemaining]); + + return ( + + + {isShowTimerDisplay() && timerDisplay} + + + } + on="click" + position="bottom center" + arrow={false} + repositionOnResize={true} + > + + +
+ + + + + +
+ + Time left +

{timerDisplay}

+
+
+
+
+
+ ); +}; + +const StyledDiv = styled.div` + display: flex; + column-gap: 5px; +`; + +const StyledIoMdStopwatch = styled(IoMdStopwatch)` + height: 1.25em; + width: 1.25em; +`; +const StyledDivTimer = styled.div` + margin: 1em; +`; + +const StyledDivDuration = styled.div` + display: flex; + column-gap: 20px; +`; + +const StyledP = styled.p` + margin-right: 20px; +`; + +const StyledDivInfo = styled(StyledDivDuration)` + margin: 8px; +`; diff --git a/src/renderer/components/Header.tsx b/src/renderer/components/Header.tsx index e87153c0..614562ef 100644 --- a/src/renderer/components/Header.tsx +++ b/src/renderer/components/Header.tsx @@ -6,6 +6,7 @@ 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; @@ -49,6 +50,7 @@ export const Header: FC = () => { ))} + @@ -61,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); `; From 1c295f6676c23cb4b2cbceabad29bcb419c61907 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 10:18:13 -0400 Subject: [PATCH 12/17] add countdown && create a component --- .../CountdownStatus/CountdownStatus.tsx | 145 +-------------- .../ExplorationStatus/ExplorationStatus.tsx | 155 ++-------------- src/renderer/components/common/Countdown.tsx | 172 ++++++++++++++++++ 3 files changed, 198 insertions(+), 274 deletions(-) create mode 100644 src/renderer/components/common/Countdown.tsx diff --git a/src/renderer/components/CountdownStatus/CountdownStatus.tsx b/src/renderer/components/CountdownStatus/CountdownStatus.tsx index 2aeadf48..b66daa90 100644 --- a/src/renderer/components/CountdownStatus/CountdownStatus.tsx +++ b/src/renderer/components/CountdownStatus/CountdownStatus.tsx @@ -1,150 +1,19 @@ import { styled } from '@/renderer/globalStyles/styled'; -import { - StyledPopup, - StyledPopupContent, - StyledPopupContainer, -} from '@/renderer/components/styles'; import { IoMdStopwatch } from 'react-icons/io'; -import { Button } from '@/renderer/components/common/Button'; -import React, { useEffect, FC, useState, ChangeEvent } from 'react'; -import { LabeledInput } from '@/renderer/components/common/LabeledInput'; +import { Countdown } from '@/renderer/components/common/Countdown'; +import React, { FC } from 'react'; export const CountdownStatus: FC = () => { - const timeDisplayDefault = '00:00'; - - const [duration, setDuration] = useState(35); - const [timeRemaining, setTimeRemaining] = useState(0); - const [isTimerActive, setIsTimerActive] = useState(false); - const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); - const [countDownDate, setCountDownDate] = useState(Date.now()); - - const updateDuration = (e: ChangeEvent) => { - setDuration(Number(e.target.value)); - }; - - const startTimer = () => { - setIsTimerActive(false); - setCountDownDate(Date.now() + duration * 60 * 1000); - setIsTimerActive(true); - }; - - const stopTimer = () => { - setIsTimerActive(false); - }; - - const isShowTimerDisplay = () => { - return timerDisplay !== '00:00'; - }; - - useEffect(() => { - let interval: ReturnType | undefined; - const intervalMs = 1000; - if (isTimerActive) { - interval = setInterval(() => { - setTimerDisplay(getTimeRemaining()); - setTimeRemaining(timeRemaining - intervalMs); - }, intervalMs); - } else if (!isTimerActive && timeRemaining !== 0) { - if (interval !== undefined) { - clearInterval(interval); - } - setTimerDisplay(timeDisplayDefault); - setTimerDisplay(timeDisplayDefault); - setTimeRemaining(0); - } - - const getTimeRemaining = () => { - const total = countDownDate - Date.now(); - - const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((total % (1000 * 60)) / 1000); - - const minutesDiplay = - minutes < 10 ? '0' + minutes.toString() : minutes.toString(); - const secondsDiplay = - seconds < 10 ? '0' + seconds.toString() : seconds.toString(); - - if (total < 0) { - setIsTimerActive(false); - } - return `${minutesDiplay}:${secondsDiplay}`; - }; - return () => clearInterval(interval); - }, [isTimerActive, countDownDate, timeRemaining]); - return ( - - - {isShowTimerDisplay() && timerDisplay} - - - } - on="click" - position="bottom center" - arrow={false} - repositionOnResize={true} - > - - -
- - - - - -
- - Time left -

{timerDisplay}

-
-
-
-
-
+ } + labelPopup={'exploration'} + durationDefault={35} + /> ); }; -const StyledDiv = styled.div` - display: flex; - column-gap: 5px; -`; - const StyledIoMdStopwatch = styled(IoMdStopwatch)` height: 1.25em; width: 1.25em; `; -const StyledDivTimer = styled.div` - margin: 1em; -`; - -const StyledDivDuration = styled.div` - display: flex; - column-gap: 20px; -`; - -const StyledP = styled.p` - margin-right: 20px; -`; - -const StyledDivInfo = styled(StyledDivDuration)` - margin: 8px; -`; diff --git a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx index bb24df77..f5f7c3c3 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -1,81 +1,25 @@ import { styled } from '@/renderer/globalStyles/styled'; import { ExplorationCancelNavigation } from './ExplorationCancelNavigation'; -import { - StyledPopup, - StyledPopupContent, - StyledPopupContainer, -} from '@/renderer/components/styles'; import { GoTelescope } from 'react-icons/go'; -import { Button } from '@/renderer/components/common/Button'; +import { Countdown } from '@/renderer/components/common/Countdown'; import { rosClient } from '@/renderer/utils/ros/rosClient'; -import React, { useEffect, FC, useState, ChangeEvent } from 'react'; -import { LabeledInput } from '@/renderer/components/common/LabeledInput'; +import React, { FC, useState } from 'react'; import { log } from '@/renderer/logger'; export const ExplorationStatus: FC = () => { - const timeDisplayDefault = '00:00'; + const [isNowStopCountdownTimer, setIsNowStopCountdownTimer] = useState(false); - const [duration, setDuration] = useState(2); - const [timeRemaining, setTimeRemaining] = useState(0); - const [isTimerActive, setIsTimerActive] = useState(false); - const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); - const [countDownDate, setCountDownDate] = useState(Date.now()); - - const updateDuration = (e: ChangeEvent) => { - setDuration(Number(e.target.value)); - }; - - const startTimer = () => { - setIsTimerActive(false); - setCountDownDate(Date.now() + duration * 60 * 1000); - setIsTimerActive(true); - startRosExplorationTimer(); + const startTimer = (duration: number) => { + setIsNowStopCountdownTimer(false); + startRosExplorationTimer(duration); }; const stopTimer = () => { - setIsTimerActive(false); + setIsNowStopCountdownTimer(true); stopRosExplorationTimer(); }; - const isShowTimerDisplay = () => { - return timerDisplay !== '00:00'; - }; - - useEffect(() => { - let interval: ReturnType | undefined; - const intervalMs = 1000; - if (isTimerActive) { - interval = setInterval(() => { - setTimerDisplay(getTimeRemaining()); - setTimeRemaining(timeRemaining - intervalMs); - }, intervalMs); - } else if (!isTimerActive && timeRemaining !== 0) { - if (interval !== undefined) { - clearInterval(interval); - } - setTimerDisplay(timeDisplayDefault); - setTimerDisplay(timeDisplayDefault); - setTimeRemaining(0); - } - - const getTimeRemaining = () => { - const total = countDownDate - Date.now(); - - const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((total % (1000 * 60)) / 1000); - - const minutesDiplay = minutes.toString().padStart(2, '0'); - const secondsDiplay = seconds.toString().padStart(2, '0'); - - if (total < 0) { - setIsTimerActive(false); - } - return `${minutesDiplay}:${secondsDiplay}`; - }; - return () => clearInterval(interval); - }, [isTimerActive, countDownDate, timeRemaining]); - - const startRosExplorationTimer = () => { + const startRosExplorationTimer = (duration: number) => { rosClient .callService( { @@ -98,82 +42,21 @@ export const ExplorationStatus: FC = () => { }; return ( - - - - {isShowTimerDisplay() && timerDisplay} - - - } - on="click" - position="bottom center" - arrow={false} - repositionOnResize={true} - > - - -
- - - - - -
- - Time left -

{timerDisplay}

- -
-
-
-
-
+ } + labelPopup={'exploration'} + durationDefault={2} + onStartClick={startTimer} + onStopClick={stopTimer} + sideElement={ + + } + isNowStopCountdownTimer={isNowStopCountdownTimer} + /> ); }; -const StyledDiv = styled.div` - display: flex; - column-gap: 5px; -`; - const StyledGoTelescope = styled(GoTelescope)` height: 1.25em; width: 1.25em; `; -const StyledDivTimer = styled.div` - margin: 1em; -`; - -const StyledDivDuration = styled.div` - display: flex; - column-gap: 20px; -`; - -const StyledP = styled.p` - margin-right: 20px; -`; - -const StyledDivInfo = styled(StyledDivDuration)` - margin: 8px; -`; diff --git a/src/renderer/components/common/Countdown.tsx b/src/renderer/components/common/Countdown.tsx new file mode 100644 index 00000000..a5c7ee62 --- /dev/null +++ b/src/renderer/components/common/Countdown.tsx @@ -0,0 +1,172 @@ +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 } from 'react'; +import { LabeledInput } from '@/renderer/components/common/LabeledInput'; + +interface CountdownProps { + icon: JSX.Element; + labelPopup: string; + durationDefault: number; + onStartClick?: (duration: number) => void; + onStopClick?: () => void; + sideElement?: JSX.Element; + isNowStopCountdownTimer?: boolean; +} + +export const Countdown: FC = ({ + icon, + labelPopup, + durationDefault, + onStartClick, + onStopClick, + sideElement, + isNowStopCountdownTimer, +}) => { + const timeDisplayDefault = '00:00'; + + const [duration, setDuration] = useState(durationDefault); + const [timeRemaining, setTimeRemaining] = useState(0); + const [isTimerActive, setIsTimerActive] = useState(false); + const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); + const [countDownDate, setCountDownDate] = useState(Date.now()); + + const updateDuration = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + const startTimer = () => { + setIsTimerActive(false); + setCountDownDate(Date.now() + duration * 60 * 1000); + setIsTimerActive(true); + if (onStartClick !== undefined) { + onStartClick(duration); + } + }; + + const stopTimer = () => { + setIsTimerActive(false); + if (onStopClick !== undefined) { + onStopClick(); + } + }; + + const isShowTimerDisplay = () => { + return timerDisplay !== '00:00'; + }; + + useEffect(() => { + let interval: ReturnType | undefined; + const intervalMs = 1000; + if (isTimerActive && !isNowStopCountdownTimer) { + interval = setInterval(() => { + setTimerDisplay(getTimeRemaining()); + setTimeRemaining(timeRemaining - intervalMs); + }, intervalMs); + } else if ( + isNowStopCountdownTimer || + (!isTimerActive && timeRemaining !== 0) + ) { + if (interval !== undefined) { + clearInterval(interval); + } + setTimerDisplay(timeDisplayDefault); + setTimerDisplay(timeDisplayDefault); + setTimeRemaining(0); + } + + const getTimeRemaining = () => { + const total = countDownDate - Date.now(); + + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = minutes.toString().padStart(2, '0'); + const secondsDiplay = seconds.toString().padStart(2, '0'); + + if (total < 0) { + setIsTimerActive(false); + } + return `${minutesDiplay}:${secondsDiplay}`; + }; + return () => clearInterval(interval); + }, [isTimerActive, isNowStopCountdownTimer, countDownDate, timeRemaining]); + + return ( + + {sideElement} + + {isShowTimerDisplay() && 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; +`; From cf66e41decb84a0abb4b20552a07a64293f9d1b3 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Wed, 14 Jun 2023 06:05:37 -0400 Subject: [PATCH 13/17] fix typo --- src/renderer/components/CountdownStatus/CountdownStatus.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/CountdownStatus/CountdownStatus.tsx b/src/renderer/components/CountdownStatus/CountdownStatus.tsx index b66daa90..36a3a86f 100644 --- a/src/renderer/components/CountdownStatus/CountdownStatus.tsx +++ b/src/renderer/components/CountdownStatus/CountdownStatus.tsx @@ -7,7 +7,7 @@ export const CountdownStatus: FC = () => { return ( } - labelPopup={'exploration'} + labelPopup={'scenario'} durationDefault={35} /> ); From 7677622f425a9cbb6e122d249c3f86a0bc4637fb Mon Sep 17 00:00:00 2001 From: Samuel Lachance Date: Wed, 14 Jun 2023 12:29:37 +0200 Subject: [PATCH 14/17] Refactor the useEffect --- src/renderer/components/common/Countdown.tsx | 142 ++++++++++++------- 1 file changed, 92 insertions(+), 50 deletions(-) diff --git a/src/renderer/components/common/Countdown.tsx b/src/renderer/components/common/Countdown.tsx index a5c7ee62..c317a157 100644 --- a/src/renderer/components/common/Countdown.tsx +++ b/src/renderer/components/common/Countdown.tsx @@ -5,9 +5,11 @@ import { StyledPopupContainer, } from '@/renderer/components/styles'; import { Button } from '@/renderer/components/common/Button'; -import React, { useEffect, FC, useState, ChangeEvent } from 'react'; +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; @@ -15,6 +17,9 @@ interface CountdownProps { onStartClick?: (duration: number) => void; onStopClick?: () => void; sideElement?: JSX.Element; + /** + * If true, the timer will be stopped. + */ isNowStopCountdownTimer?: boolean; } @@ -27,74 +32,111 @@ export const Countdown: FC = ({ sideElement, isNowStopCountdownTimer, }) => { - const timeDisplayDefault = '00:00'; + const intervalRef = useRef>(); const [duration, setDuration] = useState(durationDefault); - const [timeRemaining, setTimeRemaining] = useState(0); - const [isTimerActive, setIsTimerActive] = useState(false); - const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); - const [countDownDate, setCountDownDate] = useState(Date.now()); + const [timerDisplay, setTimerDisplay] = useState('00:00'); + const countDownDate = useRef(0); - const updateDuration = (e: ChangeEvent) => { - setDuration(Number(e.target.value)); + + 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 total - time in milliseconds + */ + const formatTime = (total: number): string => { + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = minutes.toString().padStart(2, '0'); + const secondsDiplay = seconds.toString().padStart(2, '0'); + + return `${minutesDiplay}:${secondsDiplay}`; }; const startTimer = () => { - setIsTimerActive(false); - setCountDownDate(Date.now() + duration * 60 * 1000); - setIsTimerActive(true); + 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 = () => { - setIsTimerActive(false); + clearInterval(intervalRef.current); + intervalRef.current = undefined; + countDownDate.current = 0; if (onStopClick !== undefined) { onStopClick(); } }; - const isShowTimerDisplay = () => { - return timerDisplay !== '00:00'; + /** + * 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); }; - useEffect(() => { - let interval: ReturnType | undefined; - const intervalMs = 1000; - if (isTimerActive && !isNowStopCountdownTimer) { - interval = setInterval(() => { - setTimerDisplay(getTimeRemaining()); - setTimeRemaining(timeRemaining - intervalMs); - }, intervalMs); - } else if ( - isNowStopCountdownTimer || - (!isTimerActive && timeRemaining !== 0) - ) { - if (interval !== undefined) { - clearInterval(interval); - } - setTimerDisplay(timeDisplayDefault); - setTimerDisplay(timeDisplayDefault); - setTimeRemaining(0); - } + const isTimerActive = () => { + return countDownDate.current > Date.now(); + }; - const getTimeRemaining = () => { - const total = countDownDate - Date.now(); + /** + * This function is called when the user clicks the start button. + */ + const handleStartButtonClick = () => { + startTimer(); + handleTimerTick(); + }; - const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((total % (1000 * 60)) / 1000); + /** + * This function is called when the user clicks the stop button. + */ + const handleStopButtonClick = () => { + stopTimer(); + updateTimerDisplay(); + }; - const minutesDiplay = minutes.toString().padStart(2, '0'); - const secondsDiplay = seconds.toString().padStart(2, '0'); - if (total < 0) { - setIsTimerActive(false); - } - return `${minutesDiplay}:${secondsDiplay}`; - }; - return () => clearInterval(interval); - }, [isTimerActive, isNowStopCountdownTimer, countDownDate, timeRemaining]); + /** + * 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 ( @@ -102,7 +144,7 @@ export const Countdown: FC = ({ - {isShowTimerDisplay() && timerDisplay} + {isTimerActive() && timerDisplay} {icon} } @@ -118,18 +160,18 @@ export const Countdown: FC = ({ label={`Duration of ${labelPopup} (min)`} value={duration.toString()} type="number" - onChange={updateDuration} + onChange={handleDurationChange} />