Browse Source

Updates for telemetry rework

pull/31/head
Sacha Weatherstone 4 years ago
parent
commit
d84d7d94cc
  1. 52
      package.json
  2. 1657
      pnpm-lock.yaml
  3. 1
      src/components/generic/Sidebar/SidebarOverlay.tsx
  4. 6
      src/components/layout/Sidebar/Settings/LoRa.tsx
  5. 38
      src/components/layout/Sidebar/Settings/User.tsx
  6. 19
      src/components/layout/Sidebar/Settings/modules/CannedMessage.tsx
  7. 20
      src/components/layout/Sidebar/Settings/modules/Telemetry.tsx
  8. 50
      src/components/menu/BottomNav.tsx
  9. 59
      src/core/slices/meshtasticSlice.ts
  10. 7
      src/index.tsx
  11. 4
      src/pages/Extensions/Debug.tsx
  12. 6
      src/pages/Extensions/Info.tsx
  13. 24
      src/pages/Map/index.tsx
  14. 4
      src/pages/Messages/ChannelChat.tsx
  15. 18
      src/pages/Messages/index.tsx
  16. 31
      src/pages/Nodes/index.tsx

52
package.json

@ -21,51 +21,51 @@
"homepage": "https://meshtastic.org", "homepage": "https://meshtastic.org",
"dependencies": { "dependencies": {
"@emeraldpay/hashicon-react": "^0.5.2", "@emeraldpay/hashicon-react": "^0.5.2",
"@meshtastic/eslint-config": "^1.0.7", "@meshtastic/eslint-config": "^1.0.8",
"@meshtastic/meshtasticjs": "^0.6.54", "@meshtastic/meshtasticjs": "^0.6.55",
"@reduxjs/toolkit": "^1.8.0", "@reduxjs/toolkit": "^1.8.1",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"framer-motion": "^6.2.8", "framer-motion": "^6.2.10",
"mapbox-gl": "^2.7.1", "mapbox-gl": "^2.8.0",
"prettier": "^2.6.1", "prettier": "^2.6.2",
"react": "^17.0.2", "react": "^18.0.0",
"react-dom": "^17.0.2", "react-dom": "^18.0.0",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-flow-renderer": "^10.0.6", "react-flow-renderer": "^10.1.0",
"react-hook-form": "^7.28.1", "react-hook-form": "^7.29.0",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0", "react-json-pretty": "^2.2.0",
"react-multi-select-component": "^4.2.3", "react-multi-select-component": "^4.2.4",
"react-redux": "^7.2.6", "react-redux": "^7.2.8",
"react-use-clipboard": "^1.0.7", "react-use-clipboard": "^1.0.8",
"rfc4648": "^1.5.1", "rfc4648": "^1.5.1",
"swr": "^1.2.2", "swr": "^1.3.0",
"timeago-react": "^3.0.4", "timeago-react": "^3.0.4",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"type-route": "^0.6.0", "type-route": "^0.7.1",
"vite-plugin-environment": "^1.1.0" "vite-plugin-environment": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"@hookform/devtools": "^4.0.2", "@hookform/devtools": "^4.1.0",
"@types/chrome": "^0.0.180", "@types/chrome": "^0.0.181",
"@types/mapbox-gl": "^2.6.3", "@types/mapbox-gl": "^2.6.4",
"@types/react": "^17.0.43", "@types/react": "^18.0.3",
"@types/react-dom": "^17.0.14", "@types/react-dom": "^18.0.0",
"@types/w3c-web-serial": "^1.0.2", "@types/w3c-web-serial": "^1.0.2",
"@types/web-bluetooth": "^0.0.14", "@types/web-bluetooth": "^0.0.14",
"@vitejs/plugin-react": "^1.2.0", "@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.4", "autoprefixer": "^10.4.4",
"gzipper": "^7.0.0", "gzipper": "^7.1.0",
"postcss": "^8.4.12", "postcss": "^8.4.12",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"tailwindcss": "^3.0.23", "tailwindcss": "^3.0.24",
"tar": "^6.1.11", "tar": "^6.1.11",
"typescript": "^4.6.3", "typescript": "^4.6.3",
"unimported": "^1.19.1", "unimported": "^1.19.1",
"vite": "^2.8.6", "vite": "^2.9.1",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-pwa": "^0.11.13", "vite-plugin-pwa": "^0.11.13",
"workbox-window": "^6.5.2" "workbox-window": "^6.5.3"
} }
} }

1657
pnpm-lock.yaml

