Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx"
import { createZodResolver } from "@components/Form/createZodResolver.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
import { deepCompareConfig, normalizeBytes } from "@core/utils/deepCompareConfig.ts";
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
import { fromByteArray, toByteArray } from "base64-js";
import { useEffect, useState } from "react";
Expand Down Expand Up @@ -87,7 +87,20 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
],
};

if (deepCompareConfig(config.security, payload, true)) {
// Normalize empty byte arrays -> undefined for comparison so
// empty base64 strings from the form match undefined/empty fields
// in the existing config and allow removeChange to work.
const normalizeSecurity = (s: ParsedSecurity | undefined) => {
if (!s) return s;
return {
...s,
privateKey: normalizeBytes(s.privateKey),
publicKey: normalizeBytes(s.publicKey),
adminKey: s.adminKey?.map((b) => normalizeBytes(b)) as unknown,
} as ParsedSecurity;
};

if (deepCompareConfig(normalizeSecurity(config.security), normalizeSecurity(payload), true)) {
removeChange({ type: "config", variant: "security" });
return;
}
Expand Down
51 changes: 47 additions & 4 deletions packages/web/src/core/stores/deviceStore/changeRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import type { Types } from "@meshtastic/core";

// Config type discriminators
export type ValidConfigType =
export type ValidRadioConfigType = "lora" | "security";

export type ValidDeviceConfigType =
| "device"
| "position"
| "power"
| "network"
| "display"
| "lora"
| "bluetooth"
| "security";
| "bluetooth";

export type ValidConfigType = ValidRadioConfigType | ValidDeviceConfigType;

export type ValidModuleConfigType =
| "mqtt"
Expand Down Expand Up @@ -157,6 +159,47 @@ export function getConfigChangeCount(registry: ChangeRegistry): number {
return count;
}

/**
* Get count of radio config changes
*/
export function getRadioConfigChangeCount(registry: ChangeRegistry): number {
let count = 0;
for (const keyStr of registry.changes.keys()) {
const key = deserializeKey(keyStr);
if (key.type === "config" && (key.variant === "lora" || key.variant === "security")) {
count++;
}
}
// Channel is displayed under Radio section in UI, so include channel changes in the count
count += getChannelChangeCount(registry);
return count;
}

/**
* Get count of device config changes
*/
export function getDeviceConfigChangeCount(registry: ChangeRegistry): number {
let count = 0;
for (const keyStr of registry.changes.keys()) {
const key = deserializeKey(keyStr);
if (
key.type === "config" &&
(key.variant === "device" ||
key.variant === "position" ||
key.variant === "power" ||
key.variant === "network" ||
key.variant === "display" ||
key.variant === "bluetooth")
) {
count++;
}
}
if (hasUserChange(registry)) {
count++;
}
return count;
}

/**
* Get count of module config changes
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/core/stores/deviceStore/deviceStore.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export const mockDeviceStore: Device = {
hasChannelChange: vi.fn().mockReturnValue(false),
hasUserChange: vi.fn().mockReturnValue(false),
getConfigChangeCount: vi.fn().mockReturnValue(0),
getRadioConfigChangeCount: vi.fn().mockReturnValue(0),
getDeviceConfigChangeCount: vi.fn().mockReturnValue(0),
getModuleConfigChangeCount: vi.fn().mockReturnValue(0),
getChannelChangeCount: vi.fn().mockReturnValue(0),
getAllConfigChanges: vi.fn().mockReturnValue([]),
Expand Down
22 changes: 22 additions & 0 deletions packages/web/src/core/stores/deviceStore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
getAllModuleConfigChanges,
getChannelChangeCount,
getConfigChangeCount,
getRadioConfigChangeCount,
getDeviceConfigChangeCount,
getModuleConfigChangeCount,
hasChannelChange,
hasConfigChange,
Expand Down Expand Up @@ -119,6 +121,8 @@ export interface Device extends DeviceData {
hasChannelChange: (index: Types.ChannelNumber) => boolean;
hasUserChange: () => boolean;
getConfigChangeCount: () => number;
getRadioConfigChangeCount: () => number;
getDeviceConfigChangeCount: () => number;
getModuleConfigChangeCount: () => number;
getChannelChangeCount: () => number;
getAllConfigChanges: () => Protobuf.Config.Config[];
Expand Down Expand Up @@ -817,6 +821,24 @@ function deviceFactory(
return getConfigChangeCount(device.changeRegistry);
},

getRadioConfigChangeCount: () => {
const device = get().devices.get(id);
if (!device) {
return 0;
}

return getRadioConfigChangeCount(device.changeRegistry);
},

getDeviceConfigChangeCount: () => {
const device = get().devices.get(id);
if (!device) {
return 0;
}

return getDeviceConfigChangeCount(device.changeRegistry);
},

getModuleConfigChangeCount: () => {
const device = get().devices.get(id);
if (!device) {
Expand Down
7 changes: 7 additions & 0 deletions packages/web/src/core/utils/deepCompareConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ function isUint8Array(v: unknown): v is Uint8Array {
return v instanceof Uint8Array;
}

export function normalizeBytes(value: unknown): unknown {
if (value instanceof Uint8Array && value.byteLength === 0) {
return undefined;
}
return value;
}

function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.byteLength !== b.byteLength) {
return false;
Expand Down
16 changes: 9 additions & 7 deletions packages/web/src/pages/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const ConfigPage = () => {
setModuleConfig,
addChannel,
getConfigChangeCount,
getRadioConfigChangeCount,
getDeviceConfigChangeCount,
getModuleConfigChangeCount,
getChannelChangeCount,
getAdminMessageChangeCount,
Expand All @@ -50,9 +52,9 @@ const ConfigPage = () => {
const routerState = useRouterState();
const { t } = useTranslation("config");

const configChangeCount = getConfigChangeCount();
const radioConfigChangeCount = getRadioConfigChangeCount();
const deviceConfigChangeCount = getDeviceConfigChangeCount();
const moduleConfigChangeCount = getModuleConfigChangeCount();
const channelChangeCount = getChannelChangeCount();
const adminMessageChangeCount = getAdminMessageChangeCount();

const sections = useMemo(
Expand All @@ -62,27 +64,27 @@ const ConfigPage = () => {
route: radioRoute,
label: t("navigation.radioConfig"),
icon: RadioTowerIcon,
changeCount: configChangeCount,
changeCount: radioConfigChangeCount,
component: RadioConfig,
},
{
key: "device",
route: deviceRoute,
label: t("navigation.deviceConfig"),
icon: RouterIcon,
changeCount: moduleConfigChangeCount,
changeCount: deviceConfigChangeCount,
component: DeviceConfig,
},
{
key: "module",
route: moduleRoute,
label: t("navigation.moduleConfig"),
icon: LayersIcon,
changeCount: channelChangeCount,
changeCount: moduleConfigChangeCount,
component: ModuleConfig,
},
],
[t, configChangeCount, moduleConfigChangeCount, channelChangeCount],
[t, radioConfigChangeCount, deviceConfigChangeCount, moduleConfigChangeCount],
);

const activeSection =
Expand Down Expand Up @@ -263,7 +265,7 @@ const ConfigPage = () => {
getModuleConfigChangeCount() > 0 ||
getChannelChangeCount() > 0 ||
adminMessageChangeCount > 0;
const hasPending = hasDrafts || rhfState.isDirty;
const hasPending = hasDrafts;
const buttonOpacity = hasPending ? "opacity-100" : "opacity-0";
const saveDisabled = isSaving || !rhfState.isValid || !hasPending;

Expand Down