Node view updates
This commit is contained in:
@@ -19,6 +19,7 @@ interface CurrentPosition {
|
|||||||
longitudeI: number;
|
longitudeI: number;
|
||||||
altitude: number;
|
altitude: number;
|
||||||
posTimestamp: number;
|
posTimestamp: number;
|
||||||
|
satsInView: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Node {
|
export interface Node {
|
||||||
@@ -113,7 +114,10 @@ export const meshtasticSlice = createSlice({
|
|||||||
node.currentPosition?.longitudeI,
|
node.currentPosition?.longitudeI,
|
||||||
altitude:
|
altitude:
|
||||||
action.payload.data.altitude ?? node.currentPosition?.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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-52
@@ -1,25 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { FiCode, FiXCircle } from 'react-icons/fi';
|
import { FiXCircle } from 'react-icons/fi';
|
||||||
import { MdGpsFixed, MdGpsNotFixed, MdGpsOff } from 'react-icons/md';
|
|
||||||
import TimeAgo from 'timeago-react';
|
|
||||||
|
|
||||||
import { useBreakpoint } from '@app/hooks/breakpoint';
|
import { useBreakpoint } from '@app/hooks/breakpoint';
|
||||||
import { useAppSelector } from '@app/hooks/redux';
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
import { Drawer } from '@components/generic/Drawer';
|
import { Drawer } from '@components/generic/Drawer';
|
||||||
import { IconButton } from '@components/generic/IconButton';
|
import { IconButton } from '@components/generic/IconButton';
|
||||||
import { Map } from '@components/Map';
|
import { Map } from '@components/Map';
|
||||||
import { Disclosure } from '@headlessui/react';
|
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { NodeCard } from './NodeCard';
|
||||||
|
|
||||||
export const Nodes = (): JSX.Element => {
|
export const Nodes = (): JSX.Element => {
|
||||||
const myNodeNum = useAppSelector(
|
const myNodeNum = useAppSelector(
|
||||||
(state) => state.meshtastic.radio.hardware,
|
(state) => state.meshtastic.radio.hardware,
|
||||||
).myNodeNum;
|
).myNodeNum;
|
||||||
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
const nodes = useAppSelector((state) => state.meshtastic.nodes)
|
||||||
// .filter(
|
.slice()
|
||||||
// (node) => node.number !== myNodeNum,
|
.sort((a, b) =>
|
||||||
// );
|
a.number === myNodeNum
|
||||||
|
? 1
|
||||||
|
: b?.lastHeard.getTime() - a?.lastHeard.getTime(),
|
||||||
|
);
|
||||||
const [navOpen, setNavOpen] = React.useState(false);
|
const [navOpen, setNavOpen] = React.useState(false);
|
||||||
|
|
||||||
const { breakpoint } = useBreakpoint();
|
const { breakpoint } = useBreakpoint();
|
||||||
@@ -52,51 +53,11 @@ export const Nodes = (): JSX.Element => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{nodes.map((node) => (
|
{nodes.map((node) => (
|
||||||
<Disclosure
|
<NodeCard
|
||||||
as="div"
|
|
||||||
className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700"
|
|
||||||
key={node.number}
|
key={node.number}
|
||||||
>
|
node={node}
|
||||||
<Disclosure.Button className="flex w-full gap-2 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark">
|
isMyNode={node.number === myNodeNum}
|
||||||
<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>
|
|
||||||
))}
|
))}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<Map />
|
<Map />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
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 { FiCode, FiMenu } from 'react-icons/fi';
|
||||||
import JSONPretty from 'react-json-pretty';
|
import JSONPretty from 'react-json-pretty';
|
||||||
|
import ReactSelect from 'react-select';
|
||||||
|
|
||||||
import { useAppSelector } from '@app/hooks/redux';
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
import { FormFooter } from '@components/FormFooter';
|
import { FormFooter } from '@components/FormFooter';
|
||||||
@@ -10,6 +11,7 @@ import { Card } from '@components/generic/Card';
|
|||||||
import { Cover } from '@components/generic/Cover';
|
import { Cover } from '@components/generic/Cover';
|
||||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||||
import { Input } from '@components/generic/form/Input';
|
import { Input } from '@components/generic/form/Input';
|
||||||
|
import { Label } from '@components/generic/form/Label';
|
||||||
import { Select } from '@components/generic/form/Select';
|
import { Select } from '@components/generic/form/Select';
|
||||||
import { IconButton } from '@components/generic/IconButton';
|
import { IconButton } from '@components/generic/IconButton';
|
||||||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
@@ -30,7 +32,7 @@ export const Position = ({
|
|||||||
);
|
);
|
||||||
const [debug, setDebug] = React.useState(false);
|
const [debug, setDebug] = React.useState(false);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const { register, handleSubmit, formState, reset } =
|
const { register, handleSubmit, formState, reset, control } =
|
||||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...preferences,
|
...preferences,
|
||||||
@@ -43,6 +45,12 @@ export const Position = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const watchPsk = useWatch({
|
||||||
|
control,
|
||||||
|
name: 'positionFlags',
|
||||||
|
defaultValue: 0,
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
reset(preferences);
|
reset(preferences);
|
||||||
}, [reset, preferences]);
|
}, [reset, preferences]);
|
||||||
@@ -54,6 +62,19 @@ export const Position = ({
|
|||||||
setLoading(false);
|
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 (
|
return (
|
||||||
<PrimaryTemplate
|
<PrimaryTemplate
|
||||||
title="Position"
|
title="Position"
|
||||||
@@ -93,9 +114,55 @@ export const Position = ({
|
|||||||
suffix="Seconds"
|
suffix="Seconds"
|
||||||
{...register('positionBroadcastSecs', { valueAsNumber: true })}
|
{...register('positionBroadcastSecs', { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<Select
|
|
||||||
label="Position Type"
|
<Controller
|
||||||
optionsEnum={Protobuf.PositionFlags}
|
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 })}
|
{...register('positionFlags', { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
Reference in New Issue
Block a user