2.0 Overhaul start
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { fromByteArray, toByteArray } from "base64-js";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
EyeIcon,
|
||||
EyeSlashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
import { Select } from "../form/Select.js";
|
||||
import { Toggle } from "../form/Toggle.js";
|
||||
|
||||
export interface SettingsPanelProps {
|
||||
channel: Protobuf.Channel;
|
||||
}
|
||||
|
||||
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
const { connection } = useDevice();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [keySize, setKeySize] = useState<128 | 256>(256);
|
||||
const [pskHidden, setPskHidden] = useState(true);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty },
|
||||
reset,
|
||||
control,
|
||||
setValue,
|
||||
} = 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
|
||||
title="Channel Editor"
|
||||
breadcrumbs={[
|
||||
"Channels",
|
||||
channel.settings?.name.length
|
||||
? channel.settings.name
|
||||
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||
? "Primary"
|
||||
: `Channel: ${channel.index}`,
|
||||
]}
|
||||
reset={() =>
|
||||
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)),
|
||||
})
|
||||
}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{channel?.index !== 0 && (
|
||||
<>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
label="Name"
|
||||
description="Max transmit power in dBm"
|
||||
{...register("name")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Select
|
||||
label="Key Size"
|
||||
description="Desired size of generated key."
|
||||
value={keySize}
|
||||
onChange={(e): void => {
|
||||
setKeySize(parseInt(e.target.value) as 128 | 256);
|
||||
}}
|
||||
action={{
|
||||
icon: <ArrowPathIcon className="h-4" />,
|
||||
action: () => {
|
||||
const key = new Uint8Array(keySize / 8);
|
||||
crypto.getRandomValues(key);
|
||||
setValue("psk", fromByteArray(key));
|
||||
},
|
||||
}}
|
||||
>
|
||||
<option value={128}>128 Bit</option>
|
||||
<option value={256}>256 Bit</option>
|
||||
</Select>
|
||||
<Input
|
||||
width="100%"
|
||||
label="Pre-Shared Key"
|
||||
description="Max transmit power in dBm"
|
||||
type={pskHidden ? "password" : "text"}
|
||||
action={{
|
||||
icon: pskHidden ? (
|
||||
<EyeIcon className="w-4" />
|
||||
) : (
|
||||
<EyeSlashIcon className="w-4" />
|
||||
),
|
||||
action: () => {
|
||||
setPskHidden(!pskHidden);
|
||||
},
|
||||
}}
|
||||
{...register("psk")}
|
||||
/>
|
||||
<Controller
|
||||
name="uplinkEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Uplink Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="downlinkEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Downlink Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Select } from "@app/components/form/Select.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { BluetoothValidation } from "@app/validation/config/bluetooth.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -54,43 +56,41 @@ export const Bluetooth = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Bluetooth Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.enabled?.message}
|
||||
validationMessage={errors.enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<SelectField
|
||||
<Form
|
||||
title="Bluetooth Config"
|
||||
breadcrumbs={["Config", "Bluetooth"]}
|
||||
reset={() => reset(config.bluetooth)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
label="Pairing mode"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.mode?.message}
|
||||
validationMessage={errors.mode?.message}
|
||||
{...register("mode", { valueAsNumber: true })}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_BluetoothConfig_PairingMode)}
|
||||
</SelectField>
|
||||
</Select>
|
||||
|
||||
<TextInputField
|
||||
display={
|
||||
<Input
|
||||
disabled={
|
||||
pairingMode !== Protobuf.Config_BluetoothConfig_PairingMode.FIXED_PIN
|
||||
? "none"
|
||||
: "block"
|
||||
}
|
||||
label="Pin"
|
||||
description="This is a description."
|
||||
type="number"
|
||||
isInvalid={!!errors.fixedPin?.message}
|
||||
validationMessage={errors.fixedPin?.message}
|
||||
{...register("fixedPin", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, SelectField, Switch, toaster } from "evergreen-ui";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { Select } from "@app/components/form/Select.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { DeviceValidation } from "@app/validation/config/device.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -39,7 +40,7 @@ export const Device = (): JSX.Element => {
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
toaster.success("Successfully updated device config");
|
||||
// toaster.success("Successfully updated device config");
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
@@ -47,58 +48,45 @@ export const Device = (): JSX.Element => {
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<SelectField
|
||||
<Form
|
||||
title="Device Config"
|
||||
breadcrumbs={["Config", "Device"]}
|
||||
reset={() => reset(config.device)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Select
|
||||
label="Role"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.role?.message}
|
||||
validationMessage={errors.role?.message}
|
||||
{...register("role", { valueAsNumber: true })}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_DeviceConfig_Role)}
|
||||
</SelectField>
|
||||
<FormField
|
||||
label="Serial Console Disabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.serialDisabled?.message}
|
||||
validationMessage={errors.serialDisabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="serialDisabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Factory Reset Device"
|
||||
description="Description"
|
||||
isInvalid={!!errors.factoryReset?.message}
|
||||
validationMessage={errors.factoryReset?.message}
|
||||
>
|
||||
<Controller
|
||||
name="factoryReset"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Enabled Debug Log"
|
||||
description="Description"
|
||||
isInvalid={!!errors.debugLogEnabled?.message}
|
||||
validationMessage={errors.debugLogEnabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="debugLogEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</Select>
|
||||
<Controller
|
||||
name="serialEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Serial Output Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="debugLogEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Enabled Debug Log"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Select } from "@app/components/form/Select.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { DisplayValidation } from "@app/validation/config/display.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -46,42 +48,47 @@ export const Display = (): JSX.Element => {
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="Display Config"
|
||||
breadcrumbs={["Config", "Display"]}
|
||||
reset={() => reset(config.display)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Input
|
||||
label="Screen Timeout"
|
||||
description="This is a description."
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register("screenOnSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Carousel Delay"
|
||||
description="This is a description."
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register("autoScreenCarouselSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<SelectField
|
||||
<Select
|
||||
label="GPS Display Units"
|
||||
description="This is a description."
|
||||
{...register("gpsFormat", { valueAsNumber: true })}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_DisplayConfig_GpsCoordinateFormat)}
|
||||
</SelectField>
|
||||
<FormField
|
||||
label="Compass North Top"
|
||||
description="Description"
|
||||
isInvalid={!!errors.compassNorthTop?.message}
|
||||
validationMessage={errors.compassNorthTop?.message}
|
||||
>
|
||||
<Controller
|
||||
name="compassNorthTop"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</Select>
|
||||
<Controller
|
||||
name="compassNorthTop"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Compass North Top"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Select } from "@app/components/form/Select.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { LoRaValidation } from "@app/validation/config/lora.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -14,7 +16,6 @@ import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
export const LoRa = (): JSX.Element => {
|
||||
const { config, connection } = useDevice();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [usePreset, setUsePreset] = useState(true);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -27,6 +28,12 @@ export const LoRa = (): JSX.Element => {
|
||||
resolver: classValidatorResolver(LoRaValidation),
|
||||
});
|
||||
|
||||
const usePreset = useWatch({
|
||||
control,
|
||||
name: "usePreset",
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset(config.lora);
|
||||
}, [reset, config.lora]);
|
||||
@@ -49,115 +56,109 @@ export const LoRa = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Use Preset"
|
||||
description="Description"
|
||||
isInvalid={!!errors.txDisabled?.message}
|
||||
validationMessage={errors.txDisabled?.message}
|
||||
>
|
||||
<Switch
|
||||
height={24}
|
||||
marginLeft="auto"
|
||||
checked={usePreset}
|
||||
onChange={(e) => setUsePreset(e.target.checked)}
|
||||
/>
|
||||
</FormField>
|
||||
<SelectField
|
||||
display={usePreset ? "block" : "none"}
|
||||
<Form
|
||||
title="LoRa Config"
|
||||
breadcrumbs={["Config", "LoRa"]}
|
||||
reset={() => reset(config.lora)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="usePreset"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Use Preset"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
label="Preset"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.modemPreset?.message}
|
||||
validationMessage={errors.modemPreset?.message}
|
||||
disabled={!usePreset}
|
||||
{...register("modemPreset", { valueAsNumber: true })}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
|
||||
</SelectField>
|
||||
</Select>
|
||||
|
||||
<TextInputField
|
||||
display={usePreset ? "none" : "block"}
|
||||
<Input
|
||||
label="Bandwidth"
|
||||
description="Max transmit power in dBm"
|
||||
type="number"
|
||||
hint="MHz"
|
||||
isInvalid={!!errors.bandwidth?.message}
|
||||
validationMessage={errors.bandwidth?.message}
|
||||
suffix="MHz"
|
||||
error={errors.bandwidth?.message}
|
||||
{...register("bandwidth", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
disabled={usePreset}
|
||||
/>
|
||||
<TextInputField
|
||||
display={usePreset ? "none" : "block"}
|
||||
<Input
|
||||
label="Spread Factor"
|
||||
description="Max transmit power in dBm"
|
||||
type="number"
|
||||
hint="CPS"
|
||||
isInvalid={!!errors.spreadFactor?.message}
|
||||
validationMessage={errors.spreadFactor?.message}
|
||||
suffix="CPS"
|
||||
error={errors.spreadFactor?.message}
|
||||
{...register("spreadFactor", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
disabled={usePreset}
|
||||
/>
|
||||
<TextInputField
|
||||
display={usePreset ? "none" : "block"}
|
||||
<Input
|
||||
label="Coding Rate"
|
||||
description="Max transmit power in dBm"
|
||||
type="number"
|
||||
isInvalid={!!errors.codingRate?.message}
|
||||
validationMessage={errors.codingRate?.message}
|
||||
error={errors.codingRate?.message}
|
||||
{...register("codingRate", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
disabled={usePreset}
|
||||
/>
|
||||
<TextInputField
|
||||
label="Transmit Power"
|
||||
description="Max transmit power in dBm"
|
||||
type="number"
|
||||
isInvalid={!!errors.txPower?.message}
|
||||
validationMessage={errors.txPower?.message}
|
||||
{...register("txPower", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
label="Hop Limit"
|
||||
description="This is a description."
|
||||
hint="Hops"
|
||||
type="number"
|
||||
isInvalid={!!errors.hopLimit?.message}
|
||||
validationMessage={errors.hopLimit?.message}
|
||||
{...register("hopLimit", { valueAsNumber: true })}
|
||||
/>
|
||||
<FormField
|
||||
label="Transmit Disabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.txDisabled?.message}
|
||||
validationMessage={errors.txDisabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="txDisabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Frequency Offset"
|
||||
description="This is a description."
|
||||
hint="Hz"
|
||||
suffix="Hz"
|
||||
type="number"
|
||||
isInvalid={!!errors.frequencyOffset?.message}
|
||||
validationMessage={errors.frequencyOffset?.message}
|
||||
error={errors.frequencyOffset?.message}
|
||||
{...register("frequencyOffset", { valueAsNumber: true })}
|
||||
/>
|
||||
<SelectField
|
||||
<Select
|
||||
label="Region"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.region?.message}
|
||||
validationMessage={errors.region?.message}
|
||||
{...register("region", { valueAsNumber: true })}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
|
||||
</SelectField>
|
||||
</Select>
|
||||
<Input
|
||||
label="Hop Limit"
|
||||
description="This is a description."
|
||||
suffix="Hops"
|
||||
type="number"
|
||||
error={errors.hopLimit?.message}
|
||||
{...register("hopLimit", { valueAsNumber: true })}
|
||||
/>
|
||||
<Controller
|
||||
name="txEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Transmit Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
label="Transmit Power"
|
||||
description="Max transmit power in dBm"
|
||||
type="number"
|
||||
error={errors.txPower?.message}
|
||||
{...register("txPower", { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
FormField,
|
||||
SelectField,
|
||||
Switch,
|
||||
TextInputField,
|
||||
toaster,
|
||||
} from "evergreen-ui";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Select } from "@app/components/form/Select.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { renderOptions } from "@app/core/utils/selectEnumOptions.js";
|
||||
import { NetworkValidation } from "@app/validation/config/network.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
@@ -51,7 +47,7 @@ export const Network = (): JSX.Element => {
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
toaster.success("Successfully updated Network config");
|
||||
// toaster.success("Successfully updated Network config");
|
||||
reset({ ...data });
|
||||
setLoading(false);
|
||||
await Promise.resolve();
|
||||
@@ -59,48 +55,53 @@ export const Network = (): JSX.Element => {
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="WiFi Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.wifiEnabled?.message}
|
||||
validationMessage={errors.wifiEnabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="wifiEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<SelectField
|
||||
<Form
|
||||
title="Network Config"
|
||||
breadcrumbs={["Config", "Network"]}
|
||||
reset={() => reset(config.network)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="wifiEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="WiFi Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
label="WiFi Mode"
|
||||
description="This is a description."
|
||||
disabled={!wifiEnabled}
|
||||
{...register("wifiMode", { valueAsNumber: true })}
|
||||
>
|
||||
{renderOptions(Protobuf.Config_NetworkConfig_WiFiMode)}
|
||||
</SelectField>
|
||||
<TextInputField
|
||||
</Select>
|
||||
<Input
|
||||
label="SSID"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.wifiSsid?.message}
|
||||
validationMessage={errors.wifiSsid?.message}
|
||||
error={errors.wifiSsid?.message}
|
||||
disabled={!wifiEnabled}
|
||||
{...register("wifiSsid")}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="PSK"
|
||||
type="password"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.wifiPsk?.message}
|
||||
validationMessage={errors.wifiPsk?.message}
|
||||
error={errors.wifiPsk?.message}
|
||||
disabled={!wifiEnabled}
|
||||
{...register("wifiPsk")}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="NTP Server"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.ntpServer?.message}
|
||||
validationMessage={errors.ntpServer?.message}
|
||||
error={errors.ntpServer?.message}
|
||||
{...register("ntpServer")}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormField,
|
||||
SelectMenu,
|
||||
Switch,
|
||||
TextInputField,
|
||||
} from "evergreen-ui";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { PositionValidation } from "@app/validation/config/position.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { bitwiseDecode } from "@core/utils/bitwise";
|
||||
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export const Position = (): JSX.Element => {
|
||||
const { config, connection } = useDevice();
|
||||
@@ -29,15 +22,6 @@ export const Position = (): JSX.Element => {
|
||||
} = useForm<PositionValidation>({
|
||||
defaultValues: config.position,
|
||||
resolver: classValidatorResolver(PositionValidation),
|
||||
// defaultValues: {
|
||||
// ...preferences,
|
||||
// positionBroadcastSecs:
|
||||
// preferences.positionBroadcastSecs === 0
|
||||
// ? preferences.role === Protobuf.Role.Router
|
||||
// ? 43200
|
||||
// : 900
|
||||
// : preferences.positionBroadcastSecs,
|
||||
// },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,76 +46,74 @@ export const Position = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<TextInputField
|
||||
hint="Seconds"
|
||||
<Form
|
||||
title="Position Config"
|
||||
breadcrumbs={["Config", "Position"]}
|
||||
reset={() => reset(config.position)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Input
|
||||
suffix="Seconds"
|
||||
label="Broadcast Interval"
|
||||
description="This is a description."
|
||||
type="number"
|
||||
isInvalid={!!errors.positionBroadcastSecs?.message}
|
||||
validationMessage={errors.positionBroadcastSecs?.message}
|
||||
error={errors.positionBroadcastSecs?.message}
|
||||
{...register("positionBroadcastSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<FormField
|
||||
label="Disable Smart Position"
|
||||
description="Description"
|
||||
isInvalid={!!errors.positionBroadcastSmartDisabled?.message}
|
||||
validationMessage={errors.positionBroadcastSmartDisabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="positionBroadcastSmartDisabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Use Fixed Position"
|
||||
description="Description"
|
||||
isInvalid={!!errors.fixedPosition?.message}
|
||||
validationMessage={errors.fixedPosition?.message}
|
||||
>
|
||||
<Controller
|
||||
name="fixedPosition"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Disable GPS"
|
||||
description="Description"
|
||||
isInvalid={!!errors.gpsDisabled?.message}
|
||||
validationMessage={errors.gpsDisabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="gpsDisabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
hint="Seconds"
|
||||
<Controller
|
||||
name="positionBroadcastSmartEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Enable Smart Position"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="fixedPosition"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Use Fixed Position"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="gpsEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="GPS Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
suffix="Seconds"
|
||||
label="GPS Update Interval"
|
||||
description="This is a description."
|
||||
type="number"
|
||||
isInvalid={!!errors.gpsUpdateInterval?.message}
|
||||
validationMessage={errors.gpsUpdateInterval?.message}
|
||||
error={errors.gpsUpdateInterval?.message}
|
||||
{...register("gpsUpdateInterval", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Last GPS Attempt"
|
||||
description="This is a description."
|
||||
type="number"
|
||||
isInvalid={!!errors.gpsAttemptTime?.message}
|
||||
validationMessage={errors.gpsAttemptTime?.message}
|
||||
error={errors.gpsAttemptTime?.message}
|
||||
{...register("gpsAttemptTime", { valueAsNumber: true })}
|
||||
/>
|
||||
<Controller
|
||||
{/* <Controller
|
||||
name="positionFlags"
|
||||
control={control}
|
||||
render={({ field, fieldState }): JSX.Element => {
|
||||
@@ -218,7 +200,7 @@ export const Position = (): JSX.Element => {
|
||||
</FormField>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { PowerValidation } from "@app/validation/config/power.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -44,81 +45,79 @@ export const Power = (): JSX.Element => {
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="Power Config"
|
||||
breadcrumbs={["Config", "Power"]}
|
||||
reset={() => reset(config.power)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Input
|
||||
label="Shutdown on battery delay"
|
||||
description="This is a description."
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
isInvalid={!!errors.onBatteryShutdownAfterSecs?.message}
|
||||
validationMessage={errors.onBatteryShutdownAfterSecs?.message}
|
||||
error={errors.onBatteryShutdownAfterSecs?.message}
|
||||
{...register("onBatteryShutdownAfterSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<FormField
|
||||
label="Power Saving"
|
||||
description="Description"
|
||||
isInvalid={!!errors.isPowerSaving?.message}
|
||||
validationMessage={errors.isPowerSaving?.message}
|
||||
>
|
||||
<Controller
|
||||
name="isPowerSaving"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Controller
|
||||
name="isPowerSaving"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Power Saving"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
label="ADC Multiplier Override ratio"
|
||||
description="This is a description."
|
||||
type="number"
|
||||
isInvalid={!!errors.adcMultiplierOverride?.message}
|
||||
validationMessage={errors.adcMultiplierOverride?.message}
|
||||
error={errors.adcMultiplierOverride?.message}
|
||||
{...register("adcMultiplierOverride", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Minimum Wake Time"
|
||||
description="This is a description."
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
isInvalid={!!errors.minWakeSecs?.message}
|
||||
validationMessage={errors.minWakeSecs?.message}
|
||||
error={errors.minWakeSecs?.message}
|
||||
{...register("minWakeSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Mesh SDS Timeout"
|
||||
description="This is a description."
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
isInvalid={!!errors.meshSdsTimeoutSecs?.message}
|
||||
validationMessage={errors.meshSdsTimeoutSecs?.message}
|
||||
error={errors.meshSdsTimeoutSecs?.message}
|
||||
{...register("meshSdsTimeoutSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="SDS"
|
||||
description="This is a description."
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
isInvalid={!!errors.sdsSecs?.message}
|
||||
validationMessage={errors.sdsSecs?.message}
|
||||
error={errors.sdsSecs?.message}
|
||||
{...register("sdsSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="LS"
|
||||
description="This is a description."
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
isInvalid={!!errors.lsSecs?.message}
|
||||
validationMessage={errors.lsSecs?.message}
|
||||
error={errors.lsSecs?.message}
|
||||
{...register("lsSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Wait Bluetooth"
|
||||
description="This is a description."
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
isInvalid={!!errors.waitBluetoothSecs?.message}
|
||||
validationMessage={errors.waitBluetoothSecs?.message}
|
||||
error={errors.waitBluetoothSecs?.message}
|
||||
{...register("waitBluetoothSecs", { valueAsNumber: true })}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { base16 } from "rfc4648";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Select } from "@app/components/form/Select.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { UserValidation } from "@app/validation/config/user.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -50,27 +52,39 @@ export const User = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="User Config"
|
||||
breadcrumbs={["Config", "User"]}
|
||||
reset={() => {
|
||||
reset({
|
||||
longName: myNode?.data.user?.longName,
|
||||
shortName: myNode?.data.user?.shortName,
|
||||
isLicensed: myNode?.data.user?.isLicensed,
|
||||
});
|
||||
}}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Input
|
||||
label="Device ID"
|
||||
description="Preset unique identifier for this device."
|
||||
isInvalid={!!errors.id?.message}
|
||||
validationMessage={errors.id?.message}
|
||||
error={errors.id?.message}
|
||||
{...register("id")}
|
||||
readOnly
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Device Name"
|
||||
description="Personalised name for this device."
|
||||
{...register("longName")}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Short Name"
|
||||
description="This is a description."
|
||||
maxLength={3}
|
||||
{...register("shortName")}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Mac Address"
|
||||
description="This is a description."
|
||||
disabled
|
||||
@@ -81,28 +95,26 @@ export const User = (): JSX.Element => {
|
||||
?.join(":") ?? ""
|
||||
}
|
||||
/>
|
||||
<SelectField
|
||||
<Select
|
||||
label="Hardware"
|
||||
description="This is a description."
|
||||
disabled
|
||||
value={myNode?.data.user?.hwModel}
|
||||
>
|
||||
{renderOptions(Protobuf.HardwareModel)}
|
||||
</SelectField>
|
||||
<FormField
|
||||
label="Licenced Operator?"
|
||||
description="Description"
|
||||
isInvalid={!!errors.isLicensed?.message}
|
||||
validationMessage={errors.isLicensed?.message}
|
||||
>
|
||||
<Controller
|
||||
name="isLicensed"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</Select>
|
||||
<Controller
|
||||
name="isLicensed"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Licenced Operator?"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@components/Button.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { subscribeAll } from "@core/subscriptions.js";
|
||||
import { randId } from "@core/utils/randId.js";
|
||||
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Constants, IBLEConnection } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export const BLE = (): JSX.Element => {
|
||||
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
|
||||
const { addDevice } = useDeviceStore();
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
|
||||
const updateBleDeviceList = useCallback(async (): Promise<void> => {
|
||||
setBleDevices(await navigator.bluetooth.getDevices());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void updateBleDeviceList();
|
||||
}, [updateBleDeviceList]);
|
||||
|
||||
const onConnect = async (BLEDevice: BluetoothDevice) => {
|
||||
const id = randId();
|
||||
const device = addDevice(id);
|
||||
setSelectedDevice(id);
|
||||
const connection = new IBLEConnection(id);
|
||||
await connection.connect({
|
||||
device: BLEDevice,
|
||||
});
|
||||
device.addConnection(connection);
|
||||
subscribeAll(device, connection);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4 gap-2 w-full">
|
||||
<div className="flex gap-2 flex-col h-48 overflow-y-auto">
|
||||
{bleDevices.map((device, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void onConnect(device);
|
||||
}}
|
||||
>
|
||||
{device.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
iconBefore={<PlusCircleIcon className="w-4" />}
|
||||
onClick={() => {
|
||||
void navigator.bluetooth
|
||||
.requestDevice({
|
||||
filters: [{ services: [Constants.SERVICE_UUID] }],
|
||||
})
|
||||
.then((device) => {
|
||||
const exists = bleDevices.findIndex((d) => d.id === device.id);
|
||||
if (exists === -1) {
|
||||
setBleDevices(bleDevices.concat(device));
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
New device
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import type React from "react";
|
||||
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { Button } from "@components/Button.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { subscribeAll } from "@core/subscriptions.js";
|
||||
import { randId } from "@core/utils/randId.js";
|
||||
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { IHTTPConnection } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export const HTTP = (): JSX.Element => {
|
||||
const { addDevice } = useDeviceStore();
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
const { register, handleSubmit, control } = useForm<{
|
||||
ip: string;
|
||||
tls: boolean;
|
||||
}>({
|
||||
defaultValues: {
|
||||
ip: "meshtastic.local",
|
||||
tls: false,
|
||||
},
|
||||
});
|
||||
|
||||
const TLSEnabled = useWatch({
|
||||
control,
|
||||
name: "tls",
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
const id = randId();
|
||||
const device = addDevice(id);
|
||||
setSelectedDevice(id);
|
||||
const connection = new IHTTPConnection(id);
|
||||
// TODO: Promise never resolves
|
||||
void connection.connect({
|
||||
address: data.ip,
|
||||
fetchInterval: 2000,
|
||||
tls: data.tls,
|
||||
});
|
||||
device.addConnection(connection);
|
||||
subscribeAll(device, connection);
|
||||
});
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
<form className="w-full p-4 gap-2 flex flex-col" onSubmit={onSubmit}>
|
||||
<div className="h-48 flex flex-col gap-2">
|
||||
<Input
|
||||
label="IP Address/Hostname"
|
||||
prefix={TLSEnabled ? "https://" : "http://"}
|
||||
placeholder="000.000.000.000 / meshtastic.local"
|
||||
{...register("ip")}
|
||||
/>
|
||||
<Controller
|
||||
name="tls"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Use TLS"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button iconBefore={<PlusCircleIcon className="w-4" />} type="submit">
|
||||
Connect
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@components/Button.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDeviceStore } from "@core/stores/deviceStore.js";
|
||||
import { subscribeAll } from "@core/subscriptions.js";
|
||||
import { randId } from "@core/utils/randId.js";
|
||||
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { ISerialConnection } from "@meshtastic/meshtasticjs";
|
||||
|
||||
interface USBID {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const Serial = (): JSX.Element => {
|
||||
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
|
||||
const { addDevice } = useDeviceStore();
|
||||
const { setSelectedDevice } = useAppStore();
|
||||
|
||||
const updateSerialPortList = useCallback(async () => {
|
||||
setSerialPorts(await navigator.serial.getPorts());
|
||||
}, []);
|
||||
|
||||
navigator.serial.addEventListener("connect", () => {
|
||||
void updateSerialPortList();
|
||||
});
|
||||
navigator.serial.addEventListener("disconnect", () => {
|
||||
void updateSerialPortList();
|
||||
});
|
||||
useEffect(() => {
|
||||
void updateSerialPortList();
|
||||
}, [updateSerialPortList]);
|
||||
|
||||
const onConnect = async (port: SerialPort) => {
|
||||
const id = randId();
|
||||
const device = addDevice(id);
|
||||
setSelectedDevice(id);
|
||||
const connection = new ISerialConnection(id);
|
||||
await connection.connect({
|
||||
port,
|
||||
});
|
||||
device.addConnection(connection);
|
||||
subscribeAll(device, connection);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4 gap-2 w-full">
|
||||
<div className="flex gap-2 flex-col h-48 overflow-y-auto">
|
||||
{serialPorts.map((port, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void onConnect(port);
|
||||
}}
|
||||
>
|
||||
{port.getInfo().usbVendorId} - {port.getInfo().usbProductId}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
iconBefore={<PlusCircleIcon className="w-4" />}
|
||||
onClick={() => {
|
||||
void navigator.serial.requestPort().then((port) => {
|
||||
setSerialPorts(serialPorts.concat(port));
|
||||
});
|
||||
}}
|
||||
>
|
||||
New device
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import type React from "react";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { IconButton } from "@app/components/IconButton.js";
|
||||
import { Message } from "@components/PageComponents/Messages/Message.js";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import type { Channel } from "@core/stores/deviceStore.js";
|
||||
import { MapPinIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export interface ChannelChatProps {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
export const ChannelChat = ({ channel }: ChannelChatProps): JSX.Element => {
|
||||
const { nodes, connection, ackMessage } = useDevice();
|
||||
const [currentMessage, setCurrentMessage] = useState("");
|
||||
|
||||
const sendMessage = (): void => {
|
||||
void connection?.sendText(
|
||||
currentMessage,
|
||||
undefined,
|
||||
true,
|
||||
channel.config.index,
|
||||
(id) => {
|
||||
ackMessage(channel.config.index, id);
|
||||
return Promise.resolve();
|
||||
}
|
||||
);
|
||||
setCurrentMessage("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex flex-col flex-grow">
|
||||
{channel.messages.map((message, index) => (
|
||||
<Message
|
||||
key={index}
|
||||
message={message}
|
||||
lastMsgSameUser={
|
||||
index === 0
|
||||
? false
|
||||
: channel.messages[index - 1].packet.from ===
|
||||
message.packet.from
|
||||
}
|
||||
sender={
|
||||
nodes.find((node) => node.data.num === message.packet.from)?.data
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<form
|
||||
className="w-full"
|
||||
onSubmit={(e): void => {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-grow gap-2">
|
||||
<span className="w-full">
|
||||
<Input
|
||||
minLength={2}
|
||||
label=""
|
||||
placeholder="Enter Message"
|
||||
value={currentMessage}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setCurrentMessage(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
icon={<PaperAirplaneIcon className="h-4 text-slate-500" />}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
icon={<MapPinIcon className="h-4 text-slate-500" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import type React from "react";
|
||||
|
||||
import { WaypointMessage } from "@components/PageComponents/Messages/WaypointMessage.js";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import type { AllMessageTypes } from "@core/stores/deviceStore.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
EllipsisHorizontalCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
export interface MessageProps {
|
||||
lastMsgSameUser: boolean;
|
||||
message: AllMessageTypes;
|
||||
sender?: Protobuf.NodeInfo;
|
||||
}
|
||||
|
||||
export const Message = ({
|
||||
lastMsgSameUser,
|
||||
message,
|
||||
sender,
|
||||
}: MessageProps): JSX.Element => {
|
||||
const { setPeerInfoOpen, setActivePeer } = useDevice();
|
||||
|
||||
const openPeer = (): void => {
|
||||
setActivePeer(message.packet.from);
|
||||
setPeerInfoOpen(true);
|
||||
};
|
||||
|
||||
return lastMsgSameUser ? (
|
||||
<div className="flex ml-4">
|
||||
{message.ack ? (
|
||||
<CheckCircleIcon className="my-auto text-slate-200 h-4" />
|
||||
) : (
|
||||
<EllipsisHorizontalCircleIcon className="my-auto text-slate-200 h-4" />
|
||||
)}
|
||||
{"waypointID" in message ? (
|
||||
<WaypointMessage waypointID={message.waypointID} />
|
||||
) : (
|
||||
<span
|
||||
className={`ml-4 pl-2 border-l-2 border-l-slate-200 ${
|
||||
message.ack ? "text-black" : "text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-4 gap-2 mt-2">
|
||||
<div className="flex gap-2">
|
||||
<div className="cursor-pointer w-6" onClick={openPeer}>
|
||||
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
|
||||
</div>
|
||||
<span
|
||||
className="cursor-pointer font-medium text-slate-700"
|
||||
onClick={openPeer}
|
||||
>
|
||||
{sender?.user?.longName ?? "UNK"}
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
{new Date(message.packet.rxTime).toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{message.ack ? (
|
||||
<CheckCircleIcon className="my-auto text-slate-200 h-4" />
|
||||
) : (
|
||||
<EllipsisHorizontalCircleIcon className="my-auto text-slate-200 h-4" />
|
||||
)}
|
||||
{"waypointID" in message ? (
|
||||
<WaypointMessage waypointID={message.waypointID} />
|
||||
) : (
|
||||
<span
|
||||
className={`ml-4 pl-2 border-l-2 border-l-slate-200 ${
|
||||
message.ack ? "text-black" : "text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import type React from "react";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Select } from "@app/components/form/Select.js";
|
||||
import { Button } from "@components/Button.js";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
import { renderOptions } from "@core/utils/selectEnumOptions.js";
|
||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
|
||||
enum LocationType {
|
||||
MGRS,
|
||||
LatLng,
|
||||
DecimalDegrees,
|
||||
}
|
||||
|
||||
export const NewLocationMessage = (): JSX.Element => {
|
||||
const { connection } = useDevice();
|
||||
|
||||
return (
|
||||
<div className="w-96 m-4">
|
||||
<form
|
||||
onSubmit={(e): void => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Input label="Name" />
|
||||
<Input label="Description" />
|
||||
<Select label="Type" value={LocationType.MGRS}>
|
||||
{renderOptions(LocationType)}
|
||||
</Select>
|
||||
<Input label="Coordinates" />
|
||||
<Button
|
||||
onClick={() => {
|
||||
void connection?.sendWaypoint(
|
||||
Protobuf.Waypoint.create({
|
||||
latitudeI: Math.floor(3.89103 * 1e7),
|
||||
longitudeI: Math.floor(105.87005 * 1e7),
|
||||
name: "TEST",
|
||||
description: "This is a description",
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import type React from "react";
|
||||
|
||||
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||
import { toMGRS } from "@core/utils/toMGRS.js";
|
||||
import { MapPinIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export interface WaypointMessageProps {
|
||||
waypointID: number;
|
||||
}
|
||||
|
||||
export const WaypointMessage = ({
|
||||
waypointID,
|
||||
}: WaypointMessageProps): JSX.Element => {
|
||||
const { waypoints } = useDevice();
|
||||
const waypoint = waypoints.find((wp) => wp.id === waypointID);
|
||||
|
||||
return (
|
||||
<div className="ml-4 pl-2 border-l-slate-200 border-l-2">
|
||||
<div className="gap-2 flex rounded-md p-2 shadow-md shadow-orange-300">
|
||||
<MapPinIcon className="m-auto w-6 text-slate-600" />
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<div className="font-bold">{waypoint?.name}</div>
|
||||
<span className="text-sm font-mono text-slate-500">
|
||||
{toMGRS(waypoint?.latitudeI, waypoint?.longitudeI)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm">{waypoint?.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Select } from "@app/components/form/Select.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -52,57 +54,60 @@ export const CannedMessage = (): JSX.Element => {
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Module Enabled"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.enabled?.message}
|
||||
validationMessage={errors.enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Rotary Encoder #1 Enabled"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.rotary1Enabled?.message}
|
||||
validationMessage={errors.rotary1Enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="rotary1Enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="Canned Message Config"
|
||||
breadcrumbs={["Module Config", "Canned Message"]}
|
||||
reset={() => reset(moduleConfig.cannedMessage)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="rotary1Enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Rotary Encoder #1 Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
label="Encoder Pin A"
|
||||
description="Max transmit power in dBm"
|
||||
type="number"
|
||||
disabled={moduleEnabled}
|
||||
{...register("inputbrokerPinA", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Encoder Pin B"
|
||||
description="Max transmit power in dBm"
|
||||
type="number"
|
||||
disabled={moduleEnabled}
|
||||
{...register("inputbrokerPinB", { valueAsNumber: true })}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="Endoer Pin Press"
|
||||
description="Max transmit power in dBm"
|
||||
type="number"
|
||||
disabled={moduleEnabled}
|
||||
{...register("inputbrokerPinPress", { valueAsNumber: true })}
|
||||
/>
|
||||
<SelectField
|
||||
<Select
|
||||
label="Clockwise event"
|
||||
description="This is a description."
|
||||
disabled={moduleEnabled}
|
||||
@@ -111,8 +116,8 @@ export const CannedMessage = (): JSX.Element => {
|
||||
{renderOptions(
|
||||
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
||||
)}
|
||||
</SelectField>
|
||||
<SelectField
|
||||
</Select>
|
||||
<Select
|
||||
label="Counter Clockwise event"
|
||||
description="This is a description."
|
||||
disabled={moduleEnabled}
|
||||
@@ -121,8 +126,8 @@ export const CannedMessage = (): JSX.Element => {
|
||||
{renderOptions(
|
||||
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
||||
)}
|
||||
</SelectField>
|
||||
<SelectField
|
||||
</Select>
|
||||
<Select
|
||||
label="Press event"
|
||||
description="This is a description."
|
||||
disabled={moduleEnabled}
|
||||
@@ -131,41 +136,37 @@ export const CannedMessage = (): JSX.Element => {
|
||||
{renderOptions(
|
||||
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
|
||||
)}
|
||||
</SelectField>
|
||||
<FormField
|
||||
label="Up Down enabled"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.updown1Enabled?.message}
|
||||
validationMessage={errors.updown1Enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="updown1Enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
</Select>
|
||||
<Controller
|
||||
name="updown1Enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Up Down enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
label="Allow Input Source"
|
||||
description="Max transmit power in dBm"
|
||||
disabled={moduleEnabled}
|
||||
{...register("allowInputSource")}
|
||||
/>
|
||||
<FormField
|
||||
label="Send Bell"
|
||||
description="This is a description."
|
||||
isInvalid={!!errors.sendBell?.message}
|
||||
validationMessage={errors.sendBell?.message}
|
||||
>
|
||||
<Controller
|
||||
name="sendBell"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<Controller
|
||||
name="sendBell"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Send Bell"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -50,32 +51,37 @@ export const ExternalNotification = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.enabled?.message}
|
||||
validationMessage={errors.enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="External Notification Config"
|
||||
breadcrumbs={["Module Config", "External Notification"]}
|
||||
reset={() => reset(moduleConfig.externalNotification)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Output MS"
|
||||
description="Max transmit power in dBm"
|
||||
hint="ms"
|
||||
suffix="ms"
|
||||
disabled={!moduleEnabled}
|
||||
{...register("outputMs", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
type="number"
|
||||
label="Output"
|
||||
description="Max transmit power in dBm"
|
||||
@@ -84,51 +90,42 @@ export const ExternalNotification = (): JSX.Element => {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<FormField
|
||||
label="Active"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
isInvalid={!!errors.active?.message}
|
||||
validationMessage={errors.active?.message}
|
||||
>
|
||||
<Controller
|
||||
name="active"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Message"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
isInvalid={!!errors.alertMessage?.message}
|
||||
validationMessage={errors.alertMessage?.message}
|
||||
>
|
||||
<Controller
|
||||
name="alertMessage"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Bell"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
isInvalid={!!errors.alertBell?.message}
|
||||
validationMessage={errors.alertBell?.message}
|
||||
>
|
||||
<Controller
|
||||
name="alertBell"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<Controller
|
||||
name="active"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Active"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="alertMessage"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Message"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="alertBell"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Bell"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { MQTTValidation } from "@app/validation/moduleConfig/mqtt.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -50,34 +51,39 @@ export const MQTT = (): JSX.Element => {
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.enabled?.message}
|
||||
validationMessage={errors.enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="MQTT Config"
|
||||
breadcrumbs={["Module Config", "MQTT"]}
|
||||
reset={() => reset(moduleConfig.mqtt)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
label="MQTT Server Address"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
{...register("address")}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="MQTT Username"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
{...register("username")}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
label="MQTT Password"
|
||||
description="Description"
|
||||
type="password"
|
||||
@@ -85,36 +91,30 @@ export const MQTT = (): JSX.Element => {
|
||||
disabled={!moduleEnabled}
|
||||
{...register("password")}
|
||||
/>
|
||||
<FormField
|
||||
label="Encryption Enabled"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
isInvalid={!!errors.encryptionEnabled?.message}
|
||||
validationMessage={errors.encryptionEnabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="encryptionEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="JSON Output Enabled"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
isInvalid={!!errors.jsonEnabled?.message}
|
||||
validationMessage={errors.jsonEnabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="jsonEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<Controller
|
||||
name="encryptionEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Encryption Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="jsonEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="JSON Output Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -51,46 +52,48 @@ export const RangeTest = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.enabled?.message}
|
||||
validationMessage={errors.enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="Range Test Config"
|
||||
breadcrumbs={["Module Config", "Range Test"]}
|
||||
reset={() => reset(moduleConfig.rangeTest)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Message Interval"
|
||||
description="Max transmit power in dBm"
|
||||
disabled={!moduleEnabled}
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
{...register("sender", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<FormField
|
||||
label="Save CSV to storage"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
isInvalid={!!errors.save?.message}
|
||||
validationMessage={errors.save?.message}
|
||||
>
|
||||
<Controller
|
||||
name="save"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<Controller
|
||||
name="save"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Save CSV to storage"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { SerialValidation } from "@app/validation/moduleConfig/serial.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -51,37 +52,39 @@ export const Serial = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.enabled?.message}
|
||||
validationMessage={errors.enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Echo"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
isInvalid={!!errors.echo?.message}
|
||||
validationMessage={errors.echo?.message}
|
||||
>
|
||||
<Controller
|
||||
name="echo"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="Serial Config"
|
||||
breadcrumbs={["Module Config", "Serial"]}
|
||||
reset={() => reset(moduleConfig.serial)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="echo"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Echo"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="RX"
|
||||
description="Max transmit power in dBm"
|
||||
@@ -90,7 +93,7 @@ export const Serial = (): JSX.Element => {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
type="number"
|
||||
label="TX Pin"
|
||||
description="Max transmit power in dBm"
|
||||
@@ -99,7 +102,7 @@ export const Serial = (): JSX.Element => {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
type="number"
|
||||
label="Baud Rate"
|
||||
description="Max transmit power in dBm"
|
||||
@@ -108,7 +111,7 @@ export const Serial = (): JSX.Element => {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
type="number"
|
||||
label="Timeout"
|
||||
description="Max transmit power in dBm"
|
||||
@@ -117,7 +120,7 @@ export const Serial = (): JSX.Element => {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
type="number"
|
||||
label="Mode"
|
||||
description="Max transmit power in dBm"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -51,47 +52,49 @@ export const StoreForward = (): JSX.Element => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.enabled?.message}
|
||||
validationMessage={errors.enabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Heartbeat Enabled"
|
||||
description="Description"
|
||||
disabled={!moduleEnabled}
|
||||
isInvalid={!!errors.heartbeat?.message}
|
||||
validationMessage={errors.heartbeat?.message}
|
||||
>
|
||||
<Controller
|
||||
name="heartbeat"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="Store & Forward Config"
|
||||
breadcrumbs={["Module Config", "Store & Forward"]}
|
||||
reset={() => reset(moduleConfig.storeForward)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="heartbeat"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Heartbeat Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
label="Number of records"
|
||||
description="Max transmit power in dBm"
|
||||
hint="Records"
|
||||
suffix="Records"
|
||||
disabled={!moduleEnabled}
|
||||
{...register("records", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
type="number"
|
||||
label="History return max"
|
||||
description="Max transmit power in dBm"
|
||||
@@ -100,7 +103,7 @@ export const StoreForward = (): JSX.Element => {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<TextInputField
|
||||
<Input
|
||||
type="number"
|
||||
label="History return window"
|
||||
description="Max transmit power in dBm"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { FormField, Switch, TextInputField } from "evergreen-ui";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { Input } from "@app/components/form/Input.js";
|
||||
import { Toggle } from "@app/components/form/Toggle.js";
|
||||
import { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.js";
|
||||
import { Form } from "@components/form/Form";
|
||||
import { useDevice } from "@core/providers/useDevice.js";
|
||||
@@ -44,58 +45,59 @@ export const Telemetry = (): JSX.Element => {
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
label="Measurement Enabled"
|
||||
description="Description"
|
||||
isInvalid={!!errors.environmentMeasurementEnabled?.message}
|
||||
validationMessage={errors.environmentMeasurementEnabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="environmentMeasurementEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Displayed on Screen"
|
||||
description="Description"
|
||||
isInvalid={!!errors.environmentScreenEnabled?.message}
|
||||
validationMessage={errors.environmentScreenEnabled?.message}
|
||||
>
|
||||
<Controller
|
||||
name="environmentScreenEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<TextInputField
|
||||
<Form
|
||||
title="Telemetry Config"
|
||||
breadcrumbs={["Module Config", "Telemetry"]}
|
||||
reset={() => reset(moduleConfig.telemetry)}
|
||||
loading={loading}
|
||||
dirty={isDirty}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Controller
|
||||
name="environmentMeasurementEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Module Enabled"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="environmentScreenEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Displayed on Screen"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
label="Update Interval"
|
||||
description="Max transmit power in dBm"
|
||||
hint="Seconds"
|
||||
suffix="Seconds"
|
||||
type="number"
|
||||
{...register("environmentUpdateInterval", {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
/>
|
||||
<FormField
|
||||
label="Display Farenheit"
|
||||
description="Description"
|
||||
isInvalid={!!errors.environmentDisplayFahrenheit?.message}
|
||||
validationMessage={errors.environmentDisplayFahrenheit?.message}
|
||||
>
|
||||
<Controller
|
||||
name="environmentDisplayFahrenheit"
|
||||
control={control}
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<Switch height={24} marginLeft="auto" checked={value} {...field} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<Controller
|
||||
name="environmentDisplayFahrenheit"
|
||||
control={control}
|
||||
render={({ field: { value, ...rest } }) => (
|
||||
<Toggle
|
||||
label="Display Farenheit"
|
||||
description="Description"
|
||||
checked={value}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user