diff --git a/src/core/slices/meshtasticSlice.ts b/src/core/slices/meshtasticSlice.ts index f702726d..8814780f 100644 --- a/src/core/slices/meshtasticSlice.ts +++ b/src/core/slices/meshtasticSlice.ts @@ -19,6 +19,7 @@ interface CurrentPosition { longitudeI: number; altitude: number; posTimestamp: number; + satsInView: number; } export interface Node { @@ -113,7 +114,10 @@ export const meshtasticSlice = createSlice({ node.currentPosition?.longitudeI, altitude: action.payload.data.altitude ?? node.currentPosition?.altitude, - posTimestamp: action.payload.data.posTimestamp, //maybe new date? + posTimestamp: action.payload.data.posTimestamp, + satsInView: + action.payload.data.satsInView ?? + node.currentPosition?.satsInView, }; } diff --git a/src/pages/Nodes/Index.tsx b/src/pages/Nodes/Index.tsx index 92b322ab..994dee0b 100644 --- a/src/pages/Nodes/Index.tsx +++ b/src/pages/Nodes/Index.tsx @@ -1,25 +1,26 @@ import React from 'react'; -import { FiCode, FiXCircle } from 'react-icons/fi'; -import { MdGpsFixed, MdGpsNotFixed, MdGpsOff } from 'react-icons/md'; -import TimeAgo from 'timeago-react'; +import { FiXCircle } from 'react-icons/fi'; import { useBreakpoint } from '@app/hooks/breakpoint'; import { useAppSelector } from '@app/hooks/redux'; import { Drawer } from '@components/generic/Drawer'; import { IconButton } from '@components/generic/IconButton'; import { Map } from '@components/Map'; -import { Disclosure } from '@headlessui/react'; -import { Protobuf } from '@meshtastic/meshtasticjs'; + +import { NodeCard } from './NodeCard'; export const Nodes = (): JSX.Element => { const myNodeNum = useAppSelector( (state) => state.meshtastic.radio.hardware, ).myNodeNum; - const nodes = useAppSelector((state) => state.meshtastic.nodes); - // .filter( - // (node) => node.number !== myNodeNum, - // ); + const nodes = useAppSelector((state) => state.meshtastic.nodes) + .slice() + .sort((a, b) => + a.number === myNodeNum + ? 1 + : b?.lastHeard.getTime() - a?.lastHeard.getTime(), + ); const [navOpen, setNavOpen] = React.useState(false); const { breakpoint } = useBreakpoint(); @@ -52,51 +53,11 @@ export const Nodes = (): JSX.Element => { )} {nodes.map((node) => ( - - -
new Date(Date.now() - 1000 * 60 * 15) - ? 'bg-green-500' - : node.lastHeard > new Date(Date.now() - 1000 * 60 * 30) - ? 'bg-yellow-500' - : 'bg-red-500' - }`} - /> -
{node.user?.longName}
-
- {node.lastHeard.getTime() ? ( - - ) : ( - 'Never' - )} -
- {node.currentPosition ? ( - new Date(node.positions[0].posTimestamp * 1000) > - new Date(new Date().getTime() - 1000 * 60 * 30) ? ( - } /> - ) : ( - } /> - ) - ) : ( - } /> - )} - - -
-
- {Protobuf.HardwareModel[node.user?.hwModel ?? 0]} -
-
- } /> -
-
-
- + node={node} + isMyNode={node.number === myNodeNum} + /> ))} diff --git a/src/pages/Nodes/NodeCard.tsx b/src/pages/Nodes/NodeCard.tsx new file mode 100644 index 00000000..12295a19 --- /dev/null +++ b/src/pages/Nodes/NodeCard.tsx @@ -0,0 +1,126 @@ +import React from 'react'; + +import { FaSatellite } from 'react-icons/fa'; +import { FiCode } from 'react-icons/fi'; +import { + MdAccountCircle, + MdArrowDropDown, + MdArrowDropUp, + MdGpsFixed, + MdGpsNotFixed, + MdGpsOff, + MdSignalCellularAlt, +} from 'react-icons/md'; +import TimeAgo from 'timeago-react'; + +import { IconButton } from '@components/generic/IconButton'; +import type { Node } from '@core/slices/meshtasticSlice'; +import { Disclosure } from '@headlessui/react'; +import { Protobuf } from '@meshtastic/meshtasticjs'; + +export interface NodeCardProps { + node: Node; + isMyNode: boolean; +} + +export const NodeCard = ({ node, isMyNode }: NodeCardProps): JSX.Element => { + const [snrAverage, setSnrAverage] = React.useState(0); + const [satsAverage, setSatsAverage] = React.useState(0); + React.useEffect(() => { + setSnrAverage( + node.snr + .slice(node.snr.length - 3, node.snr.length) + .reduce((a, b) => a + b) / (node.snr.length > 3 ? 3 : node.snr.length), + ); + }, [node.snr]); + + // React.useEffect(() => { + // setSatsAverage( + // node.positions + // .filter((pos) => pos.satsInView !== 0) + // .slice(node.positions.length - 3, node.positions.length) + // .reduce((a, b) => { + // return a.satsInView + b.satsInView; + // }).satsInView / (node.positions.length > 3 ? 3 : node.positions.length), + // ); + // }, [node.positions]); + + return ( + + + {isMyNode ? ( + + ) : ( +
new Date(Date.now() - 1000 * 60 * 15) + ? 'bg-green-500' + : node.lastHeard > new Date(Date.now() - 1000 * 60 * 30) + ? 'bg-yellow-500' + : node.lastHeard > new Date(Date.now() - 1000 * 60 * 60) + ? 'bg-red-500' + : 'bg-gray-500' + }`} + /> + )} +
{node.user?.longName}
+ +
+ {!isMyNode && ( + + {node.lastHeard.getTime() ? ( + + ) : ( + 'Never' + )} + + )} +
+ + {node.currentPosition ? ( + new Date(node.positions[0].posTimestamp * 1000) > + new Date(new Date().getTime() - 1000 * 60 * 30) ? ( + } /> + ) : ( + } /> + ) + ) : ( + } /> + )} + + +
+
+ {Protobuf.HardwareModel[node.user?.hwModel ?? 0]} +
+
+ } /> +
+
+
+ + SNR: + {node.snr[node.snr.length - 1] < snrAverage ? ( + + ) : ( + + )} + {node.snr[node.snr.length - 1]}, Average: {snrAverage} +
+
+ + Sats: + {(node.currentPosition?.satsInView ?? 0) < satsAverage ? ( + + ) : ( + + )} + {node.currentPosition?.satsInView ?? 0}, Average: {satsAverage} +
+
+ + ); +}; diff --git a/src/pages/settings/Position.tsx b/src/pages/settings/Position.tsx index 657257d4..ded6af49 100644 --- a/src/pages/settings/Position.tsx +++ b/src/pages/settings/Position.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import { useForm } from 'react-hook-form'; +import { Controller, useForm, useWatch } from 'react-hook-form'; import { FiCode, FiMenu } from 'react-icons/fi'; import JSONPretty from 'react-json-pretty'; +import ReactSelect from 'react-select'; import { useAppSelector } from '@app/hooks/redux'; import { FormFooter } from '@components/FormFooter'; @@ -10,6 +11,7 @@ import { Card } from '@components/generic/Card'; import { Cover } from '@components/generic/Cover'; import { Checkbox } from '@components/generic/form/Checkbox'; import { Input } from '@components/generic/form/Input'; +import { Label } from '@components/generic/form/Label'; import { Select } from '@components/generic/form/Select'; import { IconButton } from '@components/generic/IconButton'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; @@ -30,7 +32,7 @@ export const Position = ({ ); const [debug, setDebug] = React.useState(false); const [loading, setLoading] = React.useState(false); - const { register, handleSubmit, formState, reset } = + const { register, handleSubmit, formState, reset, control } = useForm({ defaultValues: { ...preferences, @@ -43,6 +45,12 @@ export const Position = ({ }, }); + const watchPsk = useWatch({ + control, + name: 'positionFlags', + defaultValue: 0, + }); + React.useEffect(() => { reset(preferences); }, [reset, preferences]); @@ -54,6 +62,19 @@ export const Position = ({ setLoading(false); }); }); + + const encode = (enums: Protobuf.PositionFlags[]): number => { + return enums.reduce((acc, curr) => acc | curr, 0); + }; + + const decode = (value: number): Protobuf.PositionFlags[] => { + const enumValues = Object.keys(Protobuf.PositionFlags) + .map(Number) + .filter(Boolean); + + return enumValues.map((b) => value & b).filter(Boolean); + }; + return ( -