Node view updates

This commit is contained in:
Sacha Weatherstone
2021-12-09 20:14:35 +11:00
parent bd92905ee4
commit bd5efb4ae4
4 changed files with 217 additions and 59 deletions

View File

@@ -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,
};
}

View File

@@ -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 => {
</span>
)}
{nodes.map((node) => (
<Disclosure
as="div"
className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700"
<NodeCard
key={node.number}
>
<Disclosure.Button className="flex w-full gap-2 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark">
<div
className={`my-auto w-3 h-3 rounded-full ${
node.lastHeard > 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'
}`}
/>
<div className="my-auto">{node.user?.longName}</div>
<div className="my-auto ml-auto text-xs font-semibold">
{node.lastHeard.getTime() ? (
<TimeAgo datetime={node.lastHeard} />
) : (
'Never'
)}
</div>
{node.currentPosition ? (
new Date(node.positions[0].posTimestamp * 1000) >
new Date(new Date().getTime() - 1000 * 60 * 30) ? (
<IconButton icon={<MdGpsFixed />} />
) : (
<IconButton icon={<MdGpsNotFixed />} />
)
) : (
<IconButton disabled icon={<MdGpsOff />} />
)}
</Disclosure.Button>
<Disclosure.Panel className="p-2">
<div className="flex">
<div className="my-auto">
{Protobuf.HardwareModel[node.user?.hwModel ?? 0]}
</div>
<div className="ml-auto">
<IconButton icon={<FiCode className="w-5 h-5" />} />
</div>
</div>
</Disclosure.Panel>
</Disclosure>
node={node}
isMyNode={node.number === myNodeNum}
/>
))}
</Drawer>
<Map />

View File

@@ -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 (
<Disclosure
as="div"
className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700"
>
<Disclosure.Button className="flex w-full gap-2 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark">
{isMyNode ? (
<MdAccountCircle className="my-auto" />
) : (
<div
className={`my-auto w-3 h-3 rounded-full ${
node.lastHeard > 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'
}`}
/>
)}
<div className="my-auto">{node.user?.longName}</div>
<div className="my-auto ml-auto text-xs font-semibold">
{!isMyNode && (
<span>
{node.lastHeard.getTime() ? (
<TimeAgo datetime={node.lastHeard} />
) : (
'Never'
)}
</span>
)}
</div>
{node.currentPosition ? (
new Date(node.positions[0].posTimestamp * 1000) >
new Date(new Date().getTime() - 1000 * 60 * 30) ? (
<IconButton icon={<MdGpsFixed />} />
) : (
<IconButton icon={<MdGpsNotFixed />} />
)
) : (
<IconButton disabled icon={<MdGpsOff />} />
)}
</Disclosure.Button>
<Disclosure.Panel className="p-2">
<div className="flex">
<div className="my-auto">
{Protobuf.HardwareModel[node.user?.hwModel ?? 0]}
</div>
<div className="ml-auto">
<IconButton icon={<FiCode className="w-5 h-5" />} />
</div>
</div>
<div className="flex">
<MdSignalCellularAlt className="my-auto" />
SNR:
{node.snr[node.snr.length - 1] < snrAverage ? (
<MdArrowDropDown className="text-red-500" />
) : (
<MdArrowDropUp className="text-green-500" />
)}
{node.snr[node.snr.length - 1]}, Average: {snrAverage}
</div>
<div className="flex">
<FaSatellite className="my-auto" />
Sats:
{(node.currentPosition?.satsInView ?? 0) < satsAverage ? (
<MdArrowDropDown className="text-red-500" />
) : (
<MdArrowDropUp className="text-green-500" />
)}
{node.currentPosition?.satsInView ?? 0}, Average: {satsAverage}
</div>
</Disclosure.Panel>
</Disclosure>
);
};

View File

@@ -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<Protobuf.RadioConfig_UserPreferences>({
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 (
<PrimaryTemplate
title="Position"
@@ -93,9 +114,55 @@ export const Position = ({
suffix="Seconds"
{...register('positionBroadcastSecs', { valueAsNumber: true })}
/>
<Select
label="Position Type"
optionsEnum={Protobuf.PositionFlags}
<Controller
name="positionFlags"
control={control}
render={({ field, fieldState }): JSX.Element => {
const { value, onChange, ...rest } = field;
const { error } = fieldState;
const label = 'Position Flags';
return (
<div className="w-full">
{label && <Label label={label} error={error?.message} />}
<ReactSelect
{...rest}
isMulti
value={decode(value).map((flag) => {
return {
value: flag,
label: Protobuf.PositionFlags[flag].replace(
'POS_',
'',
),
};
})}
options={Object.entries(Protobuf.PositionFlags)
.filter((value) => typeof value[1] !== 'number')
.filter(
(value) =>
parseInt(value[0]) !==
Protobuf.PositionFlags.POS_UNDEFINED,
)
.map((value) => {
return {
value: parseInt(value[0]),
label: value[1].toString().replace('POS_', ''),
};
})}
onChange={(e): void =>
onChange(encode(e.map((v) => v.value)))
}
/>
</div>
);
}}
/>
<Input
label="Position Type (DEBUG)"
type="number"
disabled
{...register('positionFlags', { valueAsNumber: true })}
/>
<Checkbox