WIP updates

This commit is contained in:
Sacha Weatherstone
2022-11-27 22:50:46 +10:00
parent a9561fe622
commit e457cef3da
79 changed files with 2100 additions and 1284 deletions

View File

@@ -22,14 +22,18 @@ export const Button = ({
className={`flex w-full rounded-md border border-transparent px-3 focus:outline-none focus:ring-2 focus:ring-orange-500 ${
variant === "primary"
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
: "bg-orange-100 text-orange-700 hover:bg-orange-200"
: "bg-orange-200 text-orange-700 hover:bg-orange-200"
} ${
size === "sm"
? "h-8 text-sm"
: size === "md"
? "h-10 text-sm"
: "h-10 text-base"
} ${disabled ? "cursor-not-allowed bg-red-400 focus:ring-red-500" : ""}`}
} ${
disabled
? "cursor-not-allowed bg-gray-400 hover:bg-gray-400 focus:ring-gray-500"
: ""
}`}
disabled={disabled}
{...rest}
>

View File

@@ -40,7 +40,7 @@ import {
TrashIcon,
UsersIcon,
WindowIcon,
XCircleIcon,
XCircleIcon
} from "@heroicons/react/24/outline";
import { GroupView } from "./GroupView.js";
@@ -77,58 +77,58 @@ export const CommandPalette = (): JSX.Element => {
icon: InboxIcon,
action() {
setActivePage("messages");
},
}
},
{
name: "Map",
icon: MapIcon,
action() {
setActivePage("map");
},
}
},
{
name: "Extensions",
icon: BeakerIcon,
action() {
setActivePage("extensions");
},
}
},
{
name: "Config",
icon: Cog8ToothIcon,
action() {
setActivePage("config");
},
}
},
{
name: "Channels",
icon: Square3Stack3DIcon,
action() {
setActivePage("channels");
},
}
},
{
name: "Peers",
icon: UsersIcon,
action() {
setActivePage("peers");
},
}
},
{
name: "Info",
icon: IdentificationIcon,
action() {
setActivePage("info");
},
}
},
{
name: "Logs",
icon: DocumentTextIcon,
action() {
setActivePage("logs");
},
},
],
}
}
]
},
{
name: "Manage",
@@ -138,17 +138,17 @@ export const CommandPalette = (): JSX.Element => {
name: "[WIP] Switch Node",
icon: ArrowsRightLeftIcon,
action() {
alert('This feature is not implemented');
},
alert("This feature is not implemented");
}
},
{
name: "Connect New Node",
icon: PlusIcon,
action() {
setSelectedDevice(0);
},
},
],
}
}
]
},
{
name: "Contextual",
@@ -159,20 +159,20 @@ export const CommandPalette = (): JSX.Element => {
icon: QrCodeIcon,
action() {
setQRDialogOpen(true);
},
}
},
{
name: "Reset Peers",
icon: TrashIcon,
action() {
if (connection) {
void toast.promise(connection.resetPeers(), {
void toast.promise(connection.resetPeers({}), {
loading: "Resetting...",
success: "Succesfully reset peers",
error: "No response received",
error: "No response received"
});
}
},
}
},
{
name: "Disconnect",
@@ -181,9 +181,9 @@ export const CommandPalette = (): JSX.Element => {
void connection?.disconnect();
setSelectedDevice(0);
removeDevice(selectedDevice ?? 0);
},
},
],
}
}
]
},
{
name: "Debug",
@@ -194,16 +194,16 @@ export const CommandPalette = (): JSX.Element => {
icon: ArrowPathIcon,
action() {
void connection?.configure();
},
}
},
{
name: "[WIP] Clear Messages",
icon: ArchiveBoxXMarkIcon,
action() {
alert('This feature is not implemented');
},
},
],
alert("This feature is not implemented");
}
}
]
},
{
name: "Application",
@@ -213,11 +213,11 @@ export const CommandPalette = (): JSX.Element => {
name: "[WIP] Toggle Dark Mode",
icon: MoonIcon,
action() {
alert('This feature is not implemented');
},
},
],
},
alert("This feature is not implemented");
}
}
]
}
];
const handleKeydown = (e: KeyboardEvent) => {
@@ -243,7 +243,7 @@ export const CommandPalette = (): JSX.Element => {
return `${group.name} ${command.name}`
.toLowerCase()
.includes(query.toLowerCase());
}),
})
};
})
.filter((group) => group.commands.length);

View File

@@ -8,7 +8,7 @@ export interface PaletteTransitionProps {
}
export const PaletteTransition = ({
children,
children
}: PaletteTransitionProps): JSX.Element => {
return (
<>

View File

@@ -0,0 +1,126 @@
import type React from "react";
import { useEffect, useState } from "react";
import { fromByteArray } from "base64-js";
import { toast } from "react-hot-toast";
import { QRCode } from "react-qrcode-logo";
import { Dialog } from "@headlessui/react";
import { ClipboardIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { Checkbox } from "../form/Checkbox.js";
import { Input } from "../form/Input.js";
import { IconButton } from "../IconButton.js";
export interface ImportDialogProps {
isOpen: boolean;
close: () => void;
loraConfig?: Protobuf.Config_LoRaConfig;
channels: Protobuf.Channel[];
}
export const ImportDialog = ({
isOpen,
close,
loraConfig,
channels
}: ImportDialogProps): JSX.Element => {
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
const [QRCodeURL, setQRCodeURL] = useState<string>("");
useEffect(() => {
const channelsToEncode = channels
.filter((channel) => selectedChannels.includes(channel.index))
.map((channel) => channel.settings)
.filter((ch): ch is Protobuf.ChannelSettings => !!ch);
const encoded = Protobuf.ChannelSet.toBinary({
loraConfig,
settings: channelsToEncode
});
const base64 = fromByteArray(encoded)
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
setQRCodeURL(`https://meshtastic.org/e/#${base64}`);
}, [channels, selectedChannels, loraConfig]);
return (
<Dialog open={isOpen} onClose={close}>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel>
<div className="divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
<div className="flex px-4 py-5 sm:px-6">
<div>
<h1 className="text-lg font-bold">Generate QR Code</h1>
<h5 className="text-sm text-slate-600">
The current LoRa configuration will also be shared.
</h5>
</div>
<IconButton
onClick={close}
className="my-auto ml-auto"
size="sm"
variant="secondary"
icon={<XMarkIcon className="h-4" />}
/>
</div>
<div className="flex gap-3 px-4 py-5 sm:p-6">
<div className="flex w-40 flex-col gap-1">
{channels.map((channel) => (
<Checkbox
key={channel.index}
disabled={
channel.index === 0 ||
channel.role === Protobuf.Channel_Role.DISABLED
}
label={
channel.settings?.name.length
? channel.settings.name
: channel.role === Protobuf.Channel_Role.PRIMARY
? "Primary"
: `Channel: ${channel.index}`
}
checked={selectedChannels.includes(channel.index)}
onChange={() => {
if (selectedChannels.includes(channel.index)) {
setSelectedChannels(
selectedChannels.filter((c) => c !== channel.index)
);
} else {
setSelectedChannels([
...selectedChannels,
channel.index
]);
}
}}
/>
))}
</div>
<QRCode value={QRCodeURL} size={200} qrStyle="dots" />
</div>
<div className="px-4 py-4 sm:px-6">
<Input
label="Sharable URL"
value={QRCodeURL}
disabled
action={{
icon: <ClipboardIcon className="h-4" />,
action() {
void navigator.clipboard.writeText(QRCodeURL);
toast.success("Copied URL to Clipboard");
}
}}
/>
</div>
{/* </Card> */}
</div>
</Dialog.Panel>
</div>
</Dialog>
);
};

