This commit is contained in:
Sacha Weatherstone
2021-08-03 19:37:36 +10:00
parent c543a4ef5c
commit f597600a95
19 changed files with 356 additions and 451 deletions
+48 -88
View File
@@ -1,44 +1,29 @@
import React from 'react';
import type {
IBLEConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import {
Client,
IHTTPConnection,
Protobuf,
SettingsManager,
Types,
} from '@meshtastic/meshtasticjs';
import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs';
import { Header } from './components/Header';
import { connection } from './connection';
import { useAppDispatch } from './hooks/redux';
import { Main } from './Main';
import { setMyId } from './slices/meshtasticSlice';
import { channelSubject$, nodeSubject$, preferencesSubject$ } from './streams';
import {
addChannel,
addNode,
setDeviceStatus,
setLastMeshInterraction,
setMyId,
setMyNodeInfo,
setPreferences,
setReady,
} from './slices/meshtasticSlice';
const App = (): JSX.Element => {
const dispatch = useAppDispatch();
const [deviceStatus, setDeviceStatus] =
React.useState<Types.DeviceStatusEnum>(
Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
);
const [connection, setConnection] = React.useState<
ISerialConnection | IHTTPConnection | IBLEConnection
>(new IHTTPConnection());
const [isReady, setIsReady] = React.useState<boolean>(false);
const [lastMeshInterraction, setLastMeshInterraction] =
React.useState<number>(0);
const [darkmode, setDarkmode] = React.useState<boolean>(false);
React.useEffect(() => {
const client = new Client();
const httpConnection = client.createHTTPConnection();
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
httpConnection.connect({
connection.connect({
address:
import.meta.env.NODE_ENV === 'production'
? window.location.hostname
@@ -47,77 +32,52 @@ const App = (): JSX.Element => {
tls: false,
fetchInterval: 2000,
});
setConnection(httpConnection);
}, []);
React.useEffect(() => {
const deviceStatusEvent = connection.onDeviceStatusEvent.subscribe(
(status) => {
setDeviceStatus(status);
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
setIsReady(true);
}
},
);
// const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo);
connection.onDeviceStatus.subscribe((status) => {
dispatch(setDeviceStatus(status));
const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(
(nodeInfo) => {
dispatch(setMyId(nodeInfo.myNodeNum));
},
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
dispatch(setReady(true));
}
});
connection.onMyNodeInfo.subscribe((nodeInfo) => {
dispatch(setMyNodeInfo(nodeInfo));
dispatch(setMyId(nodeInfo.myNodeNum));
});
connection.onNodeInfoPacket.subscribe((nodeInfoPacket) =>
dispatch(addNode(nodeInfoPacket.data)),
);
const nodeInfoPacketEvent = connection.onNodeInfoPacketEvent.subscribe(
(node) => nodeSubject$.next(node),
);
connection.onAdminPacket.subscribe((adminPacket) => {
switch (adminPacket.data.variant.oneofKind) {
case 'getChannelResponse':
dispatch(addChannel(adminPacket.data.variant.getChannelResponse));
break;
case 'getRadioResponse':
if (adminPacket.data.variant.getRadioResponse.preferences) {
dispatch(
setPreferences(
adminPacket.data.variant.getRadioResponse.preferences,
),
);
}
break;
}
});
const adminPacketEvent = connection.onAdminPacketEvent.subscribe(
(adminMessage) => {
switch (adminMessage.data.variant.oneofKind) {
case 'getChannelResponse':
channelSubject$.next(adminMessage.data.variant.getChannelResponse);
break;
case 'getRadioResponse':
if (adminMessage.data.variant.getRadioResponse.preferences) {
preferencesSubject$.next(
adminMessage.data.variant.getRadioResponse.preferences,
);
}
break;
default:
break;
}
},
connection.onMeshHeartbeat.subscribe((date) =>
dispatch(setLastMeshInterraction(date.getTime())),
);
const meshHeartbeat = connection.onMeshHeartbeat.subscribe(
setLastMeshInterraction,
);
return () => {
deviceStatusEvent?.unsubscribe();
myNodeInfoEvent?.unsubscribe();
nodeInfoPacketEvent?.unsubscribe();
adminPacketEvent?.unsubscribe();
meshHeartbeat?.unsubscribe();
connection.disconnect();
};
}, [connection]);
}, [dispatch]);
return (
<div className="flex flex-col h-screen w-screen">
<Header
status={deviceStatus}
IsReady={isReady}
LastMeshInterraction={lastMeshInterraction}
connection={connection}
/>
<Main
isReady={isReady}
connection={connection}
darkmode={darkmode}
setDarkmode={setDarkmode}
/>
<Header />
<Main />
</div>
);
};
+27 -45
View File
@@ -2,62 +2,44 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Types,
} from '@meshtastic/meshtasticjs';
import type { Types } from '@meshtastic/meshtasticjs';
import { ChatMessage } from './components/ChatMessage';
import { MessageBox } from './components/MessageBox';
import { Sidebar } from './components/Sidebar';
import { connection } from './connection';
interface MainProps {
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
isReady: boolean;
darkmode: boolean;
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Main = (props: MainProps): JSX.Element => {
export const Main = (): JSX.Element => {
const [messages, setMessages] = React.useState<
{ message: Types.TextPacket; ack: boolean }[]
>([]);
const { t } = useTranslation();
React.useEffect(() => {
const textPacketEvent = props.connection.onTextPacketEvent.subscribe(
(message) => {
setMessages((messages) => [
...messages,
{ message: message, ack: false },
]);
},
);
return () => textPacketEvent?.unsubscribe();
}, [props.connection]);
connection.onTextPacket.subscribe((message) => {
setMessages((messages) => [
...messages,
{ message: message, ack: false },
]);
});
}, []);
React.useEffect(() => {
const routingPacketEvent = props.connection.onRoutingPacketEvent.subscribe(
(routingPacket) => {
setMessages(
messages.map((message) => {
return routingPacket.packet.payloadVariant.oneofKind ===
'decoded' &&
message.message.packet.id ===
routingPacket.packet.payloadVariant.decoded.requestId
? {
ack: true,
message: message.message,
}
: message;
}),
);
},
);
return () => routingPacketEvent?.unsubscribe();
}, [props.connection, messages]);
connection.onRoutingPacket.subscribe((routingPacket) => {
setMessages(
messages.map((message) => {
return routingPacket.packet.payloadVariant.oneofKind === 'decoded' &&
message.message.packet.id ===
routingPacket.packet.payloadVariant.decoded.requestId
? {
ack: true,
message: message.message,
}
: message;
}),
);
});
}, [messages]);
return (
<div className="flex flex-col md:flex-row flex-grow m-3 space-y-2 md:space-y-0 space-x-0 md:space-x-2">
@@ -73,9 +55,9 @@ export const Main = (props: MainProps): JSX.Element => {
</div>
)}
</div>
<MessageBox connection={props.connection} isReady={props.isReady} />
<MessageBox />
</div>
<Sidebar connection={props.connection} />
<Sidebar />
</div>
);
};
+7 -33
View File
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import React from 'react';
import Avatar from 'boring-avatars';
import { useObservableSuspense } from 'observable-hooks';
import {
CheckCircleIcon,
@@ -10,47 +9,24 @@ import {
import type { Types } from '@meshtastic/meshtasticjs';
import { useAppSelector } from '../hooks/redux';
import { nodeResource } from '../streams';
interface ChatMessageProps {
message: { message: Types.TextPacket; ack: boolean };
}
export const ChatMessage = (props: ChatMessageProps): JSX.Element => {
const nodeSource = useObservableSuspense(nodeResource);
const myId = useAppSelector((state) => state.meshtastic.myId);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
const node = nodes.find((node) => {
node.num === props.message.message.packet.from;
});
React.useEffect(() => {
if (
nodes.findIndex(
(currentNode) => currentNode.data.num === nodeSource.data.num,
) >= 0
) {
setNodes(
nodes.map((currentNode) =>
currentNode.data.num === nodeSource.data.num
? nodeSource
: currentNode,
),
);
} else {
setNodes((nodes) => [...nodes, nodeSource]);
}
}, [nodeSource, nodes]);
const [node, setNode] = useState<Types.NodeInfoPacket>();
React.useEffect(() => {
setNode(
nodes.find((node) => node.data.num === props.message.message.packet.from),
);
}, [nodes, props.message]);
return (
<div className="flex items-end">
<Avatar
size={40}
name={node?.data.user?.longName ?? 'UNK'}
name={node?.user?.longName ?? 'UNK'}
variant="beam"
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
/>
@@ -70,9 +46,7 @@ export const ChatMessage = (props: ChatMessageProps): JSX.Element => {
}`}
>
<div className="flex text-xs text-gray-500 space-x-1">
<div className="font-medium">
{node?.data.user?.longName ?? 'UNK'}
</div>
<div className="font-medium">{node?.user?.longName ?? 'UNK'}</div>
<p>-</p>
<div className="underline">
{new Date(
+14 -21
View File
@@ -5,23 +5,18 @@ import {
StatusOfflineIcon,
StatusOnlineIcon,
} from '@heroicons/react/outline';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import { Types } from '@meshtastic/meshtasticjs';
import { useAppSelector } from '../hooks/redux';
import { Logo } from './Logo';
interface HeaderProps {
status: Types.DeviceStatusEnum;
IsReady: boolean;
LastMeshInterraction: number;
connection: IHTTPConnection | ISerialConnection | IBLEConnection;
}
export const Header = (): JSX.Element => {
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus);
const ready = useAppSelector((state) => state.meshtastic.ready);
const lastMeshInterraction = useAppSelector(
(state) => state.meshtastic.lastMeshInterraction,
);
export const Header = (props: HeaderProps): JSX.Element => {
return (
<nav className="select-none w-full shadow-md">
<div className="flex w-full container mx-auto justify-between px-6 py-4">
@@ -32,17 +27,15 @@ export const Header = (props: HeaderProps): JSX.Element => {
<div className="flex">
<div
className={`w-5 h-5 rounded-full ${
new Date(props.LastMeshInterraction) <
new Date(Date.now() - 40000)
new Date(lastMeshInterraction) < new Date(Date.now() - 40000)
? 'bg-red-400 animate-pulse'
: new Date(props.LastMeshInterraction) <
: new Date(lastMeshInterraction) <
new Date(Date.now() - 20000)
? 'bg-yellow-400 animate-pulse'
: 'bg-green-400'
}`}
></div>
{new Date(props.LastMeshInterraction) >
new Date(Date.now() - 40000) ? (
{new Date(lastMeshInterraction) > new Date(Date.now() - 40000) ? (
<StatusOnlineIcon className="m-auto ml-1 h-5 w-5" />
) : (
<StatusOfflineIcon className="m-auto ml-1 h-5 w-5" />
@@ -52,12 +45,12 @@ export const Header = (props: HeaderProps): JSX.Element => {
<div className="flex">
<div
className={`w-5 h-5 rounded-full ${
props.status <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
deviceStatus <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
? 'bg-red-400 animate-pulse'
: props.status <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
!props.IsReady
: deviceStatus <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
!ready
? 'bg-yellow-400 animate-pulse'
: props.IsReady
: ready
? 'bg-green-400'
: 'bg-gray-400'
}`}
+10 -18
View File
@@ -3,25 +3,17 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import { useAppDispatch } from '../hooks/redux';
import { connection } from '../connection';
import { useAppDispatch, useAppSelector } from '../hooks/redux';
import { toggleSidebar } from '../slices/appSlice';
export interface MessageBoxProps {
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
isReady: boolean;
}
export const MessageBox = (props: MessageBoxProps): JSX.Element => {
export const MessageBox = (): JSX.Element => {
const ready = useAppSelector((state) => state.meshtastic.ready);
const [currentMessage, setCurrentMessage] = React.useState('');
const sendMessage = () => {
if (props.isReady) {
props.connection.sendText(currentMessage, undefined, true);
if (ready) {
connection.sendText(currentMessage, undefined, true);
setCurrentMessage('');
}
};
@@ -45,24 +37,24 @@ export const MessageBox = (props: MessageBoxProps): JSX.Element => {
sendMessage();
}}
>
{props.isReady}
{ready}
<input
type="text"
placeholder={`${t('placeholder.no_messages')}...`}
disabled={!props.isReady}
disabled={!ready}
value={currentMessage}
onChange={(e) => {
setCurrentMessage(e.target.value);
}}
className={`p-3 placeholder-gray-400 text-gray-700 relative rounded-md border shadow-md focus:outline-none w-full pr-10 ${
props.isReady ? 'cursor-text' : 'cursor-not-allowed'
ready ? 'cursor-text' : 'cursor-not-allowed'
}`}
/>
<span className="flex z-10 h-full text-gray-400 absolute w-8 right-0">
<PaperAirplaneIcon
onClick={sendMessage}
className={`text-xl hover:text-gray-500 h-6 w-6 my-auto ${
props.isReady ? 'cursor-pointer' : 'cursor-not-allowed'
ready ? 'cursor-pointer' : 'cursor-not-allowed'
}`}
/>
</span>
+18 -21
View File
@@ -1,10 +1,6 @@
import React from 'react';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import { AnimatePresence, motion } from 'framer-motion';
import { useAppSelector } from '../hooks/redux';
import { Channels } from './Sidebar/Channels/Index';
@@ -12,23 +8,24 @@ import { Device } from './Sidebar/Device/Index';
import { Nodes } from './Sidebar/Nodes/Index';
import { UI } from './Sidebar/UI/Index';
interface SidebarProps {
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
}
export const Sidebar = (props: SidebarProps): JSX.Element => {
export const Sidebar = (): JSX.Element => {
const sidebarOpen = useAppSelector((state) => state.app.sidebarOpen);
return (
<div
className={`${
sidebarOpen ? 'flex' : 'hidden md:flex'
} flex-col rounded-md md:ml-0 shadow-md border w-full max-w-sm`}
>
<Nodes />
<Device connection={props.connection} />
<Channels />
<div className="flex-grow border-b"></div>
<UI />
</div>
<AnimatePresence>
{sidebarOpen && (
<motion.div
className={`${
sidebarOpen ? 'flex' : 'hidden md:flex'
} flex-col rounded-md md:ml-0 shadow-md border w-full max-w-sm`}
>
<Nodes />
<Device />
<Channels />
<div className="flex-grow border-b"></div>
<UI />
</motion.div>
)}
</AnimatePresence>
);
};
@@ -1,34 +1,12 @@
import React from 'react';
import { useObservableSuspense } from 'observable-hooks';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { channelResource } from '../../../streams';
import { useAppSelector } from '../../../hooks/redux';
import { Channel } from './Channel';
export const ChannelList = (): JSX.Element => {
const channelSource = useObservableSuspense(channelResource);
const [channels, setChannels] = React.useState<Protobuf.Channel[]>([]);
React.useEffect(() => {
if (
channels.findIndex(
(currentChannel) => currentChannel.index === channelSource.index,
) >= 0
) {
setChannels(
channels.map((currentChannel) =>
currentChannel.index === channelSource.index
? channelSource
: currentChannel,
),
);
} else {
setChannels((channels) => [...channels, channelSource]);
}
}, [channelSource, channels]);
const channels = useAppSelector((state) => state.meshtastic.channels);
return (
<>
+2 -11
View File
@@ -3,26 +3,17 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { AdjustmentsIcon } from '@heroicons/react/outline';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import { Dropdown } from '../../basic/Dropdown';
import { Settings } from './Settings';
interface DeviceProps {
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
}
export const Device = (props: DeviceProps): JSX.Element => {
export const Device = (): JSX.Element => {
const { t } = useTranslation();
return (
<Dropdown
icon={<AdjustmentsIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
title={t('settings.device')}
content={<Settings connection={props.connection} />}
content={<Settings />}
fallbackMessage={'Loading...'}
/>
);
+5 -16
View File
@@ -1,35 +1,24 @@
import React from 'react';
import { useObservableSuspense } from 'observable-hooks';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { SaveIcon } from '@heroicons/react/outline';
import type {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { preferencesResource } from '../../../streams';
import { connection } from '../../../connection';
import { useAppSelector } from '../../../hooks/redux';
interface SettingsProps {
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
}
export const Settings = (props: SettingsProps): JSX.Element => {
export const Settings = (): JSX.Element => {
const { t } = useTranslation();
const preferences = useObservableSuspense(preferencesResource);
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const { register, handleSubmit } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
const onSubmit = handleSubmit((data) =>
props.connection.setPreferences(data),
);
const onSubmit = handleSubmit((data) => connection.setPreferences(data));
return (
<form onSubmit={onSubmit}>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
+15 -24
View File
@@ -11,12 +11,12 @@ import {
GlobeIcon,
LightningBoltIcon,
} from '@heroicons/react/outline';
import type { Types } from '@meshtastic/meshtasticjs';
import type { Protobuf } from '@meshtastic/meshtasticjs';
import { useAppSelector } from '../../../hooks/redux';
export interface NodeProps {
node: Types.NodeInfoPacket;
node: Protobuf.NodeInfo;
}
export const Node = (props: NodeProps): JSX.Element => {
@@ -34,12 +34,12 @@ export const Node = (props: NodeProps): JSX.Element => {
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
)}
<div className="relative">
{props.node.data.num === myId ? (
{props.node.num === myId ? (
<FlagIcon className="absolute -right-1 -top-2 text-yellow-500 my-auto w-4 h-4" />
) : null}
<Avatar
size={30}
name={props.node.data.user?.longName ?? 'Unknown'}
name={props.node.user?.longName ?? 'Unknown'}
variant="beam"
colors={[
'#213435',
@@ -50,25 +50,16 @@ export const Node = (props: NodeProps): JSX.Element => {
]}
/>
</div>
{props.node.data.user?.longName}
{props.node.user?.longName}
</div>
</Disclosure.Button>
<Disclosure.Panel>
<div className="border-b bg-gray-100 px-2">
<p>
SNR:{' '}
{props.node.packet?.rxSnr ? props.node.packet.rxSnr : 'Unknown'}
</p>
<p>
RSSI:{' '}
{props.node.packet?.rxRssi
? props.node.packet.rxRssi
: 'Unknown'}
</p>
<p>{props.node.snr}</p>
<p>
{`Last heard: ${
props.node.data?.lastHeard
? new Date(props.node.data.lastHeard).toLocaleString()
props.node?.lastHeard
? new Date(props.node.lastHeard).toLocaleString()
: 'Unknown'
}`}{' '}
{}
@@ -76,23 +67,23 @@ export const Node = (props: NodeProps): JSX.Element => {
<div className="flex">
<GlobeIcon className="my-auto mr-2 w-5 h-5" />
<p>
{props.node.data.position?.latitudeI &&
props.node.data.position?.longitudeI
? `${props.node.data.position.latitudeI / 1e7},
${props.node.data.position.longitudeI / 1e7}`
{props.node.position?.latitudeI &&
props.node.position?.longitudeI
? `${props.node.position.latitudeI / 1e7},
${props.node.position.longitudeI / 1e7}`
: 'Unknown'}
, El:
{props.node.data.position?.altitude}
{props.node.position?.altitude}
</p>
</div>
<div className="flex">
<ClockIcon className="my-auto mr-2 w-5 h-5" />
<p>{props.node.data.position?.time}</p>
<p>{props.node.position?.time}</p>
</div>
<div className="flex">
<LightningBoltIcon className="my-auto mr-2 w-5 h-5" />
<p>{props.node.data.position?.batteryLevel}</p>
<p>{props.node.position?.batteryLevel}</p>
</div>
</div>
</Disclosure.Panel>
+2 -26
View File
@@ -1,34 +1,10 @@
import React from 'react';
import { useObservableSuspense } from 'observable-hooks';
import type { Types } from '@meshtastic/meshtasticjs';
import { nodeResource } from '../../../streams';
import { useAppSelector } from '../../../hooks/redux';
import { Node } from './Node';
export const NodeList = (): JSX.Element => {
const nodeSource = useObservableSuspense(nodeResource);
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
React.useEffect(() => {
if (
nodes.findIndex(
(currentNode) => currentNode.data.num === nodeSource.data.num,
) >= 0
) {
setNodes(
nodes.map((currentNode) =>
currentNode.data.num === nodeSource.data.num
? nodeSource
: currentNode,
),
);
} else {
setNodes((nodes) => [...nodes, nodeSource]);
}
}, [nodeSource, nodes]);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
return (
<>
+3
View File
@@ -0,0 +1,3 @@
import { IHTTPConnection } from '@meshtastic/meshtasticjs';
export const connection = new IHTTPConnection();
+6 -1
View File
@@ -1,11 +1,15 @@
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from '../store';
interface AppState {
sidebarOpen: boolean;
darkMode: boolean;
}
const initialState: AppState = {
sidebarOpen: true,
darkMode: false,
};
export const appSlice = createSlice({
@@ -25,5 +29,6 @@ export const appSlice = createSlice({
});
export const { openSidebar, closeSidebar, toggleSidebar } = appSlice.actions;
export const selectOpenState = (state: RootState): boolean =>
state.app.sidebarOpen;
export default appSlice.reducer;
+115 -1
View File
@@ -1,24 +1,138 @@
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
interface AppState {
deviceStatus: Types.DeviceStatusEnum;
myId: number;
lastMeshInterraction: number;
ready: boolean;
fromRaioPackets: Protobuf.FromRadio[];
meshPackets: Protobuf.MeshPacket[];
myNodeInfo: Protobuf.MyNodeInfo;
radioConfig: Protobuf.RadioConfig[];
routingPackets: Types.RoutingPacket[];
positionPackets: Types.PositionPacket[];
textPackets: Types.TextPacket[];
logRecords: Protobuf.LogRecord[];
//
nodes: Protobuf.NodeInfo[];
channels: Protobuf.Channel[];
preferences: Protobuf.RadioConfig_UserPreferences;
}
const initialState: AppState = {
deviceStatus: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
myId: 0,
lastMeshInterraction: 0,
ready: false,
fromRaioPackets: [],
meshPackets: [],
myNodeInfo: Protobuf.MyNodeInfo.create(),
radioConfig: [],
routingPackets: [],
positionPackets: [],
textPackets: [],
logRecords: [],
//
nodes: [],
channels: [],
preferences: Protobuf.RadioConfig_UserPreferences.create(),
};
export const meshtasticSlice = createSlice({
name: 'meshtastic',
initialState,
reducers: {
setDeviceStatus: (state, action: PayloadAction<Types.DeviceStatusEnum>) => {
state.deviceStatus = action.payload;
},
setMyId: (state, action: PayloadAction<number>) => {
state.myId = action.payload;
},
setLastMeshInterraction: (state, action: PayloadAction<number>) => {
state.lastMeshInterraction = action.payload;
},
setReady: (state, action: PayloadAction<boolean>) => {
state.ready = action.payload;
},
addFromRadioPacket: (state, action: PayloadAction<Protobuf.FromRadio>) => {
state.fromRaioPackets.push(action.payload);
},
addMeshPacket: (state, action: PayloadAction<Protobuf.MeshPacket>) => {
state.meshPackets.push(action.payload);
},
setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => {
state.myNodeInfo = action.payload;
},
addRadioConfig: (state, action: PayloadAction<Protobuf.RadioConfig>) => {
state.radioConfig.push(action.payload);
},
addRoutingPacket: (state, action: PayloadAction<Types.RoutingPacket>) => {
state.routingPackets.push(action.payload);
},
addPositionPacket: (state, action: PayloadAction<Types.PositionPacket>) => {
state.positionPackets.push(action.payload);
},
addTextPacket: (state, action: PayloadAction<Types.TextPacket>) => {
state.textPackets.push(action.payload);
},
addLogRecord: (state, action: PayloadAction<Protobuf.LogRecord>) => {
state.logRecords.push(action.payload);
},
//
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
if (
state.nodes.findIndex((node) => node.num === action.payload.num) !== -1
) {
state.nodes = state.nodes.map((node) => {
return node.num === action.payload.num ? action.payload : node;
});
} else {
state.nodes.push(action.payload);
}
},
addChannel: (state, action: PayloadAction<Protobuf.Channel>) => {
if (
state.channels.findIndex(
(channel) => channel.index === action.payload.index,
) !== -1
) {
state.channels = state.channels.map((channel) => {
return channel.index === action.payload.index
? action.payload
: channel;
});
} else {
state.channels.push(action.payload);
}
},
setPreferences: (
state,
action: PayloadAction<Protobuf.RadioConfig_UserPreferences>,
) => {
state.preferences = action.payload;
},
},
});
export const { setMyId } = meshtasticSlice.actions;
export const {
setDeviceStatus,
setMyId,
setLastMeshInterraction,
setReady,
addFromRadioPacket,
addMeshPacket,
setMyNodeInfo,
addRadioConfig,
addRoutingPacket,
addPositionPacket,
addTextPacket,
addLogRecord,
addNode,
addChannel,
setPreferences,
} = meshtasticSlice.actions;
export default meshtasticSlice.reducer;
+4
View File
@@ -8,6 +8,10 @@ export const store = configureStore({
app: appSlice,
meshtastic: meshtasticSlice,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export type RootState = ReturnType<typeof store.getState>;
-17
View File
@@ -1,17 +0,0 @@
import { ObservableResource } from 'observable-hooks';
import { Subject } from 'rxjs';
import type { Protobuf, Types } from '@meshtastic/meshtasticjs';
export const preferencesSubject$ =
new Subject<Protobuf.RadioConfig_UserPreferences>();
export const preferencesResource = new ObservableResource(preferencesSubject$);
export const nodeSubject$ = new Subject<Types.NodeInfoPacket>();
export const nodeResource = new ObservableResource(nodeSubject$);
export const channelSubject$ = new Subject<Protobuf.Channel>();
export const channelResource = new ObservableResource(channelSubject$);