Browse Source

Merge branch 'meshtastic:master' into tls-warning

pull/548/head
James Thomas 1 year ago
committed by GitHub
parent
commit
443a9ea101
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      deno.json
  2. 115
      deno.lock
  3. 5
      package.json
  4. 4
      src/components/Form/FormSelect.tsx
  5. 283
      src/components/PageComponents/Config/Network/Network.test.tsx
  6. 35
      src/components/PageComponents/Config/Network/index.tsx
  7. 10
      src/core/utils/ip.ts
  8. 3
      src/pages/Config/DeviceConfig.tsx
  9. 82
      src/validation/config/network.ts
  10. 13
      src/validation/validate.ts

5
deno.json

@ -1,5 +1,10 @@
{ {
"imports": { "imports": {
"@meshtastic/core": "jsr:@meshtastic/core@^2.6.2",
"@meshtastic/js": "jsr:@meshtastic/js@^2.3.4",
"@meshtastic/transport-http": "jsr:@meshtastic/transport-http@^0.2.1",
"@meshtastic/transport-web-bluetooth": "jsr:@meshtastic/transport-web-bluetooth@^0.1.1",
"@meshtastic/transport-web-serial": "jsr:@meshtastic/transport-web-serial@^0.2.1",
"@app/": "./src/", "@app/": "./src/",
"@pages/": "./src/pages/", "@pages/": "./src/pages/",
"@components/": "./src/components/", "@components/": "./src/components/",

115
deno.lock

@ -1,11 +1,15 @@
{ {
"version": "4", "version": "4",
"specifiers": { "specifiers": {
"jsr:@meshtastic/core@^2.6.0": "2.6.2",
"jsr:@meshtastic/core@^2.6.2": "2.6.2",
"jsr:@meshtastic/js@^2.3.4": "2.3.4",
"jsr:@meshtastic/protobufs@^2.3.12": "2.6.2",
"jsr:@meshtastic/protobufs@^2.6.2": "2.6.2",
"jsr:@meshtastic/transport-http@~0.2.1": "0.2.1",
"jsr:@meshtastic/transport-web-bluetooth@~0.1.1": "0.1.1",
"jsr:@meshtastic/transport-web-serial@~0.2.1": "0.2.1",
"npm:@bufbuild/protobuf@^2.2.3": "2.2.3", "npm:@bufbuild/protobuf@^2.2.3": "2.2.3",
"npm:@jsr/[email protected]": "2.6.0-0",
"npm:@jsr/[email protected]": "2.6.0-0",
"npm:@jsr/meshtastic__transport-http@*": "0.2.1",
"npm:@jsr/meshtastic__transport-web-serial@*": "0.2.1",
"npm:@noble/curves@^1.8.1": "1.8.1", "npm:@noble/curves@^1.8.1": "1.8.1",
"npm:@radix-ui/react-accordion@^1.2.3": "1.2.3_@[email protected]_@[email protected]__@[email protected][email protected][email protected][email protected]", "npm:@radix-ui/react-accordion@^1.2.3": "1.2.3_@[email protected]_@[email protected]__@[email protected][email protected][email protected][email protected]",
"npm:@radix-ui/react-checkbox@^1.1.4": "1.1.4_@[email protected]_@[email protected]__@[email protected][email protected][email protected][email protected]", "npm:@radix-ui/react-checkbox@^1.1.4": "1.1.4_@[email protected]_@[email protected]__@[email protected][email protected][email protected][email protected]",
@ -73,8 +77,48 @@
"npm:vite@*": "6.2.0_@[email protected]", "npm:vite@*": "6.2.0_@[email protected]",
"npm:vite@^6.2.0": "6.2.0_@[email protected]", "npm:vite@^6.2.0": "6.2.0_@[email protected]",
"npm:vitest@^3.0.7": "3.0.8_@[email protected][email protected][email protected]__@[email protected]_@[email protected][email protected][email protected]___@[email protected][email protected]___@[email protected][email protected][email protected][email protected][email protected]_____@[email protected][email protected]_____@[email protected][email protected]____@[email protected][email protected][email protected][email protected]____@[email protected][email protected][email protected][email protected][email protected][email protected]___@[email protected][email protected]___@[email protected]__@[email protected][email protected][email protected]", "npm:vitest@^3.0.7": "3.0.8_@[email protected][email protected][email protected]__@[email protected]_@[email protected][email protected][email protected]___@[email protected][email protected]___@[email protected][email protected][email protected][email protected][email protected]_____@[email protected][email protected]_____@[email protected][email protected]____@[email protected][email protected][email protected][email protected]____@[email protected][email protected][email protected][email protected][email protected][email protected]___@[email protected][email protected]___@[email protected]__@[email protected][email protected][email protected]",
"npm:zod@^3.24.2": "3.24.2",
"npm:[email protected]": "5.0.3_@[email protected][email protected][email protected]" "npm:[email protected]": "5.0.3_@[email protected][email protected][email protected]"
}, },
"jsr": {
"@meshtastic/[email protected]": {
"integrity": "5c948bbbfad280c5eb093c62edc84773f76509487b333066ec4a349f40dcacf2",
"dependencies": [
"jsr:@meshtastic/protobufs@^2.6.2",
"npm:@bufbuild/protobuf",
"npm:crc",
"npm:ste-simple-events",
"npm:tslog@^4.9.3"
]
},
"@meshtastic/[email protected]": {
"integrity": "7a81a36fb7ef1b7b68a3989c02d50f687114ac56bcd7f0452a31ef560ac99719",
"dependencies": [
"jsr:@meshtastic/protobufs@^2.3.12",
"npm:crc",
"npm:ste-simple-events",
"npm:tslog@^4.9.2"
]
},
"@meshtastic/[email protected]": {
"integrity": "55e9b98fc22ea0d28e6a7979e4ff0a5f2c94513c1bc93e67522636a89925ad69",
"dependencies": [
"npm:@bufbuild/protobuf"
]
},
"@meshtastic/[email protected]": {
"integrity": "4d086ee6d5665c3490736737c4354eb3049edf792b1d195b30a3254cb535a7d6"
},
"@meshtastic/[email protected]": {
"integrity": "f7676b98e2049ad0bca508e34054730b22cf2648019921989f11297441fe958d"
},
"@meshtastic/[email protected]": {
"integrity": "d09fa8ac278b105c8f2b3a72af9cf8a5676baac6f4e9111c6773ff6217e2d5be",
"dependencies": [
"jsr:@meshtastic/core@^2.6.0"
]
}
},
"npm": { "npm": {
"@adobe/[email protected]": { "@adobe/[email protected]": {
"integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==" "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A=="
@ -1082,54 +1126,6 @@
"@jridgewell/sourcemap-codec" "@jridgewell/sourcemap-codec"
] ]
}, },
"@jsr/[email protected]": {
"integrity": "sha512-+Ik6gzZnfi5sW+WC06bRayA6KGF2NI+zi3bqKbvA8mGDNSOPgsFhA4VZ79DKY4bSflTW170MRIUeyYo0IWQQuw==",
"dependencies": [
"@bufbuild/protobuf",
"@jsr/meshtastic__protobufs",
"crc",
"ste-simple-events",
"tslog"
]
},
"@jsr/[email protected]": {
"integrity": "sha512-Ks71sRagbBipotznULpsJZ1EMcQIqCEJQx6mf628dmCNVf2YECi2zi/i/5zErp1hGPgfbDvCz9oPogvsd/7fMA==",
"dependencies": [
"@bufbuild/protobuf",
"@jsr/meshtastic__protobufs",
"crc",
"ste-simple-events",
"tslog"
]
},
"@jsr/[email protected]": {
"integrity": "sha512-+xpZpxK6oUIVOuEs7C+LyxRr2druvc7UNNNTK9Rl8ioXj63Jz1uQXlYe2Gj0xjnRAiSQLR7QVaPef21BR/YTxA==",
"dependencies": [
"@bufbuild/protobuf",
"@jsr/meshtastic__protobufs",
"crc",
"ste-simple-events",
"tslog"
]
},
"@jsr/[email protected]": {
"integrity": "sha512-CGlgBdzAuQCZuGPrnzP8zU+EcLlmyYeeMbqFHuJ834cYfArWXDjDh1UYaPo2rI03LTjqa3MeWpfqDlzBR8kIMg==",
"dependencies": [
"@bufbuild/protobuf"
]
},
"@jsr/[email protected]": {
"integrity": "sha512-lmQKr3aIINKvtGROU4HchmSVqbZSbkIHqajowRRC8IAjsnR0zNTyxz210QyY4pFUF9hpcW3GRjwq5h/VO2JuGg==",
"dependencies": [
"@jsr/[email protected]"
]
},
"@jsr/[email protected]": {
"integrity": "sha512-yumjEGLkAuJYOC3aWKvZzbQqi/LnqaKfNpVCY7Ki7oLtAshNiZrBLiwiFhN7+ZR9FfMdJThyBMqREBDRRWTO1Q==",
"dependencies": [
"@jsr/[email protected]"
]
},
"@mapbox/[email protected]": { "@mapbox/[email protected]": {
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
"dependencies": [ "dependencies": [
@ -6869,6 +6865,9 @@
"[email protected]": { "[email protected]": {
"integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==" "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="
}, },
"[email protected]": {
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
},
"[email protected]": { "[email protected]": {
"integrity": "sha512-mla2acNCMkWXBD+c+yeUrBUrzOxYMNFdQ6FGfigGGtEVBPJx07BQeJekjt9DmH1FtZek4E9rE1eRR9qQpxACOQ==" "integrity": "sha512-mla2acNCMkWXBD+c+yeUrBUrzOxYMNFdQ6FGfigGGtEVBPJx07BQeJekjt9DmH1FtZek4E9rE1eRR9qQpxACOQ=="
}, },
@ -6882,13 +6881,16 @@
} }
}, },
"workspace": { "workspace": {
"dependencies": [
"jsr:@meshtastic/core@^2.6.2",
"jsr:@meshtastic/js@^2.3.4",
"jsr:@meshtastic/transport-http@~0.2.1",
"jsr:@meshtastic/transport-web-bluetooth@~0.1.1",
"jsr:@meshtastic/transport-web-serial@~0.2.1"
],
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@bufbuild/protobuf@^2.2.3", "npm:@bufbuild/protobuf@^2.2.3",
"npm:@jsr/[email protected]",
"npm:@jsr/[email protected]",
"npm:@jsr/meshtastic__transport-http@*",
"npm:@jsr/meshtastic__transport-web-serial@*",
"npm:@noble/curves@^1.8.1", "npm:@noble/curves@^1.8.1",
"npm:@radix-ui/react-accordion@^1.2.3", "npm:@radix-ui/react-accordion@^1.2.3",
"npm:@radix-ui/react-checkbox@^1.1.4", "npm:@radix-ui/react-checkbox@^1.1.4",
@ -6950,6 +6952,7 @@
"npm:vite-plugin-pwa@~0.21.1", "npm:vite-plugin-pwa@~0.21.1",
"npm:vite@^6.2.0", "npm:vite@^6.2.0",
"npm:vitest@^3.0.7", "npm:vitest@^3.0.7",
"npm:zod@^3.24.2",
"npm:[email protected]" "npm:[email protected]"
] ]
} }

5
package.json

@ -35,10 +35,6 @@
"homepage": "https://meshtastic.org", "homepage": "https://meshtastic.org",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.2.3", "@bufbuild/protobuf": "^2.2.3",
"@meshtastic/core": "npm:@jsr/[email protected]",
"@meshtastic/js": "npm:@jsr/[email protected]",
"@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http",
"@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial",
"@noble/curves": "^1.8.1", "@noble/curves": "^1.8.1",
"@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.1.4",
@ -73,6 +69,7 @@
"react-qrcode-logo": "^3.0.0", "react-qrcode-logo": "^3.0.0",
"rfc4648": "^1.5.4", "rfc4648": "^1.5.4",
"vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-node-polyfills": "^0.23.0",
"zod": "^3.24.2",
"zustand": "5.0.3" "zustand": "5.0.3"
}, },
"devDependencies": { "devDependencies": {

4
src/components/Form/FormSelect.tsx

@ -10,13 +10,14 @@ import {
SelectValue, SelectValue,
} from "@components/UI/Select.tsx"; } from "@components/UI/Select.tsx";
import { useController, type FieldValues } from "react-hook-form"; import { useController, type FieldValues } from "react-hook-form";
import { computeHeadingLevel } from "@core/utils/test.tsx";
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> { export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "select"; type: "select";
selectChange?: (e: string, name: string) => void; selectChange?: (e: string, name: string) => void;
validate?: (newValue: string) => Promise<boolean>; validate?: (newValue: string) => Promise<boolean>;
defaultValue?: string;
properties: BaseFormBuilderProps<T>["properties"] & { properties: BaseFormBuilderProps<T>["properties"] & {
defaultValue?: T;
enumValue: { enumValue: {
[s: string]: string | number; [s: string]: string | number;
}; };
@ -70,7 +71,6 @@ export function SelectInput<T extends FieldValues>({
onChange(Number.parseInt(newValue)); onChange(Number.parseInt(newValue));
}; };
return ( return (
<Select <Select
onValueChange={handleValueChange} onValueChange={handleValueChange}

283
src/components/PageComponents/Config/Network/Network.test.tsx

@ -0,0 +1,283 @@
// import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// import { render, screen, fireEvent, waitFor } from '@testing-library/react';
// import { Network } from '@components/PageComponents/Config/Network/index.tsx';
// import { useDevice } from "@core/stores/deviceStore.ts";
// import { Protobuf } from "@meshtastic/core";
// vi.mock('@core/stores/deviceStore', () => ({
// useDevice: vi.fn()
// }));
// vi.mock('@components/Form/DynamicForm', () => ({
// DynamicForm: vi.fn(({ onSubmit }) => {
// return (
// <div data-testid="dynamic-form">
// <select
// data-testid="role-select"
// onChange={(e) => {
// const mockData = { role: e.target.value };
// onSubmit(mockData);
// }}
// >
// {Object.entries(Protobuf.Config.Config_DeviceConfig_Role).map(([key, value]) => (
// <option key={key} value={value}>
// {key}
// </option>
// ))}
// </select>
// <button type="submit"
// data-testid="submit-button"
// onClick={() => onSubmit({ role: "CLIENT" })}
// >
// Submit
// </button>
// </div>
// );
// })
// }));
// describe('Network component', () => {
// const setWorkingConfigMock = vi.fn();
// const mockDeviceConfig = {
// role: "CLIENT",
// buttonGpio: 0,
// buzzerGpio: 0,
// rebroadcastMode: "ALL",
// nodeInfoBroadcastSecs: 300,
// doubleTapAsButtonPress: false,
// disableTripleClick: false,
// ledHeartbeatDisabled: false,
// };
// beforeEach(() => {
// vi.resetAllMocks();
// (useDevice as any).mockReturnValue({
// config: {
// device: mockDeviceConfig
// },
// setWorkingConfig: setWorkingConfigMock
// });
// });
// afterEach(() => {
// vi.clearAllMocks();
// });
// it('should render the Network form', () => {
// render(<Network />);
// expect(screen.getByTestId('dynamic-form')).toBeInTheDocument();
// });
// it('should call setWorkingConfig when form is submitted', async () => {
// render(<Network />);
// fireEvent.click(screen.getByTestId('submit-button'));
// await waitFor(() => {
// expect(setWorkingConfigMock).toHaveBeenCalledWith(
// expect.objectContaining({
// payloadVariant: {
// case: "device",
// value: expect.objectContaining({ role: "CLIENT" })
// }
// })
// );
// });
// });
// it('should create config with proper structure', async () => {
// render(<Network />);
// // Simulate form submission
// fireEvent.click(screen.getByTestId('submit-button'));
// await waitFor(() => {
// expect(setWorkingConfigMock).toHaveBeenCalledWith(
// expect.objectContaining({
// payloadVariant: {
// case: "network",
// value: expect.any(Object)
// }
// })
// );
// });
// });
// });
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Network } from '@components/PageComponents/Config/Network/index.tsx';
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
vi.mock('@core/stores/deviceStore', () => ({
useDevice: vi.fn()
}));
vi.mock('@components/Form/DynamicForm', async () => {
const React = await import('react');
const { useState } = React;
return {
DynamicForm: ({ onSubmit, defaultValues }: any) => {
const [wifiEnabled, setWifiEnabled] = useState(defaultValues.wifiEnabled ?? false);
const [ssid, setSsid] = useState(defaultValues.wifiSsid ?? '');
const [psk, setPsk] = useState(defaultValues.wifiPsk ?? '');
return (
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit({
...defaultValues,
wifiEnabled,
wifiSsid: ssid,
wifiPsk: psk,
});
}}
data-testid="dynamic-form"
>
<input
type="checkbox"
aria-label="WiFi Enabled"
checked={wifiEnabled}
onChange={(e) => setWifiEnabled(e.target.checked)}
/>
<input
aria-label="SSID"
value={ssid}
onChange={(e) => setSsid(e.target.value)}
disabled={!wifiEnabled}
/>
<input
aria-label="PSK"
value={psk}
onChange={(e) => setPsk(e.target.value)}
disabled={!wifiEnabled}
/>
<button type="submit" data-testid="submit-button">
Submit
</button>
</form>
);
},
};
});
;
describe('Network component', () => {
const setWorkingConfigMock = vi.fn();
const mockNetworkConfig = {
wifiEnabled: false,
wifiSsid: '',
wifiPsk: '',
ntpServer: '',
ethEnabled: false,
addressMode: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP,
ipv4Config: {
ip: 0,
gateway: 0,
subnet: 0,
dns: 0,
},
enabledProtocols:
Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST,
rsyslogServer: '',
};
beforeEach(() => {
vi.resetAllMocks();
(useDevice as any).mockReturnValue({
config: {
network: mockNetworkConfig
},
setWorkingConfig: setWorkingConfigMock
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('should render the Network form', () => {
render(<Network />);
expect(screen.getByTestId('dynamic-form')).toBeInTheDocument();
});
it('should disable SSID and PSK fields when wifi is off', () => {
render(<Network />);
expect(screen.getByLabelText("SSID")).toBeDisabled();
expect(screen.getByLabelText("PSK")).toBeDisabled();
});
it('should enable SSID and PSK when wifi is toggled on', async () => {
render(<Network />);
const toggle = screen.getByLabelText("WiFi Enabled");
screen.debug()
fireEvent.click(toggle); // turns wifiEnabled = true
await waitFor(() => {
expect(screen.getByLabelText("SSID")).not.toBeDisabled();
expect(screen.getByLabelText("PSK")).not.toBeDisabled();
});
});
it('should call setWorkingConfig with the right structure on submit', async () => {
render(<Network />);
fireEvent.click(screen.getByTestId("submit-button"));
await waitFor(() => {
expect(setWorkingConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
payloadVariant: {
case: "network",
value: expect.objectContaining({
wifiEnabled: false,
wifiSsid: '',
wifiPsk: '',
ntpServer: '',
ethEnabled: false,
rsyslogServer: '',
})
}
})
);
});
});
it('should submit valid data after enabling wifi and entering SSID and PSK', async () => {
render(<Network />);
fireEvent.click(screen.getByLabelText("WiFi Enabled"));
fireEvent.change(screen.getByLabelText("SSID"), {
target: { value: "MySSID" }
});
fireEvent.change(screen.getByLabelText("PSK"), {
target: { value: "MySecretPSK" }
});
fireEvent.click(screen.getByTestId("submit-button"));
await waitFor(() => {
expect(setWorkingConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
payloadVariant: {
case: "network",
value: expect.objectContaining({
wifiEnabled: true,
wifiSsid: "MySSID",
wifiPsk: "MySecretPSK"
})
}
})
);
});
});
});

35
src/components/PageComponents/Config/Network.tsx → src/components/PageComponents/Config/Network/index.tsx

@ -1,4 +1,4 @@
import type { NetworkValidation } from "@app/validation/config/network.tsx"; import { NetworkValidationSchema, type NetworkValidation } from "@app/validation/config/network.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx"; import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts"; import { useDevice } from "@core/stores/deviceStore.ts";
@ -7,11 +7,18 @@ import {
convertIpAddressToInt, convertIpAddressToInt,
} from "@core/utils/ip.ts"; } from "@core/utils/ip.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import { validateSchema } from "@app/validation/validate.ts";
export const Network = () => { export const Network = () => {
const { config, setWorkingConfig } = useDevice(); const { config, setWorkingConfig } = useDevice();
const onSubmit = (data: NetworkValidation) => { const onSubmit = (data: NetworkValidation) => {
const result = validateSchema(NetworkValidationSchema, data);
if (!result.success) {
console.error("Validation errors:", result.errors);
}
setWorkingConfig( setWorkingConfig(
create(Protobuf.Config.ConfigSchema, { create(Protobuf.Config.ConfigSchema, {
payloadVariant: { payloadVariant: {
@ -21,10 +28,10 @@ export const Network = () => {
ipv4Config: create( ipv4Config: create(
Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema,
{ {
ip: convertIpAddressToInt(data.ipv4Config.ip) ?? 0, ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""),
gateway: convertIpAddressToInt(data.ipv4Config.gateway) ?? 0, gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""),
subnet: convertIpAddressToInt(data.ipv4Config.subnet) ?? 0, subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""),
dns: convertIpAddressToInt(data.ipv4Config.dns) ?? 0, dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""),
}, },
), ),
}, },
@ -48,6 +55,8 @@ export const Network = () => {
), ),
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0), dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
}, },
enabledProtocols: config.network?.enabledProtocols ?? Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST
}} }}
fieldGroups={[ fieldGroups={[
{ {
@ -165,6 +174,22 @@ export const Network = () => {
}, },
], ],
}, },
{
label: "UDP Config",
description: "UDP over Mesh configuration",
fields: [
{
type: "select",
name: "enabledProtocols",
label: "Mesh via UDP",
properties: {
enumValue:
Protobuf.Config.Config_NetworkConfig_ProtocolFlags,
formatEnumName: true,
}
},
],
},
{ {
label: "NTP Config", label: "NTP Config",
description: "NTP configuration", description: "NTP configuration",

10
src/core/utils/ip.ts

@ -1,10 +1,12 @@
export function convertIntToIpAddress(int: number): string { export function convertIntToIpAddress(int: number): string {
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${ return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${(int >> 24) & 0xff
(int >> 24) & 0xff }`;
}`;
} }
export function convertIpAddressToInt(ip: string): number | null { export function convertIpAddressToInt(ip: string): number | undefined {
if (!ip) {
return undefined;
}
return ( return (
ip ip
.split(".") .split(".")

3
src/pages/Config/DeviceConfig.tsx

@ -2,7 +2,7 @@ import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx";
import { Device } from "../../components/PageComponents/Config/Device/index.tsx"; import { Device } from "../../components/PageComponents/Config/Device/index.tsx";
import { Display } from "@components/PageComponents/Config/Display.tsx"; import { Display } from "@components/PageComponents/Config/Display.tsx";
import { LoRa } from "@components/PageComponents/Config/LoRa.tsx"; import { LoRa } from "@components/PageComponents/Config/LoRa.tsx";
import { Network } from "@components/PageComponents/Config/Network.tsx"; import { Network } from "../../components/PageComponents/Config/Network/index.tsx";
import { Position } from "@components/PageComponents/Config/Position.tsx"; import { Position } from "@components/PageComponents/Config/Position.tsx";
import { Power } from "@components/PageComponents/Config/Power.tsx"; import { Power } from "@components/PageComponents/Config/Power.tsx";
import { Security } from "../../components/PageComponents/Config/Security/Security.tsx"; import { Security } from "../../components/PageComponents/Config/Security/Security.tsx";
@ -31,7 +31,6 @@ export const DeviceConfig = () => {
{ {
label: "Network", label: "Network",
element: Network, element: Network,
// disabled: !metadata.get(0)?.hasWifi,
}, },
{ {
label: "Display", label: "Display",

82
src/validation/config/network.ts

@ -1,61 +1,27 @@
import type { Message } from "@bufbuild/protobuf"; import { z } from "zod";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/core";
import {
IsBoolean,
IsEnum,
IsIP,
IsOptional,
IsString,
Length,
} from "class-validator";
export class NetworkValidation const AddressModeEnum = z.nativeEnum(Protobuf.Config.Config_NetworkConfig_AddressMode);
implements const ProtocolFlagsEnum = z.nativeEnum(Protobuf.Config.Config_NetworkConfig_ProtocolFlags);
Omit<Protobuf.Config.Config_NetworkConfig, keyof Message | "ipv4Config"> {
@IsBoolean() export const NetworkValidationIpV4ConfigSchema = z.object({
wifiEnabled: boolean; ip: z.string().ip(),
gateway: z.string().ip(),
subnet: z.string().ip(),
dns: z.string().ip(),
});
export const NetworkValidationSchema = z.object({
wifiEnabled: z.boolean(),
wifiSsid: z.string().min(0).max(33).optional(),
wifiPsk: z.string().min(0).max(64).optional(),
ntpServer: z.string().min(2).max(30),
ethEnabled: z.boolean(),
addressMode: AddressModeEnum,
ipv4Config: NetworkValidationIpV4ConfigSchema.optional(),
enabledProtocols: ProtocolFlagsEnum,
rsyslogServer: z.string(),
});
export type NetworkValidation = z.infer<typeof NetworkValidationSchema>;
@Length(1, 33)
@IsOptional({})
wifiSsid: string;
@Length(8, 64)
@IsOptional()
wifiPsk: string;
@Length(2, 30)
ntpServer: string;
@IsBoolean()
ethEnabled: boolean;
@IsEnum(Protobuf.Config.Config_NetworkConfig_AddressMode)
addressMode: Protobuf.Config.Config_NetworkConfig_AddressMode;
ipv4Config: NetworkValidationIpV4Config;
@IsString()
rsyslogServer: string;
}
export class NetworkValidationIpV4Config implements
Omit<
Protobuf.Config.Config_NetworkConfig_IpV4Config,
keyof Message | "ip" | "gateway" | "subnet" | "dns"
> {
@IsIP()
@IsOptional()
ip: string;
@IsIP()
@IsOptional()
gateway: string;
@IsIP()
@IsOptional()
subnet: string;
@IsIP()
@IsOptional()
dns: string;
}

13
src/validation/validate.ts

@ -0,0 +1,13 @@
import { ZodError, ZodSchema } from "zod";
export function validateSchema<T>(
schema: ZodSchema<T>,
data: unknown
): { success: true; data: T } | { success: false; errors: ZodError["issues"] } {
const result = schema.safeParse(data);
if (result.success) {
return { success: true, data: result.data };
} else {
return { success: false, errors: result.error.issues };
}
}
Loading…
Cancel
Save