Initial 1.3.x rework
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import type React from "react";
|
||||
|
||||
import { majorScale, Pane } from "evergreen-ui";
|
||||
|
||||
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||
import { DeviceWrapper } from "@app/DeviceWrapper.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
|
||||
import { NoDevice } from "../misc/NoDevice.js";
|
||||
import { Progress } from "../Progress.js";
|
||||
import { Header } from "./Header.js";
|
||||
import { Sidebar } from "./Sidebar/index.js";
|
||||
|
||||
export interface AppLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AppLayout = ({ children }: AppLayoutProps): JSX.Element => {
|
||||
const { getDevices } = useDeviceStore();
|
||||
const { selectedDevice } = useAppStore();
|
||||
|
||||
const devices = getDevices();
|
||||
|
||||
return (
|
||||
<Pane
|
||||
width="100vw"
|
||||
display="flex"
|
||||
background="tint1"
|
||||
flexDirection="column"
|
||||
minHeight="100vh"
|
||||
>
|
||||
<Header />
|
||||
<Pane display="flex" flex={1} height="100%" width="100%">
|
||||
{devices.length ? (
|
||||
devices.map((device, index) => (
|
||||
<Pane
|
||||
key={index}
|
||||
width="100%"
|
||||
height="100%"
|
||||
display={index === selectedDevice ? "grid" : "none"}
|
||||
gap={majorScale(3)}
|
||||
gridTemplateColumns="16rem 1fr"
|
||||
>
|
||||
<DeviceWrapper device={device}>
|
||||
{device && device.ready ? (
|
||||
<>
|
||||
<Sidebar />
|
||||
<Pane height="100%" display="flex">
|
||||
{children}
|
||||
</Pane>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pane
|
||||
width="100%"
|
||||
flexGrow={1}
|
||||
margin={majorScale(3)}
|
||||
borderRadius={majorScale(1)}
|
||||
background="white"
|
||||
elevation={1}
|
||||
/>
|
||||
<Progress />
|
||||
</>
|
||||
)}
|
||||
</DeviceWrapper>
|
||||
</Pane>
|
||||
))
|
||||
) : (
|
||||
<NoDevice />
|
||||
)}
|
||||
</Pane>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
CrossIcon,
|
||||
GlobeIcon,
|
||||
IconButton,
|
||||
Link,
|
||||
majorScale,
|
||||
Pane,
|
||||
PlusIcon,
|
||||
StatusIndicator,
|
||||
Tab,
|
||||
Tablist,
|
||||
Tooltip,
|
||||
} from "evergreen-ui";
|
||||
import { FiGithub } from "react-icons/fi";
|
||||
|
||||
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||
import { NewDevice } from "@components/SlideSheets/NewDevice.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { Types } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export const Header = (): JSX.Element => {
|
||||
const { getDevices, removeDevice } = useDeviceStore();
|
||||
const [newConnectionOpen, setNewConnectionOpen] = useState(false);
|
||||
const { selectedDevice, setSelectedDevice } = useAppStore();
|
||||
|
||||
return (
|
||||
<Pane
|
||||
is="nav"
|
||||
width="100%"
|
||||
position="sticky"
|
||||
top={0}
|
||||
backgroundColor="white"
|
||||
zIndex={10}
|
||||
height={majorScale(8)}
|
||||
flexShrink={0}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
borderBottom="muted"
|
||||
>
|
||||
<NewDevice
|
||||
open={newConnectionOpen}
|
||||
onClose={() => {
|
||||
setNewConnectionOpen(false);
|
||||
}}
|
||||
/>
|
||||
<Pane
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
width={majorScale(12)}
|
||||
marginRight={majorScale(22)}
|
||||
>
|
||||
<Link href="/">
|
||||
<Pane
|
||||
is="img"
|
||||
width={100}
|
||||
height={28}
|
||||
src="/Logo_Black.svg"
|
||||
cursor="pointer"
|
||||
/>
|
||||
</Link>
|
||||
</Pane>
|
||||
<Tablist display="flex" marginX={majorScale(4)}>
|
||||
{getDevices().map((device, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
gap={majorScale(1)}
|
||||
isSelected={index === selectedDevice}
|
||||
onSelect={() => {
|
||||
setSelectedDevice(index);
|
||||
}}
|
||||
>
|
||||
<Hashicon value={device.hardware.myNodeNum.toString()} size={20} />
|
||||
{device.nodes.find((n) => n.data.num === device.hardware.myNodeNum)
|
||||
?.data.user?.shortName ?? "UNK"}
|
||||
<StatusIndicator
|
||||
color={
|
||||
[
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
||||
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
|
||||
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
|
||||
].includes(device.status)
|
||||
? "success"
|
||||
: [
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTING,
|
||||
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
||||
].includes(device.status)
|
||||
? "warning"
|
||||
: "danger"
|
||||
}
|
||||
/>
|
||||
</Tab>
|
||||
))}
|
||||
</Tablist>
|
||||
<Pane
|
||||
display="flex"
|
||||
marginLeft="auto"
|
||||
gap={majorScale(1)}
|
||||
marginRight={majorScale(2)}
|
||||
>
|
||||
<Tooltip content="Connect new device">
|
||||
<Button
|
||||
display="inline-flex"
|
||||
marginY="auto"
|
||||
appearance="primary"
|
||||
iconBefore={<PlusIcon />}
|
||||
onClick={() => {
|
||||
setNewConnectionOpen(true);
|
||||
}}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{getDevices().length !== 0 && (
|
||||
<Tooltip content="Disconnect active device">
|
||||
<Button
|
||||
iconAfter={CrossIcon}
|
||||
onClick={() => {
|
||||
removeDevice(selectedDevice ?? 0);
|
||||
}}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip content="Visit GitHub">
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://github.com/meshtastic/meshtastic-web"
|
||||
>
|
||||
<IconButton icon={FiGithub} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip content="Visit Meshtastic.org">
|
||||
<Link target="_blank" href="https://meshtastic.org/">
|
||||
<IconButton icon={GlobeIcon} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</Pane>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import type React from "react";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Heading,
|
||||
Link,
|
||||
majorScale,
|
||||
MapMarkerIcon,
|
||||
Pane,
|
||||
} from "evergreen-ui";
|
||||
import { FiBluetooth, FiTerminal, FiWifi } from "react-icons/fi";
|
||||
|
||||
import { toMGRS } from "@app/core/utils/toMGRS.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { Types } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export const DeviceCard = (): JSX.Element => {
|
||||
const { hardware, nodes, status, connection } = useDevice();
|
||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
||||
|
||||
return (
|
||||
<Pane
|
||||
display="flex"
|
||||
flexGrow={1}
|
||||
flexDirection="column"
|
||||
marginTop="auto"
|
||||
gap={majorScale(1)}
|
||||
>
|
||||
<Pane display="flex" gap={majorScale(2)}>
|
||||
<Hashicon value={hardware.myNodeNum.toString()} size={42} />
|
||||
<Pane flexGrow={1}>
|
||||
<Heading>{myNode?.data.user?.longName}</Heading>
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://github.com/meshtastic/meshtastic-web/releases/"
|
||||
>
|
||||
<Badge
|
||||
color="green"
|
||||
width="100%"
|
||||
marginRight={8}
|
||||
display="flex"
|
||||
marginTop={4}
|
||||
>
|
||||
{hardware.firmwareVersion}
|
||||
</Badge>
|
||||
</Link>
|
||||
</Pane>
|
||||
</Pane>
|
||||
<Pane display="flex" gap={majorScale(1)}>
|
||||
<MapMarkerIcon />
|
||||
<Badge
|
||||
color={myNode?.data.position?.latitudeI ? "green" : "red"}
|
||||
display="flex"
|
||||
width="100%"
|
||||
>
|
||||
{toMGRS(
|
||||
myNode?.data.position?.latitudeI,
|
||||
myNode?.data.position?.longitudeI
|
||||
)}
|
||||
</Badge>
|
||||
</Pane>
|
||||
<Pane display="flex" gap={majorScale(1)}>
|
||||
{connection?.connType === "ble" && <FiBluetooth />}
|
||||
{connection?.connType === "http" && <FiWifi />}
|
||||
{connection?.connType === "serial" && <FiTerminal />}
|
||||
<Badge
|
||||
color={
|
||||
[
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
||||
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
|
||||
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
|
||||
].includes(status)
|
||||
? "green"
|
||||
: [
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTING,
|
||||
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
||||
].includes(status)
|
||||
? "orange"
|
||||
: "red"
|
||||
}
|
||||
display="flex"
|
||||
width="100%"
|
||||
>
|
||||
{[
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
||||
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
|
||||
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
|
||||
].includes(status)
|
||||
? "Connected"
|
||||
: [
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTING,
|
||||
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
|
||||
Types.DeviceStatusEnum.DEVICE_CONNECTED,
|
||||
].includes(status)
|
||||
? "Connecting"
|
||||
: "Disconnected"}
|
||||
</Badge>
|
||||
</Pane>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Device = (): JSX.Element => {
|
||||
const deviceConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.config.device,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit, formState, reset } =
|
||||
useForm<Protobuf.Config_DeviceConfig>({
|
||||
defaultValues: deviceConfig,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(deviceConfig);
|
||||
}, [reset, deviceConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
void connection.setConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'device',
|
||||
device: data,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox
|
||||
label="Serial Console Disabled"
|
||||
{...register('serialDisabled')}
|
||||
/>
|
||||
<Checkbox label="Factory Reset Device" {...register('factoryReset')} />
|
||||
<Checkbox label="Enabled Debug Log" {...register('debugLogEnabled')} />
|
||||
<Checkbox
|
||||
label="Disable Serial COnsole"
|
||||
{...register('serialDisabled')}
|
||||
/>
|
||||
<Select
|
||||
label="Role"
|
||||
optionsEnum={Protobuf.Config_DeviceConfig_Role}
|
||||
{...register('role', { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Display = (): JSX.Element => {
|
||||
const displayConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.config.display,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit, formState, reset } =
|
||||
useForm<Protobuf.Config_DisplayConfig>({
|
||||
defaultValues: displayConfig,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(displayConfig);
|
||||
}, [reset, displayConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
void connection.setConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'display',
|
||||
display: data,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Input
|
||||
label="Screen Timeout"
|
||||
type="number"
|
||||
suffix="Seconds"
|
||||
{...register('screenOnSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Carousel Delay"
|
||||
type="number"
|
||||
suffix="Seconds"
|
||||
{...register('autoScreenCarouselSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Select
|
||||
label="GPS Display Units"
|
||||
optionsEnum={Protobuf.Config_DisplayConfig_GpsCoordinateFormat}
|
||||
{...register('gpsFormat', { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,182 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
FiActivity,
|
||||
FiAlignLeft,
|
||||
FiBell,
|
||||
FiFastForward,
|
||||
FiLayers,
|
||||
FiLayout,
|
||||
FiMapPin,
|
||||
FiMessageSquare,
|
||||
FiPackage,
|
||||
FiPower,
|
||||
FiRss,
|
||||
FiSmartphone,
|
||||
FiTv,
|
||||
FiUser,
|
||||
FiWifi,
|
||||
} from 'react-icons/fi';
|
||||
|
||||
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
||||
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
|
||||
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
|
||||
import { ChannelsGroup } from '@components/layout/Sidebar/Settings/channels/ChannelsGroup';
|
||||
import { Display } from '@components/layout/Sidebar/Settings/Display';
|
||||
import { Position } from '@app/components/layout/Sidebar/Settings/Position';
|
||||
import { Interface } from '@components/layout/Sidebar/Settings/Interface';
|
||||
import { LoRa } from '@components/layout/Sidebar/Settings/LoRa';
|
||||
import { CannedMessage } from '@components/layout/Sidebar/Settings/modules/CannedMessage';
|
||||
import { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/modules/ExternalNotifications';
|
||||
import { MQTT } from '@components/layout/Sidebar/Settings/modules/MQTT';
|
||||
import { RangeTestSettingsPanel } from '@components/layout/Sidebar/Settings/modules/RangeTest';
|
||||
import { SerialSettingsPanel } from '@components/layout/Sidebar/Settings/modules/Serial';
|
||||
import { StoreForwardSettingsPanel } from '@components/layout/Sidebar/Settings/modules/StoreForward';
|
||||
import { Telemetry } from '@components/layout/Sidebar/Settings/modules/Telemetry';
|
||||
import { Power } from '@components/layout/Sidebar/Settings/Power';
|
||||
import { User } from '@components/layout/Sidebar/Settings/User';
|
||||
import { WiFi } from '@components/layout/Sidebar/Settings/WiFi';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
export interface SettingsProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
|
||||
const [modulesOpen, setModulesOpen] = useState(false);
|
||||
const [channelsOpen, setChannelsOpen] = useState(false);
|
||||
const moduleConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.moduleConfig,
|
||||
);
|
||||
|
||||
const hasGps = true;
|
||||
const hasWifi = true;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarOverlay
|
||||
title="Settings"
|
||||
open={open}
|
||||
close={(): void => {
|
||||
setOpen(false);
|
||||
}}
|
||||
direction="y"
|
||||
>
|
||||
<CollapsibleSection icon={<FiUser />} title="User">
|
||||
<User />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiSmartphone />} title="Device">
|
||||
<WiFi />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiMapPin />} title="Position">
|
||||
<Position />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiPower />} title="Power">
|
||||
<Power />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiWifi />} title="WiFi">
|
||||
<WiFi />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiTv />} title="Display">
|
||||
<Display />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection icon={<FiRss />} title="LoRa">
|
||||
<LoRa />
|
||||
</CollapsibleSection>
|
||||
<ExternalSection
|
||||
onClick={(): void => {
|
||||
setChannelsOpen(true);
|
||||
}}
|
||||
icon={<FiLayers />}
|
||||
title="Channels"
|
||||
/>
|
||||
<ExternalSection
|
||||
onClick={(): void => {
|
||||
setModulesOpen(true);
|
||||
}}
|
||||
icon={<FiPackage />}
|
||||
title="Modules"
|
||||
/>
|
||||
<CollapsibleSection icon={<FiLayout />} title="Interface">
|
||||
<Interface />
|
||||
</CollapsibleSection>
|
||||
</SidebarOverlay>
|
||||
|
||||
{/* Modules */}
|
||||
<SidebarOverlay
|
||||
title="Modules"
|
||||
open={modulesOpen}
|
||||
close={(): void => {
|
||||
setModulesOpen(false);
|
||||
}}
|
||||
direction="x"
|
||||
>
|
||||
<CollapsibleSection
|
||||
icon={<FiWifi />}
|
||||
title="MQTT"
|
||||
status={!moduleConfig.mqtt.disabled}
|
||||
>
|
||||
<MQTT />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiAlignLeft />}
|
||||
title="Serial"
|
||||
status={moduleConfig.serial.enabled}
|
||||
>
|
||||
<SerialSettingsPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiBell />}
|
||||
title="External Notifications"
|
||||
status={moduleConfig.extNotification.enabled}
|
||||
>
|
||||
<ExternalNotificationsSettingsPlanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiFastForward />}
|
||||
title="Store & Forward"
|
||||
status={moduleConfig.storeForward.enabled}
|
||||
>
|
||||
<StoreForwardSettingsPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiRss />}
|
||||
title="Range Test"
|
||||
status={moduleConfig.rangeTest.enabled}
|
||||
>
|
||||
<RangeTestSettingsPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiActivity />}
|
||||
title="Telemetry"
|
||||
status={true}
|
||||
>
|
||||
<Telemetry />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
icon={<FiMessageSquare />}
|
||||
title="Canned Message"
|
||||
status={moduleConfig.cannedMessage.enabled}
|
||||
>
|
||||
<CannedMessage />
|
||||
</CollapsibleSection>
|
||||
</SidebarOverlay>
|
||||
{/* End Modules */}
|
||||
|
||||
{/* Channels */}
|
||||
<SidebarOverlay
|
||||
title="Channels"
|
||||
open={channelsOpen}
|
||||
close={(): void => {
|
||||
setChannelsOpen(false);
|
||||
}}
|
||||
direction="x"
|
||||
>
|
||||
<ChannelsGroup />
|
||||
</SidebarOverlay>
|
||||
{/* End Channels */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
|
||||
export const Interface = (): JSX.Element => {
|
||||
return (
|
||||
<Select
|
||||
label="Language"
|
||||
options={[
|
||||
{
|
||||
name: 'English',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
name: '日本',
|
||||
value: 'jp',
|
||||
},
|
||||
{
|
||||
name: 'Português',
|
||||
value: 'pt',
|
||||
},
|
||||
]}
|
||||
onChange={(e): void => {
|
||||
console.log('changed language');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,141 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const LoRa = (): JSX.Element => {
|
||||
const myNodeNum = useAppSelector(
|
||||
(state) => state.meshtastic.radio.hardware.myNodeNum,
|
||||
);
|
||||
const loraConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.config.lora,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [usePreset, setUsePreset] = useState(true);
|
||||
// const { register, handleSubmit, formState, reset } =
|
||||
// useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||
// defaultValues: preferences,
|
||||
// });
|
||||
const { register, handleSubmit, formState, reset } =
|
||||
useForm<Protobuf.Config_LoRaConfig>({
|
||||
defaultValues: loraConfig,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(loraConfig);
|
||||
}, [reset, loraConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
|
||||
const packet = Protobuf.AdminMessage.create({
|
||||
variant: {
|
||||
oneofKind: 'setConfig',
|
||||
setConfig: {
|
||||
payloadVariant: {
|
||||
oneofKind: 'lora',
|
||||
lora: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
void connection.sendPacket(
|
||||
Protobuf.AdminMessage.toBinary(packet),
|
||||
Protobuf.PortNum.ADMIN_APP,
|
||||
myNodeNum,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
async (num) => {
|
||||
return await Promise.resolve();
|
||||
},
|
||||
);
|
||||
// void connection.setPreferences(data, async () => {
|
||||
// reset({ ...data });
|
||||
// setLoading(false);
|
||||
// await Promise.resolve();
|
||||
// });
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={usePreset}
|
||||
label="Use Presets"
|
||||
onChange={(e): void => setUsePreset(e.target.checked)}
|
||||
/>
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
{usePreset ? (
|
||||
<Select
|
||||
label="Preset"
|
||||
optionsEnum={Protobuf.Config_LoRaConfig_ModemPreset}
|
||||
{...register('modemPreset', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
label="Bandwidth"
|
||||
type="number"
|
||||
suffix="MHz"
|
||||
{...register('bandwidth', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Spread Factor"
|
||||
type="number"
|
||||
suffix="CPS"
|
||||
min={7}
|
||||
max={12}
|
||||
{...register('spreadFactor', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Coding Rate"
|
||||
type="number"
|
||||
{...register('codingRate', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Input
|
||||
label="Transmit Power"
|
||||
type="number"
|
||||
suffix="dBm"
|
||||
{...register('txPower', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Hop Count"
|
||||
type="number"
|
||||
suffix="Hops"
|
||||
{...register('hopLimit', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox label="Transmit Disabled" {...register('txDisabled')} />
|
||||
<Input
|
||||
label="Frequency Offset"
|
||||
type="number"
|
||||
suffix="Hz"
|
||||
{...register('frequencyOffset', { valueAsNumber: true })}
|
||||
/>
|
||||
<Select
|
||||
label="Region"
|
||||
optionsEnum={Protobuf.Config_LoRaConfig_RegionCode}
|
||||
{...register('region', { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,140 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { MultiSelect } from 'react-multi-select-component';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { Label } from '@components/generic/form/Label';
|
||||
import { connection } from '@core/connection';
|
||||
import { bitwiseDecode, bitwiseEncode } from '@core/utils/bitwise';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Position = (): JSX.Element => {
|
||||
const positionConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.config.position,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.Config_PositionConfig>({
|
||||
defaultValues: positionConfig,
|
||||
// defaultValues: {
|
||||
// ...preferences,
|
||||
// positionBroadcastSecs:
|
||||
// preferences.positionBroadcastSecs === 0
|
||||
// ? preferences.role === Protobuf.Role.Router
|
||||
// ? 43200
|
||||
// : 900
|
||||
// : preferences.positionBroadcastSecs,
|
||||
// },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(positionConfig);
|
||||
}, [reset, positionConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
void connection.setConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'position',
|
||||
position: data,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Input
|
||||
label="Broadcast Interval"
|
||||
type="number"
|
||||
suffix="Seconds"
|
||||
{...register('positionBroadcastSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Disable Smart Position"
|
||||
{...register('positionBroadcastSmartDisabled')}
|
||||
/>
|
||||
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
|
||||
<Checkbox
|
||||
label="Disable Location Sharing"
|
||||
{...register('locationShareDisabled')}
|
||||
/>
|
||||
<Checkbox label="Disable GPS" {...register('gpsDisabled')} />
|
||||
<Input
|
||||
label="GPS Update Interval"
|
||||
type="number"
|
||||
suffix="Seconds"
|
||||
{...register('gpsUpdateInterval', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Last GPS Attempt"
|
||||
disabled
|
||||
{...register('gpsAttemptTime', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox label="Accept 2D Fix" {...register('gpsAccept2D')} />
|
||||
<Input
|
||||
label="Max DOP"
|
||||
type="number"
|
||||
{...register('gpsMaxDop', { valueAsNumber: true })}
|
||||
/>
|
||||
<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} />}
|
||||
<MultiSelect
|
||||
options={Object.entries(
|
||||
Protobuf.Config_PositionConfig_PositionFlags,
|
||||
)
|
||||
.filter((value) => typeof value[1] !== 'number')
|
||||
.filter(
|
||||
(value) =>
|
||||
parseInt(value[0]) !==
|
||||
Protobuf.Config_PositionConfig_PositionFlags
|
||||
.POS_UNDEFINED,
|
||||
)
|
||||
.map((value) => {
|
||||
return {
|
||||
value: parseInt(value[0]),
|
||||
label: value[1].toString().replace('POS_', ''),
|
||||
};
|
||||
})}
|
||||
value={bitwiseDecode(
|
||||
value,
|
||||
Protobuf.Config_PositionConfig_PositionFlags,
|
||||
).map((flag) => {
|
||||
return {
|
||||
value: flag,
|
||||
label: Protobuf.Config_PositionConfig_PositionFlags[
|
||||
flag
|
||||
].replace('POS_', ''),
|
||||
};
|
||||
})}
|
||||
onChange={(e: { value: number; label: string }[]): void =>
|
||||
onChange(bitwiseEncode(e.map((v) => v.value)))
|
||||
}
|
||||
labelledBy="Select"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Power = (): JSX.Element => {
|
||||
const powerConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.config.power,
|
||||
);
|
||||
const deviceConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.config.device,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit, formState, reset } =
|
||||
useForm<Protobuf.Config_PowerConfig>({
|
||||
defaultValues: powerConfig,
|
||||
// defaultValues: {
|
||||
// ...preferences,
|
||||
// isLowPower:
|
||||
// preferences.role === Protobuf.Role.Router
|
||||
// ? true
|
||||
// : preferences.isLowPower,
|
||||
// },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(powerConfig);
|
||||
}, [reset, powerConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
void connection.setConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'power',
|
||||
power: data,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Select
|
||||
label="Charge current"
|
||||
optionsEnum={Protobuf.Config_PowerConfig_ChargeCurrent}
|
||||
{...register('chargeCurrent', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Powered by low power source (solar)"
|
||||
disabled={
|
||||
deviceConfig.role === Protobuf.Config_DeviceConfig_Role.Router
|
||||
}
|
||||
validationMessage={
|
||||
deviceConfig.role === Protobuf.Config_DeviceConfig_Role.Router
|
||||
? 'Enabled by default in router mode'
|
||||
: ''
|
||||
}
|
||||
{...register('isLowPower')}
|
||||
/>
|
||||
<Checkbox label="Always Powered" {...register('isAlwaysPowered')} />
|
||||
<Input
|
||||
label="Shutdown on battery delay"
|
||||
type="number"
|
||||
suffix="Seconds"
|
||||
{...register('onBatteryShutdownAfterSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox label="Power Saving" {...register('isPowerSaving')} />
|
||||
<Input
|
||||
label="ADC Multiplier Override ratio"
|
||||
type="number"
|
||||
{...register('adcMultiplierOverride', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Minumum Wake Time"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register('minWakeSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Phone Timeout"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register('phoneTimeoutSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Mesh SDS Timeout"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register('meshSdsTimeoutSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="SDS"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register('sdsSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="LS"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register('lsSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Wait Bluetooth"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register('waitBluetoothSecs', { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { base16 } from 'rfc4648';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const User = (): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const myNodeNum = useAppSelector(
|
||||
(state) => state.meshtastic.radio.hardware,
|
||||
).myNodeNum;
|
||||
const node = useAppSelector((state) => state.meshtastic.nodes).find(
|
||||
(node) => node.data.num === myNodeNum,
|
||||
);
|
||||
const { register, handleSubmit, formState, reset } = useForm<{
|
||||
longName: string;
|
||||
shortName: string;
|
||||
isLicensed: boolean;
|
||||
antAzimuth: number;
|
||||
antGainDbi: number;
|
||||
txPowerDbm: number;
|
||||
}>({
|
||||
defaultValues: {
|
||||
longName: node?.data.user?.longName,
|
||||
shortName: node?.data.user?.shortName,
|
||||
isLicensed: node?.data.user?.isLicensed,
|
||||
antAzimuth: node?.data.user?.antAzimuth,
|
||||
antGainDbi: node?.data.user?.antGainDbi,
|
||||
txPowerDbm: node?.data.user?.txPowerDbm,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
longName: node?.data.user?.longName,
|
||||
shortName: node?.data.user?.shortName,
|
||||
isLicensed: node?.data.user?.isLicensed,
|
||||
});
|
||||
}, [reset, node]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
|
||||
if (node?.data.user) {
|
||||
void connection.setOwner({ ...node.data.user, ...data }, async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Input label="Device ID" value={node?.data.user?.id} disabled />
|
||||
<Input label="Device Name" {...register('longName')} />
|
||||
<Input label="Short Name" maxLength={3} {...register('shortName')} />
|
||||
<Input
|
||||
label="Mac Address"
|
||||
defaultValue={
|
||||
base16
|
||||
.stringify(node?.data.user?.macaddr ?? [])
|
||||
.match(/.{1,2}/g)
|
||||
?.join(':') ?? ''
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
label="Hardware (DEPRECATED)"
|
||||
value={
|
||||
Protobuf.HardwareModel[
|
||||
node?.data.user?.hwModel ?? Protobuf.HardwareModel.UNSET
|
||||
]
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
<Checkbox label="Licenced Operator?" {...register('isLicensed')} />
|
||||
<Input
|
||||
label="Transmit Power"
|
||||
suffix="dBm"
|
||||
type="number"
|
||||
{...register('txPowerDbm', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Antenna Gain"
|
||||
suffix="dBi"
|
||||
type="number"
|
||||
{...register('antGainDbi', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Antenna Azimuth"
|
||||
suffix="°"
|
||||
type="number"
|
||||
{...register('antAzimuth', { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const WiFi = (): JSX.Element => {
|
||||
const wifiConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.config.wifi,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.Config_WiFiConfig>({
|
||||
defaultValues: wifiConfig,
|
||||
});
|
||||
|
||||
const WifiApMode = useWatch({
|
||||
control,
|
||||
name: 'apMode',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(wifiConfig);
|
||||
}, [reset, wifiConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
void connection.setConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'wifi',
|
||||
wifi: data,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox label="Enable WiFi AP" {...register('apMode')} />
|
||||
<Input label="WiFi SSID" disabled={WifiApMode} {...register('ssid')} />
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
label="WiFi PSK"
|
||||
disabled={WifiApMode}
|
||||
{...register('psk')}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { fromByteArray, toByteArray } from 'base64-js';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
|
||||
|
||||
import { IconButton } from '@components/generic/button/IconButton';
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export interface SettingsPanelProps {
|
||||
channel: Protobuf.Channel;
|
||||
}
|
||||
|
||||
export const Channels = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [keySize, setKeySize] = useState<128 | 256>(256);
|
||||
const [pskHidden, setPskHidden] = useState(true);
|
||||
|
||||
const { register, handleSubmit, setValue, formState, reset } = useForm<
|
||||
Omit<Protobuf.ChannelSettings, 'psk'> & { psk: string; enabled: boolean }
|
||||
>({
|
||||
defaultValues: {
|
||||
enabled: [
|
||||
Protobuf.Channel_Role.SECONDARY,
|
||||
Protobuf.Channel_Role.PRIMARY,
|
||||
].find((role) => role === channel?.role)
|
||||
? true
|
||||
: false,
|
||||
...channel?.settings,
|
||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
enabled: [
|
||||
Protobuf.Channel_Role.SECONDARY,
|
||||
Protobuf.Channel_Role.PRIMARY,
|
||||
].find((role) => role === channel?.role)
|
||||
? true
|
||||
: false,
|
||||
...channel?.settings,
|
||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||
});
|
||||
}, [channel, reset]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setLoading(true);
|
||||
const channelData = Protobuf.Channel.create({
|
||||
role:
|
||||
channel?.role === Protobuf.Channel_Role.PRIMARY
|
||||
? Protobuf.Channel_Role.PRIMARY
|
||||
: data.enabled
|
||||
? Protobuf.Channel_Role.SECONDARY
|
||||
: Protobuf.Channel_Role.DISABLED,
|
||||
index: channel?.index,
|
||||
settings: {
|
||||
...data,
|
||||
psk: toByteArray(data.psk ?? ''),
|
||||
},
|
||||
});
|
||||
|
||||
await connection.setChannel(channelData, (): Promise<void> => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
{channel?.index !== 0 && (
|
||||
<>
|
||||
<Checkbox
|
||||
label="Enabled"
|
||||
{...register('enabled', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input label="Name" {...register('name')} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Select
|
||||
label="Key Size"
|
||||
options={[
|
||||
{ name: '128 Bit', value: 128 },
|
||||
{ name: '256 Bit', value: 256 },
|
||||
]}
|
||||
value={keySize}
|
||||
onChange={(e): void => {
|
||||
setKeySize(parseInt(e.target.value) as 128 | 256);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label="Pre-Shared Key"
|
||||
type={pskHidden ? 'password' : 'text'}
|
||||
disabled
|
||||
action={
|
||||
<>
|
||||
<IconButton
|
||||
onClick={(): void => {
|
||||
setPskHidden(!pskHidden);
|
||||
}}
|
||||
icon={pskHidden ? <MdVisibility /> : <MdVisibilityOff />}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(): void => {
|
||||
const key = new Uint8Array(keySize);
|
||||
crypto.getRandomValues(key);
|
||||
setValue('psk', fromByteArray(key));
|
||||
}}
|
||||
icon={<MdRefresh />}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
{...register('psk')}
|
||||
/>
|
||||
<Checkbox label="Uplink Enabled" {...register('uplinkEnabled')} />
|
||||
<Checkbox label="Downlink Enabled" {...register('downlinkEnabled')} />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
|
||||
import { Channels } from '@components/layout/Sidebar/Settings/channels/Channels';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const ChannelsGroup = (): JSX.Element => {
|
||||
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
|
||||
|
||||
return (
|
||||
<>
|
||||
{channels.map((channel) => {
|
||||
return (
|
||||
<div key={channel.index}>
|
||||
<CollapsibleSection
|
||||
title={
|
||||
channel.settings?.name.length
|
||||
? channel.settings.name
|
||||
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||
? 'Primary'
|
||||
: `Channel: ${channel.index}`
|
||||
}
|
||||
icon={
|
||||
<div
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
channel.role === Protobuf.Channel_Role.PRIMARY
|
||||
? 'bg-orange-500'
|
||||
: channel.role === Protobuf.Channel_Role.SECONDARY
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-500'
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Channels channel={channel} />
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const CannedMessage = (): JSX.Element => {
|
||||
const cannedMessageConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.moduleConfig.cannedMessage,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.ModuleConfig_CannedMessageConfig>({
|
||||
defaultValues: cannedMessageConfig,
|
||||
});
|
||||
|
||||
const moduleEnabled = useWatch({
|
||||
control,
|
||||
name: 'rotary1Enabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(cannedMessageConfig);
|
||||
}, [reset, cannedMessageConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
void connection.setModuleConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'cannedMessage',
|
||||
cannedMessage: data,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox label="Module Enabled" {...register('enabled')} />
|
||||
<Checkbox
|
||||
label="Rotary Encoder #1 Enabled"
|
||||
{...register('rotary1Enabled')}
|
||||
/>
|
||||
<Input
|
||||
label="Encoder Pin A"
|
||||
type="number"
|
||||
disabled={moduleEnabled}
|
||||
{...register('inputbrokerPinA', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Encoder Pin B"
|
||||
type="number"
|
||||
disabled={moduleEnabled}
|
||||
{...register('inputbrokerPinB', { valueAsNumber: true })}
|
||||
/>
|
||||
<Input
|
||||
label="Endoer Pin Press"
|
||||
type="number"
|
||||
disabled={moduleEnabled}
|
||||
{...register('inputbrokerPinPress', { valueAsNumber: true })}
|
||||
/>
|
||||
<Select
|
||||
label="Clockwise event"
|
||||
disabled={moduleEnabled}
|
||||
optionsEnum={Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar}
|
||||
{...register('inputbrokerEventCw', { valueAsNumber: true })}
|
||||
/>
|
||||
<Select
|
||||
label="Counter Clockwise event"
|
||||
disabled={moduleEnabled}
|
||||
optionsEnum={Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar}
|
||||
{...register('inputbrokerEventCcw', { valueAsNumber: true })}
|
||||
/>
|
||||
<Select
|
||||
label="Press event"
|
||||
disabled={moduleEnabled}
|
||||
optionsEnum={Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar}
|
||||
{...register('inputbrokerEventPress', { valueAsNumber: true })}
|
||||
/>
|
||||
<Checkbox label="Up Down enabled" {...register('updown1Enabled')} />
|
||||
<Input
|
||||
label="Allow Input Source"
|
||||
disabled={moduleEnabled}
|
||||
{...register('allowInputSource')}
|
||||
/>
|
||||
<Checkbox label="Send Bell" {...register('sendBell')} />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const extNotificationConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.moduleConfig.extNotification,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.ModuleConfig_ExternalNotificationConfig>({
|
||||
defaultValues: extNotificationConfig,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(extNotificationConfig);
|
||||
}, [reset, extNotificationConfig]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setLoading(true);
|
||||
await connection.setModuleConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'externalNotification',
|
||||
externalNotification: data,
|
||||
},
|
||||
},
|
||||
async (): Promise<void> => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const moduleEnabled = useWatch({
|
||||
control,
|
||||
name: 'enabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox label="Module Enabled" {...register('enabled')} />
|
||||
<Input
|
||||
type="number"
|
||||
label="Output MS"
|
||||
suffix="ms"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('outputMs', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Output"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('output', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Active"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('active')}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Message"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('alertMessage')}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Bell"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('alertBell')}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const MQTT = (): JSX.Element => {
|
||||
const mqttConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.moduleConfig.mqtt,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.ModuleConfig_MQTTConfig>({
|
||||
defaultValues: mqttConfig,
|
||||
});
|
||||
|
||||
const moduleEnabled = useWatch({
|
||||
control,
|
||||
name: 'disabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(mqttConfig);
|
||||
}, [reset, mqttConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
void connection.setModuleConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'mqtt',
|
||||
mqtt: data,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox label="Module Disabled" {...register('disabled')} />
|
||||
<Input
|
||||
label="MQTT Server Address"
|
||||
disabled={moduleEnabled}
|
||||
{...register('address')}
|
||||
/>
|
||||
<Input
|
||||
label="MQTT Username"
|
||||
disabled={moduleEnabled}
|
||||
{...register('username')}
|
||||
/>
|
||||
<Input
|
||||
label="MQTT Password"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
disabled={moduleEnabled}
|
||||
{...register('password')}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Encryption Enabled"
|
||||
disabled={moduleEnabled}
|
||||
{...register('encryptionEnabled')}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const RangeTestSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const rangeTestConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.moduleConfig.rangeTest,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.ModuleConfig_RangeTestConfig>({
|
||||
defaultValues: rangeTestConfig,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(rangeTestConfig);
|
||||
}, [reset, rangeTestConfig]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setLoading(true);
|
||||
await connection.setModuleConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'rangeTest',
|
||||
rangeTest: data,
|
||||
},
|
||||
},
|
||||
async (): Promise<void> => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const moduleEnabled = useWatch({
|
||||
control,
|
||||
name: 'enabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox label="Module Enabled" {...register('enabled')} />
|
||||
<Input
|
||||
type="number"
|
||||
label="Message Interval"
|
||||
disabled={!moduleEnabled}
|
||||
suffix="Seconds"
|
||||
{...register('sender', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Save CSV to storage"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('save')}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const SerialSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const serialConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.moduleConfig.serial,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.ModuleConfig_SerialConfig>({
|
||||
defaultValues: serialConfig,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(serialConfig);
|
||||
}, [reset, serialConfig]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setLoading(true);
|
||||
await connection.setModuleConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'serial',
|
||||
serial: data,
|
||||
},
|
||||
},
|
||||
async (): Promise<void> => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const moduleEnabled = useWatch({
|
||||
control,
|
||||
name: 'enabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox label="Module Enabled" {...register('enabled')} />
|
||||
<Checkbox label="Echo" disabled={!moduleEnabled} {...register('echo')} />
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
label="RX"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('rxd', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="TX Pin"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('txd', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Baud Rate"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('baud', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Timeout"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('timeout', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Mode"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('mode', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const StoreForwardSettingsPanel = (): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const storeForwardConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.moduleConfig.storeForward,
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.ModuleConfig_StoreForwardConfig>({
|
||||
defaultValues: storeForwardConfig,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(storeForwardConfig);
|
||||
}, [reset, storeForwardConfig]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setLoading(true);
|
||||
await connection.setModuleConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'storeForward',
|
||||
storeForward: data,
|
||||
},
|
||||
},
|
||||
async (): Promise<void> => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const moduleEnabled = useWatch({
|
||||
control,
|
||||
name: 'enabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox label="Module Enabled" {...register('enabled')} />
|
||||
<Checkbox
|
||||
label="Heartbeat Enabled"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('heartbeat')}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Number of records"
|
||||
suffix="Records"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('records', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="History return max"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('historyReturnMax', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="History return window"
|
||||
disabled={!moduleEnabled}
|
||||
{...register('historyReturnWindow', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Checkbox } from '@components/generic/form/Checkbox';
|
||||
import { Form } from '@components/generic/form/Form';
|
||||
import { Input } from '@components/generic/form/Input';
|
||||
import { Select } from '@components/generic/form/Select';
|
||||
import { connection } from '@core/connection';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Telemetry = (): JSX.Element => {
|
||||
const telemetryConfig = useAppSelector(
|
||||
(state) => state.meshtastic.radio.moduleConfig.telemetry,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit, formState, reset, control } =
|
||||
useForm<Protobuf.ModuleConfig_TelemetryConfig>({
|
||||
defaultValues: telemetryConfig,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(telemetryConfig);
|
||||
}, [reset, telemetryConfig]);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
setLoading(true);
|
||||
void connection.setModuleConfig(
|
||||
{
|
||||
payloadVariant: {
|
||||
oneofKind: 'telemetry',
|
||||
telemetry: data,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
|
||||
<Checkbox
|
||||
label="Measurement Enabled"
|
||||
{...register('environmentMeasurementEnabled')}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Displayed on Screen"
|
||||
{...register('environmentScreenEnabled')}
|
||||
/>
|
||||
<Input
|
||||
label="Read Error Count Threshold"
|
||||
type="number"
|
||||
{...register('environmentReadErrorCountThreshold', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Update Interval"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register('environmentUpdateInterval', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Recovery Interval"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register('environmentRecoveryInterval', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Display Farenheit"
|
||||
{...register('environmentDisplayFahrenheit')}
|
||||
/>
|
||||
<Select
|
||||
label="Sensor Type"
|
||||
optionsEnum={Protobuf.TelemetrySensorType}
|
||||
{...register('environmentSensorType', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
label="Sensor Pin"
|
||||
type="number"
|
||||
{...register('environmentSensorPin', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
|
||||
export interface SidebarItemProps {
|
||||
selected: boolean;
|
||||
setSelected: () => void;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SidebarItem = ({
|
||||
selected,
|
||||
setSelected,
|
||||
actions,
|
||||
children,
|
||||
}: SidebarItemProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
onClick={(): void => {
|
||||
setSelected();
|
||||
}}
|
||||
className={`mx-2 flex cursor-pointer select-none rounded-md border bg-gray-200 p-2 shadow-md first:mt-2 last:mb-2 hover:border-primary dark:bg-tertiaryDark dark:hover:border-primary ${
|
||||
selected ? 'border-primary' : 'border-gray-400 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<m.div
|
||||
className="flex w-full justify-between"
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
>
|
||||
<div className="flex gap-2">{children}</div>
|
||||
|
||||
<div className="flex gap-1">{actions}</div>
|
||||
</m.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +1,120 @@
|
||||
import type React from 'react';
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Settings } from '@components/layout/Sidebar/Settings/Index';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import {
|
||||
ArrayIcon,
|
||||
GlobeIcon,
|
||||
IconComponent,
|
||||
InboxIcon,
|
||||
InfoSignIcon,
|
||||
LabTestIcon,
|
||||
LayersIcon,
|
||||
majorScale,
|
||||
Pane,
|
||||
SettingsIcon,
|
||||
Tab,
|
||||
Tablist,
|
||||
} from "evergreen-ui";
|
||||
|
||||
export interface SidebarProps {
|
||||
children: React.ReactNode;
|
||||
setSettingsOpen: (settingsOpen: boolean) => void;
|
||||
settingsOpen: boolean;
|
||||
import { PeersDialog } from "@app/components/Dialog/PeersDialog.js";
|
||||
import { Page, useDevice } from "@app/core/stores/deviceStore.js";
|
||||
|
||||
import { DeviceCard } from "./DeviceCard.js";
|
||||
|
||||
interface NavLink {
|
||||
name: string;
|
||||
icon: IconComponent;
|
||||
page: Page;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Sidebar = ({
|
||||
settingsOpen,
|
||||
setSettingsOpen,
|
||||
children,
|
||||
}: SidebarProps): JSX.Element => {
|
||||
const appState = useAppSelector((state) => state.app);
|
||||
export const Sidebar = (): JSX.Element => {
|
||||
const { activePage, setActivePage } = useDevice();
|
||||
const [PeersDialogOpen, setPeersDialogOpen] = useState(false);
|
||||
|
||||
const navLinks: NavLink[] = [
|
||||
{
|
||||
name: "Messages",
|
||||
icon: InboxIcon,
|
||||
page: "messages",
|
||||
},
|
||||
{
|
||||
name: "Map",
|
||||
icon: GlobeIcon,
|
||||
page: "map",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Extensions",
|
||||
icon: LabTestIcon,
|
||||
page: "extensions",
|
||||
},
|
||||
{
|
||||
name: "Config",
|
||||
icon: SettingsIcon,
|
||||
page: "config",
|
||||
},
|
||||
{
|
||||
name: "Channels",
|
||||
icon: LayersIcon,
|
||||
page: "channels",
|
||||
},
|
||||
{
|
||||
name: "Info",
|
||||
icon: InfoSignIcon,
|
||||
page: "info",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute z-20 h-full w-full flex-grow flex-col md:relative md:flex md:w-96 ${
|
||||
appState.mobileNavOpen ? 'flex' : 'hidden'
|
||||
}`}
|
||||
<Pane
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
flexGrow={1}
|
||||
margin={majorScale(3)}
|
||||
padding={majorScale(2)}
|
||||
borderRadius={majorScale(1)}
|
||||
background="white"
|
||||
elevation={1}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col drop-shadow-xl dark:bg-primaryDark">
|
||||
<div className="relative flex-grow gap-1">
|
||||
<div className="absolute h-full w-full">{children}</div>
|
||||
<Settings open={settingsOpen} setOpen={setSettingsOpen} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tablist>
|
||||
{navLinks.map((Link) => (
|
||||
<Tab
|
||||
key={Link.name}
|
||||
gap={majorScale(2)}
|
||||
disabled={Link.disabled}
|
||||
direction="vertical"
|
||||
isSelected={Link.page === activePage}
|
||||
onSelect={() => {
|
||||
setActivePage(Link.page);
|
||||
}}
|
||||
>
|
||||
<Link.icon />
|
||||
{Link.name}
|
||||
</Tab>
|
||||
))}
|
||||
<Tab
|
||||
gap={5}
|
||||
direction="vertical"
|
||||
isSelected={PeersDialogOpen}
|
||||
onSelect={() => {
|
||||
setPeersDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<ArrayIcon />
|
||||
Peers
|
||||
</Tab>
|
||||
</Tablist>
|
||||
<PeersDialog
|
||||
isOpen={PeersDialogOpen}
|
||||
close={() => {
|
||||
setPeersDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
<Pane display="flex" flexGrow={1}>
|
||||
<DeviceCard />
|
||||
</Pane>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { FiMessageCircle, FiSettings } from 'react-icons/fi';
|
||||
import { RiRoadMapLine } from 'react-icons/ri';
|
||||
import { VscExtensions } from 'react-icons/vsc';
|
||||
|
||||
import { ErrorFallback } from '@components/ErrorFallback';
|
||||
import { IconButton } from '@components/generic/button/IconButton';
|
||||
import { Sidebar } from '@components/layout/Sidebar';
|
||||
import type { TabProps } from '@components/Tab';
|
||||
import { Tabs } from '@components/Tabs';
|
||||
import { routes, useRoute } from '@core/router';
|
||||
|
||||
export interface LayoutProps {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
sidebarContents: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Layout = ({
|
||||
title,
|
||||
icon,
|
||||
sidebarContents,
|
||||
children,
|
||||
}: LayoutProps): JSX.Element => {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const tabs: Omit<TabProps, 'activeLeft' | 'activeRight'>[] = [
|
||||
{
|
||||
title: 'Messages',
|
||||
icon: <FiMessageCircle />,
|
||||
link: routes.messages().link,
|
||||
active: route.name === 'messages',
|
||||
},
|
||||
{
|
||||
title: 'Map',
|
||||
icon: <RiRoadMapLine />,
|
||||
link: routes.map().link,
|
||||
active: route.name === 'map',
|
||||
},
|
||||
{
|
||||
title: 'Extensions',
|
||||
icon: <VscExtensions />,
|
||||
link: routes.extensions().link,
|
||||
active: route.name === 'extensions',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full overflow-hidden bg-white dark:bg-secondaryDark">
|
||||
<div className="flex flex-grow">
|
||||
<Sidebar settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen}>
|
||||
<div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark">
|
||||
<div className="flex h-10 gap-1">
|
||||
<div className="my-auto">
|
||||
<IconButton icon={icon} />
|
||||
</div>
|
||||
<div className="my-auto text-lg font-medium dark:text-white">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">{sidebarContents}</div>
|
||||
</Sidebar>
|
||||
</div>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<div className="flex h-full w-full flex-col bg-gray-300 dark:bg-secondaryDark">
|
||||
<div className="flex w-full bg-white pt-1 dark:bg-primaryDark">
|
||||
<div className="z-10 -mr-2 h-8">
|
||||
<IconButton
|
||||
className="m-1"
|
||||
icon={<FiSettings />}
|
||||
onClick={(): void => {
|
||||
setSettingsOpen(!settingsOpen);
|
||||
}}
|
||||
active={settingsOpen}
|
||||
/>
|
||||
</div>
|
||||
<Tabs tabs={tabs} />
|
||||
</div>
|
||||
<div className="flex flex-grow">{children}</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import type React from "react";
|
||||
|
||||
import { IconComponent, majorScale, Pane, Tab, Tablist } from "evergreen-ui";
|
||||
|
||||
export interface Tab {
|
||||
key: number;
|
||||
name: string;
|
||||
icon: IconComponent;
|
||||
element: () => JSX.Element;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface TabbedContentProps {
|
||||
active: number;
|
||||
setActive: (index: number) => void;
|
||||
tabs: Tab[];
|
||||
actions?: (() => JSX.Element)[];
|
||||
}
|
||||
|
||||
export const TabbedContent = ({
|
||||
active,
|
||||
setActive,
|
||||
tabs,
|
||||
actions,
|
||||
}: TabbedContentProps): JSX.Element => {
|
||||
return (
|
||||
<Pane
|
||||
margin={majorScale(3)}
|
||||
borderRadius={majorScale(1)}
|
||||
background="white"
|
||||
elevation={1}
|
||||
display="flex"
|
||||
flexGrow={1}
|
||||
flexDirection="column"
|
||||
padding={majorScale(2)}
|
||||
gap={majorScale(2)}
|
||||
>
|
||||
<Pane borderBottom="muted" paddingBottom={majorScale(2)}>
|
||||
<Pane display="flex">
|
||||
<Tablist>
|
||||
{tabs.map((Entry) => (
|
||||
<Tab
|
||||
key={Entry.key}
|
||||
disabled={Entry.disabled}
|
||||
gap={5}
|
||||
onSelect={() => setActive(Entry.key)}
|
||||
isSelected={active === Entry.key}
|
||||
>
|
||||
<Entry.icon />
|
||||
{Entry.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tablist>
|
||||
|
||||
<Pane marginLeft="auto">
|
||||
{actions?.map((Action, index) => (
|
||||
<Action key={index} />
|
||||
))}
|
||||
</Pane>
|
||||
</Pane>
|
||||
</Pane>
|
||||
{tabs.map((Entry) => (
|
||||
<Pane
|
||||
key={Entry.key}
|
||||
display={active === Entry.key ? "flex" : "none"}
|
||||
flexDirection="column"
|
||||
flexGrow={1}
|
||||
>
|
||||
<Entry.element />
|
||||
</Pane>
|
||||
))}
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user