mirror of
https://github.com/meshtastic/web.git
synced 2026-05-08 06:34:01 -04:00
Node view updates
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
126
src/pages/Nodes/NodeCard.tsx
Normal file
126
src/pages/Nodes/NodeCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user