View File

@@ -24,7 +24,7 @@ export const QRDialog = ({
isOpen,
close,
loraConfig,
channels,
channels
}: QRDialogProps): JSX.Element => {
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
const [QRCodeURL, setQRCodeURL] = useState<string>("");
@@ -37,7 +37,7 @@ export const QRDialog = ({
const encoded = Protobuf.ChannelSet.toBinary(
Protobuf.ChannelSet.create({
loraConfig,
settings: channelsToEncode,
settings: channelsToEncode
})
);
const base64 = fromByteArray(encoded)
@@ -94,7 +94,7 @@ export const QRDialog = ({
} else {
setSelectedChannels([
...selectedChannels,
channel.index,
channel.index
]);
}
}}
@@ -114,7 +114,7 @@ export const QRDialog = ({
action() {
void navigator.clipboard.writeText(QRCodeURL);
toast.success("Copied URL to Clipboard");
},
}
}}
/>
</div>

50
src/components/Drawer.tsx Normal file
View File

@@ -0,0 +1,50 @@
import type React from "react";
import { useState } from "react";
import { useDevice } from "@app/core/providers/useDevice.js";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
export const Drawer = (): JSX.Element => {
const [drawerOpen, setDrawerOpen] = useState(false);
const tabs = [{ title: "Notifications" }, { title: "Debug" }];
const { config, moduleConfig, hardware, nodes, waypoints, connection } =
useDevice();
const [serialLogs, setSerialLogs] = useState<string>("");
connection?.onDeviceDebugLog.subscribe((packet) => {
setSerialLogs(serialLogs + new TextDecoder().decode(packet));
});
return (
<div className={`shadow-md ${drawerOpen ? "h-40" : "h-8"}`}>
<div className="flex h-8 bg-slate-50">
<div
onClick={() => {
setDrawerOpen(!drawerOpen);
}}
className="ml-auto flex px-2 hover:cursor-pointer hover:bg-slate-100"
>
<div className="m-auto">
{drawerOpen ? (
<ChevronDownIcon className="h-4 text-gray-700" />
) : (
<ChevronUpIcon className="h-4 text-gray-700" />
)}
</div>
</div>
</div>
<div className={`${drawerOpen ? "flex" : "hidden"}`}>
<div>
{serialLogs.split("\n").map((line, index) => (
<div key={index} className="text-sm">
{line}
</div>
))}
</div>
</div>
</div>
);
};

View File

@@ -18,7 +18,7 @@ export const Dropdown = ({
stat,
icon,
defaultOpen,
children,
children
}: DropdownProps): JSX.Element => {
return (
<Disclosure defaultOpen={defaultOpen}>

View File

@@ -15,13 +15,13 @@ export const NewDevice = () => {
element: BLE,
disabled: !navigator.bluetooth,
disabledMessage:
"WebBluetooth is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
"WebBluetooth is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility"
},
{
name: "HTTP",
icon: <FiWifi className="h-4" />,
element: HTTP,
disabled: false,
disabled: false
},
{
name: "Serial",
@@ -29,8 +29,8 @@ export const NewDevice = () => {
element: Serial,
disabled: !navigator.serial,
disabledMessage:
"WebSerial is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
},
"WebSerial is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility"
}
]);
return (

View File

@@ -0,0 +1,132 @@
import type React from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { Button } from "@app/components/Button.js";
import { InfoWrapper } from "@app/components/form/InfoWrapper.js";
import { Input } from "@app/components/form/Input.js";
import { Toggle } from "@app/components/form/Toggle.js";
import { IconButton } from "@app/components/IconButton.js";
import { useAppStore } from "@app/core/stores/appStore.js";
import { MapValidation } from "@app/validation/appConfig/map.js";
import { Form } from "@components/form/Form";
import { TrashIcon } from "@heroicons/react/24/outline";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const Map = (): JSX.Element => {
const { rasterSources, setRasterSources } = useAppStore();
const {
register,
handleSubmit,
formState: { errors, isDirty },
control,
reset
} = useForm<MapValidation>({
defaultValues: {
// wmsSources: wmsSources ?? [
// {
// url: "",
// tileSize: 512,
// type: "raster"
// }
// ]
},
resolver: classValidatorResolver(MapValidation)
});
const { fields, append, remove, insert } = useFieldArray({
control,
name: "rasterSources"
});
const onSubmit = handleSubmit((data) => {
setRasterSources(data.rasterSources);
});
// useEffect(() => {
// reset(rasterSources);
// }, [reset, rasterSources]);
return (
<Form
title="Map Config"
breadcrumbs={["App Config", "Map"]}
reset={() =>
reset({
rasterSources
})
}
dirty={isDirty}
onSubmit={onSubmit}
>
<InfoWrapper label="WMS Sources">
<div className="flex flex-col gap-2">
{fields.map((field, index) => (
<div key={field.id} className="flex w-full gap-2">
<Controller
name={`rasterSources.${index}.enabled`}
control={control}
render={({ field: { value, ...rest } }) => (
<Toggle checked={value} {...rest} />
)}
/>
<Input
placeholder="Name"
error={
errors.rasterSources
? errors.rasterSources[index]?.title?.message
: undefined
}
{...register(`rasterSources.${index}.title`)}
/>
<Input
placeholder="Tile Size"
type="number"
error={
errors.rasterSources
? errors.rasterSources[index]?.tileSize?.message
: undefined
}
{...register(`rasterSources.${index}.tileSize`, {
valueAsNumber: true
})}
/>
<Input
placeholder="URL"
error={
errors.rasterSources
? errors.rasterSources[index]?.tiles?.message
: undefined
}
{...register(`rasterSources.${index}.tiles`)}
/>
<IconButton
className="shrink-0"
icon={<TrashIcon className="w-4" />}
onClick={() => {
remove(index);
}}
/>
</div>
))}
<Button
variant="secondary"
onClick={() => {
append({
enabled: true,
title: "",
tiles: [
"https://img.nj.gov/imagerywms/Natural2015?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:3857&transparent=true&width=256&height=256&layers=Natural2015"
],
tileSize: 512
});
}}
>
New Source
</Button>
</div>
</InfoWrapper>
</Form>
);
};

View File

@@ -14,7 +14,7 @@ import { useDevice } from "@core/providers/useDevice.js";
import {
ArrowPathIcon,
EyeIcon,
EyeSlashIcon,
EyeSlashIcon
} from "@heroicons/react/24/outline";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
@@ -34,39 +34,39 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
formState: { errors, isDirty },
reset,
control,
setValue,
setValue
} = useForm<ChannelSettingsValidation>({
defaultValues: {
enabled: [
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
Protobuf.Channel_Role.PRIMARY
].find((role) => role === channel?.role)
? true
: false,
...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0))
},
resolver: classValidatorResolver(ChannelSettingsValidation),
resolver: classValidatorResolver(ChannelSettingsValidation)
});
useEffect(() => {
reset({
enabled: [
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
Protobuf.Channel_Role.PRIMARY
].find((role) => role === channel?.role)
? true
: false,
...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0))
});
}, [channel, reset]);
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setChannel(
{
connection.setChannel({
channel: {
role:
channel?.role === Protobuf.Channel_Role.PRIMARY
? Protobuf.Channel_Role.PRIMARY
@@ -76,18 +76,18 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
index: channel?.index,
settings: {
...data,
psk: toByteArray(data.psk ?? ""),
},
psk: toByteArray(data.psk ?? "")
}
},
(): Promise<void> => {
callback: (): Promise<void> => {
reset({ ...data });
return Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Channel",
error: "No response received",
error: "No response received"
}
);
}
@@ -102,18 +102,18 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
? channel.settings.name
: channel.role === Protobuf.Channel_Role.PRIMARY
? "Primary"
: `Channel: ${channel.index}`,
: `Channel: ${channel.index}`
]}
reset={() =>
reset({
enabled: [
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
Protobuf.Channel_Role.PRIMARY
].find((role) => role === channel?.role)
? true
: false,
...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0))
})
}
dirty={isDirty}
@@ -153,8 +153,10 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
action: () => {
const key = new Uint8Array(keySize / 8);
crypto.getRandomValues(key);
setValue("psk", fromByteArray(key));
},
setValue("psk", fromByteArray(key), {
shouldDirty: true
});
}
}}
>
<option value={128}>128 Bit</option>
@@ -173,7 +175,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
),
action: () => {
setPskHidden(!pskHidden);
},
}
}}
error={errors.psk?.message}
{...register("psk")}

