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; + } +`;