Initial 1.3.x rework

This commit is contained in:
Sacha Weatherstone
2022-07-26 08:52:23 +10:00
parent ff6019098b
commit a2280ab96e
163 changed files with 6383 additions and 14397 deletions
@@ -0,0 +1,117 @@
import type React from "react";
import { useEffect, useState } from "react";
import {
FormField,
SelectField,
Switch,
TextInputField,
toaster,
} from "evergreen-ui";
import { Controller, useForm } from "react-hook-form";
import { DeviceValidation } from "@app/validation/config/device.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { renderOptions } from "@core/utils/selectEnumOptions.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
export const Device = (): JSX.Element => {
const { config, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
control,
reset,
} = useForm<DeviceValidation>({
defaultValues: config.device,
resolver: classValidatorResolver(DeviceValidation),
});
useEffect(() => {
reset(config.device);
}, [reset, config.device]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "device",
device: data,
},
},
async () => {
toaster.success("Successfully updated device config");
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<SelectField
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>
<TextInputField
label="NTP Server"
description="This is a description."
isInvalid={!!errors.ntpServer?.message}
validationMessage={errors.ntpServer?.message}
{...register("ntpServer")}
/>
</Form>
);
};
@@ -0,0 +1,72 @@
import type React from "react";
import { useEffect, useState } from "react";
import { SelectField, TextInputField } from "evergreen-ui";
import { useForm } from "react-hook-form";
import { DisplayValidation } from "@app/validation/config/display.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { renderOptions } from "@core/utils/selectEnumOptions.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
export const Display = (): JSX.Element => {
const { config, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
} = useForm<Protobuf.Config_DisplayConfig>({
defaultValues: config.display,
resolver: classValidatorResolver(DisplayValidation),
});
useEffect(() => {
reset(config.display);
}, [reset, config.display]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "display",
display: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<TextInputField
label="Screen Timeout"
description="This is a description."
hint="Seconds"
type="number"
{...register("screenOnSecs", { valueAsNumber: true })}
/>
<TextInputField
label="Carousel Delay"
description="This is a description."
hint="Seconds"
type="number"
{...register("autoScreenCarouselSecs", { valueAsNumber: true })}
/>
<SelectField
label="GPS Display Units"
description="This is a description."
{...register("gpsFormat", { valueAsNumber: true })}
>
{renderOptions(Protobuf.Config_DisplayConfig_GpsCoordinateFormat)}
</SelectField>
</Form>
);
};
@@ -0,0 +1,163 @@
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 { LoRaValidation } from "@app/validation/config/lora.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { renderOptions } from "@core/utils/selectEnumOptions.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
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,
handleSubmit,
formState: { errors, isDirty },
control,
reset,
} = useForm<LoRaValidation>({
defaultValues: config.lora,
resolver: classValidatorResolver(LoRaValidation),
});
useEffect(() => {
reset(config.lora);
}, [reset, config.lora]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "lora",
lora: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
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"}
label="Preset"
description="This is a description."
isInvalid={!!errors.modemPreset?.message}
validationMessage={errors.modemPreset?.message}
{...register("modemPreset", { valueAsNumber: true })}
>
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
</SelectField>
<TextInputField
display={usePreset ? "none" : "block"}
label="Bandwidth"
description="Max transmit power in dBm"
type="number"
hint="MHz"
isInvalid={!!errors.bandwidth?.message}
validationMessage={errors.bandwidth?.message}
{...register("bandwidth", {
valueAsNumber: true,
})}
/>
<TextInputField
display={usePreset ? "none" : "block"}
label="Spread Factor"
description="Max transmit power in dBm"
type="number"
hint="CPS"
isInvalid={!!errors.spreadFactor?.message}
validationMessage={errors.spreadFactor?.message}
{...register("spreadFactor", {
valueAsNumber: true,
})}
/>
<TextInputField
display={usePreset ? "none" : "block"}
label="Coding Rate"
description="Max transmit power in dBm"
type="number"
isInvalid={!!errors.codingRate?.message}
validationMessage={errors.codingRate?.message}
{...register("codingRate", {
valueAsNumber: true,
})}
/>
<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 Count"
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
label="Frequency Offset"
description="This is a description."
hint="Hz"
type="number"
isInvalid={!!errors.frequencyOffset?.message}
validationMessage={errors.frequencyOffset?.message}
{...register("frequencyOffset", { valueAsNumber: true })}
/>
<SelectField
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>
</Form>
);
};
@@ -0,0 +1,224 @@
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 { PositionValidation } from "@app/validation/config/position.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.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();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<PositionValidation>({
defaultValues: config.position,
resolver: classValidatorResolver(PositionValidation),
// defaultValues: {
// ...preferences,
// positionBroadcastSecs:
// preferences.positionBroadcastSecs === 0
// ? preferences.role === Protobuf.Role.Router
// ? 43200
// : 900
// : preferences.positionBroadcastSecs,
// },
});
useEffect(() => {
reset(config.position);
}, [reset, config.position]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "position",
position: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<TextInputField
hint="Seconds"
label="Broadcast Interval"
description="This is a description."
type="number"
isInvalid={!!errors.positionBroadcastSecs?.message}
validationMessage={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"
label="GPS Update Interval"
description="This is a description."
type="number"
isInvalid={!!errors.gpsUpdateInterval?.message}
validationMessage={errors.gpsUpdateInterval?.message}
{...register("gpsUpdateInterval", { valueAsNumber: true })}
/>
<TextInputField
label="Last GPS Attempt"
description="This is a description."
type="number"
isInvalid={!!errors.gpsAttemptTime?.message}
validationMessage={errors.gpsAttemptTime?.message}
{...register("gpsAttemptTime", { valueAsNumber: true })}
/>
<Controller
name="positionFlags"
control={control}
render={({ field, fieldState }): JSX.Element => {
const { value, onChange, ...rest } = field;
const { error } = fieldState;
const options = Object.entries(
Protobuf.Config_PositionConfig_PositionFlags
)
.filter((value) => typeof value[1] !== "number")
.filter(
(value) =>
parseInt(value[0]) !==
Protobuf.Config_PositionConfig_PositionFlags.POS_UNDEFINED
)
.map((value) => {
return {
value: parseInt(value[0]),
label: value[1].toString().replace("POS_", "").toLowerCase(),
};
});
const selected = bitwiseDecode(
value,
Protobuf.Config_PositionConfig_PositionFlags
).map((flag) =>
Protobuf.Config_PositionConfig_PositionFlags[flag]
.replace("POS_", "")
.toLowerCase()
);
// onChange={(e: { value: number; label: string }[]): void =>
// onChange(bitwiseEncode(e.map((v) => v.value)))
// }
return (
<FormField
label="Position Flags"
description="Description"
isInvalid={!!errors.positionFlags?.message}
validationMessage={errors.positionFlags?.message}
>
<SelectMenu
isMultiSelect
title="Select multiple names"
options={options}
selected={selected}
// onSelect={(item) => {
// const selected = [...selectedItemsState, item.value]
// const selectedItems = selected
// const selectedItemsLength = selectedItems.length
// let selectedNames = ''
// if (selectedItemsLength === 0) {
// selectedNames = ''
// } else if (selectedItemsLength === 1) {
// selectedNames = selectedItems.toString()
// } else if (selectedItemsLength > 1) {
// selectedNames = selectedItemsLength.toString() + ' selected...'
// }
// setSelectedItems(selectedItems)
// setSelectedItemNames(selectedNames)
// }}
// onDeselect={(item) => {
// const deselectedItemIndex = selectedItemsState.indexOf(item.value)
// const selectedItems = selectedItemsState.filter((_item, i) => i !== deselectedItemIndex)
// const selectedItemsLength = selectedItems.length
// let selectedNames = ''
// if (selectedItemsLength === 0) {
// selectedNames = ''
// } else if (selectedItemsLength === 1) {
// selectedNames = selectedItems.toString()
// } else if (selectedItemsLength > 1) {
// selectedNames = selectedItemsLength.toString() + ' selected...'
// }
// setSelectedItems(selectedItems)
// setSelectedItemNames(selectedNames)
// }}
>
<Button>
{selected.map(
(item, index) =>
`${item}${index !== selected.length - 1 ? ", " : ""}`
)}
</Button>
</SelectMenu>
</FormField>
);
}}
/>
</Form>
);
};
@@ -0,0 +1,137 @@
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 { PowerValidation } from "@app/validation/config/power.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { renderOptions } from "@core/utils/selectEnumOptions.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
export const Power = (): JSX.Element => {
const { config, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<PowerValidation>({
defaultValues: config.power,
resolver: classValidatorResolver(PowerValidation),
});
useEffect(() => {
reset(config.power);
}, [reset, config.power]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "power",
power: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<SelectField
label="Charge current"
description="This is a description."
isInvalid={!!errors.chargeCurrent?.message}
validationMessage={errors.chargeCurrent?.message}
{...register("chargeCurrent", { valueAsNumber: true })}
>
{renderOptions(Protobuf.Config_PowerConfig_ChargeCurrent)}
</SelectField>
<TextInputField
label="Shutdown on battery delay"
description="This is a description."
hint="Seconds"
type="number"
isInvalid={!!errors.onBatteryShutdownAfterSecs?.message}
validationMessage={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
label="ADC Multiplier Override ratio"
description="This is a description."
type="number"
isInvalid={!!errors.adcMultiplierOverride?.message}
validationMessage={errors.adcMultiplierOverride?.message}
{...register("adcMultiplierOverride", { valueAsNumber: true })}
/>
<TextInputField
label="Minimum Wake Time"
description="This is a description."
hint="Seconds"
type="number"
isInvalid={!!errors.minWakeSecs?.message}
validationMessage={errors.minWakeSecs?.message}
{...register("minWakeSecs", { valueAsNumber: true })}
/>
<TextInputField
label="Mesh SDS Timeout"
description="This is a description."
hint="Seconds"
type="number"
isInvalid={!!errors.meshSdsTimeoutSecs?.message}
validationMessage={errors.meshSdsTimeoutSecs?.message}
{...register("meshSdsTimeoutSecs", { valueAsNumber: true })}
/>
<TextInputField
label="SDS"
description="This is a description."
hint="Seconds"
type="number"
isInvalid={!!errors.sdsSecs?.message}
validationMessage={errors.sdsSecs?.message}
{...register("sdsSecs", { valueAsNumber: true })}
/>
<TextInputField
label="LS"
description="This is a description."
hint="Seconds"
type="number"
isInvalid={!!errors.lsSecs?.message}
validationMessage={errors.lsSecs?.message}
{...register("lsSecs", { valueAsNumber: true })}
/>
<TextInputField
label="Wait Bluetooth"
description="This is a description."
hint="Seconds"
type="number"
isInvalid={!!errors.waitBluetoothSecs?.message}
validationMessage={errors.waitBluetoothSecs?.message}
{...register("waitBluetoothSecs", { valueAsNumber: true })}
/>
</Form>
);
};
@@ -0,0 +1,141 @@
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 { UserValidation } from "@app/validation/config/user.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { renderOptions } from "@core/utils/selectEnumOptions.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
export const User = (): JSX.Element => {
const { hardware, nodes, connection } = useDevice();
const [loading, setLoading] = useState(false);
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<UserValidation>({
defaultValues: myNode?.data.user,
resolver: classValidatorResolver(UserValidation),
});
useEffect(() => {
reset({
longName: myNode?.data.user?.longName,
shortName: myNode?.data.user?.shortName,
isLicensed: myNode?.data.user?.isLicensed,
});
}, [reset, myNode]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
if (myNode?.data.user) {
void connection?.setOwner({ ...myNode.data.user, ...data }, async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
});
}
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
{JSON.stringify(errors.antAzimuth?.message)}
{JSON.stringify(errors.antGainDbi?.message)}
{JSON.stringify(errors.hwModel?.message)}
{JSON.stringify(errors.id?.message)}
{JSON.stringify(errors.isLicensed?.message)}
{JSON.stringify(errors.longName?.message)}
{JSON.stringify(errors.shortName?.message)}
{JSON.stringify(errors.txPowerDbm?.message)}
<TextInputField
label="Device ID"
description="Preset unique identifier for this device."
isInvalid={!!errors.id?.message}
validationMessage={errors.id?.message}
{...register("id")}
readOnly
/>
<TextInputField
label="Device Name"
description="Personalised name for this device."
{...register("longName")}
/>
<TextInputField
label="Short Name"
description="This is a description."
maxLength={3}
{...register("shortName")}
/>
<TextInputField
label="Mac Address"
description="This is a description."
disabled
value={
base16
.stringify(myNode?.data.user?.macaddr ?? [])
.match(/.{1,2}/g)
?.join(":") ?? ""
}
readOnly
/>
<SelectField
label="Hardware"
description="This is a description."
disabled
isInvalid={!!errors.hwModel?.message}
validationMessage={errors.hwModel?.message}
{...register("hwModel", { valueAsNumber: true })}
// readOnly
>
{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>
<TextInputField
label="Transmit Power"
description="This is a description."
hint="dBm"
type="number"
{...register("txPowerDbm", { valueAsNumber: true })}
/>
<TextInputField
label="Antenna Gain"
description="This is a description."
hint="dBi"
type="number"
{...register("antGainDbi", { valueAsNumber: true })}
/>
<TextInputField
label="Antenna Azimuth"
description="This is a description."
hint="°"
type="number"
{...register("antAzimuth", { valueAsNumber: true })}
/>
</Form>
);
};
@@ -0,0 +1,94 @@
import type React from "react";
import { useEffect, useState } from "react";
import { FormField, Switch, TextInputField, toaster } from "evergreen-ui";
import { Controller, useForm } from "react-hook-form";
import { WiFiValidation } from "@app/validation/config/wifi.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const WiFi = (): JSX.Element => {
const { config, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
control,
reset,
} = useForm<WiFiValidation>({
defaultValues: config.wifi,
resolver: classValidatorResolver(WiFiValidation),
});
useEffect(() => {
reset(config.wifi);
}, [reset, config.wifi]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "wifi",
wifi: data,
},
},
async () => {
toaster.success("Your source is now sending data");
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<TextInputField
label="SSID"
description="This is a description."
isInvalid={!!errors.ssid?.message}
validationMessage={errors.ssid?.message}
{...register("ssid", { valueAsNumber: true })}
/>
<TextInputField
label="PSK"
description="This is a description."
type="password"
isInvalid={!!errors.psk?.message}
validationMessage={errors.psk?.message}
{...register("psk", { valueAsNumber: true })}
/>
<FormField
label="Enable WiFi AP"
description="Description"
isInvalid={!!errors.apMode?.message}
validationMessage={errors.apMode?.message}
>
<Controller
name="apMode"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<FormField
label="Don't broadcast SSID"
description="Description"
isInvalid={!!errors.apHidden?.message}
validationMessage={errors.apHidden?.message}
>
<Controller
name="apHidden"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
</Form>
);
};
@@ -0,0 +1,96 @@
import type React from "react";
import { useEffect, useState } from "react";
import { FormField, Switch, TextInputField, toaster } from "evergreen-ui";
import { Controller, useForm } from "react-hook-form";
import { WiFiValidation } from "@app/validation/config/wifi.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const WiFi = (): JSX.Element => {
const { config, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
control,
reset,
} = useForm<WiFiValidation>({
defaultValues: config.wifi,
resolver: classValidatorResolver(WiFiValidation),
});
useEffect(() => {
reset(config.wifi);
}, [reset, config.wifi]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "wifi",
wifi: data,
},
},
async () => {
toaster.success("Successfully updated WiFi config");
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<TextInputField
label="SSID"
description="This is a description."
isInvalid={!!errors.ssid?.message}
validationMessage={errors.ssid?.message}
{...register("ssid")}
/>
<TextInputField
label="PSK"
type="password"
description="This is a description."
isInvalid={!!errors.psk?.message}
validationMessage={errors.psk?.message}
{...register("psk")}
/>
<FormField
label="Enable WiFi AP"
description="Description"
isInvalid={!!errors.apMode?.message}
validationMessage={errors.apMode?.message}
>
<Controller
name="apMode"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<FormField
label="Don't broadcast SSID"
description="Description"
isInvalid={!!errors.apHidden?.message}
validationMessage={errors.apHidden?.message}
>
<Controller
name="apHidden"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
</Form>
);
};
@@ -0,0 +1,171 @@
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 { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { renderOptions } from "@core/utils/selectEnumOptions.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
export const CannedMessage = (): JSX.Element => {
const { moduleConfig, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<CannedMessageValidation>({
defaultValues: moduleConfig.cannedMessage,
resolver: classValidatorResolver(CannedMessageValidation),
});
const moduleEnabled = useWatch({
control,
name: "rotary1Enabled",
defaultValue: false,
});
useEffect(() => {
reset(moduleConfig.cannedMessage);
}, [reset, moduleConfig.cannedMessage]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setModuleConfig(
{
payloadVariant: {
oneofKind: "cannedMessage",
cannedMessage: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={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
label="Encoder Pin A"
description="Max transmit power in dBm"
type="number"
disabled={moduleEnabled}
{...register("inputbrokerPinA", { valueAsNumber: true })}
/>
<TextInputField
label="Encoder Pin B"
description="Max transmit power in dBm"
type="number"
disabled={moduleEnabled}
{...register("inputbrokerPinB", { valueAsNumber: true })}
/>
<TextInputField
label="Endoer Pin Press"
description="Max transmit power in dBm"
type="number"
disabled={moduleEnabled}
{...register("inputbrokerPinPress", { valueAsNumber: true })}
/>
<SelectField
label="Clockwise event"
description="This is a description."
disabled={moduleEnabled}
{...register("inputbrokerEventCw", { valueAsNumber: true })}
>
{renderOptions(
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
)}
</SelectField>
<SelectField
label="Counter Clockwise event"
description="This is a description."
disabled={moduleEnabled}
{...register("inputbrokerEventCcw", { valueAsNumber: true })}
>
{renderOptions(
Protobuf.ModuleConfig_CannedMessageConfig_InputEventChar
)}
</SelectField>
<SelectField
label="Press event"
description="This is a description."
disabled={moduleEnabled}
{...register("inputbrokerEventPress", { valueAsNumber: true })}
>
{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
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>
</Form>
);
};
@@ -0,0 +1,134 @@
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 { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const ExternalNotification = (): JSX.Element => {
const { moduleConfig, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<ExternalNotificationValidation>({
defaultValues: moduleConfig.externalNotification,
resolver: classValidatorResolver(ExternalNotificationValidation),
});
useEffect(() => {
reset(moduleConfig.externalNotification);
}, [reset, moduleConfig.externalNotification]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection?.setModuleConfig(
{
payloadVariant: {
oneofKind: "externalNotification",
externalNotification: data,
},
},
async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
});
return (
<Form loading={loading} dirty={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
type="number"
label="Output MS"
description="Max transmit power in dBm"
hint="ms"
disabled={!moduleEnabled}
{...register("outputMs", {
valueAsNumber: true,
})}
/>
<TextInputField
type="number"
label="Output"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("output", {
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>
</Form>
);
};
@@ -0,0 +1,105 @@
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 { MQTTValidation } from "@app/validation/moduleConfig/mqtt.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const MQTT = (): JSX.Element => {
const { moduleConfig, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<MQTTValidation>({
defaultValues: moduleConfig.mqtt,
resolver: classValidatorResolver(MQTTValidation),
});
const moduleEnabled = useWatch({
control,
name: "disabled",
defaultValue: false,
});
useEffect(() => {
reset(moduleConfig.mqtt);
}, [reset, moduleConfig.mqtt]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setModuleConfig(
{
payloadVariant: {
oneofKind: "mqtt",
mqtt: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<FormField
label="Module Disabled"
description="Description"
isInvalid={!!errors.disabled?.message}
validationMessage={errors.disabled?.message}
>
<Controller
name="disabled"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<TextInputField
label="MQTT Server Address"
description="Max transmit power in dBm"
disabled={moduleEnabled}
{...register("address")}
/>
<TextInputField
label="MQTT Username"
description="Max transmit power in dBm"
disabled={moduleEnabled}
{...register("username")}
/>
<TextInputField
label="MQTT Password"
description="Max transmit power in dBm"
type="password"
autoComplete="off"
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>
</Form>
);
};
@@ -0,0 +1,96 @@
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 { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const RangeTest = (): JSX.Element => {
const { moduleConfig, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<RangeTestValidation>({
defaultValues: moduleConfig.rangeTest,
resolver: classValidatorResolver(RangeTestValidation),
});
useEffect(() => {
reset(moduleConfig.rangeTest);
}, [reset, moduleConfig.rangeTest]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection?.setModuleConfig(
{
payloadVariant: {
oneofKind: "rangeTest",
rangeTest: data,
},
},
async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
});
return (
<Form loading={loading} dirty={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
type="number"
label="Message Interval"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
hint="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>
</Form>
);
};
@@ -0,0 +1,131 @@
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 { SerialValidation } from "@app/validation/moduleConfig/serial.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const Serial = (): JSX.Element => {
const { moduleConfig, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<SerialValidation>({
defaultValues: moduleConfig.serial,
resolver: classValidatorResolver(SerialValidation),
});
useEffect(() => {
reset(moduleConfig.serial);
}, [reset, moduleConfig.serial]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection?.setModuleConfig(
{
payloadVariant: {
oneofKind: "serial",
serial: data,
},
},
async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
});
return (
<Form loading={loading} dirty={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
type="number"
label="RX"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("rxd", {
valueAsNumber: true,
})}
/>
<TextInputField
type="number"
label="TX Pin"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("txd", {
valueAsNumber: true,
})}
/>
<TextInputField
type="number"
label="Baud Rate"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("baud", {
valueAsNumber: true,
})}
/>
<TextInputField
type="number"
label="Timeout"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("timeout", {
valueAsNumber: true,
})}
/>
<TextInputField
type="number"
label="Mode"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("mode", {
valueAsNumber: true,
})}
/>
</Form>
);
};
@@ -0,0 +1,114 @@
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 { StoreForwardValidation } from "@app/validation/moduleConfig/storeForward.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const StoreForward = (): JSX.Element => {
const { moduleConfig, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<StoreForwardValidation>({
defaultValues: moduleConfig.storeForward,
resolver: classValidatorResolver(StoreForwardValidation),
});
useEffect(() => {
reset(moduleConfig.storeForward);
}, [reset, moduleConfig.storeForward]);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
await connection?.setModuleConfig(
{
payloadVariant: {
oneofKind: "storeForward",
storeForward: data,
},
},
async (): Promise<void> => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
});
return (
<Form loading={loading} dirty={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
type="number"
label="Number of records"
description="Max transmit power in dBm"
hint="Records"
disabled={!moduleEnabled}
{...register("records", {
valueAsNumber: true,
})}
/>
<TextInputField
type="number"
label="History return max"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("historyReturnMax", {
valueAsNumber: true,
})}
/>
<TextInputField
type="number"
label="History return window"
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("historyReturnWindow", {
valueAsNumber: true,
})}
/>
</Form>
);
};
@@ -0,0 +1,135 @@
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 { TelemetryValidation } from "@app/validation/moduleConfig/telemetry.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { renderOptions } from "@core/utils/selectEnumOptions.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
export const Telemetry = (): JSX.Element => {
const { moduleConfig, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
reset,
control,
} = useForm<TelemetryValidation>({
defaultValues: moduleConfig.telemetry,
resolver: classValidatorResolver(TelemetryValidation),
});
useEffect(() => {
reset(moduleConfig.telemetry);
}, [reset, moduleConfig.telemetry]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setModuleConfig(
{
payloadVariant: {
oneofKind: "telemetry",
telemetry: data,
},
},
async () => {
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={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
label="Read Error Count Threshold"
description="Max transmit power in dBm"
type="number"
{...register("environmentReadErrorCountThreshold", {
valueAsNumber: true,
})}
/>
<TextInputField
label="Update Interval"
description="Max transmit power in dBm"
hint="Seconds"
type="number"
{...register("environmentUpdateInterval", {
valueAsNumber: true,
})}
/>
<TextInputField
label="Recovery Interval"
description="Max transmit power in dBm"
hint="Seconds"
type="number"
{...register("environmentRecoveryInterval", {
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>
<SelectField
label="Sensor Type"
description="This is a description."
{...register("environmentSensorType", { valueAsNumber: true })}
>
{renderOptions(Protobuf.TelemetrySensorType)}
</SelectField>
<TextInputField
label="Sensor Pin"
description="Max transmit power in dBm"
type="number"
{...register("environmentSensorPin", {
valueAsNumber: true,
})}
/>
</Form>
);
};