This commit is contained in:
Sacha Weatherstone
2022-02-07 00:00:55 +11:00
parent e43b04990b
commit 8d184578f0
109 changed files with 3495 additions and 3632 deletions
@@ -0,0 +1,160 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { FiExternalLink, FiX } from 'react-icons/fi';
import {
RiArrowDownLine,
RiArrowUpDownLine,
RiArrowUpLine,
} from 'react-icons/ri';
import { ListItem } from '@app/components/generic/ListItem';
import type { ChannelData } from '@app/core/slices/meshtasticSlice';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input, Select, Tooltip } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Channels = (): JSX.Element => {
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
const adminChannel =
channels.find(
(channel) => channel.channel.role === Protobuf.Channel_Role.PRIMARY,
) ?? channels[0];
const [usePreset, setUsePreset] = React.useState(true);
const [loading, setLoading] = React.useState(false);
const [selectedChannel, setSelectedChannel] = React.useState<
ChannelData | undefined
>();
const { register, handleSubmit, reset, formState } = useForm<
DeepOmit<Protobuf.Channel, 'psk'>
>({
defaultValues: {
...adminChannel.channel,
},
});
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
const channelData = Protobuf.Channel.create({
...data,
settings: {
...data.settings,
psk: adminChannel.channel.settings?.psk,
},
});
await connection.setChannel(channelData, (): Promise<void> => {
reset({ ...data });
setLoading(false);
return Promise.resolve();
});
});
return (
<>
{adminChannel && (
<>
<Checkbox
checked={usePreset}
label="Use Presets"
onChange={(e): void => setUsePreset(e.target.checked)}
/>
<form onSubmit={onSubmit}>
{usePreset ? (
<Select
label="Preset"
optionsEnum={Protobuf.ChannelSettings_ModemConfig}
{...register('settings.modemConfig', {
valueAsNumber: true,
})}
/>
) : (
<>
<Input
label="Bandwidth"
type="number"
suffix="MHz"
{...register('settings.bandwidth', {
valueAsNumber: true,
})}
/>
<Input
label="Spread Factor"
type="number"
suffix="CPS"
min={7}
max={12}
{...register('settings.spreadFactor', {
valueAsNumber: true,
})}
/>
<Input
label="Coding Rate"
type="number"
{...register('settings.codingRate', {
valueAsNumber: true,
})}
/>
</>
)}
<Input
label="Transmit Power"
type="number"
suffix="dBm"
{...register('settings.txPower', { valueAsNumber: true })}
/>
</form>
</>
)}
{channels.map((channel) => (
<ListItem
key={channel.channel.index}
onClick={(): void => {
setSelectedChannel(channel);
}}
status={
<div
className={`my-auto h-3 w-3 rounded-full ${
[
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
].find((role) => role === channel.channel.role)
? 'bg-green-500'
: 'bg-gray-400'
}`}
/>
}
selected={selectedChannel?.channel.index === channel.channel.index}
selectedIcon={<FiExternalLink />}
actions={
<Tooltip content={`MQTT Status`}>
<div className="rounded-md p-2">
{channel.channel.settings?.uplinkEnabled &&
channel.channel.settings?.downlinkEnabled ? (
<RiArrowUpDownLine className="p-0.5 group-active:scale-90" />
) : channel.channel.settings?.uplinkEnabled ? (
<RiArrowUpLine className="p-0.5 group-active:scale-90" />
) : channel.channel.settings?.downlinkEnabled ? (
<RiArrowDownLine className="p-0.5 group-active:scale-90" />
) : (
<FiX className="p-0.5" />
)}
</div>
</Tooltip>
}
>
<div>
{channel.channel.settings?.name.length
? channel.channel.settings.name
: channel.channel.role === Protobuf.Channel_Role.PRIMARY
? 'Primary'
: `Channel: ${channel.channel.index}`}
</div>
</ListItem>
))}
</>
);
};
@@ -0,0 +1,131 @@
import React from 'react';
import {
FiAlignLeft,
FiBell,
FiFastForward,
FiLayers,
FiLayout,
FiMapPin,
FiPackage,
FiRadio,
FiRss,
FiUser,
FiWifi,
FiZap,
} from 'react-icons/fi';
import { CollapsibleSection } from '@app/components/layout/Sidebar/sections/CollapsibleSection';
import { ExternalSection } from '@app/components/layout/Sidebar/sections/ExternalSection';
import { SidebarOverlay } from '@app/components/layout/Sidebar/sections/SidebarOverlay';
import { SidebarPrimary } from '@app/components/layout/Sidebar/sections/SidebarPrimary';
import { Channels } from '@app/components/layout/Sidebar/Settings/Channels';
import { ExternalNotificationsSettingsPlanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/ExternalNotifications/SettingsPlanel';
import { RangeTestSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/RangeTest/SettingsPanel';
import { SerialSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/Serial/SettingsPanel';
import { StoreForwardSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/StoreForward/SettingsPanel';
import { Position } from '@app/components/layout/Sidebar/Settings/Position';
import { Power } from '@app/components/layout/Sidebar/Settings/Power';
import { Radio } from '@app/components/layout/Sidebar/Settings/Radio';
import { User } from '@app/components/layout/Sidebar/Settings/User';
import { WiFi } from '@app/components/layout/Sidebar/Settings/WiFi';
import { Interface } from './Interface';
import { ChannelsGroup } from './radio/channels/panels/ChannelsGroup';
export interface SettingsProps {
open: boolean;
setOpen: (open: boolean) => void;
}
export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
const [pluginsOpen, setPluginsOpen] = React.useState(false);
const [channelsOpen, setChannelsOpen] = React.useState(false);
// const { hasGps, hasWifi } = useAppSelector((state) => state.meshtastic.radio.hardware);
const hasGps = true;
const hasWifi = true;
return (
<>
<SidebarPrimary
title="Settings"
open={open}
close={(): void => {
setOpen(false);
}}
>
<CollapsibleSection icon={<FiWifi />} title="WiFi & MQTT">
<WiFi />
</CollapsibleSection>
<CollapsibleSection icon={<FiMapPin />} title="Position">
<Position />
</CollapsibleSection>
<CollapsibleSection icon={<FiUser />} title="User">
<User />
</CollapsibleSection>
<CollapsibleSection icon={<FiZap />} title="Power">
<Power />
</CollapsibleSection>
<CollapsibleSection icon={<FiRadio />} title="Radio">
<Radio />
</CollapsibleSection>
<CollapsibleSection icon={<FiLayers />} title="Primary Channel">
<Channels />
</CollapsibleSection>
<ExternalSection
onClick={(): void => {
setChannelsOpen(true);
}}
icon={<FiLayers />}
title="Channels"
/>
<ExternalSection
onClick={(): void => {
setPluginsOpen(true);
}}
icon={<FiPackage />}
title="Plugins"
/>
<CollapsibleSection icon={<FiLayout />} title="Interface">
<Interface />
</CollapsibleSection>
</SidebarPrimary>
{/* Plugins */}
<SidebarOverlay
title="Plugins"
open={pluginsOpen}
close={(): void => {
setPluginsOpen(false);
}}
>
<CollapsibleSection title="Range Test" icon={<FiRss />}>
<RangeTestSettingsPanel />
</CollapsibleSection>
<CollapsibleSection title="External Notifications" icon={<FiBell />}>
<ExternalNotificationsSettingsPlanel />
</CollapsibleSection>
<CollapsibleSection title="Serial" icon={<FiAlignLeft />}>
<SerialSettingsPanel />
</CollapsibleSection>
<CollapsibleSection title="Store & Forward" icon={<FiFastForward />}>
<StoreForwardSettingsPanel />
</CollapsibleSection>
</SidebarOverlay>
{/* End Plugins */}
{/* Channels */}
<SidebarOverlay
title="Channels"
open={channelsOpen}
close={(): void => {
setChannelsOpen(false);
}}
>
<ChannelsGroup />
</SidebarOverlay>
{/* End Channels */}
</>
);
};
@@ -0,0 +1,28 @@
import type React from 'react';
import { Select } from '@meshtastic/components';
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');
}}
/>
);
};
@@ -0,0 +1,140 @@
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
import { MultiSelect } from 'react-multi-select-component';
import { bitwiseEncode } from '@app/core/utils/bitwise';
import { Label } from '@components/generic/form/Label';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Position = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: {
...preferences,
positionBroadcastSecs:
preferences.positionBroadcastSecs === 0
? preferences.isRouter
? 43200
: 900
: preferences.positionBroadcastSecs,
},
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const encode = (enums: Protobuf.PositionFlags[]): number => {
return enums.reduce((acc, curr) => acc | curr, 0);
};
const decode = (value: number): Protobuf.PositionFlags[] => {
const enumValues = Object.keys(Protobuf.PositionFlags)
.map(Number)
.filter(Boolean);
return enumValues.map((b) => value & b).filter(Boolean);
};
return (
<form className="space-y-2" onSubmit={onSubmit}>
<Input
label="Broadcast Interval"
type="number"
suffix="Seconds"
{...register('positionBroadcastSecs', { 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.PositionFlags)
.filter((value) => typeof value[1] !== 'number')
.filter(
(value) =>
parseInt(value[0]) !==
Protobuf.PositionFlags.POS_UNDEFINED,
)
.map((value) => {
return {
value: parseInt(value[0]),
label: value[1].toString().replace('POS_', ''),
};
})}
value={decode(value).map((flag) => {
return {
value: flag,
label: Protobuf.PositionFlags[flag].replace('POS_', ''),
};
})}
onChange={(e: { value: number; label: string }[]): void =>
onChange(bitwiseEncode(e.map((v) => v.value)))
}
labelledBy="Select"
/>
</div>
);
}}
/>
<Input
label="Position Type (DEBUG)"
type="number"
disabled
{...register('positionFlags', { valueAsNumber: true })}
/>
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
<Select
label="Location Sharing"
optionsEnum={Protobuf.LocationSharing}
{...register('locationShare', { valueAsNumber: true })}
/>
<Select
label="GPS Mode"
optionsEnum={Protobuf.GpsOperation}
{...register('gpsOperation', { valueAsNumber: true })}
/>
<Select
label="Display Format"
optionsEnum={Protobuf.GpsCoordinateFormat}
{...register('gpsFormat', { valueAsNumber: true })}
/>
<Checkbox label="Accept 2D Fix" {...register('gpsAccept2D')} />
<Input
label="Max DOP"
type="number"
{...register('gpsMaxDop', { valueAsNumber: true })}
/>
<Input
label="Last GPS Attempt"
disabled
{...register('gpsAttemptTime', { valueAsNumber: true })}
/>
</form>
);
};
@@ -0,0 +1,53 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Power = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: {
...preferences,
isLowPower: preferences.isRouter ? true : preferences.isLowPower,
},
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
return (
<form className="space-y-2" onSubmit={onSubmit}>
<Select
label="Charge current"
optionsEnum={Protobuf.ChargeCurrent}
{...register('chargeCurrent', { valueAsNumber: true })}
/>
<Checkbox label="Always powered" {...register('isAlwaysPowered')} />
<Checkbox
label="Powered by low power source (solar)"
disabled={preferences.isRouter}
validationMessage={
preferences.isRouter ? 'Enabled by default in router mode' : ''
}
{...register('isLowPower')}
/>
</form>
);
};
@@ -0,0 +1,44 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Radio = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState, reset } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
return (
<form className="space-y-2" onSubmit={onSubmit}>
<Checkbox label="Is Router" {...register('isRouter')} />
<Select
label="Region"
optionsEnum={Protobuf.RegionCode}
{...register('region', { valueAsNumber: true })}
/>
<Checkbox label="Debug Log" {...register('debugLogEnabled')} />
<Checkbox label="Serial Disabled" {...register('serialDisabled')} />
</form>
);
};
@@ -0,0 +1,114 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { base16 } from 'rfc4648';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const User = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware,
).myNodeNum;
const node = useAppSelector((state) => state.meshtastic.nodes).find(
(node) => node.number === myNodeNum,
);
const { register, handleSubmit, formState, reset } = useForm<{
longName: string;
shortName: string;
isLicensed: boolean;
team: Protobuf.Team;
antAzimuth: number;
antGainDbi: number;
txPowerDbm: number;
}>({
defaultValues: {
longName: node?.user?.longName,
shortName: node?.user?.shortName,
isLicensed: node?.user?.isLicensed,
team: node?.user?.team,
antAzimuth: node?.user?.antAzimuth,
antGainDbi: node?.user?.antGainDbi,
txPowerDbm: node?.user?.txPowerDbm,
},
});
React.useEffect(() => {
reset({
longName: node?.user?.longName,
shortName: node?.user?.shortName,
isLicensed: node?.user?.isLicensed,
team: node?.user?.team,
});
}, [reset, node]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
if (node?.user) {
void connection.setOwner({ ...node.user, ...data }, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
// TODO: can be removed once getUser is implemented
// dispatch(
// addUser({ ...node.user, ...{ data: { ...node.user.data, ...data } } }),
// );
}
});
return (
<form className="space-y-2" onSubmit={onSubmit}>
<Input label="Device ID" value={node?.user?.id} disabled />
<Input
label="Hardware"
value={
Protobuf.HardwareModel[
node?.user?.hwModel ?? Protobuf.HardwareModel.UNSET
]
}
disabled
/>
<Input
label="Mac Address"
defaultValue={
base16
.stringify(node?.user?.macaddr ?? [])
.match(/.{1,2}/g)
?.join(':') ?? ''
}
disabled
/>
<Input label="Device Name" {...register('longName')} />
<Input label="Short Name" maxLength={3} {...register('shortName')} />
<Checkbox label="Licenced Operator?" {...register('isLicensed')} />
<Select
label="Team"
optionsEnum={Protobuf.Team}
{...register('team', { valueAsNumber: true })}
/>
<Input
label="Antenna Azimuth"
suffix="°"
type="number"
{...register('antAzimuth', { valueAsNumber: true })}
/>
<Input
label="Antenna Gain"
suffix="dBi"
type="number"
{...register('antGainDbi', { valueAsNumber: true })}
/>
<Input
label="Transmit Power"
suffix="dBm"
type="number"
{...register('txPowerDbm', { valueAsNumber: true })}
/>
</form>
);
};
@@ -0,0 +1,79 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const WiFi = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
const watchWifiApMode = useWatch({
control,
name: 'wifiApMode',
defaultValue: false,
});
const watchMQTTDisabled = useWatch({
control,
name: 'mqttDisabled',
defaultValue: false,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection.setPreferences(data, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
return (
<form className="space-y-2" onSubmit={onSubmit}>
<Checkbox label="Enable WiFi AP" {...register('wifiApMode')} />
<Input
label="WiFi SSID"
disabled={watchWifiApMode}
{...register('wifiSsid')}
/>
<Input
type="password"
autoComplete="off"
label="WiFi PSK"
disabled={watchWifiApMode}
{...register('wifiPassword')}
/>
<Checkbox label="Disable MQTT" {...register('mqttDisabled')} />
<Input
label="MQTT Server Address"
disabled={watchMQTTDisabled}
{...register('mqttServer')}
/>
<Input
label="MQTT Username"
disabled={watchMQTTDisabled}
{...register('mqttUsername')}
/>
<Input
label="MQTT Password"
type="password"
autoComplete="off"
disabled={watchMQTTDisabled}
{...register('mqttPassword')}
/>
</form>
);
};
@@ -0,0 +1,31 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
export const ExternalNotificationsDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell: preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs,
};
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</>
);
};
@@ -0,0 +1,96 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'extNotificationPluginEnabled',
defaultValue: false,
});
return (
<>
<Form loading={loading}>
<Checkbox
label="Plugin Enabled"
{...register('extNotificationPluginEnabled')}
/>
<Checkbox
label="Active"
disabled={!pluginEnabled}
{...register('extNotificationPluginActive')}
/>
<Checkbox
label="Bell"
disabled={!pluginEnabled}
{...register('extNotificationPluginAlertBell')}
/>
<Checkbox
label="Message"
disabled={!pluginEnabled}
{...register('extNotificationPluginAlertMessage')}
/>
<Input
type="number"
label="Output"
disabled={!pluginEnabled}
{...register('extNotificationPluginOutput', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Output MS"
suffix="ms"
disabled={!pluginEnabled}
{...register('extNotificationPluginOutputMs', {
valueAsNumber: true,
})}
/>
</Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
);
};
@@ -0,0 +1,27 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
export const RangeTestDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
rangeTestPluginSave: preferences.rangeTestPluginSave,
rangeTestPluginSender: preferences.rangeTestPluginSender,
};
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</>
);
};
@@ -0,0 +1,78 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const RangeTestSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'rangeTestPluginEnabled',
defaultValue: false,
});
return (
<>
<Form loading={loading}>
<Checkbox
label="Range Test Plugin Enabled?"
{...register('rangeTestPluginEnabled')}
/>
<Checkbox
label="Range Test Plugin Save?"
disabled={!pluginEnabled}
{...register('rangeTestPluginSave')}
/>
<Input
type="number"
label="Message Interval"
disabled={!pluginEnabled}
suffix="Seconds"
{...register('rangeTestPluginSender', {
valueAsNumber: true,
})}
/>
</Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
);
};
@@ -0,0 +1,30 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
export const SerialDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTxd: preferences.serialpluginTxd,
serialpluginTimeout: preferences.serialpluginTimeout,
};
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</>
);
};
@@ -0,0 +1,99 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const SerialSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'serialpluginEnabled',
defaultValue: false,
});
return (
<>
<Form loading={loading}>
<Checkbox label="Plugin Enabled" {...register('serialpluginEnabled')} />
<Checkbox
label="Echo"
disabled={!pluginEnabled}
{...register('serialpluginEcho')}
/>
<Input
type="number"
label="RX"
disabled={!pluginEnabled}
{...register('serialpluginRxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX"
disabled={!pluginEnabled}
{...register('serialpluginTxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Mode"
disabled={!pluginEnabled}
{...register('serialpluginMode', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Timeout"
disabled={!pluginEnabled}
{...register('serialpluginTimeout', {
valueAsNumber: true,
})}
/>
</Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
);
};
@@ -0,0 +1,31 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
export const StoreForwardDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
storeForwardPluginHeartbeat: preferences.storeForwardPluginHeartbeat,
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
storeForwardPluginHistoryReturnMax:
preferences.storeForwardPluginHistoryReturnMax,
storeForwardPluginHistoryReturnWindow:
preferences.storeForwardPluginHistoryReturnWindow,
};
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</>
);
};
@@ -0,0 +1,94 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const StoreForwardSettingsPanel = (): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const { register, handleSubmit, formState, reset, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
React.useEffect(() => {
reset(preferences);
}, [reset, preferences]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection.setPreferences(data, async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
});
const pluginEnabled = useWatch({
control,
name: 'storeForwardPluginEnabled',
defaultValue: false,
});
return (
<>
<Form loading={loading}>
<Checkbox
label="Plugin Enabled"
{...register('storeForwardPluginEnabled')}
/>
<Checkbox
label="Heartbeat Enabled"
disabled={!pluginEnabled}
{...register('storeForwardPluginHeartbeat')}
/>
<Input
type="number"
label="Number of records"
suffix="Records"
disabled={!pluginEnabled}
{...register('storeForwardPluginRecords', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return max"
disabled={!pluginEnabled}
{...register('storeForwardPluginHistoryReturnMax', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="History return window"
disabled={!pluginEnabled}
{...register('storeForwardPluginHistoryReturnWindow', {
valueAsNumber: true,
})}
/>
</Form>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</>
);
};
@@ -0,0 +1,59 @@
import type React from 'react';
import { FaQrcode } from 'react-icons/fa';
import { FiCode, FiSave } from 'react-icons/fi';
import { CollapsibleSection } from '@app/components/layout/Sidebar/sections/CollapsibleSection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { IconButton } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { SettingsPanel } from './SettingsPanel';
export const ChannelsGroup = (): JSX.Element => {
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
return (
<>
{channels.map((channel) => {
return (
<div key={channel.channel.index}>
<CollapsibleSection
title={
channel.channel.settings?.name.length
? channel.channel.settings.name
: channel.channel.role === Protobuf.Channel_Role.PRIMARY
? 'Primary'
: `Channel: ${channel.channel.index}`
}
icon={
<div
className={`h-3 w-3 rounded-full ${
channel.channel.role === Protobuf.Channel_Role.PRIMARY
? 'bg-orange-500'
: channel.channel.role === Protobuf.Channel_Role.SECONDARY
? 'bg-green-500'
: 'bg-gray-500'
}`}
/>
}
actions={
<>
<IconButton icon={<FiCode />} />
<IconButton icon={<FaQrcode />} />
<IconButton icon={<FiSave />} />
</>
}
>
<>
{/* <DebugPanel channel={channel.channel} /> */}
{/* <QRCodePanel channel={channel.channel} /> */}
<SettingsPanel channel={channel.channel} />
</>
</CollapsibleSection>
</div>
);
})}
</>
);
};
@@ -0,0 +1,21 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface DebugPanelProps {
channel: Protobuf.Channel;
}
export const DebugPanel = ({ channel }: DebugPanelProps): JSX.Element => {
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(channel)} />
</div>
<JSONPretty className="max-w-sm" data={channel} />
</>
);
};
@@ -0,0 +1,20 @@
import type React from 'react';
import QRCode from 'react-qr-code';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface QRCodePanelProps {
channel: Protobuf.Channel;
}
export const QRCodePanel = ({ channel }: QRCodePanelProps): JSX.Element => {
return (
<div className="m-auto">
<QRCode
className="rounded-md"
value={`https://www.meshtastic.org/d/#${channel.index}`}
/>
</div>
);
};
@@ -0,0 +1,137 @@
import React from 'react';
import { fromByteArray, toByteArray } from 'base64-js';
import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface SettingsPanelProps {
channel: Protobuf.Channel;
}
export const SettingsPanel = ({ channel }: SettingsPanelProps): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const [keySize, setKeySize] = React.useState<128 | 256>(256);
const [pskHidden, setPskHidden] = React.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)),
},
});
React.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 (
<div className="flex w-full flex-col">
<Form loading={loading}>
{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>
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton
disabled={!formState.isDirty}
onClick={async (): Promise<void> => {
await onSubmit();
}}
icon={<FiSave />}
/>
</div>
</div>
</div>
);
};