File diff suppressed because it is too large

1
src/components/generic/Sidebar/SidebarOverlay.tsx

@ -34,6 +34,7 @@ export const SidebarOverlay = ({
} }
transition={{ type: 'just' }} transition={{ type: 'just' }}
> >
{/* @ts-expect-error */}
<AnimateSharedLayout> <AnimateSharedLayout>
{/* <div className="flex gap-2 border-b border-gray-400 p-2 dark:border-gray-600"> */} {/* <div className="flex gap-2 border-b border-gray-400 p-2 dark:border-gray-600"> */}
<div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark"> <div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark">

6
src/components/layout/Sidebar/Settings/LoRa.tsx

@ -47,12 +47,6 @@ export const LoRa = (): JSX.Element => {
optionsEnum={Protobuf.Role} optionsEnum={Protobuf.Role}
{...register('role')} {...register('role')}
/> />
<Input
label="Send Owner Interval"
type="number"
suffix="Seconds"
{...register('sendOwnerInterval', { valueAsNumber: true })}
/>
<Input <Input
label="Frequency Offset" label="Frequency Offset"
type="number" type="number"

38
src/components/layout/Sidebar/Settings/User.tsx

@ -18,7 +18,7 @@ export const User = (): JSX.Element => {
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
).myNodeNum; ).myNodeNum;
const node = useAppSelector((state) => state.meshtastic.nodes).find( const node = useAppSelector((state) => state.meshtastic.nodes).find(
(node) => node.num === myNodeNum, (node) => node.data.num === myNodeNum,
); );
const { register, handleSubmit, formState, reset } = useForm<{ const { register, handleSubmit, formState, reset } = useForm<{
longName: string; longName: string;
@ -30,51 +30,47 @@ export const User = (): JSX.Element => {
txPowerDbm: number; txPowerDbm: number;
}>({ }>({
defaultValues: { defaultValues: {
longName: node?.user?.longName, longName: node?.data.user?.longName,
shortName: node?.user?.shortName, shortName: node?.data.user?.shortName,
isLicensed: node?.user?.isLicensed, isLicensed: node?.data.user?.isLicensed,
team: node?.user?.team, team: node?.data.user?.team,
antAzimuth: node?.user?.antAzimuth, antAzimuth: node?.data.user?.antAzimuth,
antGainDbi: node?.user?.antGainDbi, antGainDbi: node?.data.user?.antGainDbi,
txPowerDbm: node?.user?.txPowerDbm, txPowerDbm: node?.data.user?.txPowerDbm,
}, },
}); });
useEffect(() => { useEffect(() => {
reset({ reset({
longName: node?.user?.longName, longName: node?.data.user?.longName,
shortName: node?.user?.shortName, shortName: node?.data.user?.shortName,
isLicensed: node?.user?.isLicensed, isLicensed: node?.data.user?.isLicensed,
team: node?.user?.team, team: node?.data.user?.team,
}); });
}, [reset, node]); }, [reset, node]);
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
setLoading(true); setLoading(true);
if (node?.user) { if (node?.data.user) {
void connection.setOwner({ ...node.user, ...data }, async () => { void connection.setOwner({ ...node.data.user, ...data }, async () => {
reset({ ...data }); reset({ ...data });
setLoading(false); setLoading(false);
await Promise.resolve(); await Promise.resolve();
}); });
// TODO: can be removed once getUser is implemented
// dispatch(
// addUser({ ...node.user, ...{ data: { ...node.user.data, ...data } } }),
// );
} }
}); });
return ( return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Input label="Device ID" value={node?.user?.id} disabled /> <Input label="Device ID" value={node?.data.user?.id} disabled />
<Input label="Device Name" {...register('longName')} /> <Input label="Device Name" {...register('longName')} />
<Input label="Short Name" maxLength={3} {...register('shortName')} /> <Input label="Short Name" maxLength={3} {...register('shortName')} />
<Input <Input
label="Mac Address" label="Mac Address"
defaultValue={ defaultValue={
base16 base16
.stringify(node?.user?.macaddr ?? []) .stringify(node?.data.user?.macaddr ?? [])
.match(/.{1,2}/g) .match(/.{1,2}/g)
?.join(':') ?? '' ?.join(':') ?? ''
} }
@ -84,7 +80,7 @@ export const User = (): JSX.Element => {
label="Hardware (DEPRECATED)" label="Hardware (DEPRECATED)"
value={ value={
Protobuf.HardwareModel[ Protobuf.HardwareModel[
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET node?.data.user?.hwModel ?? Protobuf.HardwareModel.UNSET
] ]
} }
disabled disabled

19
src/components/layout/Sidebar/Settings/modules/CannedMessage.tsx

@ -50,41 +50,42 @@ export const CannedMessage = (): JSX.Element => {
{...register('rotary1Enabled')} {...register('rotary1Enabled')}
/> />
<Input <Input
label="Encoder #1 Pin A" label="Encoder Pin A"
type="number" type="number"
disabled={moduleEnabled} disabled={moduleEnabled}
{...register('rotary1PinA', { valueAsNumber: true })} {...register('inputbrokerPinA', { valueAsNumber: true })}
/> />
<Input <Input
label="Encoder #1 Pin B" label="Encoder Pin B"
type="number" type="number"
disabled={moduleEnabled} disabled={moduleEnabled}
{...register('rotary1PinB', { valueAsNumber: true })} {...register('inputbrokerPinB', { valueAsNumber: true })}
/> />
<Input <Input
label="Endoer #1 Pin Press" label="Endoer Pin Press"
type="number" type="number"
disabled={moduleEnabled} disabled={moduleEnabled}
{...register('rotary1PinPress', { valueAsNumber: true })} {...register('inputbrokerPinPress', { valueAsNumber: true })}
/> />
<Select <Select
label="Clockwise event" label="Clockwise event"
disabled={moduleEnabled} disabled={moduleEnabled}
optionsEnum={Protobuf.InputEventChar} optionsEnum={Protobuf.InputEventChar}
{...register('rotary1EventCw', { valueAsNumber: true })} {...register('inputbrokerEventCw', { valueAsNumber: true })}
/> />
<Select <Select
label="Counter Clockwise event" label="Counter Clockwise event"
disabled={moduleEnabled} disabled={moduleEnabled}
optionsEnum={Protobuf.InputEventChar} optionsEnum={Protobuf.InputEventChar}
{...register('rotary1EventCcw', { valueAsNumber: true })} {...register('inputbrokerEventCcw', { valueAsNumber: true })}
/> />
<Select <Select
label="Press event" label="Press event"
disabled={moduleEnabled} disabled={moduleEnabled}
optionsEnum={Protobuf.InputEventChar} optionsEnum={Protobuf.InputEventChar}
{...register('rotary1EventPress', { valueAsNumber: true })} {...register('inputbrokerEventPress', { valueAsNumber: true })}
/> />
<Checkbox label="Up Down enabled" {...register('updown1Enabled')} />
<Input <Input
label="Allow Input Source" label="Allow Input Source"
disabled={moduleEnabled} disabled={moduleEnabled}

20
src/components/layout/Sidebar/Settings/modules/Telemetry.tsx

@ -37,16 +37,16 @@ export const Telemetry = (): JSX.Element => {
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox <Checkbox
label="Measurement Enabled" label="Measurement Enabled"
{...register('telemetryModuleMeasurementEnabled')} {...register('telemetryModuleEnvironmentMeasurementEnabled')}
/> />
<Checkbox <Checkbox
label="Displayed on Screen" label="Displayed on Screen"
{...register('telemetryModuleScreenEnabled')} {...register('telemetryModuleEnvironmentScreenEnabled')}
/> />
<Input <Input
label="Read Error Count Threshold" label="Read Error Count Threshold"
type="number" type="number"
{...register('telemetryModuleReadErrorCountThreshold', { {...register('telemetryModuleEnvironmentReadErrorCountThreshold', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -54,7 +54,7 @@ export const Telemetry = (): JSX.Element => {
label="Update Interval" label="Update Interval"
suffix="Seconds" suffix="Seconds"
type="number" type="number"
{...register('telemetryModuleUpdateInterval', { {...register('telemetryModuleEnvironmentUpdateInterval', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -62,23 +62,27 @@ export const Telemetry = (): JSX.Element => {
label="Recovery Interval" label="Recovery Interval"
suffix="Seconds" suffix="Seconds"
type="number" type="number"
{...register('telemetryModuleRecoveryInterval', { {...register('telemetryModuleEnvironmentRecoveryInterval', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
<Checkbox <Checkbox
label="Display Farenheit" label="Display Farenheit"
{...register('telemetryModuleDisplayFahrenheit')} {...register('telemetryModuleEnvironmentDisplayFahrenheit')}
/> />
<Select <Select
label="Sensor Type" label="Sensor Type"
optionsEnum={Protobuf.RadioConfig_UserPreferences_TelemetrySensorType} optionsEnum={Protobuf.RadioConfig_UserPreferences_TelemetrySensorType}
{...register('telemetryModuleSensorType', { valueAsNumber: true })} {...register('telemetryModuleEnvironmentSensorType', {
valueAsNumber: true,
})}
/> />
<Input <Input
label="Sensor Pin" label="Sensor Pin"
type="number" type="number"
{...register('telemetryModuleSensorPin', { valueAsNumber: true })} {...register('telemetryModuleEnvironmentSensorPin', {
valueAsNumber: true,
})}
/> />
</Form> </Form>
); );

50
src/components/menu/BottomNav.tsx

@ -5,15 +5,18 @@ import {
FiBluetooth, FiBluetooth,
FiCpu, FiCpu,
FiGitBranch, FiGitBranch,
FiHexagon,
FiMenu, FiMenu,
FiMoon, FiMoon,
FiSun, FiSun,
FiWifi, FiWifi,
FiX, FiX,
} from 'react-icons/fi'; } from 'react-icons/fi';
import {
IoBatteryChargingOutline,
IoBatteryDeadOutline,
IoBatteryFullOutline,
} from 'react-icons/io5';
import { MdUpgrade } from 'react-icons/md'; import { MdUpgrade } from 'react-icons/md';
import { AiOutlineHeart, AiFillHeart } from 'react-icons/ai';
import { import {
RiArrowDownLine, RiArrowDownLine,
RiArrowUpDownLine, RiArrowUpDownLine,
@ -31,13 +34,6 @@ import {
import { useAppDispatch } from '@hooks/useAppDispatch'; import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf, Types } from '@meshtastic/meshtasticjs'; import { Protobuf, Types } from '@meshtastic/meshtasticjs';
import {
IoBatteryChargingOutline,
IoBatteryDeadOutline,
IoBatteryFullOutline,
IoBatteryHalfOutline,
} from 'react-icons/io5';
import { FaTrafficLight } from 'react-icons/fa';
export const BottomNav = (): JSX.Element => { export const BottomNav = (): JSX.Element => {
const [showVersionInfo, setShowVersionInfo] = useState(false); const [showVersionInfo, setShowVersionInfo] = useState(false);
@ -47,8 +43,8 @@ export const BottomNav = (): JSX.Element => {
const primaryChannelSettings = useAppSelector( const primaryChannelSettings = useAppSelector(
(state) => state.meshtastic.radio.channels, (state) => state.meshtastic.radio.channels,
).find((channel) => channel.role === Protobuf.Channel_Role.PRIMARY)?.settings; ).find((channel) => channel.role === Protobuf.Channel_Role.PRIMARY)?.settings;
const telemetry = const metrics =
meshtasticState.nodes[meshtasticState.radio.hardware.myNodeNum]?.telemetry; meshtasticState.nodes[meshtasticState.radio.hardware.myNodeNum]?.metrics;
return ( return (
<div className="z-20 flex justify-between divide-x divide-gray-400 border-t border-gray-400 bg-white dark:divide-gray-600 dark:border-gray-600 dark:bg-secondaryDark"> <div className="z-20 flex justify-between divide-x divide-gray-400 border-t border-gray-400 bg-white dark:divide-gray-600 dark:border-gray-600 dark:bg-secondaryDark">
@ -89,32 +85,26 @@ export const BottomNav = (): JSX.Element => {
)} )}
<div className="truncate text-xs font-medium"> <div className="truncate text-xs font-medium">
{meshtasticState.nodes.find( {meshtasticState.nodes.find(
(node) => node.num === meshtasticState.radio.hardware.myNodeNum, (node) =>
)?.user?.longName ?? 'Disconnected'} node.data.num === meshtasticState.radio.hardware.myNodeNum,
)?.data.user?.longName ?? 'Disconnected'}
</div> </div>
</BottomNavItem> </BottomNavItem>
<BottomNavItem tooltip="Router Heartbeat">
{telemetry?.routerHeartbeat ? (
<AiFillHeart className="h-4" />
) : (
<AiOutlineHeart className="h-4" />
)}
</BottomNavItem>
<BottomNavItem tooltip="Battery Level"> <BottomNavItem tooltip="Battery Level">
{!telemetry?.batteryLevel ? ( {!metrics?.batteryLevel ? (
<IoBatteryDeadOutline className="h-4" /> <IoBatteryDeadOutline className="h-4" />
) : telemetry?.batteryLevel > 50 ? ( ) : metrics?.batteryLevel > 50 ? (
<IoBatteryFullOutline className="h-4" /> <IoBatteryFullOutline className="h-4" />
) : telemetry?.batteryLevel > 0 ? ( ) : metrics?.batteryLevel > 0 ? (
<IoBatteryFullOutline className="h-4" /> <IoBatteryFullOutline className="h-4" />
) : ( ) : (
<IoBatteryChargingOutline className="h-4" /> <IoBatteryChargingOutline className="h-4" />
)} )}
<div className="truncate text-xs font-medium"> <div className="truncate text-xs font-medium">
{telemetry?.batteryLevel {metrics?.batteryLevel
? `${telemetry?.batteryLevel}% - ${telemetry?.voltage}v` ? `${metrics?.batteryLevel}% - ${metrics?.voltage}v`
: 'No Battery'} : 'No Battery'}
</div> </div>
</BottomNavItem> </BottomNavItem>
@ -122,22 +112,22 @@ export const BottomNav = (): JSX.Element => {
<BottomNavItem tooltip="Network Utilization"> <BottomNavItem tooltip="Network Utilization">
<div className="m-auto h-3 w-3 rounded-full bg-primary" /> <div className="m-auto h-3 w-3 rounded-full bg-primary" />
<div className="truncate text-xs font-medium"> <div className="truncate text-xs font-medium">
{`${telemetry?.airUtilTx ?? 0}% - Air`} | {`${metrics?.airUtilTx ?? 0}% - Air`} |
</div> </div>
<div <div
className={`m-auto h-3 w-3 rounded-full ${ className={`m-auto h-3 w-3 rounded-full ${
!telemetry?.channelUtilization !metrics?.channelUtilization
? 'bg-primary' ? 'bg-primary'
: telemetry?.channelUtilization > 50 : metrics?.channelUtilization > 50
? 'bg-red-400' ? 'bg-red-400'
: telemetry?.channelUtilization > 24 : metrics?.channelUtilization > 24
? 'bg-yellow-400' ? 'bg-yellow-400'
: 'bg-primary' : 'bg-primary'
}`} }`}
/> />
<div className="truncate text-xs font-medium"> <div className="truncate text-xs font-medium">
{`${telemetry?.channelUtilization ?? 0}% - Ch`} {`${metrics?.channelUtilization ?? 0}% - Ch`}
</div> </div>
</BottomNavItem> </BottomNavItem>

59
src/core/slices/meshtasticSlice.ts

@ -13,14 +13,6 @@ export interface Chat {
messages: MessageWithAck[]; messages: MessageWithAck[];
} }
interface CurrentPosition {
latitudeI: number;
longitudeI: number;
altitude: number;
posTimestamp: number;
satsInView: number;
}
type ChatEntries = { type ChatEntries = {
[key in number]: Chat; [key in number]: Chat;
}; };
@ -33,6 +25,11 @@ interface Route {
//speed stats? //speed stats?
} }
export interface Node {
metrics: Protobuf.DeviceMetrics;
data: Protobuf.NodeInfo;
}
export interface Radio { export interface Radio {
channels: Protobuf.Channel[]; channels: Protobuf.Channel[];
preferences: Protobuf.RadioConfig_UserPreferences; preferences: Protobuf.RadioConfig_UserPreferences;
@ -43,7 +40,7 @@ interface MeshtasticState {
deviceStatus: Types.DeviceStatusEnum; deviceStatus: Types.DeviceStatusEnum;
lastMeshInterraction: number; lastMeshInterraction: number;
ready: boolean; ready: boolean;
nodes: Protobuf.NodeInfo[]; nodes: Node[];
radio: Radio; radio: Radio;
chats: ChatEntries; chats: ChatEntries;
logs: Types.LogEventPacket[]; logs: Types.LogEventPacket[];
@ -87,46 +84,56 @@ export const meshtasticSlice = createSlice({
}, },
addUser: (state, action: PayloadAction<Types.UserPacket>) => { addUser: (state, action: PayloadAction<Types.UserPacket>) => {
const node = state.nodes.find( const node = state.nodes.find(
(node) => node.num === action.payload.packet.from, (node) => node.data.num === action.payload.packet.from,
); );
if (node) { if (node) {
node.user = action.payload.data; node.data.user = action.payload.data;
if (action.payload.packet.rxTime) { if (action.payload.packet.rxTime) {
node.lastHeard = new Date( node.data.lastHeard = new Date(
action.payload.packet.rxTime * 1000, action.payload.packet.rxTime * 1000,
).getTime(); ).getTime();
} }
} else { } else {
state.nodes.push({ state.nodes.push({
num: action.payload.packet.from, data: {
snr: action.payload.packet.rxSnr, num: action.payload.packet.from,
lastHeard: new Date().getTime(), snr: action.payload.packet.rxSnr,
...action.payload.packet, lastHeard: new Date().getTime(),
...action.payload.packet,
},
metrics: Protobuf.DeviceMetrics.create(),
}); });
} }
}, },
addPosition: (state, action: PayloadAction<Types.PositionPacket>) => { addPosition: (state, action: PayloadAction<Types.PositionPacket>) => {
const node = state.nodes.find( const node = state.nodes.find(
(node) => node.num === action.payload.packet.from, (node) => node.data.num === action.payload.packet.from,
); );
if (node) { if (node) {
node.position = action.payload.data; node.data.position = action.payload.data;
if (action.payload.packet.rxTime) { if (action.payload.packet.rxTime) {
node.lastHeard = new Date( node.data.lastHeard = new Date(
action.payload.packet.rxTime * 1000, action.payload.packet.rxTime * 1000,
).getTime(); ).getTime();
} }
} }
}, },
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => { addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
const node = state.nodes.find((node) => node.num === action.payload.num); const node = state.nodes.find(
(node) => node.data.num === action.payload.num,
);
if (node) { if (node) {
node.lastHeard = new Date(action.payload.lastHeard * 1000).getTime(); node.data.lastHeard = new Date(
node.snr = action.payload.snr; action.payload.lastHeard * 1000,
).getTime();
node.data.snr = action.payload.snr;
} else { } else {
state.nodes.push(action.payload); state.nodes.push({
data: action.payload,
metrics: Protobuf.DeviceMetrics.create(),
});
} }
}, },
addChannel: (state, action: PayloadAction<Protobuf.Channel>) => { addChannel: (state, action: PayloadAction<Protobuf.Channel>) => {
@ -205,9 +212,11 @@ export const meshtasticSlice = createSlice({
state, state,
action: PayloadAction<{ id: number; time: Date }>, action: PayloadAction<{ id: number; time: Date }>,
) => { ) => {
const node = state.nodes.find((node) => node.num === action.payload.id); const node = state.nodes.find(
(node) => node.data.num === action.payload.id,
);
if (node) { if (node) {
node.lastHeard = action.payload.time.getTime(); node.data.lastHeard = action.payload.time.getTime();
} }
}, },
addChat: (state, action: PayloadAction<number>) => { addChat: (state, action: PayloadAction<number>) => {

7
src/index.tsx

@ -2,9 +2,9 @@ import '@app/index.css';
import type React from 'react'; import type React from 'react';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { render } from 'react-dom';
import { domAnimation, LazyMotion } from 'framer-motion'; import { domAnimation, LazyMotion } from 'framer-motion';
import { createRoot } from 'react-dom/client';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
@ -14,7 +14,9 @@ import { ReloadPrompt } from '@components/pwa/ReloadPrompt';
import { RouteProvider } from '@core/router'; import { RouteProvider } from '@core/router';
import { store } from '@core/store'; import { store } from '@core/store';
render( const container = document.getElementById('root') as HTMLElement;
const root = createRoot(container);
root.render(
<StrictMode> <StrictMode>
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary FallbackComponent={ErrorFallback}>
<RouteProvider> <RouteProvider>
@ -27,5 +29,4 @@ render(
</RouteProvider> </RouteProvider>
</ErrorBoundary> </ErrorBoundary>
</StrictMode>, </StrictMode>,
document.getElementById('root'),
); );

4
src/pages/Extensions/Debug.tsx

@ -10,7 +10,9 @@ export const Debug = (): JSX.Element => {
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
); );
const node = useAppSelector((state) => const node = useAppSelector((state) =>
state.meshtastic.nodes.find((node) => node.num === hardwareInfo.myNodeNum), state.meshtastic.nodes.find(
(node) => node.data.num === hardwareInfo.myNodeNum,
),
); );
return ( return (

6
src/pages/Extensions/Info.tsx

@ -14,7 +14,9 @@ export const Info = (): JSX.Element => {
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
); );
const node = useAppSelector((state) => const node = useAppSelector((state) =>
state.meshtastic.nodes.find((node) => node.num === hardwareInfo.myNodeNum), state.meshtastic.nodes.find(
(node) => node.data.num === hardwareInfo.myNodeNum,
),
); );
return ( return (
@ -27,7 +29,7 @@ export const Info = (): JSX.Element => {
<div className="m-auto flex flex-col gap-2"> <div className="m-auto flex flex-col gap-2">
<Hashicon value={hardwareInfo.myNodeNum.toString()} size={180} /> <Hashicon value={hardwareInfo.myNodeNum.toString()} size={180} />
<div className="text-center text-lg font-medium dark:text-white"> <div className="text-center text-lg font-medium dark:text-white">
{node?.user?.longName || 'Unknown'} {node?.data.user?.longName || 'Unknown'}
</div> </div>
</div> </div>
{/* <img {/* <img

24
src/pages/Map/index.tsx

@ -8,10 +8,10 @@ import { RiRoadMapLine } from 'react-icons/ri';
import { Layout } from '@components/layout'; import { Layout } from '@components/layout';
import { MapboxProvider } from '@components/MapBox/MapboxProvider'; import { MapboxProvider } from '@components/MapBox/MapboxProvider';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
import { MapContainer } from '@pages/Map/MapContainer'; import { MapContainer } from '@pages/Map/MapContainer';
import { Marker } from '@pages/Map/Marker'; import { Marker } from '@pages/Map/Marker';
import { NodeCard } from '@pages/Nodes/NodeCard'; import { NodeCard } from '@pages/Nodes/NodeCard';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const Map = (): JSX.Element => { export const Map = (): JSX.Element => {
const [selectedNode, setSelectedNode] = useState<Protobuf.NodeInfo>(); const [selectedNode, setSelectedNode] = useState<Protobuf.NodeInfo>();
@ -25,22 +25,22 @@ export const Map = (): JSX.Element => {
<MapboxProvider> <MapboxProvider>
{nodes.map((node) => { {nodes.map((node) => {
return ( return (
node.position && ( node.data.position && (
<Marker <Marker
key={node.num} key={node.data.num}
center={ center={
new mapboxgl.LngLat( new mapboxgl.LngLat(
node.position.longitudeI / 1e7, node.data.position.longitudeI / 1e7,
node.position.latitudeI / 1e7, node.data.position.latitudeI / 1e7,
) )
} }
> >
<div <div
onClick={(): void => { onClick={(): void => {
setSelectedNode(node); setSelectedNode(node.data);
}} }}
className={`z-50 rounded-full border-2 bg-opacity-30 ${ className={`z-50 rounded-full border-2 bg-opacity-30 ${
node.num === selectedNode?.num node.data.num === selectedNode?.num
? 'border-green-500 bg-green-500' ? 'border-green-500 bg-green-500'
: 'border-blue-500 bg-blue-500' : 'border-blue-500 bg-blue-500'
}`} }`}
@ -66,12 +66,12 @@ export const Map = (): JSX.Element => {
{nodes.map((node) => ( {nodes.map((node) => (
<NodeCard <NodeCard
key={node.num} key={node.data.num}
node={node} node={node.data}
isMyNode={node.num === myNodeNum} isMyNode={node.data.num === myNodeNum}
selected={selectedNode?.num === node.num} selected={selectedNode?.num === node.data.num}
setSelected={(): void => { setSelected={(): void => {
setSelectedNode(node); setSelectedNode(node.data);
}} }}
/> />
))} ))}

4
src/pages/Messages/ChannelChat.tsx

@ -27,7 +27,7 @@ export const ChannelChat = ({
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
).myNodeNum; ).myNodeNum;
const nodes = useAppSelector((state) => state.meshtastic.nodes).filter( const nodes = useAppSelector((state) => state.meshtastic.nodes).filter(
(node) => node.num !== myNodeNum, (node) => node.data.num !== myNodeNum,
); );
const chats = useAppSelector((state) => state.meshtastic.chats); const chats = useAppSelector((state) => state.meshtastic.chats);
const channels = useAppSelector( const channels = useAppSelector(
@ -80,7 +80,7 @@ export const ChannelChat = ({
<Tooltip <Tooltip
key={nodeId} key={nodeId}
content={ content={
nodes.find((node) => node.num === nodeId)?.user nodes.find((node) => node.data.num === nodeId)?.data.user
?.longName ?? 'UNK' ?.longName ?? 'UNK'
} }
> >

18
src/pages/Messages/index.tsx

@ -52,11 +52,11 @@ export const Messages = (): JSX.Element => {
<div className="mx-2 border-b border-gray-400 dark:border-gray-600" /> <div className="mx-2 border-b border-gray-400 dark:border-gray-600" />
)} )}
{nodes {nodes
.filter((node) => node.num !== myNodeNum) .filter((node) => node.data.num !== myNodeNum)
.map((node) => ( .map((node) => (
<DmChat <DmChat
key={node.num} key={node.data.num}
node={node} node={node.data}
selectedIndex={selectedChatIndex} selectedIndex={selectedChatIndex}
setSelectedIndex={setSelectedChatIndex} setSelectedIndex={setSelectedChatIndex}
/> />
@ -81,8 +81,8 @@ export const Messages = (): JSX.Element => {
</span> </span>
) : ( ) : (
<span className="text-gray-500 dark:text-gray-400"> <span className="text-gray-500 dark:text-gray-400">
{nodes.find((node) => node.num === selectedChatIndex)?.user {nodes.find((node) => node.data.num === selectedChatIndex)
?.longName ?? 'Unknown'} ?.data.user?.longName ?? 'Unknown'}
</span> </span>
)} )}
</div> </div>
@ -105,9 +105,11 @@ export const Messages = (): JSX.Element => {
: chats[selectedChatIndex].messages[index - 1].message : chats[selectedChatIndex].messages[index - 1].message
.packet.from === message.message.packet.from .packet.from === message.message.packet.from
} }
sender={nodes.find( sender={
(node) => node.num === message.message.packet.from, nodes.find(
)} (node) => node.data.num === message.message.packet.from,
)?.data
}
/> />
))} ))}
</div> </div>

31
src/pages/Nodes/index.tsx

@ -37,8 +37,8 @@ export const Nodes = (): JSX.Element => {
nodes.map((node, index) => { nodes.map((node, index) => {
tmpNodes.push({ tmpNodes.push({
id: node.num.toString(), id: node.data.num.toString(),
data: { label: node.user?.longName ?? `Unknown ${node.num}` }, data: { label: node.data.user?.longName ?? `Unknown ${node.data.num}` },
position: { x: index * 160 + 500, y: 100 + 500 }, position: { x: index * 160 + 500, y: 100 + 500 },
}); });
}); });
@ -50,7 +50,7 @@ export const Nodes = (): JSX.Element => {
const tmpEdges: Edge[] = []; const tmpEdges: Edge[] = [];
nodes.map((node, index) => { nodes.map((node, index) => {
if (node.num === myNodeNum) { if (node.data.num === myNodeNum) {
tmpEdges.push({ tmpEdges.push({
id: `e${1}-${myNodeNum}`, id: `e${1}-${myNodeNum}`,
source: '1', source: '1',
@ -85,17 +85,17 @@ export const Nodes = (): JSX.Element => {
<> <>
{nodes.map((node) => ( {nodes.map((node) => (
<SidebarItem <SidebarItem
key={node.num} key={node.data.num}
selected={node.num === selected} selected={node.data.num === selected}
setSelected={(): void => { setSelected={(): void => {
setSelected(node.num); setSelected(node.data.num);
}} }}
actions={ actions={
<IconButton <IconButton
nested nested
onClick={(e): void => { onClick={(e): void => {
e.stopPropagation(); e.stopPropagation();
setSelected(node.num); setSelected(node.data.num);
}} }}
icon={<FiSettings />} icon={<FiSettings />}
/> />
@ -103,7 +103,7 @@ export const Nodes = (): JSX.Element => {
> >
<div className="flex dark:text-white"> <div className="flex dark:text-white">
<div className="relative m-auto"> <div className="relative m-auto">
{node.num === myNodeNum && ( {node.data.num === myNodeNum && (
<Tooltip content="Your Node"> <Tooltip content="Your Node">
<m.div <m.div
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
@ -113,15 +113,18 @@ export const Nodes = (): JSX.Element => {
</m.div> </m.div>
</Tooltip> </Tooltip>
)} )}
<Hashicon value={node.num.toString()} size={32} /> <Hashicon value={node.data.num.toString()} size={32} />
</div> </div>
</div> </div>
<div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400"> <div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400">
{node.lastHeard {node.data.lastHeard
? new Date(node.lastHeard).toLocaleTimeString(undefined, { ? new Date(node.data.lastHeard).toLocaleTimeString(
hour: '2-digit', undefined,
minute: '2-digit', {
}) hour: '2-digit',
minute: '2-digit',
},
)
: 'Never'} : 'Never'}
</div> </div>
</SidebarItem> </SidebarItem>

Loading…
Cancel
Save