From 35c4087d16ce44a83438db197cbe8417676f3c22 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Sun, 9 Jun 2024 22:57:25 -0700 Subject: [PATCH 01/14] page set up --- .../views/active/rocket-monitoring-view.tsx | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 client/src/views/active/rocket-monitoring-view.tsx diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx new file mode 100644 index 000000000..48c36be3f --- /dev/null +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -0,0 +1,87 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import api from '../../services/api'; +import { IRocketPopulated } from '../../utils/entities'; +import { Box, Button, ButtonGroup, Stack, Tooltip } from '@mui/material'; +import Header, { Breadcrumb } from '../../components/Header'; +import { ViewKeys } from '../../utils/viewProviderContext'; +import { useActiveMission } from '../../utils/ActiveMissionContext'; +import { Settings, WifiTethering } from '@mui/icons-material'; + +interface IRocketMonitoringViewProps { + // rocketId: string; +} + +const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRocketMonitoringViewProps) => { + const [rocket, setRocket] = useState<IRocketPopulated>({} as IRocketPopulated); + + // const getActiveRocket = useCallback(async (): Promise<IRocketPopulated> => { + // const response = await api.getRocket(rocketId || ''); + // const data = response.data as IRocketPopulated; + // setRocket(data); + // return data; + // }, [rocketId]); + + useEffect(() => { + // getActiveRocket(); + }, []); + + const activeContext = useActiveMission(); + + const breadCrumbs: Breadcrumb[] = [ + { name: "Ground Support", viewKey: ViewKeys.PLATFORM_SELECTION_KEY, active: false }, + { name: activeContext.rocket.Name, viewKey: ViewKeys.ROCKET_DETAILS_KEY, active: false }, + { name: activeContext.activeMission.Name || "New Mission", viewKey: ViewKeys.ACTIVE_FLIGHT_KEY, active: true } + ]; + + return ( + <Box sx={{ width: '100vw', height: '100vh' }}> + <Stack + height={'100%'} + padding={4} + direction="column" + gap={4} + overflow={'none'} + > + <Stack direction="row" alignItems={'center'} justifyContent={'space-between'}> + <Header icon="ROCKET_MONITORING" breadCrumbs={breadCrumbs} /> + <Stack direction="row" spacing={1}> + <Tooltip title="Configuration" placement="top"> + <Button variant={'text'} onClick={() => {}}> + <Settings /> + </Button> + </Tooltip> + <ButtonGroup + variant="contained" + > + <Tooltip title={"Serial Status"}> + <Button + aria-readonly + disabled + color={"success"} + > + Telemetry Lock + </Button> + </Tooltip> + <Button + variant="contained" + size={'large'} + startIcon={<WifiTethering/>} + sx={{ width: 180 }} + onClick={() => { + // setOpenConnection(!openConnection); + // socketContext.toggleConnection(); + }} + > + Connect + {/* {socketContext.connect ? "Disconnect" : "Connect"} */} + </Button> + </ButtonGroup> + </Stack> + </Stack> + </Stack> + </Box> + ); +}; + +export default RocketMonitoringView; \ No newline at end of file From b029269e8ad604a4337fa677b70143857c51fce9 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Sun, 9 Jun 2024 22:58:11 -0700 Subject: [PATCH 02/14] View foundation --- client/src/utils/viewProviderContext.tsx | 3 ++- client/src/views/active/rocket-monitoring-view.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/utils/viewProviderContext.tsx b/client/src/utils/viewProviderContext.tsx index e445b9cb5..3e9c0eab2 100644 --- a/client/src/utils/viewProviderContext.tsx +++ b/client/src/utils/viewProviderContext.tsx @@ -15,6 +15,7 @@ import ActiveMissionView from "../views/active-mission"; import { ActiveMissionProvider } from "./ActiveMissionContext"; import PlatformSelector from "../views/platform-selector"; import EngineMonitoringView from "../views/pdp-monitoring/engine-monitoring-view"; +import RocketMonitoringView from "../views/active/rocket-monitoring-view"; export enum ViewKeys { @@ -84,7 +85,7 @@ export const ViewContextProvider = ({ children }: PropsWithChildren<any>) => { case ViewKeys.ACTIVE_FLIGHT_KEY: return { viewKey: ViewKeys.ACTIVE_FLIGHT_KEY, - currentView: <ActiveMissionView /> + currentView: <RocketMonitoringView /> } case ViewKeys.MISSION_REPLAY_KEY: return { diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx index 48c36be3f..b23540256 100644 --- a/client/src/views/active/rocket-monitoring-view.tsx +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -60,7 +60,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock disabled color={"success"} > - Telemetry Lock + GPS Lock </Button> </Tooltip> <Button From fbd1a66ebf8b4935c35dfad1aafd6139e60c0126 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Mon, 10 Jun 2024 12:41:18 -0700 Subject: [PATCH 03/14] rocket status component setup --- .../rocket-monitoring/RocketStatus.tsx | 21 ++++++++++++++++ .../views/active/rocket-monitoring-view.tsx | 25 ++++++++++++------- 2 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 client/src/components/rocket-monitoring/RocketStatus.tsx diff --git a/client/src/components/rocket-monitoring/RocketStatus.tsx b/client/src/components/rocket-monitoring/RocketStatus.tsx new file mode 100644 index 000000000..459555a1c --- /dev/null +++ b/client/src/components/rocket-monitoring/RocketStatus.tsx @@ -0,0 +1,21 @@ +import { Paper } from '@mui/material'; +import React, { useEffect, useState } from 'react'; + +interface IRocketStatusProps { +} + +const RocketStatus: React.FC = (props: IRocketStatusProps) => { + const {} = props; + return ( + <Paper + sx={{ + borderRadius: 2, + padding: 2 + }} + > + <h1>Rocket Status</h1> + </Paper> + ); +} + +export default RocketStatus; \ No newline at end of file diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx index b23540256..90f832b80 100644 --- a/client/src/views/active/rocket-monitoring-view.tsx +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -7,6 +7,7 @@ import Header, { Breadcrumb } from '../../components/Header'; import { ViewKeys } from '../../utils/viewProviderContext'; import { useActiveMission } from '../../utils/ActiveMissionContext'; import { Settings, WifiTethering } from '@mui/icons-material'; +import RocketStatus from '../../components/rocket-monitoring/RocketStatus'; interface IRocketMonitoringViewProps { // rocketId: string; @@ -54,15 +55,20 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock <ButtonGroup variant="contained" > - <Tooltip title={"Serial Status"}> - <Button - aria-readonly - disabled - color={"success"} - > - GPS Lock - </Button> - </Tooltip> + <Button + aria-readonly + disabled + color={"success"} + > + GPS Lock + </Button> + <Button + aria-readonly + disabled + color={"success"} + > + GPS Lock + </Button> <Button variant="contained" size={'large'} @@ -79,6 +85,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock </ButtonGroup> </Stack> </Stack> + <RocketStatus /> </Stack> </Box> ); From e7d42f9127c1390d1557719561aef5e3a923aa07 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Tue, 11 Jun 2024 21:54:43 -0700 Subject: [PATCH 04/14] Socket context to useWebSocket hook --- client/src/utils/socket-context.tsx | 135 +++++++++++++++++----------- 1 file changed, 81 insertions(+), 54 deletions(-) diff --git a/client/src/utils/socket-context.tsx b/client/src/utils/socket-context.tsx index f3f0ca515..8f32193b6 100644 --- a/client/src/utils/socket-context.tsx +++ b/client/src/utils/socket-context.tsx @@ -1,37 +1,48 @@ -import { createContext, PropsWithChildren, useContext, useEffect, useState, useReducer } from 'react'; +import { + createContext, + PropsWithChildren, + useContext, + useEffect, + useState, + useReducer +} from 'react'; +import useWebSocket from 'react-use-websocket'; interface IPacket { Data: {}; Type: string; } +enum Protocol { + APRS = 'APRS', + LoRa = 'LoRa' +} + export interface SocketContext { + gpsLock: boolean; + isConnected: boolean; + toggleConnection: () => void; logs: string[]; - aprsPacket: IPacket; - loRaPacket: IPacket; - setPacketFrequency: (frequency: number) => void; - setProtocol: (protocol: string) => void; + packet: IPacket; + frequency: number; + setFrequency: (frequency: number) => void; + protocol: string; + setProtocol: (protocol: Protocol) => void; } export const Context = createContext<SocketContext>({ + gpsLock: false, + isConnected: false, + toggleConnection: () => {}, logs: [], - aprsPacket: {} as IPacket, - loRaPacket: {} as IPacket, - setPacketFrequency: (frequency: number) => {}, - setProtocol: (protocol: string) => {} + packet: {} as IPacket, + frequency: 433.92, + setFrequency: (frequency: number) => {}, + protocol: Protocol.APRS, + setProtocol: (protocol: Protocol) => {} }); -function aprsReducer(state: IPacket, action: { type: string; payload: any }) { - switch (action.type) { - case 'SET_PACKET': - return action.payload; - - default: - throw new Error(`Unhandled action type: ${action.type}`); - } -} - -function loraReducer(state: IPacket, action: { type: string; payload: any }) { +function packetReducer(state: IPacket, action: { type: string; payload: any }) { switch (action.type) { case 'SET_PACKET': return action.payload; @@ -43,55 +54,71 @@ function loraReducer(state: IPacket, action: { type: string; payload: any }) { export const SocketGateway = ({ children }: PropsWithChildren<any>) => { const [logs, setLogs] = useState<string[]>([]); - const [aprsPacket, aprsDispatch] = useReducer(aprsReducer, {} as IPacket); - const [loRaPacket, loraDispatch] = useReducer(loraReducer, {} as IPacket); - const [packetFrequency, setPacketFrequency] = useState<number>(3); + const [packet, packetDispatch] = useReducer(packetReducer, {} as IPacket); + const [frequency, setFrequency] = useState<number>(433.92); const [protocol, setProtocol] = useState<string>('APRS'); + const [isConnected, setIsConnect] = useState<boolean>(false); + const [gpsLock, setGpsLock] = useState<boolean>(false); const port = import.meta.env.TELEMETRY_SERVER_PORT ? import.meta.env.TELEMETRY_SERVER_PORT : 9193; - const ws = new WebSocket(`ws://localhost:${port}`); - - const enableLiveMode = () => { - ws.send(JSON.stringify({ type: 'establish_stream', frequency: packetFrequency })); - }; - - const changePacketFrequency = (frequency: number) => { - ws.send(JSON.stringify({ type: 'change_frequency', frequency: frequency })); - }; + const [uri, setUri] = useState<string | null>(isConnected ? `ws://localhost:${port}` : null); + + const { + sendJsonMessage, + lastMessage, + lastJsonMessage + } = useWebSocket(uri, { + shouldReconnect: (closeEvent) => isConnected, + onClose: (closeEvent) => { + setIsConnect(false); + console.log('Socket Closed:', closeEvent); + }, + onError: (error) => { + console.log('Socket Error:', error); + }, + onOpen: () => { + console.log('Telemetry socket Opened'); + setIsConnect(true); + }, + share: true + }); - const changeProtocol = (protocol: string) => { - ws.send(JSON.stringify({ type: 'change_protocol', protocol: protocol })); - }; + const toggleConnection = () => { + setIsConnect(prevIsConnected => { + if (prevIsConnected) { + // If the WebSocket is currently connected, disconnect it + setUri(null); + } else { + // If the WebSocket is currently disconnected, connect it + setUri(`ws://localhost:${port}`); + } + return !prevIsConnected; + }); + }; useEffect(() => { - if (ws.readyState === ws.OPEN) { - changePacketFrequency(packetFrequency); + if (lastMessage) { + setLogs([...logs, lastMessage]); } - }, [packetFrequency]); + }, [lastMessage]); useEffect(() => { - if (ws.readyState === ws.OPEN) { - changeProtocol(protocol); + if (lastJsonMessage) { + packetDispatch({ type: 'SET_PACKET', payload: lastJsonMessage }); } - }, [protocol]); - - ws.addEventListener('open', () => { - console.log('Socket Connected'); - enableLiveMode(); - }); - - ws.addEventListener('message', (message) => { - const data = JSON.parse(message.data); - console.log(data); - }); + }, [lastJsonMessage]); return ( <Context.Provider value={{ + isConnected, + gpsLock, + toggleConnection, logs, - aprsPacket, - loRaPacket, - setPacketFrequency, + packet, + frequency, + setFrequency, + protocol, setProtocol }} > From 9eb37fd6b8725809dc15432db8d3d340adfc33cb Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Tue, 11 Jun 2024 21:55:25 -0700 Subject: [PATCH 05/14] Telemetry status + packet + dialog base for components --- .../rocket-monitoring/ConnectionDialog.tsx | 108 ++++++++++++++++++ .../rocket-monitoring/RocketStatus.tsx | 21 ---- .../rocket-monitoring/TelemetryPacket.tsx | 77 +++++++++++++ .../rocket-monitoring/TelemetryStatus.tsx | 76 ++++++++++++ .../views/active/rocket-monitoring-view.tsx | 23 ++-- 5 files changed, 276 insertions(+), 29 deletions(-) create mode 100644 client/src/components/rocket-monitoring/ConnectionDialog.tsx delete mode 100644 client/src/components/rocket-monitoring/RocketStatus.tsx create mode 100644 client/src/components/rocket-monitoring/TelemetryPacket.tsx create mode 100644 client/src/components/rocket-monitoring/TelemetryStatus.tsx diff --git a/client/src/components/rocket-monitoring/ConnectionDialog.tsx b/client/src/components/rocket-monitoring/ConnectionDialog.tsx new file mode 100644 index 000000000..d2eb48b22 --- /dev/null +++ b/client/src/components/rocket-monitoring/ConnectionDialog.tsx @@ -0,0 +1,108 @@ +import { Autocomplete, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { useSocketContext } from "../../utils/socket-context"; + +interface IConnectionDialogProps { + isOpen: boolean; + onClose: () => void; +} + +const ConnectionDialog: React.FC<IConnectionDialogProps> = (props: IConnectionDialogProps) => { + const socketContext = useSocketContext(); + const [protocol, setProtocol] = useState<string>('APRS'); + const [frequency, setFrequency] = useState<number>(433.92); + const [launchAltitude, setLaunchAltitude] = useState<number>(0); + + // useEffect(() => { + // setProtocol(socketContext.protocol); + // setFrequency(socketContext.frequency); + // }, [socketContext.protocol, socketContext.frequency]); + + return( + <Dialog open={props.isOpen}> + <DialogTitle sx={{ fontWeight: 600 }}>Connection Status</DialogTitle> + <DialogContent> + <Stack direction="column" spacing={2} alignItems="center" sx={{ paddingTop: 1 }}> + <Autocomplete + fullWidth + options={['APRS', 'LoRa' ]} + value={protocol} + renderInput={ + (params) => + <TextField + {...params} + label="Protocol" + variant="outlined" + size="small" + /> + } + onChange={(_: any, value: any) => { + setProtocol(value); + }} + /> + <TextField + fullWidth + required + value={frequency} + onChange={(event: React.ChangeEvent<HTMLInputElement>) => { + setFrequency(event.target.value as number); + }} + label="Frequency" + variant="outlined" + size="small" + type="number" + InputProps={{ + endAdornment: <Typography>MHz</Typography>, + }} + InputLabelProps={{ + shrink: true, + }} + /> + <TextField + fullWidth + required + value={launchAltitude} + onChange={(event: React.ChangeEvent<HTMLInputElement>) => { + setLaunchAltitude(event.target.value as number); + }} + label="Launch Altitude" + variant="outlined" + size="small" + type="number" + InputProps={{ + endAdornment: <Typography>FT</Typography>, + }} + InputLabelProps={{ + shrink: true, + }} + /> + </Stack> + </DialogContent> + <DialogActions> + <Button + variant={"text"} + component="label" + onClick={props.onClose} + > + Cancel + </Button> + <Button + variant={"contained"} + component="label" + onClick={ + () => { + // socketContext.setProtocol(protocol); + socketContext.setFrequency(frequency); + // socketContext.toggleConnection(); + props.onClose() + } + } + > + Connect + </Button> + </DialogActions> + </Dialog> + ); +} + +export default ConnectionDialog; \ No newline at end of file diff --git a/client/src/components/rocket-monitoring/RocketStatus.tsx b/client/src/components/rocket-monitoring/RocketStatus.tsx deleted file mode 100644 index 459555a1c..000000000 --- a/client/src/components/rocket-monitoring/RocketStatus.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Paper } from '@mui/material'; -import React, { useEffect, useState } from 'react'; - -interface IRocketStatusProps { -} - -const RocketStatus: React.FC = (props: IRocketStatusProps) => { - const {} = props; - return ( - <Paper - sx={{ - borderRadius: 2, - padding: 2 - }} - > - <h1>Rocket Status</h1> - </Paper> - ); -} - -export default RocketStatus; \ No newline at end of file diff --git a/client/src/components/rocket-monitoring/TelemetryPacket.tsx b/client/src/components/rocket-monitoring/TelemetryPacket.tsx new file mode 100644 index 000000000..08e6a707b --- /dev/null +++ b/client/src/components/rocket-monitoring/TelemetryPacket.tsx @@ -0,0 +1,77 @@ +import { Button, Chip, Icon, Paper, Stack, Typography } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { useSocketContext } from '../../utils/socket-context'; +import { Badge, Public, Height, ShowChart, HorizontalRule } from '@mui/icons-material'; + +interface IPacketContent { + label: string; + value: string; + icon: any; +} + +const TelemetryPacket: React.FC = () => { + const socketContext = useSocketContext(); + + const content: IPacketContent[] = [ + { + label: 'Call Sign', + value: 'N/A', + icon: <Badge /> + }, + { + label: 'Altitude', + value: 'ft (AGL)', + icon: <ShowChart /> + }, + { + label: 'Longitude', + value: '0.00 °', + icon: <Height /> + }, + { + label: 'Latitude', + value: '0.00 °', + icon: <HorizontalRule /> + } + ] + + useEffect(() => { + console.log(socketContext.packet); + }, [socketContext.packet]); + + return ( + <Paper + sx={{ + borderRadius: 2, + padding: 2, + width: 250 + }} + > + <Stack spacing={2} direction={'column'}> + <Typography variant='h6' fontWeight={600}> + Telemetry Packet + </Typography> + {content.map((item: IPacketContent, index: number) => ( + <Stack direction={'row'} justifyContent={'space-between'} key={index}> + <Stack spacing={1} direction={'row'} alignItems={'center'}> + {item.icon} + <Typography fontWeight={500}> + {item.label} + </Typography> + </Stack> + <Chip + label={item.value} + sx={{ + fontWeight: 600, + borderRadius: 1 + }} + color='default' + /> + </Stack> + ))} + </Stack> + </Paper> + ); +} + +export default TelemetryPacket; \ No newline at end of file diff --git a/client/src/components/rocket-monitoring/TelemetryStatus.tsx b/client/src/components/rocket-monitoring/TelemetryStatus.tsx new file mode 100644 index 000000000..38a41773b --- /dev/null +++ b/client/src/components/rocket-monitoring/TelemetryStatus.tsx @@ -0,0 +1,76 @@ +import { Button, Chip, IconButton, Paper, Stack, Tooltip, Typography } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { useSocketContext } from '../../utils/socket-context'; + +import { GpsFixed, GpsOff, GpsNotFixed } from '@mui/icons-material'; + +const TelemetryStatus: React.FC = () => { + const socketContext = useSocketContext(); + + const [protocol, setProtocol] = useState<string>('APRS'); + useEffect(() => { + setProtocol(socketContext.protocol); + }, [socketContext.protocol]); + + const [frequency, setFrequency] = useState<number>(433.92); + useEffect(() => { + setFrequency(socketContext.frequency); + }, [socketContext.frequency]); + + const [ launchAltitude, setLaunchAltitude ] = useState<number>(0); + + return ( + <Paper + sx={{ + borderRadius: 2, + padding: 2 + }} + > + <Stack spacing={2} direction={'row'} alignItems={'center'} justifyContent={'space-between'}> + <Typography variant='h6' fontWeight={600}>Telemetry Information</Typography> + <Stack spacing={2} direction={'row'} alignItems={'center'}> + <Tooltip title={'GPS Lock if green'}> + <IconButton + disabled={!socketContext.isConnected} + color={socketContext.gpsLock ? 'success' : 'error'} + > + {socketContext.isConnected ? + socketContext.gpsLock ? + (<GpsNotFixed />) + : ( <GpsFixed />) + : ( + <GpsOff /> + )} + </IconButton> + </Tooltip> + <Chip + label={`Launch Altitude: ${launchAltitude} FT`} + sx={{ + fontWeight: 600, + borderRadius: 1 + }} + color='default' + /> + <Chip + label={`Frequency: ${frequency} MHz`} + sx={{ + fontWeight: 600, + borderRadius: 1 + }} + color='default' + /> + <Chip + label={`Protocol: ${protocol}`} + sx={{ + fontWeight: 600, + borderRadius: 1 + }} + color='default' + /> + </Stack> + </Stack> + </Paper> + ); +} + +export default TelemetryStatus; \ No newline at end of file diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx index 90f832b80..c80aa4bcc 100644 --- a/client/src/views/active/rocket-monitoring-view.tsx +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -7,7 +7,10 @@ import Header, { Breadcrumb } from '../../components/Header'; import { ViewKeys } from '../../utils/viewProviderContext'; import { useActiveMission } from '../../utils/ActiveMissionContext'; import { Settings, WifiTethering } from '@mui/icons-material'; -import RocketStatus from '../../components/rocket-monitoring/RocketStatus'; +import TelemetryStatus from '../../components/rocket-monitoring/TelemetryStatus'; +import ConnectionDialog from '../../components/rocket-monitoring/ConnectionDialog'; +import { useSocketContext } from '../../utils/socket-context'; +import TelemetryPacket from '../../components/rocket-monitoring/TelemetryPacket'; interface IRocketMonitoringViewProps { // rocketId: string; @@ -35,13 +38,17 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock { name: activeContext.activeMission.Name || "New Mission", viewKey: ViewKeys.ACTIVE_FLIGHT_KEY, active: true } ]; + const [openConnection, setOpenConnection] = useState<boolean>(false); + const [frequency, setFrequency] = useState<number>(0); + const socketContext = useSocketContext(); + return ( <Box sx={{ width: '100vw', height: '100vh' }}> <Stack height={'100%'} padding={4} direction="column" - gap={4} + gap={2} overflow={'none'} > <Stack direction="row" alignItems={'center'} justifyContent={'space-between'}> @@ -67,7 +74,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock disabled color={"success"} > - GPS Lock + Rocket Connected </Button> <Button variant="contained" @@ -75,18 +82,18 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock startIcon={<WifiTethering/>} sx={{ width: 180 }} onClick={() => { - // setOpenConnection(!openConnection); - // socketContext.toggleConnection(); + setOpenConnection(!openConnection); }} > - Connect - {/* {socketContext.connect ? "Disconnect" : "Connect"} */} + {socketContext.isConnected ? "Disconnect" : "Connect"} </Button> </ButtonGroup> </Stack> </Stack> - <RocketStatus /> + <TelemetryStatus /> + <TelemetryPacket /> </Stack> + <ConnectionDialog isOpen={openConnection} onClose={() => setOpenConnection(false)} /> </Box> ); }; From 964c50eb9017f39582ec8813f2977bca3f7f29b0 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Tue, 11 Jun 2024 22:59:10 -0700 Subject: [PATCH 06/14] fixed socket context & altitude --- .../rocket-monitoring/ConnectionDialog.tsx | 13 ++++--- .../rocket-monitoring/TelemetryStatus.tsx | 12 ++++++- client/src/utils/socket-context.tsx | 34 +++++++++++++++---- client/src/utils/viewProviderContext.tsx | 5 ++- .../views/active/rocket-monitoring-view.tsx | 15 +++----- 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/client/src/components/rocket-monitoring/ConnectionDialog.tsx b/client/src/components/rocket-monitoring/ConnectionDialog.tsx index d2eb48b22..a1dcee4e4 100644 --- a/client/src/components/rocket-monitoring/ConnectionDialog.tsx +++ b/client/src/components/rocket-monitoring/ConnectionDialog.tsx @@ -5,13 +5,14 @@ import { useSocketContext } from "../../utils/socket-context"; interface IConnectionDialogProps { isOpen: boolean; onClose: () => void; + updateAltitude: (altitude: number) => void; } const ConnectionDialog: React.FC<IConnectionDialogProps> = (props: IConnectionDialogProps) => { const socketContext = useSocketContext(); - const [protocol, setProtocol] = useState<string>('APRS'); - const [frequency, setFrequency] = useState<number>(433.92); - const [launchAltitude, setLaunchAltitude] = useState<number>(0); + const [protocol, setProtocol] = useState<string>(''); + const [frequency, setFrequency] = useState<number>(); + const [launchAltitude, setLaunchAltitude] = useState<number>(); // useEffect(() => { // setProtocol(socketContext.protocol); @@ -89,10 +90,12 @@ const ConnectionDialog: React.FC<IConnectionDialogProps> = (props: IConnectionDi <Button variant={"contained"} component="label" + disabled={!protocol || !frequency || !launchAltitude} onClick={ () => { - // socketContext.setProtocol(protocol); - socketContext.setFrequency(frequency); + socketContext.updateProtocol(protocol); + socketContext.updateFrequency(frequency); + props.updateAltitude(launchAltitude); // socketContext.toggleConnection(); props.onClose() } diff --git a/client/src/components/rocket-monitoring/TelemetryStatus.tsx b/client/src/components/rocket-monitoring/TelemetryStatus.tsx index 38a41773b..34ac82a06 100644 --- a/client/src/components/rocket-monitoring/TelemetryStatus.tsx +++ b/client/src/components/rocket-monitoring/TelemetryStatus.tsx @@ -4,7 +4,11 @@ import { useSocketContext } from '../../utils/socket-context'; import { GpsFixed, GpsOff, GpsNotFixed } from '@mui/icons-material'; -const TelemetryStatus: React.FC = () => { +interface ITelemetryStatusProps { + launchAltitude?: number; +} + +const TelemetryStatus: React.FC<ITelemetryStatusProps> = (props: ITelemetryStatusProps) => { const socketContext = useSocketContext(); const [protocol, setProtocol] = useState<string>('APRS'); @@ -19,6 +23,12 @@ const TelemetryStatus: React.FC = () => { const [ launchAltitude, setLaunchAltitude ] = useState<number>(0); + useEffect(() => { + if (props.launchAltitude) { + setLaunchAltitude(props.launchAltitude); + } + }, [props.launchAltitude]); + return ( <Paper sx={{ diff --git a/client/src/utils/socket-context.tsx b/client/src/utils/socket-context.tsx index 8f32193b6..0e0b609ab 100644 --- a/client/src/utils/socket-context.tsx +++ b/client/src/utils/socket-context.tsx @@ -25,9 +25,9 @@ export interface SocketContext { logs: string[]; packet: IPacket; frequency: number; - setFrequency: (frequency: number) => void; + updateFrequency: (frequency: number) => void; protocol: string; - setProtocol: (protocol: Protocol) => void; + updateProtocol: (protocol: Protocol) => void; } export const Context = createContext<SocketContext>({ @@ -37,9 +37,9 @@ export const Context = createContext<SocketContext>({ logs: [], packet: {} as IPacket, frequency: 433.92, - setFrequency: (frequency: number) => {}, + updateFrequency: (frequency: number) => {}, protocol: Protocol.APRS, - setProtocol: (protocol: Protocol) => {} + updateProtocol: (protocol: Protocol) => {} }); function packetReducer(state: IPacket, action: { type: string; payload: any }) { @@ -63,12 +63,32 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { const port = import.meta.env.TELEMETRY_SERVER_PORT ? import.meta.env.TELEMETRY_SERVER_PORT : 9193; const [uri, setUri] = useState<string | null>(isConnected ? `ws://localhost:${port}` : null); + useEffect(() => { + if (frequency) { + console.log('Frequency:', frequency); + } + + if (protocol) { + console.log('Protocol:', protocol); + } + }, [frequency, protocol]); + + const updateFrequency = (frequency: number) => { + console.log('Frequency:', frequency); + setFrequency(frequency); + }; + + const updateProtocol = (protocol: Protocol) => { + console.log('Protocol:', protocol); + setProtocol(protocol); + }; + const { sendJsonMessage, lastMessage, lastJsonMessage } = useWebSocket(uri, { - shouldReconnect: (closeEvent) => isConnected, + shouldReconnect: (_) => isConnected, onClose: (closeEvent) => { setIsConnect(false); console.log('Socket Closed:', closeEvent); @@ -117,9 +137,9 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { logs, packet, frequency, - setFrequency, + updateFrequency, protocol, - setProtocol + updateProtocol }} > {children} diff --git a/client/src/utils/viewProviderContext.tsx b/client/src/utils/viewProviderContext.tsx index 3e9c0eab2..b9732a952 100644 --- a/client/src/utils/viewProviderContext.tsx +++ b/client/src/utils/viewProviderContext.tsx @@ -16,6 +16,7 @@ import { ActiveMissionProvider } from "./ActiveMissionContext"; import PlatformSelector from "../views/platform-selector"; import EngineMonitoringView from "../views/pdp-monitoring/engine-monitoring-view"; import RocketMonitoringView from "../views/active/rocket-monitoring-view"; +import { SocketGateway } from "./socket-context"; export enum ViewKeys { @@ -85,7 +86,9 @@ export const ViewContextProvider = ({ children }: PropsWithChildren<any>) => { case ViewKeys.ACTIVE_FLIGHT_KEY: return { viewKey: ViewKeys.ACTIVE_FLIGHT_KEY, - currentView: <RocketMonitoringView /> + currentView: <SocketGateway> + <RocketMonitoringView /> + </SocketGateway> } case ViewKeys.MISSION_REPLAY_KEY: return { diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx index c80aa4bcc..cb6db1e43 100644 --- a/client/src/views/active/rocket-monitoring-view.tsx +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -42,6 +42,8 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock const [frequency, setFrequency] = useState<number>(0); const socketContext = useSocketContext(); + const [launchAltitude, setLaunchAltitude] = useState<number>(0); + return ( <Box sx={{ width: '100vw', height: '100vh' }}> <Stack @@ -67,14 +69,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock disabled color={"success"} > - GPS Lock - </Button> - <Button - aria-readonly - disabled - color={"success"} - > - Rocket Connected + Server Connected </Button> <Button variant="contained" @@ -90,10 +85,10 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock </ButtonGroup> </Stack> </Stack> - <TelemetryStatus /> + <TelemetryStatus launchAltitude={launchAltitude} /> <TelemetryPacket /> </Stack> - <ConnectionDialog isOpen={openConnection} onClose={() => setOpenConnection(false)} /> + <ConnectionDialog updateAltitude={setLaunchAltitude} isOpen={openConnection} onClose={() => setOpenConnection(false)} /> </Box> ); }; From 1d62c29684850a1ee3975fe03b388ea566e8acad Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Tue, 11 Jun 2024 23:40:15 -0700 Subject: [PATCH 07/14] Telemetry log + condition rendering --- .../src/components/logging/TelemetryLog.tsx | 76 +++++++++---------- .../rocket-monitoring/ConnectionDialog.tsx | 4 +- .../rocket-monitoring/TelemetryPacket.tsx | 43 ++++++++--- client/src/utils/socket-context.tsx | 2 +- .../views/active/rocket-monitoring-view.tsx | 16 +++- 5 files changed, 83 insertions(+), 58 deletions(-) diff --git a/client/src/components/logging/TelemetryLog.tsx b/client/src/components/logging/TelemetryLog.tsx index 305e6b5fd..5a08486c4 100644 --- a/client/src/components/logging/TelemetryLog.tsx +++ b/client/src/components/logging/TelemetryLog.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Button, InputAdornment, Stack, TextField, Tooltip } from "@mui/material"; +import { Button, InputAdornment, Paper, Stack, TextField, TextareaAutosize, Tooltip } from "@mui/material"; import { Box } from "@mui/material"; import { Download } from "@mui/icons-material"; import GpsOffIcon from '@mui/icons-material/GpsOff'; @@ -10,10 +10,10 @@ import { useActiveMission } from "../../utils/ActiveMissionContext"; import { saveAs } from "file-saver"; interface TelemetryLogProps { - packet: any; - width: string; - maxRows: number; - telemetryConnected: boolean; + packet?: any; + width?: string; + maxRows?: number; + telemetryConnected?: boolean; } const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => { @@ -29,19 +29,19 @@ const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => const [log, setLog] = useState<string[]>(activeContext.logs.map((log) => log.Log)); - useEffect(() => { - if (props.packet) { - setLog((prev) => [...prev, JSON.stringify(props.packet)]); - setTimeSincePacket(0); - } - }, [props.packet]); + // useEffect(() => { + // if (props.packet) { + // setLog((prev) => [...prev, JSON.stringify(props.packet)]); + // setTimeSincePacket(0); + // } + // }, [props.packet]); - useEffect(() => { - setTelemetryConnected(props.telemetryConnected); - if (telemetryConnected) { - setTimeSincePacket(0); - } - }, [props.telemetryConnected]); + // useEffect(() => { + // setTelemetryConnected(props.telemetryConnected); + // if (telemetryConnected) { + // setTimeSincePacket(0); + // } + // }, [props.telemetryConnected]); useEffect(() => { if (telemetryConnected) { @@ -53,34 +53,26 @@ const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => }, [timeSincePacket, telemetryConnected]); return ( - <Box style={{ - backgroundColor: "#282C34", - borderRadius: 5, - width: props.width + <Paper + sx={{ + borderRadius: 2, + width: '100%', + minWidth: 'fit-content', + height: 'fit-content' }} - boxShadow={3} > <Stack direction={"column"}> - <TextField - value={log} - variant={"standard"} - multiline - rows={props.maxRows} + <TextareaAutosize style={{ - color: "#CA4D33", - backgroundColor: "#282C34", - borderRadius: 13, - padding: "0px 0px 0px 20px" - }} - InputProps={{ - disableUnderline: true, - readOnly: true, - style: { - color: "white", - }, - multiline: true, - rows: props.maxRows, + border: "none", + backgroundColor: "#23282F", + color: "#ffffff" }} + minRows={10} + maxRows={10} + readOnly + placeholder="Valve Cart Log" + value={log} /> <Stack direction={'row'} bottom={0}> {[ @@ -108,7 +100,7 @@ const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => endAdornment: <InputAdornment position="end">s</InputAdornment>, }} value={timeSincePacket} - sx={{ width: 110 }} + sx={{ width: 100 }} /> </Tooltip> </Stack> @@ -122,7 +114,7 @@ const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => </Button> </Stack> </Stack> - </Box> + </Paper> ); } diff --git a/client/src/components/rocket-monitoring/ConnectionDialog.tsx b/client/src/components/rocket-monitoring/ConnectionDialog.tsx index a1dcee4e4..87849b005 100644 --- a/client/src/components/rocket-monitoring/ConnectionDialog.tsx +++ b/client/src/components/rocket-monitoring/ConnectionDialog.tsx @@ -10,8 +10,8 @@ interface IConnectionDialogProps { const ConnectionDialog: React.FC<IConnectionDialogProps> = (props: IConnectionDialogProps) => { const socketContext = useSocketContext(); - const [protocol, setProtocol] = useState<string>(''); - const [frequency, setFrequency] = useState<number>(); + const [protocol, setProtocol] = useState<string>(socketContext.protocol); + const [frequency, setFrequency] = useState<number>(socketContext.frequency); const [launchAltitude, setLaunchAltitude] = useState<number>(); // useEffect(() => { diff --git a/client/src/components/rocket-monitoring/TelemetryPacket.tsx b/client/src/components/rocket-monitoring/TelemetryPacket.tsx index 08e6a707b..a598fcc2a 100644 --- a/client/src/components/rocket-monitoring/TelemetryPacket.tsx +++ b/client/src/components/rocket-monitoring/TelemetryPacket.tsx @@ -1,17 +1,30 @@ import { Button, Chip, Icon, Paper, Stack, Typography } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { useSocketContext } from '../../utils/socket-context'; -import { Badge, Public, Height, ShowChart, HorizontalRule } from '@mui/icons-material'; +import { Badge, Height, ShowChart, HorizontalRule, Timer } from '@mui/icons-material'; + -interface IPacketContent { - label: string; - value: string; - icon: any; -} const TelemetryPacket: React.FC = () => { const socketContext = useSocketContext(); + const [timeSincePacket, setTimeSincePacket] = useState<number>(0); + + useEffect(() => { + if ((socketContext.packet && socketContext.isConnected)) { + const interval = setInterval(() => { + setTimeSincePacket(timeSincePacket + 1); + }, 1000); + return () => clearInterval(interval); + } + }, [timeSincePacket, socketContext.packet]); + + interface IPacketContent { + label: string; + value: string; + icon: any; + } + const content: IPacketContent[] = [ { label: 'Call Sign', @@ -20,7 +33,7 @@ const TelemetryPacket: React.FC = () => { }, { label: 'Altitude', - value: 'ft (AGL)', + value: 'ft (ALG)', icon: <ShowChart /> }, { @@ -44,12 +57,13 @@ const TelemetryPacket: React.FC = () => { sx={{ borderRadius: 2, padding: 2, - width: 250 + width: 300, + minWidth: 'fit-content', }} > <Stack spacing={2} direction={'column'}> <Typography variant='h6' fontWeight={600}> - Telemetry Packet + Current Packet </Typography> {content.map((item: IPacketContent, index: number) => ( <Stack direction={'row'} justifyContent={'space-between'} key={index}> @@ -66,9 +80,20 @@ const TelemetryPacket: React.FC = () => { borderRadius: 1 }} color='default' + size='small' /> </Stack> ))} + <Chip + label={`Time Since: ${timeSincePacket}s`} + icon={<Timer />} + size='medium' + sx={{ + fontWeight: 600, + borderRadius: 1 + }} + color='default' + /> </Stack> </Paper> ); diff --git a/client/src/utils/socket-context.tsx b/client/src/utils/socket-context.tsx index 0e0b609ab..310a25505 100644 --- a/client/src/utils/socket-context.tsx +++ b/client/src/utils/socket-context.tsx @@ -61,7 +61,7 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { const [gpsLock, setGpsLock] = useState<boolean>(false); const port = import.meta.env.TELEMETRY_SERVER_PORT ? import.meta.env.TELEMETRY_SERVER_PORT : 9193; - const [uri, setUri] = useState<string | null>(isConnected ? `ws://localhost:${port}` : null); + const [uri, setUri] = useState<string | null>(isConnected ? `ws://localhost:${9193}` : null); useEffect(() => { if (frequency) { diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx index cb6db1e43..8f762730c 100644 --- a/client/src/views/active/rocket-monitoring-view.tsx +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import api from '../../services/api'; import { IRocketPopulated } from '../../utils/entities'; -import { Box, Button, ButtonGroup, Stack, Tooltip } from '@mui/material'; +import { Alert, Box, Button, ButtonGroup, Stack, Tooltip } from '@mui/material'; import Header, { Breadcrumb } from '../../components/Header'; import { ViewKeys } from '../../utils/viewProviderContext'; import { useActiveMission } from '../../utils/ActiveMissionContext'; @@ -11,6 +11,7 @@ import TelemetryStatus from '../../components/rocket-monitoring/TelemetryStatus' import ConnectionDialog from '../../components/rocket-monitoring/ConnectionDialog'; import { useSocketContext } from '../../utils/socket-context'; import TelemetryPacket from '../../components/rocket-monitoring/TelemetryPacket'; +import TelemetryLog from '../../components/logging/TelemetryLog'; interface IRocketMonitoringViewProps { // rocketId: string; @@ -39,9 +40,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock ]; const [openConnection, setOpenConnection] = useState<boolean>(false); - const [frequency, setFrequency] = useState<number>(0); const socketContext = useSocketContext(); - const [launchAltitude, setLaunchAltitude] = useState<number>(0); return ( @@ -86,7 +85,16 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock </Stack> </Stack> <TelemetryStatus launchAltitude={launchAltitude} /> - <TelemetryPacket /> + {socketContext.isConnected ? ( + <Stack direction="row" gap={2}> + <TelemetryPacket /> + <TelemetryLog /> + </Stack> + ) : ( + <Alert severity="info"> + Connect to telemetry to see data + </Alert> + )} </Stack> <ConnectionDialog updateAltitude={setLaunchAltitude} isOpen={openConnection} onClose={() => setOpenConnection(false)} /> </Box> From 7a2566a3d323bff25117c4a58a151d763c5b8015 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Wed, 12 Jun 2024 00:48:36 -0700 Subject: [PATCH 08/14] Add logging animation --- .../src/components/logging/TelemetryLog.tsx | 6 ++-- .../rocket-monitoring/ConnectionDialog.tsx | 4 +-- .../rocket-monitoring/TelemetryStatus.tsx | 4 +-- client/src/utils/socket-context.tsx | 12 ++++++++ .../views/active/rocket-monitoring-view.tsx | 29 +++++++++++++++++-- 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/client/src/components/logging/TelemetryLog.tsx b/client/src/components/logging/TelemetryLog.tsx index 5a08486c4..a65cdafbb 100644 --- a/client/src/components/logging/TelemetryLog.tsx +++ b/client/src/components/logging/TelemetryLog.tsx @@ -66,12 +66,14 @@ const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => style={{ border: "none", backgroundColor: "#23282F", - color: "#ffffff" + color: "#ffffff", + paddingTop: 10, + paddingLeft: 10 }} minRows={10} maxRows={10} readOnly - placeholder="Valve Cart Log" + placeholder="Telemetry Log" value={log} /> <Stack direction={'row'} bottom={0}> diff --git a/client/src/components/rocket-monitoring/ConnectionDialog.tsx b/client/src/components/rocket-monitoring/ConnectionDialog.tsx index 87849b005..d94337060 100644 --- a/client/src/components/rocket-monitoring/ConnectionDialog.tsx +++ b/client/src/components/rocket-monitoring/ConnectionDialog.tsx @@ -96,12 +96,12 @@ const ConnectionDialog: React.FC<IConnectionDialogProps> = (props: IConnectionDi socketContext.updateProtocol(protocol); socketContext.updateFrequency(frequency); props.updateAltitude(launchAltitude); - // socketContext.toggleConnection(); + socketContext.toggleConnection(); props.onClose() } } > - Connect + {socketContext.isConnected ? 'Disconnect' : 'Connect' } </Button> </DialogActions> </Dialog> diff --git a/client/src/components/rocket-monitoring/TelemetryStatus.tsx b/client/src/components/rocket-monitoring/TelemetryStatus.tsx index 34ac82a06..169b7c7e2 100644 --- a/client/src/components/rocket-monitoring/TelemetryStatus.tsx +++ b/client/src/components/rocket-monitoring/TelemetryStatus.tsx @@ -46,8 +46,8 @@ const TelemetryStatus: React.FC<ITelemetryStatusProps> = (props: ITelemetryStatu > {socketContext.isConnected ? socketContext.gpsLock ? - (<GpsNotFixed />) - : ( <GpsFixed />) + (<GpsFixed />) + : ( <GpsNotFixed/>) : ( <GpsOff /> )} diff --git a/client/src/utils/socket-context.tsx b/client/src/utils/socket-context.tsx index 310a25505..5d98850ac 100644 --- a/client/src/utils/socket-context.tsx +++ b/client/src/utils/socket-context.tsx @@ -1,3 +1,4 @@ +import { Alert, Snackbar } from '@mui/material'; import { createContext, PropsWithChildren, @@ -59,6 +60,7 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { const [protocol, setProtocol] = useState<string>('APRS'); const [isConnected, setIsConnect] = useState<boolean>(false); const [gpsLock, setGpsLock] = useState<boolean>(false); + const [wsError, setWsError] = useState<any | null>(null); const port = import.meta.env.TELEMETRY_SERVER_PORT ? import.meta.env.TELEMETRY_SERVER_PORT : 9193; const [uri, setUri] = useState<string | null>(isConnected ? `ws://localhost:${9193}` : null); @@ -94,6 +96,7 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { console.log('Socket Closed:', closeEvent); }, onError: (error) => { + setWsError(error); console.log('Socket Error:', error); }, onOpen: () => { @@ -117,12 +120,14 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { }; useEffect(() => { + console.log('Last Message:', lastMessage); if (lastMessage) { setLogs([...logs, lastMessage]); } }, [lastMessage]); useEffect(() => { + console.log('Last JSON Message:', lastJsonMessage); if (lastJsonMessage) { packetDispatch({ type: 'SET_PACKET', payload: lastJsonMessage }); } @@ -142,6 +147,13 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { updateProtocol }} > + <Snackbar + open={wsError !== null} + message="Could not connect to socket server" + autoHideDuration={6000} + > + <Alert variant='filled' severity="error" onClose={() => setWsError(null)}>Could not connect to socket server</Alert> + </Snackbar> {children} </Context.Provider> ); diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx index 8f762730c..4ed86ec85 100644 --- a/client/src/views/active/rocket-monitoring-view.tsx +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import api from '../../services/api'; import { IRocketPopulated } from '../../utils/entities'; -import { Alert, Box, Button, ButtonGroup, Stack, Tooltip } from '@mui/material'; +import { Alert, Box, Button, ButtonGroup, Skeleton, Stack, Tooltip } from '@mui/material'; import Header, { Breadcrumb } from '../../components/Header'; import { ViewKeys } from '../../utils/viewProviderContext'; import { useActiveMission } from '../../utils/ActiveMissionContext'; @@ -42,6 +42,20 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock const [openConnection, setOpenConnection] = useState<boolean>(false); const socketContext = useSocketContext(); const [launchAltitude, setLaunchAltitude] = useState<number>(0); + const [loading, setLoading] = useState<boolean>(true); + + useEffect(() => { + if (socketContext.isConnected) { + const interval = setInterval(() => { + setLoading(false); + }, 2500); + return () => { + clearInterval(interval); + setLoading(true); + } + } + }, [socketContext.isConnected]); + return ( <Box sx={{ width: '100vw', height: '100vh' }}> @@ -85,7 +99,12 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock </Stack> </Stack> <TelemetryStatus launchAltitude={launchAltitude} /> - {socketContext.isConnected ? ( + {socketContext.isConnected ? loading ? ( + <Stack direction="row" gap={2}> + <Skeleton variant="rectangular" height={300} width={'30%'} sx={{ borderRadius: 2 }} /> + <Skeleton variant="rectangular" height={300} width={'70%'} sx={{ borderRadius: 2 }} /> + </Stack> + ) :( <Stack direction="row" gap={2}> <TelemetryPacket /> <TelemetryLog /> @@ -96,7 +115,11 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock </Alert> )} </Stack> - <ConnectionDialog updateAltitude={setLaunchAltitude} isOpen={openConnection} onClose={() => setOpenConnection(false)} /> + <ConnectionDialog + updateAltitude={setLaunchAltitude} + isOpen={openConnection} + onClose={() => setOpenConnection(false)} + /> </Box> ); }; From 582997ed980605c5975a0d3e74b30f4adc4f6ac6 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Fri, 14 Jun 2024 02:47:22 -0700 Subject: [PATCH 09/14] Map + altitude components --- client/index.html | 6 + client/package-lock.json | 59 ++++++ client/package.json | 4 + .../src/components/logging/TelemetryLog.tsx | 107 +++++----- .../rocket-monitoring/ConnectionDialog.tsx | 50 +++-- .../rocket-monitoring/PublishMission.tsx | 41 ++++ .../TelemetryAltitudeGraph.tsx | 73 +++++++ .../rocket-monitoring/TelemetryMap.tsx | 183 ++++++++++++++++++ .../rocket-monitoring/TelemetryPacket.tsx | 37 ++-- .../rocket-monitoring/TelemetryStatus.tsx | 11 +- client/src/utils/socket-context.tsx | 68 ++++--- .../views/active/rocket-monitoring-view.tsx | 115 +++++++---- services/telemetry/tools/decode_test.sh | 2 +- 13 files changed, 576 insertions(+), 180 deletions(-) create mode 100644 client/src/components/rocket-monitoring/PublishMission.tsx create mode 100644 client/src/components/rocket-monitoring/TelemetryAltitudeGraph.tsx create mode 100644 client/src/components/rocket-monitoring/TelemetryMap.tsx diff --git a/client/index.html b/client/index.html index 612412ff3..2754bb795 100644 --- a/client/index.html +++ b/client/index.html @@ -4,6 +4,12 @@ <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon" href="./src/static/images/PlatformHubLogo.png" /> + <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" + integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" + crossorigin=""/> + <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" + integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" + crossorigin=""></script> <title>Ground Support</title> </head> <body> diff --git a/client/package-lock.json b/client/package-lock.json index 381e8163e..d49906101 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -24,11 +24,14 @@ "concurrently": "^7.5.0", "google-map-react": "^2.2.1", "http": "^0.0.1-security", + "leaflet": "^1.9.4", + "leaflet.offline": "^3.0.0-rc.4", "lodash": "^4.17.21", "mui-datatables": "^4.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.5.0", + "react-leaflet": "^4.2.1", "react-material-file-upload": "^0.0.4", "react-syntax-highlighter": "^15.5.0", "react-use": "^17.5.0", @@ -42,6 +45,7 @@ "devDependencies": { "@types/file-saver": "^2.0.5", "@types/google-map-react": "^2.1.7", + "@types/leaflet": "^1.9.12", "@types/lodash": "^4.14.191", "@types/mui-datatables": "^4.3.5", "@types/react-syntax-highlighter": "^15.5.7", @@ -1414,6 +1418,16 @@ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@reactflow/background": { "version": "11.3.13", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.13.tgz", @@ -2185,6 +2199,15 @@ "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" }, + "node_modules/@types/leaflet": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz", + "integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.200", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz", @@ -3758,6 +3781,11 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.5.tgz", "integrity": "sha512-fedL7PRwmeVkgyhu9hLeTBaI6wcGk7JGJswdaRsa5aUbkXI1kr1xZwTPBtaYPpwf56878iDek6VbVnuWMebJmw==" }, + "node_modules/idb": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/idb/-/idb-6.1.5.tgz", + "integrity": "sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4175,6 +4203,24 @@ "node": ">=6" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, + "node_modules/leaflet.offline": { + "version": "3.0.0-rc.4", + "resolved": "https://registry.npmjs.org/leaflet.offline/-/leaflet.offline-3.0.0-rc.4.tgz", + "integrity": "sha512-M5Vw7Lzm19KYV+WpeVmETRwBCtbG76hFkJDydyrk+eD2gonK2U4aRgxVXwDv5wcmbr35Kgxi8YV/xnLE+XUSrA==", + "dependencies": { + "idb": "^6.1.4", + "leaflet": "^1.6.0", + "typescript": "^4.5.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4813,6 +4859,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", diff --git a/client/package.json b/client/package.json index 6c8b12889..10610cae9 100644 --- a/client/package.json +++ b/client/package.json @@ -19,11 +19,14 @@ "concurrently": "^7.5.0", "google-map-react": "^2.2.1", "http": "^0.0.1-security", + "leaflet": "^1.9.4", + "leaflet.offline": "^3.0.0-rc.4", "lodash": "^4.17.21", "mui-datatables": "^4.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.5.0", + "react-leaflet": "^4.2.1", "react-material-file-upload": "^0.0.4", "react-syntax-highlighter": "^15.5.0", "react-use": "^17.5.0", @@ -61,6 +64,7 @@ "devDependencies": { "@types/file-saver": "^2.0.5", "@types/google-map-react": "^2.1.7", + "@types/leaflet": "^1.9.12", "@types/lodash": "^4.14.191", "@types/mui-datatables": "^4.3.5", "@types/react-syntax-highlighter": "^15.5.7", diff --git a/client/src/components/logging/TelemetryLog.tsx b/client/src/components/logging/TelemetryLog.tsx index a65cdafbb..00593eb71 100644 --- a/client/src/components/logging/TelemetryLog.tsx +++ b/client/src/components/logging/TelemetryLog.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Button, InputAdornment, Paper, Stack, TextField, TextareaAutosize, Tooltip } from "@mui/material"; +import { Button, Chip, InputAdornment, Paper, Stack, TextField, TextareaAutosize, Tooltip } from "@mui/material"; import { Box } from "@mui/material"; import { Download } from "@mui/icons-material"; import GpsOffIcon from '@mui/icons-material/GpsOff'; @@ -8,49 +8,44 @@ import TimerIcon from '@mui/icons-material/Timer'; import { IAprsTelemetryPacket, formatPacket } from "../../utils/TelemetryTypes"; import { useActiveMission } from "../../utils/ActiveMissionContext"; import { saveAs } from "file-saver"; +import { Packet, useSocketContext } from "../../utils/socket-context"; +import { useTheme } from "@emotion/react"; -interface TelemetryLogProps { - packet?: any; - width?: string; - maxRows?: number; - telemetryConnected?: boolean; -} - -const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => { - const [locked, setLocked] = useState<boolean>(false); +const TelemetryLog: React.FC = () => { const [timeSincePacket, setTimeSincePacket] = useState<number>(0); - const [telemetryConnected, setTelemetryConnected] = useState<boolean>(false); - const activeContext = useActiveMission(); + const [logs, setLogs] = useState<string[]>([]); + const socketContext = useSocketContext(); + const theme = useTheme(); const exportLog = () => { - const blob = new Blob([JSON.stringify(activeContext.logs)], {type: "text/plain;charset=utf-8"}); + const blob = new Blob([JSON.stringify(socketContext.logs)], {type: "text/plain;charset=utf-8"}); saveAs(blob, "packet-log.txt"); } - const [log, setLog] = useState<string[]>(activeContext.logs.map((log) => log.Log)); - - // useEffect(() => { - // if (props.packet) { - // setLog((prev) => [...prev, JSON.stringify(props.packet)]); - // setTimeSincePacket(0); - // } - // }, [props.packet]); - - // useEffect(() => { - // setTelemetryConnected(props.telemetryConnected); - // if (telemetryConnected) { - // setTimeSincePacket(0); - // } - // }, [props.telemetryConnected]); + useEffect(() => { + formatLog(); + setTimeSincePacket(0); + }, [socketContext.packet]); useEffect(() => { - if (telemetryConnected) { - const interval = setInterval(() => { - setTimeSincePacket(timeSincePacket + 1); - }, 1000); - return () => clearInterval(interval); + const interval = setInterval(() => { + setTimeSincePacket((prev) => prev + 1); + }, 1000); + return () => { + clearInterval(interval); } - }, [timeSincePacket, telemetryConnected]); + }, [timeSincePacket]); + + const formatLog = () => { + const newLogs = socketContext.logs.map((packet: Packet) => { + if (socketContext.packet.id - packet.id < 2) { + return `[${new Date().toLocaleTimeString()}] Id:${packet.id} - Altitude: ${packet.data.altitude}`; + } + return undefined; + }).filter(log => log !== undefined); + + setLogs([...logs, ...newLogs as string[]]); + }; return ( <Paper @@ -61,20 +56,23 @@ const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => height: 'fit-content' }} > - <Stack direction={"column"}> + <Stack direction={"column"} width={'100%'}> <TextareaAutosize style={{ border: "none", - backgroundColor: "#23282F", + //@ts-ignore + backgroundColor: theme.palette.background.default, color: "#ffffff", - paddingTop: 10, - paddingLeft: 10 + paddingTop: '20px', + paddingLeft: '20px', + maxWidth: `100%`, + width: '99%', }} - minRows={10} - maxRows={10} + minRows={50} + maxRows={13} readOnly placeholder="Telemetry Log" - value={log} + value={logs.join('\n')} /> <Stack direction={'row'} bottom={0}> {[ @@ -86,25 +84,14 @@ const TelemetryLog: React.FC<TelemetryLogProps> = (props: TelemetryLogProps) => </Stack> <Stack direction='row' padding={2} alignItems={'center'} justifyContent={'space-between'}> <Stack direction='row' gap={2}> - <Tooltip title="GPS lock"> - <Button - disabled - variant={"contained"} - sx={{ color: 'grey' }} - > {locked ? <GpsFixedIcon color="success"/> : <GpsOffIcon color="error"/>} </Button> - - </Tooltip> - <Tooltip title="Time since last packet"> - <TextField - size="small" - InputProps={{ - startAdornment: <InputAdornment position="start"><TimerIcon/></InputAdornment>, - endAdornment: <InputAdornment position="end">s</InputAdornment>, - }} - value={timeSincePacket} - sx={{ width: 100 }} - /> - </Tooltip> + <Chip + icon={<TimerIcon />} + sx={{ + borderRadius: 2, + fontWeight: 600 + }} + label={`Time since: ${timeSincePacket} s`} + /> </Stack> <Button variant={"contained"} diff --git a/client/src/components/rocket-monitoring/ConnectionDialog.tsx b/client/src/components/rocket-monitoring/ConnectionDialog.tsx index d94337060..274329a86 100644 --- a/client/src/components/rocket-monitoring/ConnectionDialog.tsx +++ b/client/src/components/rocket-monitoring/ConnectionDialog.tsx @@ -1,4 +1,4 @@ -import { Autocomplete, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material"; +import { Autocomplete, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Tooltip, Typography } from "@mui/material"; import React, { useEffect, useState } from "react"; import { useSocketContext } from "../../utils/socket-context"; @@ -12,12 +12,7 @@ const ConnectionDialog: React.FC<IConnectionDialogProps> = (props: IConnectionDi const socketContext = useSocketContext(); const [protocol, setProtocol] = useState<string>(socketContext.protocol); const [frequency, setFrequency] = useState<number>(socketContext.frequency); - const [launchAltitude, setLaunchAltitude] = useState<number>(); - - // useEffect(() => { - // setProtocol(socketContext.protocol); - // setFrequency(socketContext.frequency); - // }, [socketContext.protocol, socketContext.frequency]); + const [packetRetrievalFrequency, setPacketRetrievalFrequency] = useState<number>(socketContext.packetStreamingInterval); return( <Dialog open={props.isOpen}> @@ -59,24 +54,25 @@ const ConnectionDialog: React.FC<IConnectionDialogProps> = (props: IConnectionDi shrink: true, }} /> - <TextField - fullWidth - required - value={launchAltitude} - onChange={(event: React.ChangeEvent<HTMLInputElement>) => { - setLaunchAltitude(event.target.value as number); - }} - label="Launch Altitude" - variant="outlined" - size="small" - type="number" - InputProps={{ - endAdornment: <Typography>FT</Typography>, - }} - InputLabelProps={{ - shrink: true, - }} - /> + <Tooltip + title="How often packets are retrieved from the Telemetry server" + > + <TextField + fullWidth + required + value={packetRetrievalFrequency} + onChange={(event: React.ChangeEvent<HTMLInputElement>) => { + setPacketRetrievalFrequency(event.target.value as number); + }} + label="Packet Retrieval Frequency" + variant="outlined" + size="small" + type="number" + InputProps={{ + endAdornment: <Typography>s</Typography>, + }} + /> + </Tooltip> </Stack> </DialogContent> <DialogActions> @@ -90,12 +86,12 @@ const ConnectionDialog: React.FC<IConnectionDialogProps> = (props: IConnectionDi <Button variant={"contained"} component="label" - disabled={!protocol || !frequency || !launchAltitude} + disabled={!protocol || !frequency || !packetRetrievalFrequency} onClick={ () => { socketContext.updateProtocol(protocol); socketContext.updateFrequency(frequency); - props.updateAltitude(launchAltitude); + socketContext.updatePacketStreamingInterval(packetRetrievalFrequency); socketContext.toggleConnection(); props.onClose() } diff --git a/client/src/components/rocket-monitoring/PublishMission.tsx b/client/src/components/rocket-monitoring/PublishMission.tsx new file mode 100644 index 000000000..93df346fb --- /dev/null +++ b/client/src/components/rocket-monitoring/PublishMission.tsx @@ -0,0 +1,41 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useSocketContext } from '../../utils/socket-context'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; + +interface IPublishMissionDialogProps { + isOpen: boolean; + onClose: () => void; +} + +const PublishMissionDialog: React.FC<IPublishMissionDialogProps> = (props: IPublishMissionDialogProps) => { + const { isOpen, onClose } = props; + return ( + <Dialog + open={isOpen} + onClose={() => onClose()} + > + <DialogTitle> + Publish Mission + </DialogTitle> + <DialogContent> + + </DialogContent> + <DialogActions> + <Button + variant='text' + onClick={() => onClose()} + > + Cancel + </Button> + <Button + variant='text' + onClick={() => {}} + > + Continue + </Button> + </DialogActions> + </Dialog> + ); +} + +export default PublishMissionDialog; \ No newline at end of file diff --git a/client/src/components/rocket-monitoring/TelemetryAltitudeGraph.tsx b/client/src/components/rocket-monitoring/TelemetryAltitudeGraph.tsx new file mode 100644 index 000000000..45f03909e --- /dev/null +++ b/client/src/components/rocket-monitoring/TelemetryAltitudeGraph.tsx @@ -0,0 +1,73 @@ +import { useTheme } from '@emotion/react'; +import { Paper, Typography } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import { useSocketContext } from '../../utils/socket-context'; + +const TelemetryAltitudeGraph: React.FC = () => { + const theme = useTheme(); + const socketContext = useSocketContext(); + interface IAltitudeData { + Id: number; + Altitude: number; + } + const [data, setData] = useState<IAltitudeData[]>([]); + useEffect(() => { + if ((socketContext.packet && socketContext.isConnected)) { + setData((prev) => [ + ...prev, + { + Id: socketContext.packet.id, + Altitude: socketContext.packet.data.altitude + } + ]); + } + }, [socketContext.packet]); + + return ( + <Paper + sx={{ + borderRadius: 2, + padding: 2, + zIndex: 4, + minWidth: 'fit-content', + height: '100%' + }} + > + <Typography variant='h6' fontWeight={600}> + Real Time Altitude + </Typography> + <ResponsiveContainer + width="100%" + height={208} + > + <LineChart + data={data} + > + <XAxis + dataKey="Id" + domain={['auto', 'auto']} + /> + <YAxis + orientation='right' + dataKey={"Altitude"} + domain={[0, 'auto']} + /> + <Tooltip /> + {/* <Legend/> */} + <Line + type="monotone" + dataKey="Altitude" + //@ts-ignore + stroke={theme.palette.uvr.red} + strokeWidth={3} + activeDot={{ r: 8 }} + isAnimationActive={false} + /> + </LineChart> + </ResponsiveContainer> + </Paper> + ); +}; + +export default TelemetryAltitudeGraph; diff --git a/client/src/components/rocket-monitoring/TelemetryMap.tsx b/client/src/components/rocket-monitoring/TelemetryMap.tsx new file mode 100644 index 000000000..bec3c71e8 --- /dev/null +++ b/client/src/components/rocket-monitoring/TelemetryMap.tsx @@ -0,0 +1,183 @@ +import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import { useSocketContext } from '../../utils/socket-context'; +import "leaflet.offline"; +import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'; +import { Button, Chip, Drawer, Fab, Paper, Stack, Typography, useTheme } from '@mui/material'; +import { useActiveMission } from '../../utils/ActiveMissionContext'; +import { LocationOn } from '@mui/icons-material'; + +interface ITelemetryMapProps { + width?: number; +} + +const TelemetryMap: React.FC<ITelemetryMapProps> = (props: ITelemetryMapProps) => { + const { width } = props; + + const theme = useTheme(); + const socketContext = useSocketContext(); + const activeMissionContext = useActiveMission(); + + const [offlineMap, setOfflineMap] = useState<boolean>(false); + const [mapState, setMapState] = useState(null); + const [mapDetails, setMapDetails] = useState<boolean>(false); + + // UVic Coordinates 48.461140, -123.310637 + const position = [ + activeMissionContext.activeMission.Coordinates.Latitude, + activeMissionContext.activeMission.Coordinates.Longitude, + ]; + + + const displayMap = useMemo(() => ( + <MapContainer + //@ts-ignore + center={position} + //@ts-ignore + ref={setMapState} + zoom={13} + scrollWheelZoom={true} + style={{ + height: 180, + width: '100%', + borderRadius: '5px', + zIndex: 4 + }} + > + + <TileLayer + attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' + url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" + /> + <Marker + //@ts-ignore + position={position} + > + <Popup> + Launch Position + </Popup> + </Marker> + </MapContainer> + ), []); + + // @ts-ignore + interface IMapProps { + map: any; + } + + const CurrentPosition = (props: IMapProps) => { + const { map } = props; + + const [currentMapPosition, setCurrentMapPosition] = useState(() => map.getCenter() as any); + + const onMove = useCallback(() => { + setCurrentMapPosition(map.getCenter()) + }, [map]); + + useEffect(() => { + map.on('move', onMove) + return () => { + map.off('move', onMove) + } + }, [map, onMove]); + + return ( + <Stack direction='row' spacing={1}> + <Chip + icon={<LocationOn />} + //@ts-ignore + label={`(${currentMapPosition.lat.toFixed(4)}, ${currentMapPosition.lng.toFixed(4)})`} + color='default' + sx={{ + fontWeight: 600, + borderRadius: 2 + }} + /> + </Stack> + ); + }; + + return ( + <Paper + sx={{ + borderRadius: 2, + padding: 2, + width: width || '100%', + minWidth: 'fit-content', + height: 'fit-content' + }} + > + <Stack direction={'column'} spacing={2}> + <Drawer + sx={{ + position: 'relative', + flexShrink: 0, + '& .MuiDrawer-paper': { + borderRadius: 2, + padding: 2, + boxShadow: 5, + height: 'fit-content', + width: '100%', + boxSizing: 'border-box', + position: 'absolute', + backgroundColor: theme.palette.background.paper + }, + backgroundColor: theme.palette.background.paper, + width: 400, + height: 'fit-content', + zIndex: 5, + // boxShadow: 5 + }} + elevation={3} + variant="persistent" + open={mapDetails} + onClose={() => setMapDetails(false)} + transitionDuration={{ + enter: theme.transitions.duration.leavingScreen, + exit: theme.transitions.duration.leavingScreen + }} + anchor='right' + > + <Typography + variant='subtitle1' + fontWeight={600} + > + Launch Coordinates + </Typography> + <Stack direction='row' spacing={1}> + <Chip + sx={{ + width: '100%', + borderRadius: 2, + fontWeight: 600 + }} + label={`Longitude: ${activeMissionContext.activeMission.Coordinates.Longitude}`} + color='default' + /> + <Chip + sx={{ + width: '100%', + borderRadius: 2, + fontWeight: 600 + }} + label={`Latitude: ${activeMissionContext.activeMission.Coordinates.Latitude}`} + color='default' + /> + </Stack> + </Drawer> + { displayMap } + <Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'}> + { mapState ? <CurrentPosition map={mapState} /> : null } + <Button + variant='contained' + onClick={() => setMapDetails(!mapDetails)} + > + Map Details + </Button> + </Stack> + </Stack> + + </Paper> + ); +} + +export default TelemetryMap; \ No newline at end of file diff --git a/client/src/components/rocket-monitoring/TelemetryPacket.tsx b/client/src/components/rocket-monitoring/TelemetryPacket.tsx index a598fcc2a..c0641d39a 100644 --- a/client/src/components/rocket-monitoring/TelemetryPacket.tsx +++ b/client/src/components/rocket-monitoring/TelemetryPacket.tsx @@ -1,27 +1,26 @@ import { Button, Chip, Icon, Paper, Stack, Typography } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { useSocketContext } from '../../utils/socket-context'; -import { Badge, Height, ShowChart, HorizontalRule, Timer } from '@mui/icons-material'; +import { Badge, Height, ShowChart, HorizontalRule, Timer, Numbers } from '@mui/icons-material'; const TelemetryPacket: React.FC = () => { const socketContext = useSocketContext(); - const [timeSincePacket, setTimeSincePacket] = useState<number>(0); + const [packetId, setPacketId] = useState<number>(0); + const [altitude, setAltitude] = useState<number>(0); useEffect(() => { if ((socketContext.packet && socketContext.isConnected)) { - const interval = setInterval(() => { - setTimeSincePacket(timeSincePacket + 1); - }, 1000); - return () => clearInterval(interval); + setPacketId(socketContext.packet.id); + setAltitude(socketContext.packet.data.altitude); } - }, [timeSincePacket, socketContext.packet]); + }, [packetId, socketContext.packet]); interface IPacketContent { label: string; - value: string; + value: string | number; icon: any; } @@ -33,37 +32,33 @@ const TelemetryPacket: React.FC = () => { }, { label: 'Altitude', - value: 'ft (ALG)', + value: `${altitude} ft`, icon: <ShowChart /> }, { label: 'Longitude', - value: '0.00 °', + value: 'NAN °', icon: <Height /> }, { label: 'Latitude', - value: '0.00 °', + value: 'NAN °', icon: <HorizontalRule /> } - ] - - useEffect(() => { - console.log(socketContext.packet); - }, [socketContext.packet]); + ]; return ( <Paper sx={{ borderRadius: 2, padding: 2, - width: 300, minWidth: 'fit-content', + height: 'fit-content' }} > <Stack spacing={2} direction={'column'}> <Typography variant='h6' fontWeight={600}> - Current Packet + Most Recent Packet </Typography> {content.map((item: IPacketContent, index: number) => ( <Stack direction={'row'} justifyContent={'space-between'} key={index}> @@ -85,14 +80,14 @@ const TelemetryPacket: React.FC = () => { </Stack> ))} <Chip - label={`Time Since: ${timeSincePacket}s`} - icon={<Timer />} + label={`Packet Id: ${packetId}`} + icon={<Numbers />} size='medium' sx={{ fontWeight: 600, borderRadius: 1 }} - color='default' + color='primary' /> </Stack> </Paper> diff --git a/client/src/components/rocket-monitoring/TelemetryStatus.tsx b/client/src/components/rocket-monitoring/TelemetryStatus.tsx index 169b7c7e2..389a24913 100644 --- a/client/src/components/rocket-monitoring/TelemetryStatus.tsx +++ b/client/src/components/rocket-monitoring/TelemetryStatus.tsx @@ -21,13 +21,10 @@ const TelemetryStatus: React.FC<ITelemetryStatusProps> = (props: ITelemetryStatu setFrequency(socketContext.frequency); }, [socketContext.frequency]); - const [ launchAltitude, setLaunchAltitude ] = useState<number>(0); - + const [packetRetrievalFrequency, setPacketRetrievalFrequency] = useState<number>(socketContext.packetStreamingInterval); useEffect(() => { - if (props.launchAltitude) { - setLaunchAltitude(props.launchAltitude); - } - }, [props.launchAltitude]); + setPacketRetrievalFrequency(socketContext.packetStreamingInterval); + }, [socketContext.packetStreamingInterval]); return ( <Paper @@ -54,7 +51,7 @@ const TelemetryStatus: React.FC<ITelemetryStatusProps> = (props: ITelemetryStatu </IconButton> </Tooltip> <Chip - label={`Launch Altitude: ${launchAltitude} FT`} + label={`Packet retrieval rate: ${packetRetrievalFrequency} s`} sx={{ fontWeight: 600, borderRadius: 1 diff --git a/client/src/utils/socket-context.tsx b/client/src/utils/socket-context.tsx index 5d98850ac..d6a04e313 100644 --- a/client/src/utils/socket-context.tsx +++ b/client/src/utils/socket-context.tsx @@ -9,9 +9,14 @@ import { } from 'react'; import useWebSocket from 'react-use-websocket'; -interface IPacket { - Data: {}; - Type: string; +export type Packet = { + data: { + altitude: number; + latitude?: number; + longitude?: number; + call_sign?: string; + }; + id: number; } enum Protocol { @@ -19,12 +24,20 @@ enum Protocol { LoRa = 'LoRa' } +type IncomingPacket = { + type: 'data' | 'no_data_available'; + packets: Packet[]; + last_id: number; +}; + export interface SocketContext { gpsLock: boolean; isConnected: boolean; + packetStreamingInterval: number; + updatePacketStreamingInterval: (interval: number) => void; toggleConnection: () => void; - logs: string[]; - packet: IPacket; + logs: Packet[]; + packet: Packet; frequency: number; updateFrequency: (frequency: number) => void; protocol: string; @@ -34,16 +47,18 @@ export interface SocketContext { export const Context = createContext<SocketContext>({ gpsLock: false, isConnected: false, + packetStreamingInterval: 2, + updatePacketStreamingInterval: (interval: number) => {}, toggleConnection: () => {}, logs: [], - packet: {} as IPacket, + packet: {} as Packet, frequency: 433.92, updateFrequency: (frequency: number) => {}, protocol: Protocol.APRS, updateProtocol: (protocol: Protocol) => {} }); -function packetReducer(state: IPacket, action: { type: string; payload: any }) { +function packetReducer(state: Packet, action: { type: string; payload: any }) { switch (action.type) { case 'SET_PACKET': return action.payload; @@ -54,40 +69,33 @@ function packetReducer(state: IPacket, action: { type: string; payload: any }) { } export const SocketGateway = ({ children }: PropsWithChildren<any>) => { - const [logs, setLogs] = useState<string[]>([]); - const [packet, packetDispatch] = useReducer(packetReducer, {} as IPacket); + const [logs, setLogs] = useState<Packet[]>([]); + const [packet, packetDispatch] = useReducer(packetReducer, {} as Packet); const [frequency, setFrequency] = useState<number>(433.92); const [protocol, setProtocol] = useState<string>('APRS'); const [isConnected, setIsConnect] = useState<boolean>(false); const [gpsLock, setGpsLock] = useState<boolean>(false); const [wsError, setWsError] = useState<any | null>(null); + const [packetStreamingInterval, setPacketStreamingInterval] = useState<number>(2); // Packets collected from the telemetry server sent every X seconds const port = import.meta.env.TELEMETRY_SERVER_PORT ? import.meta.env.TELEMETRY_SERVER_PORT : 9193; const [uri, setUri] = useState<string | null>(isConnected ? `ws://localhost:${9193}` : null); - useEffect(() => { - if (frequency) { - console.log('Frequency:', frequency); - } - - if (protocol) { - console.log('Protocol:', protocol); - } - }, [frequency, protocol]); const updateFrequency = (frequency: number) => { - console.log('Frequency:', frequency); setFrequency(frequency); }; const updateProtocol = (protocol: Protocol) => { - console.log('Protocol:', protocol); setProtocol(protocol); }; + const updatePacketStreamingInterval = (interval: number) => { + setPacketStreamingInterval(interval); + } + const { sendJsonMessage, - lastMessage, lastJsonMessage } = useWebSocket(uri, { shouldReconnect: (_) => isConnected, @@ -102,6 +110,7 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { onOpen: () => { console.log('Telemetry socket Opened'); setIsConnect(true); + sendJsonMessage({ type: 'establish_stream', frequency: packetStreamingInterval }); }, share: true }); @@ -119,17 +128,18 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { }); }; - useEffect(() => { - console.log('Last Message:', lastMessage); - if (lastMessage) { - setLogs([...logs, lastMessage]); + + const handleIncomingPacket = (data: IncomingPacket) => { + if (data.type === 'data') { + const packet = data.packets.find((packet) => packet.id === data.last_id); + packetDispatch({ type: 'SET_PACKET', payload: packet }); + setLogs((prevLogs) => [...prevLogs, ...data.packets]); } - }, [lastMessage]); + } useEffect(() => { - console.log('Last JSON Message:', lastJsonMessage); if (lastJsonMessage) { - packetDispatch({ type: 'SET_PACKET', payload: lastJsonMessage }); + handleIncomingPacket(lastJsonMessage as IncomingPacket); } }, [lastJsonMessage]); @@ -138,6 +148,8 @@ export const SocketGateway = ({ children }: PropsWithChildren<any>) => { value={{ isConnected, gpsLock, + packetStreamingInterval, + updatePacketStreamingInterval, toggleConnection, logs, packet, diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx index 4ed86ec85..f044bbac7 100644 --- a/client/src/views/active/rocket-monitoring-view.tsx +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -2,16 +2,19 @@ import React, { useCallback, useEffect, useState } from 'react'; import api from '../../services/api'; import { IRocketPopulated } from '../../utils/entities'; -import { Alert, Box, Button, ButtonGroup, Skeleton, Stack, Tooltip } from '@mui/material'; +import { Alert, AlertTitle, Box, Button, ButtonGroup, IconButton, Link, Skeleton, Snackbar, SnackbarContent, Stack, Tooltip, Typography } from '@mui/material'; import Header, { Breadcrumb } from '../../components/Header'; import { ViewKeys } from '../../utils/viewProviderContext'; import { useActiveMission } from '../../utils/ActiveMissionContext'; -import { Settings, WifiTethering } from '@mui/icons-material'; +import { Chat, Settings, WifiTethering, Save, Close } from '@mui/icons-material'; import TelemetryStatus from '../../components/rocket-monitoring/TelemetryStatus'; import ConnectionDialog from '../../components/rocket-monitoring/ConnectionDialog'; import { useSocketContext } from '../../utils/socket-context'; import TelemetryPacket from '../../components/rocket-monitoring/TelemetryPacket'; import TelemetryLog from '../../components/logging/TelemetryLog'; +import TelemetryAltitudeGraph from '../../components/rocket-monitoring/TelemetryAltitudeGraph'; +import TelemetryMap from '../../components/rocket-monitoring/TelemetryMap'; +import { useTheme } from '@emotion/react'; interface IRocketMonitoringViewProps { // rocketId: string; @@ -32,6 +35,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock }, []); const activeContext = useActiveMission(); + const theme = useTheme(); const breadCrumbs: Breadcrumb[] = [ { name: "Ground Support", viewKey: ViewKeys.PLATFORM_SELECTION_KEY, active: false }, @@ -43,6 +47,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock const socketContext = useSocketContext(); const [launchAltitude, setLaunchAltitude] = useState<number>(0); const [loading, setLoading] = useState<boolean>(true); + const [publishMission, setPublishMission] = useState<boolean>(false); useEffect(() => { if (socketContext.isConnected) { @@ -58,7 +63,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock return ( - <Box sx={{ width: '100vw', height: '100vh' }}> + <Box sx={{ width: '100vw', height: '100vh', overflowX: 'none' }}> <Stack height={'100%'} padding={4} @@ -69,49 +74,87 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock <Stack direction="row" alignItems={'center'} justifyContent={'space-between'}> <Header icon="ROCKET_MONITORING" breadCrumbs={breadCrumbs} /> <Stack direction="row" spacing={1}> - <Tooltip title="Configuration" placement="top"> - <Button variant={'text'} onClick={() => {}}> - <Settings /> - </Button> - </Tooltip> - <ButtonGroup - variant="contained" - > - <Button - aria-readonly - disabled - color={"success"} + <Stack direction="column" spacing={1}> + <ButtonGroup + variant="contained" > - Server Connected - </Button> - <Button - variant="contained" - size={'large'} - startIcon={<WifiTethering/>} - sx={{ width: 180 }} - onClick={() => { - setOpenConnection(!openConnection); - }} + <Button + startIcon={<Save/>} + disabled={!socketContext.isConnected} + onClick={() => setPublishMission(true)} + > + Publish Mission + </Button> + <Button + variant="contained" + size={'large'} + startIcon={<WifiTethering/>} + onClick={() => { + if (socketContext.isConnected) { + socketContext.toggleConnection(); + } else { + setOpenConnection(!openConnection); + } + }} + > + {socketContext.isConnected ? "Disconnect" : "Connect"} + </Button> + </ButtonGroup> + <Snackbar + open={publishMission} + anchorOrigin={{ vertical: 'top', horizontal: 'right' }} > - {socketContext.isConnected ? "Disconnect" : "Connect"} - </Button> - </ButtonGroup> + <Alert + severity="warning" + onClose={() => setPublishMission(false)} + > + <AlertTitle>Are you sure?</AlertTitle> + <Stack alignItems={'end'} gap={1}> + Once a mission is published no more data can be recorded and the mission be available in replay mode only. + <Button + color="inherit" + variant='contained' + size="small" + sx={{ + width: 'fit-content', + }} + onClick={() => { + activeContext.activeMission.Published = true; + // TODO: update mission with published status & data + setPublishMission(false) + }} + > + Continue + </Button> + </Stack> + </Alert> + </Snackbar> + </Stack> </Stack> </Stack> <TelemetryStatus launchAltitude={launchAltitude} /> {socketContext.isConnected ? loading ? ( - <Stack direction="row" gap={2}> - <Skeleton variant="rectangular" height={300} width={'30%'} sx={{ borderRadius: 2 }} /> - <Skeleton variant="rectangular" height={300} width={'70%'} sx={{ borderRadius: 2 }} /> + <Stack gap={2}> + <Stack direction="row" gap={2}> + <Skeleton variant="rectangular" height={300} width={'20%'} sx={{ borderRadius: 2 }} /> + <Skeleton variant="rectangular" height={300} width={'40%'} sx={{ borderRadius: 2 }} /> + <Skeleton variant="rectangular" height={300} width={'40%'} sx={{ borderRadius: 2 }} /> + </Stack> + <Skeleton variant="rectangular" height={300} width={'100%'} sx={{ borderRadius: 2 }} /> </Stack> + ) :( - <Stack direction="row" gap={2}> - <TelemetryPacket /> - <TelemetryLog /> - </Stack> + <Stack direction="column" gap={2} width={'100%'}> + <Stack direction="row" gap={2}> + <TelemetryPacket /> + <TelemetryLog /> + <TelemetryMap /> + </Stack> + <TelemetryAltitudeGraph /> + </Stack> ) : ( <Alert severity="info"> - Connect to telemetry to see data + Connect to telemetry to see data. <Link href='#' color={'inherit'}>FAQ</Link> for common issues and solutions. </Alert> )} </Stack> diff --git a/services/telemetry/tools/decode_test.sh b/services/telemetry/tools/decode_test.sh index 753783a52..817810ba6 100755 --- a/services/telemetry/tools/decode_test.sh +++ b/services/telemetry/tools/decode_test.sh @@ -1,2 +1,2 @@ # -u = no buffering on stdout. -python3 -u ./tools/test_data.py \ No newline at end of file +python -u ./tools/test_data.py \ No newline at end of file From d995a3128091fd2f7a8848eb7f3cc7086bc6721c Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Sat, 15 Jun 2024 22:05:33 -0700 Subject: [PATCH 10/14] Mission Data Saving + mission Publishing logic + export data on publish mission click --- .../src/components/logging/TelemetryLog.tsx | 2 +- client/src/utils/entities.ts | 11 ++++ .../views/active/rocket-monitoring-view.tsx | 63 ++++++++++++++++--- client/src/views/tabs/rocket-missions-tab.tsx | 49 ++++++++++----- services/server/src/models/MissionModel.ts | 9 ++- .../server/src/models/TelemetryPacketModel.ts | 27 ++++++++ services/server/src/routes/MissionRoute.ts | 3 +- 7 files changed, 133 insertions(+), 31 deletions(-) create mode 100644 services/server/src/models/TelemetryPacketModel.ts diff --git a/client/src/components/logging/TelemetryLog.tsx b/client/src/components/logging/TelemetryLog.tsx index 00593eb71..b80017684 100644 --- a/client/src/components/logging/TelemetryLog.tsx +++ b/client/src/components/logging/TelemetryLog.tsx @@ -69,7 +69,7 @@ const TelemetryLog: React.FC = () => { width: '99%', }} minRows={50} - maxRows={13} + maxRows={12} readOnly placeholder="Telemetry Log" value={logs.join('\n')} diff --git a/client/src/utils/entities.ts b/client/src/utils/entities.ts index b0671d3e1..5816535a5 100644 --- a/client/src/utils/entities.ts +++ b/client/src/utils/entities.ts @@ -107,3 +107,14 @@ export interface IDataPoint { FieldId: string; PacketNumber: number; }; + +export interface ITelemetryPacket { + _id?: string; + Data: { + Altitude: number; + Latitude?: number; + Longitude?: number; + CallSign?: string; + }; + PacketId: number; +} diff --git a/client/src/views/active/rocket-monitoring-view.tsx b/client/src/views/active/rocket-monitoring-view.tsx index f044bbac7..c9239ef99 100644 --- a/client/src/views/active/rocket-monitoring-view.tsx +++ b/client/src/views/active/rocket-monitoring-view.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useEffect, useState } from 'react'; import api from '../../services/api'; -import { IRocketPopulated } from '../../utils/entities'; +import { IRocketPopulated, ITelemetryPacket } from '../../utils/entities'; import { Alert, AlertTitle, Box, Button, ButtonGroup, IconButton, Link, Skeleton, Snackbar, SnackbarContent, Stack, Tooltip, Typography } from '@mui/material'; import Header, { Breadcrumb } from '../../components/Header'; -import { ViewKeys } from '../../utils/viewProviderContext'; +import { ViewKeys, useViewProvider } from '../../utils/viewProviderContext'; import { useActiveMission } from '../../utils/ActiveMissionContext'; import { Chat, Settings, WifiTethering, Save, Close } from '@mui/icons-material'; import TelemetryStatus from '../../components/rocket-monitoring/TelemetryStatus'; @@ -15,13 +15,13 @@ import TelemetryLog from '../../components/logging/TelemetryLog'; import TelemetryAltitudeGraph from '../../components/rocket-monitoring/TelemetryAltitudeGraph'; import TelemetryMap from '../../components/rocket-monitoring/TelemetryMap'; import { useTheme } from '@emotion/react'; +// import upsertMissionData from '../../services/api'; interface IRocketMonitoringViewProps { // rocketId: string; } const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRocketMonitoringViewProps) => { - const [rocket, setRocket] = useState<IRocketPopulated>({} as IRocketPopulated); // const getActiveRocket = useCallback(async (): Promise<IRocketPopulated> => { // const response = await api.getRocket(rocketId || ''); @@ -35,6 +35,8 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock }, []); const activeContext = useActiveMission(); + const socketContext = useSocketContext(); + const viewProviderContext = useViewProvider(); const theme = useTheme(); const breadCrumbs: Breadcrumb[] = [ @@ -44,10 +46,11 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock ]; const [openConnection, setOpenConnection] = useState<boolean>(false); - const socketContext = useSocketContext(); const [launchAltitude, setLaunchAltitude] = useState<number>(0); const [loading, setLoading] = useState<boolean>(true); const [publishMission, setPublishMission] = useState<boolean>(false); + const [publishFeedback, setPublishFeedback] = useState<boolean>(false); + const [publishErrorMessage, setPublishErrorMessage] = useState<string>(''); useEffect(() => { if (socketContext.isConnected) { @@ -61,6 +64,29 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock } }, [socketContext.isConnected]); + const publishMissionData = async () => { + if (!activeContext.activeMission._id) { + setPublishErrorMessage("No mission id attached to active mission."); + setPublishFeedback(false); + return + } + const missionData = socketContext.logs.map((packet) => ( + { + PacketId: packet.id, + Data: { + Altitude: packet.data.altitude, + Latitude: packet.data.latitude, + Longitude: packet.data.longitude, + CallSign: packet.data.call_sign + } + } as ITelemetryPacket + )); + console.log(missionData); + const response = await api.upsertMissionData(activeContext.activeMission._id, missionData); + response.error.error ? setPublishErrorMessage(`Api error: ${response.error.status}`) : setPublishFeedback(true); + + }; + return ( <Box sx={{ width: '100vw', height: '100vh', overflowX: 'none' }}> @@ -119,12 +145,18 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock width: 'fit-content', }} onClick={() => { - activeContext.activeMission.Published = true; - // TODO: update mission with published status & data - setPublishMission(false) + publishMissionData(); + setPublishMission(false); + // wait a few seconds before changing view + + // if (publishFeedback) { + setTimeout(() => { + viewProviderContext.updateViewKey(ViewKeys.ROCKET_DETAILS_KEY); + }, 2000); + // } }} > - Continue + Publish </Button> </Stack> </Alert> @@ -143,7 +175,7 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock <Skeleton variant="rectangular" height={300} width={'100%'} sx={{ borderRadius: 2 }} /> </Stack> - ) :( + ) : ( <Stack direction="column" gap={2} width={'100%'}> <Stack direction="row" gap={2}> <TelemetryPacket /> @@ -154,10 +186,21 @@ const RocketMonitoringView: React.FC<IRocketMonitoringViewProps> = (props: IRock </Stack> ) : ( <Alert severity="info"> - Connect to telemetry to see data. <Link href='#' color={'inherit'}>FAQ</Link> for common issues and solutions. + Connect to telemetry to see data. See the <Link href='#' color={'inherit'}>FAQ</Link> for common issues and solutions. </Alert> )} </Stack> + <Snackbar + open={publishFeedback} + autoHideDuration={6000} + onClose={() => setPublishFeedback(false)} + > + <Alert + severity={publishErrorMessage ? 'error' : 'success'} + > + { publishErrorMessage ? publishErrorMessage : "Mission data published successfully." } + </Alert> + </Snackbar> <ConnectionDialog updateAltitude={setLaunchAltitude} isOpen={openConnection} diff --git a/client/src/views/tabs/rocket-missions-tab.tsx b/client/src/views/tabs/rocket-missions-tab.tsx index 133a620c8..35f85acb1 100644 --- a/client/src/views/tabs/rocket-missions-tab.tsx +++ b/client/src/views/tabs/rocket-missions-tab.tsx @@ -6,17 +6,19 @@ import { ViewKeys, useViewProvider } from '../../utils/viewProviderContext'; import { Alert, Chip, Icon, IconButton, Snackbar, Stack, Typography } from '@mui/material'; import { CheckBox, CheckBoxOutlineBlank, ShowChart, Height, HorizontalRule, Edit, DateRange } from '@mui/icons-material'; import MissionConfig from '../../components/MissionConfig'; +import { saveAs } from 'file-saver'; interface FormattedMissionData { _id?: string; Name: string; Date: Date; - IsTest: string; + IsTest: boolean; Longitude: number; Latitude: number; LaunchAltitude: Number; - Published: string; + Published: boolean; Components: string[]; + Data?: any; } interface ITableColumns { @@ -40,18 +42,29 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { const viewProviderContext = useViewProvider(); const activeMissionContext = useActiveMission(); + const exportMissionData = (mission: FormattedMissionData) => { + if (!mission.Data) { + return; + } + if (mission.Published) { + const blob = new Blob([JSON.stringify(mission.Data)], { type: 'application/json' }); + saveAs(blob, `${mission.Name}-data.json`); + } + } + const getMissions = useCallback(async () => { rocket.Missions.map(async (mission: IMission) => { const m: FormattedMissionData = { _id: mission._id, Name: mission.Name, Date: mission.Date, - IsTest: String(mission.IsTest), + IsTest: mission.IsTest, Longitude: mission.Coordinates.Longitude, Latitude: mission.Coordinates.Latitude, LaunchAltitude: mission.LaunchAltitude, - Published: String(mission.Published), - Components: mission.Components + Published: mission.Published, + Components: mission.Components, + Data: mission.Data, }; if (missions.length === 0) { missions.push(m); @@ -69,15 +82,19 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { const options: MUIDataTableOptions = { filter: true, responsive: 'standard', - onCellClick: (colData, cellMeta) => { + onCellClick: (_, cellMeta) => { if (cellMeta.colIndex === 0) { return; } - console.log(colData, cellMeta); - console.log('Navigating to mission replay view'); activeMissionContext.updateMission(rocket.Missions[cellMeta.dataIndex]); activeMissionContext.updateRocket(rocket); - viewProviderContext.updateViewKey(ViewKeys.ACTIVE_FLIGHT_KEY) + if (rocket.Missions[cellMeta.dataIndex].Published) { + exportMissionData(missions[cellMeta.dataIndex]); + // TODO: uncomment when view is completed + // viewProviderContext.updateViewKey(ViewKeys.MISSION_REPLAY_KEY); + } else { + viewProviderContext.updateViewKey(ViewKeys.ACTIVE_FLIGHT_KEY) + } } }; @@ -115,7 +132,7 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { filter: true, sort: true, viewColumns: true, - customBodyRender(value, tableMeta, updateValue) { + customBodyRender(value) { return ( <Typography variant="body1" @@ -136,7 +153,7 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { filter: true, sort: true, viewColumns: true, - customBodyRender(value, tableMeta, updateValue) { + customBodyRender(value) { return ( <Chip icon={<DateRange />} @@ -157,7 +174,7 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { filter: true, sort: true, viewColumns: true, - customBodyRender(value, tableMeta, updateValue) { + customBodyRender(value) { return ( <Chip label={value === "true" ? "Test" : "Mission"} @@ -177,7 +194,7 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { filter: true, sort: true, viewColumns: true, - customBodyRender(value, tableMeta, updateValue) { + customBodyRender(value) { return ( <Stack direction={'row'}> <Height /> @@ -194,7 +211,7 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { filter: true, sort: true, viewColumns: true, - customBodyRender(value, tableMeta, updateValue) { + customBodyRender(value) { return ( <Stack direction={'row'} spacing={1}> <HorizontalRule /> @@ -211,7 +228,7 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { filter: true, sort: true, viewColumns: true, - customBodyRender(value, tableMeta, updateValue) { + customBodyRender(value) { return ( <Stack direction={'row'} spacing={1}> <ShowChart /> @@ -228,7 +245,7 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { filter: true, sort: true, viewColumns: true, - customBodyRender(value, tableMeta, updateValue) { + customBodyRender(value) { return value ? (<CheckBox color='success' />) : (<CheckBoxOutlineBlank color='grey' />); } } diff --git a/services/server/src/models/MissionModel.ts b/services/server/src/models/MissionModel.ts index ebda1e025..55b52123b 100644 --- a/services/server/src/models/MissionModel.ts +++ b/services/server/src/models/MissionModel.ts @@ -1,6 +1,6 @@ import mongoose, { Document, Schema, Types } from "mongoose"; import { isValidLongitude, isValidLatitude } from "../library/CoordinateValidation"; -import { DataPointSchema, IDataPoint } from "./DataPointModel"; +import { ITelemetryPacket, TelemetryPacketSchema } from "./TelemetryPacketModel"; interface ICoordinates { Latitude: number; @@ -16,7 +16,7 @@ export interface IMission { LaunchAltitude: number; Components: [Types.ObjectId]; Published: boolean; - Data?: IDataPoint[]; + Data?: ITelemetryPacket[]; }; export interface IMissionModel extends IMission, Document { }; @@ -73,7 +73,10 @@ export const MissionSchema: Schema = new Schema( default: false, required: true }, - Data: [DataPointSchema] + Data: { + type: [TelemetryPacketSchema], + required: false + } }, { versionKey: false, diff --git a/services/server/src/models/TelemetryPacketModel.ts b/services/server/src/models/TelemetryPacketModel.ts new file mode 100644 index 000000000..fb5180509 --- /dev/null +++ b/services/server/src/models/TelemetryPacketModel.ts @@ -0,0 +1,27 @@ +import mongoose, { Document, Schema, Types } from "mongoose"; + +export interface ITelemetryPacket { + Data: { + Altitude: number; + Latitude?: number; + Longitude?: number; + CallSign?: string; + }; + Id: number; +}; + +export const TelemetryPacketSchema: Schema = new Schema( + { + Data: { + Altitude: Number, + Latitude: Number, + Longitude: Number, + CallSign: String + }, + PacketId: Number + }, + { + versionKey: false, + timestamps: false + } +); \ No newline at end of file diff --git a/services/server/src/routes/MissionRoute.ts b/services/server/src/routes/MissionRoute.ts index bf499b1a2..6f1f83d46 100644 --- a/services/server/src/routes/MissionRoute.ts +++ b/services/server/src/routes/MissionRoute.ts @@ -28,7 +28,8 @@ router.put('/data/:id', async (req: Request, res: Response, next: NextFunction) { $push: { Data: req.body - } + }, + Published: true }, { new: true, From 6d53e47a24bf3fdce17babe39e53f215237be6a1 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Sat, 15 Jun 2024 22:36:06 -0700 Subject: [PATCH 11/14] Mission Data export dialog + json/txt supported --- .../ExportMissionDataDialog.tsx | 80 +++++++++++++++++++ client/src/views/tabs/rocket-missions-tab.tsx | 23 +++--- 2 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 client/src/components/rocket-monitoring/ExportMissionDataDialog.tsx diff --git a/client/src/components/rocket-monitoring/ExportMissionDataDialog.tsx b/client/src/components/rocket-monitoring/ExportMissionDataDialog.tsx new file mode 100644 index 000000000..f3284b72b --- /dev/null +++ b/client/src/components/rocket-monitoring/ExportMissionDataDialog.tsx @@ -0,0 +1,80 @@ +import { Download } from '@mui/icons-material'; +import { Autocomplete, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Stack, TextField, Tooltip } from '@mui/material'; +import { saveAs } from 'file-saver'; +import React, { useState } from 'react'; + +interface IExportMissionDataDialogProps { + isOpen: boolean; + onClose: () => void; + mission: any; +} + +const ExportMissionDataDialog: React.FC<IExportMissionDataDialogProps> = (props: IExportMissionDataDialogProps) => { + const { isOpen, onClose, mission } = props; + const [extensionType, setExtensionType] = useState<any>(); + + const exportData = () => { + const blob = new Blob([JSON.stringify(mission.Data)], { type: extensionType.value }); + saveAs(blob, `${mission.Name}-data.${extensionType.label}`); + console.log('Exporting mission data'); + } + + return ( + <Dialog + open={isOpen} + onClose={() => onClose()} + > + <DialogTitle + sx={{fontWeight: 600 }} + > + Export Mission + </DialogTitle> + <DialogContent> + <Stack direction='row' spacing={2} p={1}> + <Autocomplete + options={[ + { + label: 'json', + value: 'application/json' + }, + { + label: 'txt', + value: 'text/plain;charset=utf-8' + } + ]} + fullWidth + value={extensionType} + onChange={(_, value) => setExtensionType(value)} + getOptionLabel={(option) => option.label} + renderInput={(params) => <TextField {...params} label='Extension Type' />} + /> + <IconButton + onClick={exportData} + disabled={!extensionType} + > + <Download /> + </IconButton> + </Stack> + + </DialogContent> + <DialogActions> + <Button + variant='text' + onClick={() => onClose()} + > + Cancel + </Button> + + <Button + variant='text' + onClick={() => {}} + disabled + > + Go to Mission Replay + </Button> + </DialogActions> + </Dialog> + ); +} + +export default ExportMissionDataDialog; \ No newline at end of file diff --git a/client/src/views/tabs/rocket-missions-tab.tsx b/client/src/views/tabs/rocket-missions-tab.tsx index 35f85acb1..4f5bc9cdc 100644 --- a/client/src/views/tabs/rocket-missions-tab.tsx +++ b/client/src/views/tabs/rocket-missions-tab.tsx @@ -7,6 +7,7 @@ import { Alert, Chip, Icon, IconButton, Snackbar, Stack, Typography } from '@mui import { CheckBox, CheckBoxOutlineBlank, ShowChart, Height, HorizontalRule, Edit, DateRange } from '@mui/icons-material'; import MissionConfig from '../../components/MissionConfig'; import { saveAs } from 'file-saver'; +import ExportMissionDataDialog from '../../components/rocket-monitoring/ExportMissionDataDialog'; interface FormattedMissionData { _id?: string; @@ -38,17 +39,14 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { const [ selectedMissionId, setSelectedMissionId ] = useState<string | null>(null); const [missionEditDialog, setMissionEditDialog] = useState<boolean>(false); const [tableWarning, setTableWarning] = useState<boolean>(true); - + const [exportMissionDataDialog, setExportMissionDataDialog] = useState<boolean>(false); + const [selectedMission, setSelectedMission] = useState<FormattedMissionData | null>(null) const viewProviderContext = useViewProvider(); const activeMissionContext = useActiveMission(); const exportMissionData = (mission: FormattedMissionData) => { - if (!mission.Data) { - return; - } - if (mission.Published) { - const blob = new Blob([JSON.stringify(mission.Data)], { type: 'application/json' }); - saveAs(blob, `${mission.Name}-data.json`); + if (mission.Data && mission.Published) { + setExportMissionDataDialog(true); } } @@ -86,6 +84,7 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { if (cellMeta.colIndex === 0) { return; } + setSelectedMission(missions[cellMeta.dataIndex]) activeMissionContext.updateMission(rocket.Missions[cellMeta.dataIndex]); activeMissionContext.updateRocket(rocket); if (rocket.Missions[cellMeta.dataIndex].Published) { @@ -175,10 +174,11 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { sort: true, viewColumns: true, customBodyRender(value) { + console.log(value); return ( <Chip - label={value === "true" ? "Test" : "Mission"} - color={value === "true" ? "warning" : "primary"} + label={value ? "Test" : "Mission"} + color={value ? "warning" : "primary"} sx={{ borderRadius: 2, }} @@ -276,6 +276,11 @@ const RocketDetailsTab: React.FC<Props> = (props: Props) => { onClose={() => setMissionEditDialog(false)} onSave={() => {}} /> + <ExportMissionDataDialog + isOpen={exportMissionDataDialog} + onClose={() => setExportMissionDataDialog(false)} + mission={selectedMission as FormattedMissionData} + /> </> ); }; From 3a0e1ecb7d2ab2d0e96f72054954cf0751aafcc5 Mon Sep 17 00:00:00 2001 From: Mateo Carreras <mateo.carreras@gmail.com> Date: Thu, 20 Jun 2024 19:44:27 -0700 Subject: [PATCH 12/14] Add new command line option to telemetry service to use binaries in path instead of the vendored Also fix a bug where the site crashes when empty packets are received --- .../TelemetryAltitudeGraph.tsx | 26 +++++++++---------- .../rocket-monitoring/TelemetryPacket.tsx | 4 +-- services/telemetry/src/main.rs | 4 +++ services/telemetry/src/state.rs | 5 +++- services/telemetry/src/telemetry.rs | 10 +++++-- services/telemetry/tools/decode_test.sh | 2 +- .../telemetry/tools/decode_with_sys_bin.sh | 14 ++++++++++ 7 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 services/telemetry/tools/decode_with_sys_bin.sh diff --git a/client/src/components/rocket-monitoring/TelemetryAltitudeGraph.tsx b/client/src/components/rocket-monitoring/TelemetryAltitudeGraph.tsx index 45f03909e..3673601e8 100644 --- a/client/src/components/rocket-monitoring/TelemetryAltitudeGraph.tsx +++ b/client/src/components/rocket-monitoring/TelemetryAltitudeGraph.tsx @@ -13,16 +13,16 @@ const TelemetryAltitudeGraph: React.FC = () => { } const [data, setData] = useState<IAltitudeData[]>([]); useEffect(() => { - if ((socketContext.packet && socketContext.isConnected)) { + if ((socketContext.packet && socketContext.isConnected && socketContext.packet?.data)) { setData((prev) => [ - ...prev, + ...prev, { Id: socketContext.packet.id, Altitude: socketContext.packet.data.altitude } ]); } - }, [socketContext.packet]); + }, [socketContext.packet]); return ( <Paper @@ -37,20 +37,20 @@ const TelemetryAltitudeGraph: React.FC = () => { <Typography variant='h6' fontWeight={600}> Real Time Altitude </Typography> - <ResponsiveContainer - width="100%" - height={208} + <ResponsiveContainer + width="100%" + height={208} > - <LineChart + <LineChart data={data} > - <XAxis - dataKey="Id" - domain={['auto', 'auto']} + <XAxis + dataKey="Id" + domain={['auto', 'auto']} /> - <YAxis - orientation='right' - dataKey={"Altitude"} + <YAxis + orientation='right' + dataKey={"Altitude"} domain={[0, 'auto']} /> <Tooltip /> diff --git a/client/src/components/rocket-monitoring/TelemetryPacket.tsx b/client/src/components/rocket-monitoring/TelemetryPacket.tsx index c0641d39a..d037253d3 100644 --- a/client/src/components/rocket-monitoring/TelemetryPacket.tsx +++ b/client/src/components/rocket-monitoring/TelemetryPacket.tsx @@ -12,7 +12,7 @@ const TelemetryPacket: React.FC = () => { const [altitude, setAltitude] = useState<number>(0); useEffect(() => { - if ((socketContext.packet && socketContext.isConnected)) { + if ((socketContext.packet && socketContext.isConnected && socketContext.packet?.data)) { setPacketId(socketContext.packet.id); setAltitude(socketContext.packet.data.altitude); } @@ -94,4 +94,4 @@ const TelemetryPacket: React.FC = () => { ); } -export default TelemetryPacket; \ No newline at end of file +export default TelemetryPacket; diff --git a/services/telemetry/src/main.rs b/services/telemetry/src/main.rs index b17aae1a8..d1b08555e 100644 --- a/services/telemetry/src/main.rs +++ b/services/telemetry/src/main.rs @@ -43,6 +43,10 @@ struct Cli { /// server. This is currently meant for testing purposes. #[clap(long)] client: bool, + /// When this option is set, the binaries installed on the system will be + /// used instead of the ones in the repo. + #[clap(long)] + system_binaries: bool } /// A transmission protocol that is used to transmit data from the rocket. diff --git a/services/telemetry/src/state.rs b/services/telemetry/src/state.rs index 50a0d5b73..cb2e9dc94 100644 --- a/services/telemetry/src/state.rs +++ b/services/telemetry/src/state.rs @@ -19,6 +19,8 @@ pub struct State { /// buffer. This offset is used to keep track of the first packet in the /// packet buffer. pub packet_buffer_offset: u64, + /// When true, system-wide binaries are used instead of the ones in the repo. + pub system_binaries: bool } impl State { @@ -31,6 +33,7 @@ impl State { last_packet_id: None, packet_buffer: Vec::new(), packet_buffer_offset: 0, + system_binaries: false } } @@ -86,4 +89,4 @@ impl State { Some((self_last_id, packets)) } -} \ No newline at end of file +} diff --git a/services/telemetry/src/telemetry.rs b/services/telemetry/src/telemetry.rs index fbf34b793..8a95d4b8e 100644 --- a/services/telemetry/src/telemetry.rs +++ b/services/telemetry/src/telemetry.rs @@ -58,13 +58,19 @@ pub async fn start_telemetry(state_ref: Arc<Mutex<State>>) -> io::Result<()> { let frequency = state_ref.lock().unwrap().frequency; println!("Starting telemetry on {}", frequency); + let decode_script_path = if state_ref.lock().unwrap().system_binaries { + "./tools/decode.sh" + } else { + "./tools/decode_with_sys_bin.sh" + }; + let mut decode_aprs = Command::new("sh") - .arg("./tools/decode_test.sh") + .arg(decode_script_path) // .arg(frequency.to_string()) .stdout(Stdio::piped()) .spawn() .expect("failed to start decoder"); - + let mut child_out = BufReader::new(decode_aprs.stdout.as_mut().unwrap()); let mut readbuf = vec![0; 256]; diff --git a/services/telemetry/tools/decode_test.sh b/services/telemetry/tools/decode_test.sh index 817810ba6..bc5366dcd 100755 --- a/services/telemetry/tools/decode_test.sh +++ b/services/telemetry/tools/decode_test.sh @@ -1,2 +1,2 @@ # -u = no buffering on stdout. -python -u ./tools/test_data.py \ No newline at end of file +python3 -u ./tools/test_data.py diff --git a/services/telemetry/tools/decode_with_sys_bin.sh b/services/telemetry/tools/decode_with_sys_bin.sh new file mode 100644 index 000000000..713cb33db --- /dev/null +++ b/services/telemetry/tools/decode_with_sys_bin.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +# This script is used to decode APRS packets from the RTL-SDR dongle +# +# This version is for using system-wide binaries instead of in the repo. + +if [ $# -ge 1 ] +then + FREQ=$1 +else + FREQ=433.92M +fi + +rtl_fm -f $FREQ -r 24k -s 260k -o 4 -p 93 -g 49.6 - | direwolf -c ./tools/direwolf.conf -n 1 -r 24000 -b 16 - From 9f01141698142f81eecb7e421a3700a6086ce9c8 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Wed, 17 Jul 2024 08:49:47 -0700 Subject: [PATCH 13/14] first 3 comments of PR --- client/src/components/MissionConfig.tsx | 2 +- client/src/views/rocket-details-view.tsx | 33 +++++------------------- documentation/installation.md | 18 ++++++------- 3 files changed, 16 insertions(+), 37 deletions(-) diff --git a/client/src/components/MissionConfig.tsx b/client/src/components/MissionConfig.tsx index c7d2c2636..d328af8b6 100644 --- a/client/src/components/MissionConfig.tsx +++ b/client/src/components/MissionConfig.tsx @@ -119,7 +119,7 @@ const MissionConfig: React.FC<MissionConfigProps> = (props: MissionConfigProps) Mission Configuration </DialogTitle> <DialogContent> - <Stack direction="column" spacing={3} alignItems="left"> + <Stack direction="column" spacing={3} alignItems="left" paddingY={1}> <Stack direction="row" spacing={2} alignItems="center"> <TextField InputLabelProps={{ diff --git a/client/src/views/rocket-details-view.tsx b/client/src/views/rocket-details-view.tsx index bc883e1fe..f3232755b 100644 --- a/client/src/views/rocket-details-view.tsx +++ b/client/src/views/rocket-details-view.tsx @@ -63,11 +63,7 @@ export default function RocketDetailsView() { 'rgba(69, 136, 201, 1)' ]; - const breadCrumbs: Breadcrumb[] = [ - { name: 'Ground Support', viewKey: ViewKeys.PLATFORM_SELECTION_KEY, active: false }, - { name: 'Rocket Selection', viewKey: ViewKeys.ROCKET_SELECT_KEY, active: false }, - { name: 'Rocket Details', viewKey: ViewKeys.ROCKET_DETAILS_KEY, active: true } - ]; + //value is for tab things const [value, setValue] = useState<number>(0); @@ -124,6 +120,12 @@ export default function RocketDetailsView() { getRocket(); }, []); + const breadCrumbs: Breadcrumb[] = [ + { name: 'Ground Support', viewKey: ViewKeys.PLATFORM_SELECTION_KEY, active: false }, + { name: 'Rocket Selection', viewKey: ViewKeys.ROCKET_SELECT_KEY, active: false }, + { name: rocketData?.Name, viewKey: ViewKeys.ROCKET_DETAILS_KEY, active: true } + ]; + return ( <Box sx={{ width: '100vw', height: '100vh' }}> <Stack @@ -138,27 +140,6 @@ export default function RocketDetailsView() { <Header icon={'ROCKET_MONITORING'} breadCrumbs={breadCrumbs} /> </Grid> {/* Rocket Title */} - <Grid item> - <Paper elevation={2} sx={{ padding: 2 }}> - <Stack direction="row" alignItems={'center'} justifyContent={'space-between'}> - <Stack direction="row" alignItems={'center'} spacing={2}> - <RocketLaunchIcon color={'primary'} /> - <Typography align='left' variant='h5'> - {rocketData?.Name || 'Rocket Not found'} - </Typography> - </Stack> - <Button - variant="contained" - size={'large'} - startIcon={<LaunchIcon/>} - onClick={() => console.log('Start Mission')} - disabled={!isMissionActive} - > - Continue Mission - </Button> - </Stack> - </Paper> - </Grid> <Grid item overflow={'scroll'}> <Box sx={{ padding: 0.5 }}> <Stack direction="row" spacing={3} justifyContent="flex-start"> diff --git a/documentation/installation.md b/documentation/installation.md index 015684f27..ab12bc755 100644 --- a/documentation/installation.md +++ b/documentation/installation.md @@ -209,7 +209,7 @@ TBD (@JackCotter) once rust backend is more functional ### 1. Install WSL 1. `Open terminal` -2. `wsl --install or wsl --install -d Ubuntu --web-download` +2. `wsl --install` or `wsl --install -d Ubuntu --web-download` ### 2. Install Docker 1. `sudo apt-get update` @@ -220,22 +220,20 @@ TBD (@JackCotter) once rust backend is more functional 6. `sudo apt-get update` 7. `sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin` -### 3.Git Install +### 3. Git Install 1. `sudo apt-get install git` 2. `git config --global user.name "Your Name"` 3. `git config --global user.email "youremail@domain.com"` -### 4.Install Rust +### 4. Install Rust 1. `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` 2. `sudo apt-get update` 3. `sudo apt install build-essential` -### 5.Install Ground-Support -1. `https://github.com/UVicRocketry/Ground-Support.git` +### 5. Install Ground-Support +1. `sudo git clone https://github.com/UVicRocketry/Ground-Support.git` -### 6.Docker Start Up +### 6. Docker Start Up 1. `sudo service docker start` -2. `docker compose build` -3. `docker compose up` - - +2. `sudo docker compose build` +3. `sudo docker compose up` From f4c9f0f545802a0125854e657658d804db723372 Mon Sep 17 00:00:00 2001 From: klemie <lemieuxkristopher@gmail.com> Date: Wed, 17 Jul 2024 22:18:49 -0700 Subject: [PATCH 14/14] installation guide PR --- documentation/installation.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/documentation/installation.md b/documentation/installation.md index ab12bc755..db44050eb 100644 --- a/documentation/installation.md +++ b/documentation/installation.md @@ -237,3 +237,10 @@ TBD (@JackCotter) once rust backend is more functional 1. `sudo service docker start` 2. `sudo docker compose build` 3. `sudo docker compose up` +<<<<<<< Updated upstream +======= + +### 7. Run Ground Support +1. Open a browser and navigate to `localhost:3000` + +>>>>>>> Stashed changes