View File

@@ -22,10 +22,10 @@ export const Bluetooth = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
control,
reset,
reset
} = useForm<BluetoothValidation>({
defaultValues: config.bluetooth,
resolver: classValidatorResolver(BluetoothValidation),
resolver: classValidatorResolver(BluetoothValidation)
});
useEffect(() => {
@@ -35,22 +35,22 @@ export const Bluetooth = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setConfig(
{
connection.setConfig({
config: {
payloadVariant: {
oneofKind: "bluetooth",
bluetooth: data,
},
bluetooth: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Bluetooth Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -59,7 +59,7 @@ export const Bluetooth = (): JSX.Element => {
const pairingMode = useWatch({
control,
name: "mode",
defaultValue: Protobuf.Config_BluetoothConfig_PairingMode.RANDOM_PIN,
defaultValue: Protobuf.Config_BluetoothConfig_PairingMode.RANDOM_PIN
});
return (
@@ -98,7 +98,7 @@ export const Bluetooth = (): JSX.Element => {
description="Pin to use when pairing"
type="number"
{...register("fixedPin", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
</Form>

View File

@@ -4,6 +4,7 @@ import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
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 { DeviceValidation } from "@app/validation/config/device.js";
@@ -20,10 +21,10 @@ export const Device = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
control,
reset,
reset
} = useForm<DeviceValidation>({
defaultValues: config.device,
resolver: classValidatorResolver(DeviceValidation),
resolver: classValidatorResolver(DeviceValidation)
});
useEffect(() => {
@@ -33,22 +34,22 @@ export const Device = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setConfig(
{
connection.setConfig({
config: {
payloadVariant: {
oneofKind: "device",
device: data,
},
device: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Device Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -93,6 +94,20 @@ export const Device = (): JSX.Element => {
/>
)}
/>
<Input
label="Button Pin"
description="Button pin override"
type="number"
error={errors.buttonGpio?.message}
{...register("buttonGpio", { valueAsNumber: true })}
/>
<Input
label="Buzzer Pin"
description="Buzzer pin override"
type="number"
error={errors.buzzerGpio?.message}
{...register("buzzerGpio", { valueAsNumber: true })}
/>
</Form>
);
};

View File

@@ -21,10 +21,10 @@ export const Display = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<Protobuf.Config_DisplayConfig>({
defaultValues: config.display,
resolver: classValidatorResolver(DisplayValidation),
resolver: classValidatorResolver(DisplayValidation)
});
useEffect(() => {
@@ -34,22 +34,22 @@ export const Display = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setConfig(
{
connection.setConfig({
config: {
payloadVariant: {
oneofKind: "display",
display: data,
},
display: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Display Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -115,6 +115,13 @@ export const Display = (): JSX.Element => {
>
{renderOptions(Protobuf.Config_DisplayConfig_DisplayUnits)}
</Select>
<Select
label="OLED Type"
description="Type of OLED screen attached to the device"
{...register("oled", { valueAsNumber: true })}
>
{renderOptions(Protobuf.Config_DisplayConfig_OledType)}
</Select>
</Form>
);
};

View File

@@ -23,16 +23,16 @@ export const LoRa = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
control,
reset,
reset
} = useForm<LoRaValidation>({
defaultValues: config.lora,
resolver: classValidatorResolver(LoRaValidation),
resolver: classValidatorResolver(LoRaValidation)
});
const usePreset = useWatch({
control,
name: "usePreset",
defaultValue: true,
defaultValue: true
});
useEffect(() => {
@@ -42,22 +42,22 @@ export const LoRa = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setConfig(
{
connection.setConfig({
config: {
payloadVariant: {
oneofKind: "lora",
lora: data,
},
lora: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved LoRa Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -99,7 +99,7 @@ export const LoRa = (): JSX.Element => {
suffix="MHz"
error={errors.bandwidth?.message}
{...register("bandwidth", {
valueAsNumber: true,
valueAsNumber: true
})}
disabled={usePreset}
/>
@@ -110,7 +110,7 @@ export const LoRa = (): JSX.Element => {
suffix="CPS"
error={errors.spreadFactor?.message}
{...register("spreadFactor", {
valueAsNumber: true,
valueAsNumber: true
})}
disabled={usePreset}
/>
@@ -120,7 +120,7 @@ export const LoRa = (): JSX.Element => {
type="number"
error={errors.codingRate?.message}
{...register("codingRate", {
valueAsNumber: true,
valueAsNumber: true
})}
disabled={usePreset}
/>

View File

@@ -6,12 +6,14 @@ import { toast } from "react-hot-toast";
import { FormSection } from "@app/components/form/FormSection.js";
import { Input } from "@app/components/form/Input.js";
import { IPAddress } from "@app/components/form/IPAddress.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";
import { useDevice } from "@core/providers/useDevice.js";
import { ErrorMessage } from "@hookform/error-message";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
@@ -22,22 +24,22 @@ export const Network = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
control,
reset,
reset
} = useForm<NetworkValidation>({
defaultValues: config.network,
resolver: classValidatorResolver(NetworkValidation),
resolver: classValidatorResolver(NetworkValidation)
});
const wifiEnabled = useWatch({
control,
name: "wifiEnabled",
defaultValue: false,
defaultValue: false
});
const ethEnabled = useWatch({
control,
name: "ethEnabled",
defaultValue: false,
defaultValue: false
});
useEffect(() => {
@@ -45,24 +47,32 @@ export const Network = (): JSX.Element => {
}, [reset, config.network]);
const onSubmit = handleSubmit((data) => {
console.log(data);
if (connection) {
const tmp = Protobuf.Config_NetworkConfig.create({
ethEnabled: true,
ethMode: Protobuf.Config_NetworkConfig_EthMode.DHCP
});
void toast.promise(
connection.setConfig(
{
payloadVariant: {
oneofKind: "network",
network: data,
connection
.setConfig({
config: {
payloadVariant: {
oneofKind: "network",
network: tmp
}
},
},
async () => {
reset({ ...data });
await Promise.resolve();
}
),
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
})
.catch((e) => console.log(e)),
{
loading: "Saving...",
success: "Saved Network Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -76,6 +86,19 @@ export const Network = (): JSX.Element => {
dirty={isDirty}
onSubmit={onSubmit}
>
<ErrorMessage errors={errors} name="wifiEnabled" />
<ErrorMessage errors={errors} name="wifiMode" />
<ErrorMessage errors={errors} name="wifiSsid" />
<ErrorMessage errors={errors} name="wifiPsk" />
<ErrorMessage errors={errors} name="ntpServer" />
<ErrorMessage errors={errors} name="ethEnabled" />
<ErrorMessage errors={errors} name="ethMode" />
<ErrorMessage errors={errors} name="ethConfig" />
<ErrorMessage errors={errors} name="ip" />
<ErrorMessage errors={errors} name="gateway" />
<ErrorMessage errors={errors} name="subnet" />
<ErrorMessage errors={errors} name="dns" />
<FormSection title="WiFi Config">
<Controller
name="wifiEnabled"
@@ -83,26 +106,18 @@ export const Network = (): JSX.Element => {
render={({ field: { value, ...rest } }) => (
<Toggle
label="WiFi Enabled"
description="Enable or disbale the WiFi radio"
description="Enable or disable the WiFi radio"
checked={value}
{...rest}
/>
)}
/>
<Select
label="WiFi Mode"
description="How the WiFi radio should be used"
disabled={!wifiEnabled}
{...register("wifiMode", { valueAsNumber: true })}
>
{renderOptions(Protobuf.Config_NetworkConfig_WiFiMode)}
</Select>
<Input
label="SSID"
description="Network name"
error={errors.wifiSsid?.message}
disabled={!wifiEnabled}
{...register("wifiSsid")}
{...register("wifiSsid", { disabled: !wifiEnabled })}
/>
<Input
label="PSK"
@@ -110,7 +125,7 @@ export const Network = (): JSX.Element => {
description="Network password"
error={errors.wifiPsk?.message}
disabled={!wifiEnabled}
{...register("wifiPsk")}
{...register("wifiPsk", { disabled: !wifiEnabled })}
/>
</FormSection>
<FormSection title="Ethernet Config">
@@ -130,35 +145,43 @@ export const Network = (): JSX.Element => {
label="Ethernet Mode"
description="Address assignment selection"
disabled={!ethEnabled}
{...register("ethMode", { valueAsNumber: true })}
{...register("ethMode", {
valueAsNumber: true,
disabled: !ethEnabled
})}
>
{renderOptions(Protobuf.Config_NetworkConfig_EthMode)}
</Select>
</FormSection>
<FormSection title="IP Config">
<IPAddress label="IP" description="IP Address" />
<Input
label="IP"
type="number"
description="IP Address"
error={errors.ethConfig?.ip?.message}
{...register("ethConfig.ip")}
error={errors.ipv4Config?.ip?.message}
{...register("ipv4Config.ip", { valueAsNumber: true })}
/>
<Input
label="Gateway"
type="number"
description="Default Gateway"
error={errors.ethConfig?.gateway?.message}
{...register("ethConfig.gateway")}
error={errors.ipv4Config?.gateway?.message}
{...register("ipv4Config.gateway", { valueAsNumber: true })}
/>
<Input
label="Subnet"
type="number"
description="Subnet Mask"
error={errors.ethConfig?.subnet?.message}
{...register("ethConfig.subnet")}
error={errors.ipv4Config?.subnet?.message}
{...register("ipv4Config.subnet", { valueAsNumber: true })}
/>
<Input
label="DNS"
type="number"
description="DNS Server"
error={errors.ethConfig?.dns?.message}
{...register("ethConfig.dns")}
error={errors.ipv4Config?.dns?.message}
{...register("ipv4Config.dns", { valueAsNumber: true })}
/>
</FormSection>
<Input

View File

@@ -4,6 +4,7 @@ import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { toast } from "react-hot-toast";
import { BitwiseSelect } from "@app/components/form/BitwiseSelect.js";
import { FormSection } from "@app/components/form/FormSection.js";
import { Input } from "@app/components/form/Input.js";
import { Toggle } from "@app/components/form/Toggle.js";
@@ -23,21 +24,21 @@ export const Position = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<PositionValidation>({
defaultValues: {
fixedAlt: myNode?.data.position?.altitude,
fixedLat: (myNode?.data.position?.latitudeI ?? 0) / 1e7,
fixedLng: (myNode?.data.position?.longitudeI ?? 0) / 1e7,
...config.position,
...config.position
},
resolver: classValidatorResolver(PositionValidation),
resolver: classValidatorResolver(PositionValidation)
});
const fixedPositionEnabled = useWatch({
control,
name: "fixedPosition",
defaultValue: false,
defaultValue: false
});
useEffect(() => {
@@ -45,7 +46,7 @@ export const Position = (): JSX.Element => {
fixedAlt: myNode?.data.position?.altitude,
fixedLat: (myNode?.data.position?.latitudeI ?? 0) / 1e7,
fixedLng: (myNode?.data.position?.longitudeI ?? 0) / 1e7,
...config.position,
...config.position
});
}, [reset, config.position, myNode?.data.position]);
@@ -59,49 +60,41 @@ export const Position = (): JSX.Element => {
if (connection) {
void toast.promise(
connection.sendPacket(
Protobuf.Position.toBinary(
Protobuf.Position.create({
altitude: fixedAlt,
latitudeI: fixedLat * 1e7,
longitudeI: fixedLng * 1e7,
})
),
Protobuf.PortNum.POSITION_APP,
undefined,
true,
undefined,
true,
false,
async () => {
connection.setPosition({
position: Protobuf.Position.create({
altitude: fixedAlt,
latitudeI: fixedLat * 1e7,
longitudeI: fixedLng * 1e7
}),
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Channel",
error: "No response received",
error: "No response received"
}
);
if (configHasChanged) {
void toast.promise(
connection.setConfig(
{
connection.setConfig({
config: {
payloadVariant: {
oneofKind: "position",
position: rest,
},
position: rest
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Position Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -201,94 +194,65 @@ export const Position = (): JSX.Element => {
error={errors.gpsAttemptTime?.message}
{...register("gpsAttemptTime", { valueAsNumber: true })}
/>
{/* <Controller
<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.UNSET
)
.map((value) => {
return {
value: parseInt(value[0]),
label: value[1].toString().replace("POS_", "").toLowerCase(),
};
});
// const options = Object.entries(
// Protobuf.Config_PositionConfig_PositionFlags
// )
// .filter((value) => typeof value[1] !== "number")
// .filter(
// (value) =>
// parseInt(value[0]) !==
// Protobuf.Config_PositionConfig_PositionFlags.UNSET
// )
// .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()
);
// 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
<BitwiseSelect
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>
error={error?.message}
selected={value}
decodeEnun={Protobuf.Config_PositionConfig_PositionFlags}
onChange={onChange}
/>
);
}}
/> */}
/>
<Input
label="RX Pin"
description="GPS Module RX pin override"
type="number"
error={errors.rxGpio?.message}
{...register("rxGpio", { valueAsNumber: true })}
/>
<Input
label="TX Pin"
description="GPS Module TX pin override"
type="number"
error={errors.txGpio?.message}
{...register("txGpio", { valueAsNumber: true })}
/>
</Form>
);
};

View File

@@ -19,10 +19,10 @@ export const Power = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<PowerValidation>({
defaultValues: config.power,
resolver: classValidatorResolver(PowerValidation),
resolver: classValidatorResolver(PowerValidation)
});
useEffect(() => {
@@ -32,22 +32,22 @@ export const Power = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setConfig(
{
connection.setConfig({
config: {
payloadVariant: {
oneofKind: "power",
power: data,
},
power: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Power Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}

View File

@@ -25,31 +25,34 @@ export const User = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<UserValidation>({
defaultValues: myNode?.data.user,
resolver: classValidatorResolver(UserValidation),
resolver: classValidatorResolver(UserValidation)
});
useEffect(() => {
reset({
longName: myNode?.data.user?.longName,
shortName: myNode?.data.user?.shortName,
isLicensed: myNode?.data.user?.isLicensed,
isLicensed: myNode?.data.user?.isLicensed
});
}, [reset, myNode]);
const onSubmit = handleSubmit((data) => {
if (connection && myNode?.data.user) {
void toast.promise(
connection.setOwner({ ...myNode.data.user, ...data }, async () => {
reset({ ...data });
await Promise.resolve();
connection.setOwner({
owner: { ...myNode.data.user, ...data },
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
}),
{
loading: "Saving...",
success: "Saved User, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -63,7 +66,7 @@ export const User = (): JSX.Element => {
reset({
longName: myNode?.data.user?.longName,
shortName: myNode?.data.user?.shortName,
isLicensed: myNode?.data.user?.isLicensed,
isLicensed: myNode?.data.user?.isLicensed
});
}}
dirty={isDirty}

View File

@@ -29,7 +29,7 @@ export const BLE = (): JSX.Element => {
setSelectedDevice(id);
const connection = new IBLEConnection(id);
await connection.connect({
device: BLEDevice,
device: BLEDevice
});
device.addConnection(connection);
subscribeAll(device, connection);
@@ -58,7 +58,7 @@ export const BLE = (): JSX.Element => {
onClick={() => {
void navigator.bluetooth
.requestDevice({
filters: [{ services: [Constants.serviceUUID] }],
filters: [{ services: [Constants.serviceUUID] }]
})
.then((device) => {
const exists = bleDevices.findIndex((d) => d.id === device.id);

View File

@@ -21,14 +21,14 @@ export const HTTP = (): JSX.Element => {
}>({
defaultValues: {
ip: "meshtastic.local",
tls: location.protocol === "https:",
},
tls: location.protocol === "https:"
}
});
const TLSEnabled = useWatch({
control,
name: "tls",
defaultValue: location.protocol === "https:",
defaultValue: location.protocol === "https:"
});
const onSubmit = handleSubmit((data) => {
@@ -40,7 +40,7 @@ export const HTTP = (): JSX.Element => {
void connection.connect({
address: data.ip,
fetchInterval: 2000,
tls: data.tls,
tls: data.tls
});
device.addConnection(connection);
subscribeAll(device, connection);

View File

@@ -38,7 +38,7 @@ export const Serial = (): JSX.Element => {
.connect({
port,
baudRate: undefined,
concurrentLogOutput: true,
concurrentLogOutput: true
})
.catch((e: Error) => console.log(`Unable to Connect: ${e.message}`));
device.addConnection(connection);

View File

@@ -6,7 +6,7 @@ import type { AllMessageTypes } from "@core/stores/deviceStore.js";
import { Hashicon } from "@emeraldpay/hashicon-react";
import {
CheckCircleIcon,
EllipsisHorizontalCircleIcon,
EllipsisHorizontalCircleIcon
} from "@heroicons/react/24/outline";
import type { Protobuf } from "@meshtastic/meshtasticjs";
@@ -19,7 +19,7 @@ export interface MessageProps {
export const Message = ({
lastMsgSameUser,
message,
sender,
sender
}: MessageProps): JSX.Element => {
const { setPeerInfoOpen, setActivePeer, connection } = useDevice();
@@ -62,7 +62,7 @@ export const Message = ({
<span className="text-sm">
{new Date(message.packet.rxTime).toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
minute: "2-digit"
})}
</span>
</div>

View File

@@ -20,21 +20,20 @@ export const MessageInput = ({ channel }: MessageInputProps): JSX.Element => {
message: string;
}>({
defaultValues: {
message: "",
},
message: ""
}
});
const onSubmit = handleSubmit((data) => {
void connection?.sendText(
data.message,
undefined,
true,
channel.config.index as Types.ChannelNumber,
(id) => {
void connection?.sendText({
text: data.message,
wantAck: true,
channel: channel.config.index as Types.ChannelNumber,
callback: (id) => {
ackMessage(channel.config.index, id);
return Promise.resolve();
}
);
});
});
return (

View File

@@ -10,7 +10,7 @@ import { Protobuf } from "@meshtastic/meshtasticjs";
enum LocationType {
MGRS,
LatLng,
DecimalDegrees,
DecimalDegrees
}
export const NewLocationMessage = (): JSX.Element => {
@@ -31,14 +31,15 @@ export const NewLocationMessage = (): JSX.Element => {
<Input label="Coordinates" />
<Button
onClick={() => {
void connection?.sendWaypoint(
Protobuf.Waypoint.create({
void connection?.sendWaypoint({
waypoint: Protobuf.Waypoint.create({
latitudeI: Math.floor(3.89103 * 1e7),
longitudeI: Math.floor(105.87005 * 1e7),
name: "TEST",
description: "This is a description",
})
);
description: "This is a description"
}),
destination: "broadcast"
});
}}
>
Send

View File

@@ -9,7 +9,7 @@ export interface WaypointMessageProps {
}
export const WaypointMessage = ({
waypointID,
waypointID
}: WaypointMessageProps): JSX.Element => {
const { waypoints } = useDevice();
const waypoint = waypoints.find((wp) => wp.id === waypointID);

View File

@@ -21,16 +21,16 @@ export const CannedMessage = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<CannedMessageValidation>({
defaultValues: moduleConfig.cannedMessage,
resolver: classValidatorResolver(CannedMessageValidation),
resolver: classValidatorResolver(CannedMessageValidation)
});
const moduleEnabled = useWatch({
control,
name: "rotary1Enabled",
defaultValue: false,
defaultValue: false
});
useEffect(() => {
@@ -40,22 +40,22 @@ export const CannedMessage = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setModuleConfig(
{
connection.setModuleConfig({
moduleConfig: {
payloadVariant: {
oneofKind: "cannedMessage",
cannedMessage: data,
},
cannedMessage: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Canned Message Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}

View File

@@ -18,10 +18,10 @@ export const ExternalNotification = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<ExternalNotificationValidation>({
defaultValues: moduleConfig.externalNotification,
resolver: classValidatorResolver(ExternalNotificationValidation),
resolver: classValidatorResolver(ExternalNotificationValidation)
});
useEffect(() => {
reset(moduleConfig.externalNotification);
@@ -30,22 +30,22 @@ export const ExternalNotification = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setModuleConfig(
{
connection.setModuleConfig({
moduleConfig: {
payloadVariant: {
oneofKind: "externalNotification",
externalNotification: data,
},
externalNotification: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved External Notification Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -54,7 +54,7 @@ export const ExternalNotification = (): JSX.Element => {
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
defaultValue: false
});
return (
@@ -84,7 +84,7 @@ export const ExternalNotification = (): JSX.Element => {
suffix="ms"
disabled={!moduleEnabled}
{...register("outputMs", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Input
@@ -93,7 +93,7 @@ export const ExternalNotification = (): JSX.Element => {
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("output", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Controller
@@ -132,6 +132,18 @@ export const ExternalNotification = (): JSX.Element => {
/>
)}
/>
<Controller
name="usePwm"
control={control}
render={({ field: { value, ...rest } }) => (
<Toggle
label="Use PWM"
description="Description"
checked={value}
{...rest}
/>
)}
/>
</Form>
);
};

View File

@@ -18,16 +18,16 @@ export const MQTT = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<MQTTValidation>({
defaultValues: moduleConfig.mqtt,
resolver: classValidatorResolver(MQTTValidation),
resolver: classValidatorResolver(MQTTValidation)
});
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
defaultValue: false
});
useEffect(() => {
@@ -37,22 +37,22 @@ export const MQTT = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setModuleConfig(
{
connection.setModuleConfig({
moduleConfig: {
payloadVariant: {
oneofKind: "mqtt",
mqtt: data,
},
mqtt: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved MQTT Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}

View File

@@ -18,10 +18,10 @@ export const RangeTest = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<RangeTestValidation>({
defaultValues: moduleConfig.rangeTest,
resolver: classValidatorResolver(RangeTestValidation),
resolver: classValidatorResolver(RangeTestValidation)
});
useEffect(() => {
@@ -31,22 +31,22 @@ export const RangeTest = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setModuleConfig(
{
connection.setModuleConfig({
moduleConfig: {
payloadVariant: {
oneofKind: "rangeTest",
rangeTest: data,
},
rangeTest: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Range Test Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -55,7 +55,7 @@ export const RangeTest = (): JSX.Element => {
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
defaultValue: false
});
return (
@@ -85,7 +85,7 @@ export const RangeTest = (): JSX.Element => {
disabled={!moduleEnabled}
suffix="Seconds"
{...register("sender", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Controller

View File

@@ -18,10 +18,10 @@ export const Serial = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<SerialValidation>({
defaultValues: moduleConfig.serial,
resolver: classValidatorResolver(SerialValidation),
resolver: classValidatorResolver(SerialValidation)
});
useEffect(() => {
@@ -31,22 +31,22 @@ export const Serial = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setModuleConfig(
{
connection.setModuleConfig({
moduleConfig: {
payloadVariant: {
oneofKind: "serial",
serial: data,
},
serial: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Serial Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -55,7 +55,7 @@ export const Serial = (): JSX.Element => {
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
defaultValue: false
});
return (
@@ -96,7 +96,7 @@ export const Serial = (): JSX.Element => {
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("rxd", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Input
@@ -105,7 +105,7 @@ export const Serial = (): JSX.Element => {
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("txd", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Input
@@ -114,7 +114,7 @@ export const Serial = (): JSX.Element => {
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("baud", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Input
@@ -123,7 +123,7 @@ export const Serial = (): JSX.Element => {
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("timeout", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Input
@@ -132,7 +132,7 @@ export const Serial = (): JSX.Element => {
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("mode", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
</Form>

View File

@@ -18,10 +18,10 @@ export const StoreForward = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<StoreForwardValidation>({
defaultValues: moduleConfig.storeForward,
resolver: classValidatorResolver(StoreForwardValidation),
resolver: classValidatorResolver(StoreForwardValidation)
});
useEffect(() => {
@@ -31,22 +31,22 @@ export const StoreForward = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setModuleConfig(
{
connection.setModuleConfig({
moduleConfig: {
payloadVariant: {
oneofKind: "storeForward",
storeForward: data,
},
storeForward: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Store & Forward Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -55,7 +55,7 @@ export const StoreForward = (): JSX.Element => {
const moduleEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
defaultValue: false
});
return (
@@ -97,7 +97,7 @@ export const StoreForward = (): JSX.Element => {
suffix="Records"
disabled={!moduleEnabled}
{...register("records", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Input
@@ -106,7 +106,7 @@ export const StoreForward = (): JSX.Element => {
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("historyReturnMax", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Input
@@ -115,7 +115,7 @@ export const StoreForward = (): JSX.Element => {
description="Max transmit power in dBm"
disabled={!moduleEnabled}
{...register("historyReturnWindow", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
</Form>

View File

@@ -18,10 +18,10 @@ export const Telemetry = (): JSX.Element => {
handleSubmit,
formState: { errors, isDirty },
reset,
control,
control
} = useForm<TelemetryValidation>({
defaultValues: moduleConfig.telemetry,
resolver: classValidatorResolver(TelemetryValidation),
resolver: classValidatorResolver(TelemetryValidation)
});
useEffect(() => {
@@ -31,22 +31,22 @@ export const Telemetry = (): JSX.Element => {
const onSubmit = handleSubmit((data) => {
if (connection) {
void toast.promise(
connection.setModuleConfig(
{
connection.setModuleConfig({
moduleConfig: {
payloadVariant: {
oneofKind: "telemetry",
telemetry: data,
},
telemetry: data
}
},
async () => {
callback: async () => {
reset({ ...data });
await Promise.resolve();
}
),
}),
{
loading: "Saving...",
success: "Saved Telemetry Config, Restarting Node",
error: "No response received",
error: "No response received"
}
);
}
@@ -90,7 +90,7 @@ export const Telemetry = (): JSX.Element => {
suffix="Seconds"
type="number"
{...register("environmentUpdateInterval", {
valueAsNumber: true,
valueAsNumber: true
})}
/>
<Controller

View File

@@ -10,7 +10,7 @@ import {
InboxIcon,
MapIcon,
Square3Stack3DIcon,
UsersIcon,
UsersIcon
} from "@heroicons/react/24/outline";
export const PageNav = (): JSX.Element => {
@@ -26,43 +26,43 @@ export const PageNav = (): JSX.Element => {
{
name: "Messages",
icon: <InboxIcon />,
page: "messages",
page: "messages"
},
{
name: "Map",
icon: <MapIcon />,
page: "map",
page: "map"
},
{
name: "Extensions",
icon: <BeakerIcon />,
page: "extensions",
page: "extensions"
},
{
name: "Config",
icon: <Cog8ToothIcon />,
page: "config",
page: "config"
},
{
name: "Channels",
icon: <Square3Stack3DIcon />,
page: "channels",
page: "channels"
},
{
name: "Peers",
icon: <UsersIcon />,
page: "peers",
page: "peers"
},
{
name: "Info",
icon: <IdentificationIcon />,
page: "info",
page: "info"
},
{
name: "Logs",
icon: <DocumentTextIcon />,
page: "logs",
},
page: "logs"
}
];
return (

View File

@@ -13,7 +13,7 @@ export interface BatteryWidgetProps {
export const BatteryWidget = ({
batteryLevel,
voltage,
voltage
}: BatteryWidgetProps): JSX.Element => {
const { nodes, hardware } = useDevice();

View File

@@ -14,7 +14,7 @@ export const ConfiguringWidget = (): JSX.Element => {
moduleConfig,
setReady,
nodes,
connection,
connection
} = useDevice();
useEffect(() => {
@@ -32,7 +32,7 @@ export const ConfiguringWidget = (): JSX.Element => {
channels,
hardware.maxChannels,
hardware.myNodeNum,
setReady,
setReady
]);
return (
@@ -80,7 +80,7 @@ export interface StatusIndicatorProps {
const StatusIndicator = ({
title,
current,
total,
total
}: StatusIndicatorProps): JSX.Element => {
return (
<li className="relative">

View File

@@ -19,7 +19,7 @@ export const DeviceWidget = ({
nodeNum,
disconnected,
disconnect,
reconnect,
reconnect
}: DeviceWidgetProps): JSX.Element => {
return (
<Card className="relative shrink-0 flex-col">

View File

@@ -11,7 +11,7 @@ export interface NodeInfoWidgetProps {
}
export const NodeInfoWidget = ({
hardware,
hardware
}: NodeInfoWidgetProps): JSX.Element => {
return (
<Card className="flex-col">

View File

@@ -3,7 +3,7 @@ import type React from "react";
import { useDevice } from "@app/core/providers/useDevice.js";
import {
EllipsisHorizontalIcon,
UserGroupIcon,
UserGroupIcon
} from "@heroicons/react/24/outline";
import type { Protobuf } from "@meshtastic/meshtasticjs";

View File

@@ -0,0 +1,82 @@
import React, { useState } from "react";
import {
bitwiseDecode,
bitwiseEncode,
enumLike
} from "@app/core/utils/bitwise.js";
import { Listbox } from "@headlessui/react";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { InfoWrapper } from "./InfoWrapper.js";
export interface BitwiseSelectProps {
label?: string;
description?: string;
error?: string;
selected: number;
decodeEnun: enumLike;
onChange: (value: number) => void;
}
export const BitwiseSelect = ({
label,
description,
error,
selected,
decodeEnun,
onChange
}: BitwiseSelectProps): JSX.Element => {
const [decodedSelected, setDecodedSelected] = useState<string[]>([]);
const options = Object.entries(decodeEnun)
.filter((value) => typeof value[1] !== "number")
.map((value) => {
return {
value: parseInt(value[0]),
label: value[1]
.toString()
.replace("POS_", "")
.toLowerCase()
.toLocaleUpperCase() //TODO: Investigate
};
});
React.useEffect(() => {
setDecodedSelected(
bitwiseDecode(selected, Protobuf.Config_PositionConfig_PositionFlags).map(
(flag) =>
Protobuf.Config_PositionConfig_PositionFlags[flag]
.replace("POS_", "")
.toLowerCase()
)
);
}, [selected]);
return (
<InfoWrapper label={label} description={description} error={error}>
<Listbox
value={bitwiseDecode(selected, decodeEnun)}
onChange={(value) => {
onChange(bitwiseEncode(value));
}}
multiple
>
<Listbox.Button
className={`flex h-10 w-full items-center gap-2 rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500`}
>
{decodedSelected.map((option) => (
<span className="rounded-md bg-orange-300 p-1">{option}</span>
))}
</Listbox.Button>
<Listbox.Options>
{options.map((option) => (
<Listbox.Option key={option.value} value={option.value}>
{option.label}
</Listbox.Option>
))}
</Listbox.Options>
</Listbox>
</InfoWrapper>
);
};

View File

@@ -7,7 +7,7 @@ import { Button } from "@components/Button.js";
import {
ArrowUturnLeftIcon,
ChevronRightIcon,
HomeIcon,
HomeIcon
} from "@heroicons/react/24/outline";
export interface FormProps extends HTMLProps<HTMLFormElement> {

View File

@@ -7,7 +7,7 @@ export interface FormSectionProps {
export const FormSection = ({
title,
children,
children
}: FormSectionProps): JSX.Element => {
return (
<div className="relative">

View File

@@ -0,0 +1,133 @@
import React, { InputHTMLAttributes, useState } from "react";
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
export interface IPAddressProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
description?: string;
prefix?: string;
suffix?: string;
action?: {
icon: JSX.Element;
action: () => void;
};
error?: string;
}
export const IPAddress = ({
label,
description,
action,
error,
disabled,
...rest
}: IPAddressProps): JSX.Element => {
const [value, setValue] = useState<[number, number, number, number]>([
0, 0, 0, 0
]);
// const getRange = (el) => {
// var cuRange, tbRange, headRange, range, dupRange, ret = {};
// if (el.setSelectionRange) {
// // standard
// ret.begin = el.selectionStart;
// ret.end = el.selectionEnd;
// ret.result = el.value.substring(ret.begin, ret.end);
// } else if (document.selection) {
// // ie
// if (el.tagName.toLowerCase() === 'input') {
// cuRange = document.selection.createRange();
// tbRange = el.createTextRange();
// tbRange.collapse(true);
// tbRange.select();
// headRange = document.selection.createRange();
// headRange.setEndPoint('EndToEnd', cuRange);
// ret.begin = headRange.text.length - cuRange.text.length;
// ret.end = headRange.text.length;
// ret.result = cuRange.text;
// cuRange.select();
// } else if (el.tagName.toLowerCase() === 'textarea') {
// range = document.selection.createRange();
// dupRange = range.duplicate();
// dupRange.moveToElementText(el);
// dupRange.setEndPoint('EndToEnd', range);
// ret.begin = dupRange.text.length - range.text.length;
// ret.end = dupRange.text.length;
// ret.result = range.text;
// }
// }
// el.focus();
// return ret;
// }
// const isValidIPItemValue = (val) => {
// val = parseInt(val);
// return !isNaN(val) && val >= 0 && val <= 255;
// }
// const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, index: number) => {
// /* 37 = ←, 39 = →, 8 = backspace, 110 or 190 = . */
// let domId = index;
// if ((event.keyCode === 37 || event.keyCode === 8) && getRange(event.target).end === 0 && index > 0) { domId = index - 1; }
// if (event.keyCode === 39 && getRange(event.target).end === event.target.value.length && index < 3) { domId = index + 1; }
// if (event.keyCode === 110 || event.keyCode === 190) {
// event.preventDefault();
// if(i < 3) {
// domId = i + 1;
// }
// }
// this[`_input-${domId}`].focus();
// }
// useEffect(() => {
// }, [])
// const ip = value.map(val => isNaN(val) ? '' : val).join('.');
return (
<div>
{/* Label */}
<label className="block text-sm font-medium text-gray-700">{label}</label>
{/* */}
<div className="relative flex gap-1 rounded-md">
{value.map((octet, index) => (
<>
<input
key={index}
// ref={ref}
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm shadow-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
action ? "rounded-r-none" : ""
} ${
disabled
? "cursor-not-allowed bg-orange-50 text-orange-200"
: ""
}`}
disabled={disabled}
{...rest}
/>
{index !== 3 && <i className="text-xl">.</i>}
</>
))}
{action && (
<button
type="button"
onClick={action.action}
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-orange-200 px-4 py-2 text-sm font-medium hover:bg-orange-300 focus:outline-none focus:ring-2 focus:ring-orange-500"
>
{action.icon}
</button>
)}
{error && (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
</div>
)}
</div>
{description && (
<p className="mt-2 text-sm text-gray-500">{description}</p>
)}
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
</div>
);
};

View File

@@ -0,0 +1,39 @@
import type React from "react";
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
export interface InfoWrapperProps {
label?: string;
description?: string;
error?: string;
children: React.ReactNode;
}
export const InfoWrapper = ({
label,
description,
error,
children
}: InfoWrapperProps): JSX.Element => {
return (
<div className="w-full">
{/* Label */}
{label && (
<label className="block text-sm font-medium text-gray-700">
{label}
</label>
)}
{/* */}
{children}
{error && (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
</div>
)}
{description && (
<p className="mt-2 text-sm text-gray-500">{description}</p>
)}
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
</div>
);
};

View File

@@ -3,16 +3,17 @@ import { forwardRef, InputHTMLAttributes } from "react";
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
description?: string;
import { InfoWrapper, InfoWrapperProps } from "./InfoWrapper.js";
export interface InputProps
extends InputHTMLAttributes<HTMLInputElement>,
Omit<InfoWrapperProps, "children"> {
prefix?: string;
suffix?: string;
action?: {
icon: JSX.Element;
action: () => void;
};
error?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
@@ -29,10 +30,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
ref
) {
return (
<div>
{/* Label */}
<label className="block text-sm font-medium text-gray-700">{label}</label>
{/* */}
<InfoWrapper label={label} description={description} error={error}>
<div className="relative flex rounded-md shadow-sm">
{prefix && (
<span className="inline-flex items-center rounded-l-md border-gray-300 bg-orange-200 px-3 font-mono text-sm">
@@ -70,10 +68,6 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
</div>
)}
</div>
{description && (
<p className="mt-2 text-sm text-gray-500">{description}</p>
)}
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
</div>
</InfoWrapper>
);
});

View File

@@ -1,17 +1,16 @@
import type React from "react";
import { forwardRef, SelectHTMLAttributes } from "react";
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
label: string;
description?: string;
import { InfoWrapper, InfoWrapperProps } from "./InfoWrapper.js";
export interface SelectProps
extends SelectHTMLAttributes<HTMLSelectElement>,
Omit<InfoWrapperProps, "children"> {
options?: string[];
prefix?: string;
suffix?: string;
action?: {
icon: JSX.Element;
action: () => void;
};
error?: string;
}
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
@@ -19,30 +18,22 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
label,
description,
options,
prefix,
suffix,
action,
error,
disabled,
error,
children,
...rest
}: SelectProps,
ref
) {
return (
<div>
<label
htmlFor="location"
className="block text-sm font-medium text-gray-700"
>
{label}
</label>
<InfoWrapper label={label} description={description} error={error}>
<div className="flex rounded-md shadow-sm">
<select
ref={ref}
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
prefix ? "rounded-l-none" : ""
} ${action ? "rounded-r-none" : ""} ${
action ? "rounded-r-none" : ""
} ${
disabled ? "cursor-not-allowed bg-orange-50 text-orange-200" : ""
}`}
disabled={disabled}
@@ -64,9 +55,6 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
</button>
)}
</div>
{description && (
<p className="mt-2 text-sm text-gray-500">{description}</p>
)}
</div>
</InfoWrapper>
);
});

View File

@@ -3,33 +3,37 @@ import type React from "react";
import { Switch } from "@headlessui/react";
export interface ToggleProps {
label: string;
description: string;
checked: boolean;
label?: string;
description?: string;
disabled?: boolean;
onChange?: (checked: boolean) => void;
}
export const Toggle = ({
checked,
label,
description,
checked,
disabled,
onChange,
onChange
}: ToggleProps): JSX.Element => {
return (
<Switch.Group as="div" className="flex items-center justify-between">
<span className="flex flex-grow flex-col">
<Switch.Label
as="span"
className="text-sm font-medium text-gray-900"
passive
>
{label}
</Switch.Label>
<Switch.Description as="span" className="text-sm text-gray-500">
{description}
</Switch.Description>
{label && (
<Switch.Label
as="span"
className="text-sm font-medium text-gray-900"
passive
>
{label}
</Switch.Label>
)}
{description && (
<Switch.Description as="span" className="text-sm text-gray-500">
{description}
</Switch.Description>
)}
</span>
<Switch
checked={checked}

View File

@@ -19,7 +19,7 @@ export interface TabbedContentProps {
export const TabbedContent = ({
tabs,
actions,
actions
}: TabbedContentProps): JSX.Element => {
return (
<Tab.Group as="div" className="flex flex-grow flex-col gap-2 p-4">