Feat(config): Align settings menu to match android/ios (#906)
* feat: aligned settings menu to match android/ios * updated sidebar text size. * Update packages/web/public/i18n/locales/en/config.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/web/public/i18n/locales/en/config.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/web/src/components/PageComponents/Settings/User.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * linting/formatting fixes * fixed formatting issue --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import type { Types } from "@meshtastic/core";
|
||||
|
||||
// Config type discriminators
|
||||
export type ValidConfigType =
|
||||
| "device"
|
||||
| "position"
|
||||
| "power"
|
||||
| "network"
|
||||
| "display"
|
||||
| "lora"
|
||||
| "bluetooth"
|
||||
| "security";
|
||||
|
||||
export type ValidModuleConfigType =
|
||||
| "mqtt"
|
||||
| "serial"
|
||||
| "externalNotification"
|
||||
| "storeForward"
|
||||
| "rangeTest"
|
||||
| "telemetry"
|
||||
| "cannedMessage"
|
||||
| "audio"
|
||||
| "neighborInfo"
|
||||
| "ambientLighting"
|
||||
| "detectionSensor"
|
||||
| "paxcounter";
|
||||
|
||||
// Unified config change key type
|
||||
export type ConfigChangeKey =
|
||||
| { type: "config"; variant: ValidConfigType }
|
||||
| { type: "moduleConfig"; variant: ValidModuleConfigType }
|
||||
| { type: "channel"; index: Types.ChannelNumber }
|
||||
| { type: "user" };
|
||||
|
||||
// Serialized key for Map storage
|
||||
export type ConfigChangeKeyString = string;
|
||||
|
||||
// Registry entry
|
||||
export interface ChangeEntry {
|
||||
key: ConfigChangeKey;
|
||||
value: unknown;
|
||||
timestamp: number;
|
||||
originalValue?: unknown;
|
||||
}
|
||||
|
||||
// The unified registry
|
||||
export interface ChangeRegistry {
|
||||
changes: Map<ConfigChangeKeyString, ChangeEntry>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert structured key to string for Map lookup
|
||||
*/
|
||||
export function serializeKey(key: ConfigChangeKey): ConfigChangeKeyString {
|
||||
switch (key.type) {
|
||||
case "config":
|
||||
return `config:${key.variant}`;
|
||||
case "moduleConfig":
|
||||
return `moduleConfig:${key.variant}`;
|
||||
case "channel":
|
||||
return `channel:${key.index}`;
|
||||
case "user":
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse operation for type-safe retrieval
|
||||
*/
|
||||
export function deserializeKey(keyStr: ConfigChangeKeyString): ConfigChangeKey {
|
||||
const [type, variant] = keyStr.split(":");
|
||||
|
||||
switch (type) {
|
||||
case "config":
|
||||
return { type: "config", variant: variant as ValidConfigType };
|
||||
case "moduleConfig":
|
||||
return {
|
||||
type: "moduleConfig",
|
||||
variant: variant as ValidModuleConfigType,
|
||||
};
|
||||
case "channel":
|
||||
return {
|
||||
type: "channel",
|
||||
index: Number(variant) as Types.ChannelNumber,
|
||||
};
|
||||
case "user":
|
||||
return { type: "user" };
|
||||
default:
|
||||
throw new Error(`Unknown key type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty change registry
|
||||
*/
|
||||
export function createChangeRegistry(): ChangeRegistry {
|
||||
return {
|
||||
changes: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a config variant has changes
|
||||
*/
|
||||
export function hasConfigChange(
|
||||
registry: ChangeRegistry,
|
||||
variant: ValidConfigType,
|
||||
): boolean {
|
||||
return registry.changes.has(serializeKey({ type: "config", variant }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module config variant has changes
|
||||
*/
|
||||
export function hasModuleConfigChange(
|
||||
registry: ChangeRegistry,
|
||||
variant: ValidModuleConfigType,
|
||||
): boolean {
|
||||
return registry.changes.has(serializeKey({ type: "moduleConfig", variant }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a channel has changes
|
||||
*/
|
||||
export function hasChannelChange(
|
||||
registry: ChangeRegistry,
|
||||
index: Types.ChannelNumber,
|
||||
): boolean {
|
||||
return registry.changes.has(serializeKey({ type: "channel", index }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user config has changes
|
||||
*/
|
||||
export function hasUserChange(registry: ChangeRegistry): boolean {
|
||||
return registry.changes.has(serializeKey({ type: "user" }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of config changes
|
||||
*/
|
||||
export function getConfigChangeCount(registry: ChangeRegistry): number {
|
||||
let count = 0;
|
||||
for (const keyStr of registry.changes.keys()) {
|
||||
const key = deserializeKey(keyStr);
|
||||
if (key.type === "config") {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of module config changes
|
||||
*/
|
||||
export function getModuleConfigChangeCount(registry: ChangeRegistry): number {
|
||||
let count = 0;
|
||||
for (const keyStr of registry.changes.keys()) {
|
||||
const key = deserializeKey(keyStr);
|
||||
if (key.type === "moduleConfig") {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of channel changes
|
||||
*/
|
||||
export function getChannelChangeCount(registry: ChangeRegistry): number {
|
||||
let count = 0;
|
||||
for (const keyStr of registry.changes.keys()) {
|
||||
const key = deserializeKey(keyStr);
|
||||
if (key.type === "channel") {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all config changes as an array
|
||||
*/
|
||||
export function getAllConfigChanges(registry: ChangeRegistry): ChangeEntry[] {
|
||||
const changes: ChangeEntry[] = [];
|
||||
for (const entry of registry.changes.values()) {
|
||||
if (entry.key.type === "config") {
|
||||
changes.push(entry);
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all module config changes as an array
|
||||
*/
|
||||
export function getAllModuleConfigChanges(
|
||||
registry: ChangeRegistry,
|
||||
): ChangeEntry[] {
|
||||
const changes: ChangeEntry[] = [];
|
||||
for (const entry of registry.changes.values()) {
|
||||
if (entry.key.type === "moduleConfig") {
|
||||
changes.push(entry);
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all channel changes as an array
|
||||
*/
|
||||
export function getAllChannelChanges(registry: ChangeRegistry): ChangeEntry[] {
|
||||
const changes: ChangeEntry[] = [];
|
||||
for (const entry of registry.changes.values()) {
|
||||
if (entry.key.type === "channel") {
|
||||
changes.push(entry);
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
@@ -19,9 +19,7 @@ export const mockDeviceStore: Device = {
|
||||
channels: new Map(),
|
||||
config: {} as Protobuf.LocalOnly.LocalConfig,
|
||||
moduleConfig: {} as Protobuf.LocalOnly.LocalModuleConfig,
|
||||
workingConfig: [],
|
||||
workingModuleConfig: [],
|
||||
workingChannelConfig: [],
|
||||
changeRegistry: { changes: new Map() },
|
||||
hardware: {} as Protobuf.Mesh.MyNodeInfo,
|
||||
metadata: new Map(),
|
||||
traceroutes: new Map(),
|
||||
@@ -56,17 +54,8 @@ export const mockDeviceStore: Device = {
|
||||
setStatus: vi.fn(),
|
||||
setConfig: vi.fn(),
|
||||
setModuleConfig: vi.fn(),
|
||||
setWorkingConfig: vi.fn(),
|
||||
setWorkingModuleConfig: vi.fn(),
|
||||
getWorkingConfig: vi.fn(),
|
||||
getWorkingModuleConfig: vi.fn(),
|
||||
removeWorkingConfig: vi.fn(),
|
||||
removeWorkingModuleConfig: vi.fn(),
|
||||
getEffectiveConfig: vi.fn(),
|
||||
getEffectiveModuleConfig: vi.fn(),
|
||||
setWorkingChannelConfig: vi.fn(),
|
||||
getWorkingChannelConfig: vi.fn(),
|
||||
removeWorkingChannelConfig: vi.fn(),
|
||||
setHardware: vi.fn(),
|
||||
setActiveNode: vi.fn(),
|
||||
setPendingSettingsChanges: vi.fn(),
|
||||
@@ -90,4 +79,21 @@ export const mockDeviceStore: Device = {
|
||||
getUnreadCount: vi.fn().mockReturnValue(0),
|
||||
getNeighborInfo: vi.fn(),
|
||||
addNeighborInfo: vi.fn(),
|
||||
|
||||
// New unified change tracking methods
|
||||
setChange: vi.fn(),
|
||||
removeChange: vi.fn(),
|
||||
hasChange: vi.fn().mockReturnValue(false),
|
||||
getChange: vi.fn(),
|
||||
clearAllChanges: vi.fn(),
|
||||
hasConfigChange: vi.fn().mockReturnValue(false),
|
||||
hasModuleConfigChange: vi.fn().mockReturnValue(false),
|
||||
hasChannelChange: vi.fn().mockReturnValue(false),
|
||||
hasUserChange: vi.fn().mockReturnValue(false),
|
||||
getConfigChangeCount: vi.fn().mockReturnValue(0),
|
||||
getModuleConfigChangeCount: vi.fn().mockReturnValue(0),
|
||||
getChannelChangeCount: vi.fn().mockReturnValue(0),
|
||||
getAllConfigChanges: vi.fn().mockReturnValue([]),
|
||||
getAllModuleConfigChanges: vi.fn().mockReturnValue([]),
|
||||
getAllChannelChanges: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
@@ -52,25 +52,7 @@ function makeChannel(index: number) {
|
||||
function makeWaypoint(id: number, expire?: number) {
|
||||
return create(Protobuf.Mesh.WaypointSchema, { id, expire });
|
||||
}
|
||||
function makeConfig(fields: Record<string, any>) {
|
||||
return create(Protobuf.Config.ConfigSchema, {
|
||||
payloadVariant: {
|
||||
case: "device",
|
||||
value: create(Protobuf.Config.Config_DeviceConfigSchema, fields),
|
||||
},
|
||||
});
|
||||
}
|
||||
function makeModuleConfig(fields: Record<string, any>) {
|
||||
return create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
||||
payloadVariant: {
|
||||
case: "mqtt",
|
||||
value: create(
|
||||
Protobuf.ModuleConfig.ModuleConfig_MQTTConfigSchema,
|
||||
fields,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function makeAdminMessage(fields: Record<string, any>) {
|
||||
return create(Protobuf.Admin.AdminMessageSchema, fields);
|
||||
}
|
||||
@@ -114,13 +96,13 @@ describe("DeviceStore – basic map ops & retention", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("DeviceStore – working/effective config API", () => {
|
||||
describe("DeviceStore – change registry API", () => {
|
||||
beforeEach(() => {
|
||||
idbMem.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("setWorkingConfig/getWorkingConfig replaces by variant and getEffectiveConfig merges base + working", async () => {
|
||||
it("setChange/hasChange/getChange for config and getEffectiveConfig merges base + working", async () => {
|
||||
const { useDeviceStore } = await freshStore(false);
|
||||
const state = useDeviceStore.getState();
|
||||
const device = state.addDevice(42);
|
||||
@@ -138,14 +120,17 @@ describe("DeviceStore – working/effective config API", () => {
|
||||
);
|
||||
|
||||
// working deviceConfig.role = ROUTER
|
||||
device.setWorkingConfig(
|
||||
makeConfig({
|
||||
role: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||
}),
|
||||
);
|
||||
const routerConfig = create(Protobuf.Config.Config_DeviceConfigSchema, {
|
||||
role: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||
});
|
||||
device.setChange({ type: "config", variant: "device" }, routerConfig);
|
||||
|
||||
// expect working deviceConfig.role = ROUTER
|
||||
const working = device.getWorkingConfig("device");
|
||||
// expect change is tracked
|
||||
expect(device.hasConfigChange("device")).toBe(true);
|
||||
const working = device.getChange({
|
||||
type: "config",
|
||||
variant: "device",
|
||||
}) as Protobuf.Config.Config_DeviceConfig;
|
||||
expect(working?.role).toBe(Protobuf.Config.Config_DeviceConfig_Role.ROUTER);
|
||||
|
||||
// expect effective deviceConfig.role = ROUTER
|
||||
@@ -154,30 +139,27 @@ describe("DeviceStore – working/effective config API", () => {
|
||||
Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||
);
|
||||
|
||||
// remove working, effective should equal base
|
||||
device.removeWorkingConfig("device");
|
||||
expect(device.getWorkingConfig("device")).toBeUndefined();
|
||||
// remove change, effective should equal base
|
||||
device.removeChange({ type: "config", variant: "device" });
|
||||
expect(device.hasConfigChange("device")).toBe(false);
|
||||
expect(device.getEffectiveConfig("device")?.role).toBe(
|
||||
Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
|
||||
);
|
||||
|
||||
// add multiple, then clear all
|
||||
device.setWorkingConfig(makeConfig({}));
|
||||
device.setWorkingConfig(
|
||||
makeConfig({
|
||||
deviceRole: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||
}),
|
||||
);
|
||||
device.removeWorkingConfig(); // clears all
|
||||
expect(device.getWorkingConfig("device")).toBeUndefined();
|
||||
device.setChange({ type: "config", variant: "device" }, routerConfig);
|
||||
device.setChange({ type: "config", variant: "position" }, {});
|
||||
device.clearAllChanges();
|
||||
expect(device.hasConfigChange("device")).toBe(false);
|
||||
expect(device.hasConfigChange("position")).toBe(false);
|
||||
});
|
||||
|
||||
it("setWorkingModuleConfig/getWorkingModuleConfig and getEffectiveModuleConfig", async () => {
|
||||
it("setChange/hasChange for moduleConfig and getEffectiveModuleConfig", async () => {
|
||||
const { useDeviceStore } = await freshStore(false);
|
||||
const state = useDeviceStore.getState();
|
||||
const device = state.addDevice(7);
|
||||
|
||||
// base moduleConfig.mqtt empty; add working mqtt host
|
||||
// base moduleConfig.mqtt with base address
|
||||
device.setModuleConfig(
|
||||
create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
||||
payloadVariant: {
|
||||
@@ -188,58 +170,82 @@ describe("DeviceStore – working/effective config API", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
device.setWorkingModuleConfig(
|
||||
makeModuleConfig({ address: "mqtt://working" }),
|
||||
|
||||
// working mqtt config
|
||||
const workingMqtt = create(
|
||||
Protobuf.ModuleConfig.ModuleConfig_MQTTConfigSchema,
|
||||
{ address: "mqtt://working" },
|
||||
);
|
||||
device.setChange({ type: "moduleConfig", variant: "mqtt" }, workingMqtt);
|
||||
|
||||
expect(device.hasModuleConfigChange("mqtt")).toBe(true);
|
||||
const mqtt = device.getChange({
|
||||
type: "moduleConfig",
|
||||
variant: "mqtt",
|
||||
}) as Protobuf.ModuleConfig.ModuleConfig_MQTTConfig;
|
||||
expect(mqtt?.address).toBe("mqtt://working");
|
||||
|
||||
// effective should return working value
|
||||
expect(device.getEffectiveModuleConfig("mqtt")?.address).toBe(
|
||||
"mqtt://working",
|
||||
);
|
||||
|
||||
const mqtt = device.getWorkingModuleConfig("mqtt");
|
||||
expect(mqtt?.address).toBe("mqtt://working");
|
||||
expect(mqtt?.address).toBe("mqtt://working");
|
||||
|
||||
device.removeWorkingModuleConfig("mqtt");
|
||||
expect(device.getWorkingModuleConfig("mqtt")).toBeUndefined();
|
||||
// remove change
|
||||
device.removeChange({ type: "moduleConfig", variant: "mqtt" });
|
||||
expect(device.hasModuleConfigChange("mqtt")).toBe(false);
|
||||
expect(device.getEffectiveModuleConfig("mqtt")?.address).toBe(
|
||||
"mqtt://base",
|
||||
);
|
||||
|
||||
// Clear all
|
||||
device.setWorkingModuleConfig(makeModuleConfig({ address: "x" }));
|
||||
device.setWorkingModuleConfig(makeModuleConfig({ address: "y" }));
|
||||
device.removeWorkingModuleConfig();
|
||||
expect(device.getWorkingModuleConfig("mqtt")).toBeUndefined();
|
||||
device.setChange({ type: "moduleConfig", variant: "mqtt" }, workingMqtt);
|
||||
device.clearAllChanges();
|
||||
expect(device.hasModuleConfigChange("mqtt")).toBe(false);
|
||||
});
|
||||
|
||||
it("channel working config add/update/remove/get", async () => {
|
||||
it("channel change tracking add/update/remove/get", async () => {
|
||||
const { useDeviceStore } = await freshStore(false);
|
||||
const state = useDeviceStore.getState();
|
||||
const device = state.addDevice(9);
|
||||
|
||||
device.setWorkingChannelConfig(makeChannel(0));
|
||||
device.setWorkingChannelConfig(
|
||||
create(Protobuf.Channel.ChannelSchema, {
|
||||
index: 1,
|
||||
settings: { name: "one" },
|
||||
}),
|
||||
);
|
||||
expect(device.getWorkingChannelConfig(0)?.index).toBe(0);
|
||||
expect(device.getWorkingChannelConfig(1)?.settings?.name).toBe("one");
|
||||
const channel0 = makeChannel(0);
|
||||
const channel1 = create(Protobuf.Channel.ChannelSchema, {
|
||||
index: 1,
|
||||
settings: { name: "one" },
|
||||
});
|
||||
|
||||
device.setChange({ type: "channel", index: 0 }, channel0);
|
||||
device.setChange({ type: "channel", index: 1 }, channel1);
|
||||
|
||||
expect(device.hasChannelChange(0)).toBe(true);
|
||||
expect(device.hasChannelChange(1)).toBe(true);
|
||||
const ch0 = device.getChange({ type: "channel", index: 0 }) as
|
||||
| Protobuf.Channel.Channel
|
||||
| undefined;
|
||||
expect(ch0?.index).toBe(0);
|
||||
const ch1 = device.getChange({ type: "channel", index: 1 }) as
|
||||
| Protobuf.Channel.Channel
|
||||
| undefined;
|
||||
expect(ch1?.settings?.name).toBe("one");
|
||||
|
||||
// update channel 1
|
||||
device.setWorkingChannelConfig(
|
||||
create(Protobuf.Channel.ChannelSchema, {
|
||||
index: 1,
|
||||
settings: { name: "uno" },
|
||||
}),
|
||||
);
|
||||
expect(device.getWorkingChannelConfig(1)?.settings?.name).toBe("uno");
|
||||
const channel1Updated = create(Protobuf.Channel.ChannelSchema, {
|
||||
index: 1,
|
||||
settings: { name: "uno" },
|
||||
});
|
||||
device.setChange({ type: "channel", index: 1 }, channel1Updated);
|
||||
const ch1Updated = device.getChange({ type: "channel", index: 1 }) as
|
||||
| Protobuf.Channel.Channel
|
||||
| undefined;
|
||||
expect(ch1Updated?.settings?.name).toBe("uno");
|
||||
|
||||
// remove specific
|
||||
device.removeWorkingChannelConfig(1);
|
||||
expect(device.getWorkingChannelConfig(1)).toBeUndefined();
|
||||
device.removeChange({ type: "channel", index: 1 });
|
||||
expect(device.hasChannelChange(1)).toBe(false);
|
||||
|
||||
// remove all
|
||||
device.removeWorkingChannelConfig();
|
||||
expect(device.getWorkingChannelConfig(0)).toBeUndefined();
|
||||
device.clearAllChanges();
|
||||
expect(device.hasChannelChange(0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -349,7 +355,8 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa
|
||||
|
||||
// Old device with myNodeNum=777 and some waypoints (one expired)
|
||||
const oldDevice = state.addDevice(1);
|
||||
oldDevice.connection = { sendWaypoint: vi.fn() } as any;
|
||||
const mockSendWaypoint = vi.fn();
|
||||
oldDevice.addConnection({ sendWaypoint: mockSendWaypoint } as any);
|
||||
|
||||
oldDevice.setHardware(makeHardware(777));
|
||||
oldDevice.addWaypoint(
|
||||
@@ -393,10 +400,10 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa
|
||||
|
||||
// Remove waypoint
|
||||
oldDevice.removeWaypoint(102, false);
|
||||
expect(oldDevice.connection?.sendWaypoint).not.toHaveBeenCalled();
|
||||
expect(mockSendWaypoint).not.toHaveBeenCalled();
|
||||
|
||||
await oldDevice.removeWaypoint(101, true); // toMesh=true
|
||||
expect(oldDevice.connection?.sendWaypoint).toHaveBeenCalled();
|
||||
expect(mockSendWaypoint).toHaveBeenCalled();
|
||||
|
||||
expect(useDeviceStore.getState().devices.get(1)!.waypoints.length).toBe(98);
|
||||
|
||||
|
||||
@@ -10,6 +10,21 @@ import {
|
||||
persist,
|
||||
subscribeWithSelector,
|
||||
} from "zustand/middleware";
|
||||
import type { ChangeRegistry, ConfigChangeKey } from "./changeRegistry.ts";
|
||||
import {
|
||||
createChangeRegistry,
|
||||
getAllChannelChanges,
|
||||
getAllConfigChanges,
|
||||
getAllModuleConfigChanges,
|
||||
getChannelChangeCount,
|
||||
getConfigChangeCount,
|
||||
getModuleConfigChangeCount,
|
||||
hasChannelChange,
|
||||
hasConfigChange,
|
||||
hasModuleConfigChange,
|
||||
hasUserChange,
|
||||
serializeKey,
|
||||
} from "./changeRegistry.ts";
|
||||
import type {
|
||||
Dialogs,
|
||||
DialogVariant,
|
||||
@@ -42,9 +57,7 @@ export interface Device extends DeviceData {
|
||||
channels: Map<Types.ChannelNumber, Protobuf.Channel.Channel>;
|
||||
config: Protobuf.LocalOnly.LocalConfig;
|
||||
moduleConfig: Protobuf.LocalOnly.LocalModuleConfig;
|
||||
workingConfig: Protobuf.Config.Config[];
|
||||
workingModuleConfig: Protobuf.ModuleConfig.ModuleConfig[];
|
||||
workingChannelConfig: Protobuf.Channel.Channel[];
|
||||
changeRegistry: ChangeRegistry; // Unified change tracking
|
||||
hardware: Protobuf.Mesh.MyNodeInfo;
|
||||
metadata: Map<number, Protobuf.Mesh.DeviceMetadata>;
|
||||
connection?: MeshDevice;
|
||||
@@ -58,27 +71,12 @@ export interface Device extends DeviceData {
|
||||
setStatus: (status: Types.DeviceStatusEnum) => void;
|
||||
setConfig: (config: Protobuf.Config.Config) => void;
|
||||
setModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => void;
|
||||
setWorkingConfig: (config: Protobuf.Config.Config) => void;
|
||||
setWorkingModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => void;
|
||||
getWorkingConfig<K extends ValidConfigType>(
|
||||
payloadVariant: K,
|
||||
): Protobuf.LocalOnly.LocalConfig[K] | undefined;
|
||||
getWorkingModuleConfig<K extends ValidModuleConfigType>(
|
||||
payloadVariant: K,
|
||||
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined;
|
||||
removeWorkingConfig: (payloadVariant?: ValidConfigType) => void;
|
||||
removeWorkingModuleConfig: (payloadVariant?: ValidModuleConfigType) => void;
|
||||
getEffectiveConfig<K extends ValidConfigType>(
|
||||
payloadVariant: K,
|
||||
): Protobuf.LocalOnly.LocalConfig[K] | undefined;
|
||||
getEffectiveModuleConfig<K extends ValidModuleConfigType>(
|
||||
payloadVariant: K,
|
||||
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined;
|
||||
setWorkingChannelConfig: (channelNum: Protobuf.Channel.Channel) => void;
|
||||
getWorkingChannelConfig: (
|
||||
index: Types.ChannelNumber,
|
||||
) => Protobuf.Channel.Channel | undefined;
|
||||
removeWorkingChannelConfig: (channelNum?: Types.ChannelNumber) => void;
|
||||
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => void;
|
||||
setActiveNode: (node: number) => void;
|
||||
setPendingSettingsChanges: (state: boolean) => void;
|
||||
@@ -116,6 +114,27 @@ export interface Device extends DeviceData {
|
||||
neighborInfo: Protobuf.Mesh.NeighborInfo,
|
||||
) => void;
|
||||
getNeighborInfo: (nodeNum: number) => Protobuf.Mesh.NeighborInfo | undefined;
|
||||
|
||||
// New unified change tracking methods
|
||||
setChange: (
|
||||
key: ConfigChangeKey,
|
||||
value: unknown,
|
||||
originalValue?: unknown,
|
||||
) => void;
|
||||
removeChange: (key: ConfigChangeKey) => void;
|
||||
hasChange: (key: ConfigChangeKey) => boolean;
|
||||
getChange: (key: ConfigChangeKey) => unknown | undefined;
|
||||
clearAllChanges: () => void;
|
||||
hasConfigChange: (variant: ValidConfigType) => boolean;
|
||||
hasModuleConfigChange: (variant: ValidModuleConfigType) => boolean;
|
||||
hasChannelChange: (index: Types.ChannelNumber) => boolean;
|
||||
hasUserChange: () => boolean;
|
||||
getConfigChangeCount: () => number;
|
||||
getModuleConfigChangeCount: () => number;
|
||||
getChannelChangeCount: () => number;
|
||||
getAllConfigChanges: () => Protobuf.Config.Config[];
|
||||
getAllModuleConfigChanges: () => Protobuf.ModuleConfig.ModuleConfig[];
|
||||
getAllChannelChanges: () => Protobuf.Channel.Channel[];
|
||||
}
|
||||
|
||||
export interface deviceState {
|
||||
@@ -157,9 +176,7 @@ function deviceFactory(
|
||||
channels: new Map(),
|
||||
config: create(Protobuf.LocalOnly.LocalConfigSchema),
|
||||
moduleConfig: create(Protobuf.LocalOnly.LocalModuleConfigSchema),
|
||||
workingConfig: [],
|
||||
workingModuleConfig: [],
|
||||
workingChannelConfig: [],
|
||||
changeRegistry: createChangeRegistry(),
|
||||
hardware: create(Protobuf.Mesh.MyNodeInfoSchema),
|
||||
metadata: new Map(),
|
||||
connection: undefined,
|
||||
@@ -302,130 +319,6 @@ function deviceFactory(
|
||||
}),
|
||||
);
|
||||
},
|
||||
setWorkingConfig: (config: Protobuf.Config.Config) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const index = device.workingConfig.findIndex(
|
||||
(wc) => wc.payloadVariant.case === config.payloadVariant.case,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
device.workingConfig[index] = config;
|
||||
} else {
|
||||
device.workingConfig.push(config);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
setWorkingModuleConfig: (
|
||||
moduleConfig: Protobuf.ModuleConfig.ModuleConfig,
|
||||
) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const index = device.workingModuleConfig.findIndex(
|
||||
(wmc) =>
|
||||
wmc.payloadVariant.case === moduleConfig.payloadVariant.case,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
device.workingModuleConfig[index] = moduleConfig;
|
||||
} else {
|
||||
device.workingModuleConfig.push(moduleConfig);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
getWorkingConfig<K extends ValidConfigType>(payloadVariant: K) {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workingConfig = device.workingConfig.find(
|
||||
(c) => c.payloadVariant.case === payloadVariant,
|
||||
);
|
||||
|
||||
if (
|
||||
workingConfig?.payloadVariant.case === "deviceUi" ||
|
||||
workingConfig?.payloadVariant.case === "sessionkey"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return workingConfig?.payloadVariant
|
||||
.value as Protobuf.LocalOnly.LocalConfig[K];
|
||||
},
|
||||
getWorkingModuleConfig<K extends ValidModuleConfigType>(
|
||||
payloadVariant: K,
|
||||
): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
return device.workingModuleConfig.find(
|
||||
(c) => c.payloadVariant.case === payloadVariant,
|
||||
)?.payloadVariant.value as Protobuf.LocalOnly.LocalModuleConfig[K];
|
||||
},
|
||||
|
||||
removeWorkingConfig: (payloadVariant?: ValidConfigType) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payloadVariant) {
|
||||
device.workingConfig = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const index = device.workingConfig.findIndex(
|
||||
(wc: Protobuf.Config.Config) =>
|
||||
wc.payloadVariant.case === payloadVariant,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
device.workingConfig.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
removeWorkingModuleConfig: (payloadVariant?: ValidModuleConfigType) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payloadVariant) {
|
||||
device.workingModuleConfig = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const index = device.workingModuleConfig.findIndex(
|
||||
(wc: Protobuf.ModuleConfig.ModuleConfig) =>
|
||||
wc.payloadVariant.case === payloadVariant,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
device.workingModuleConfig.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
getEffectiveConfig<K extends ValidConfigType>(
|
||||
payloadVariant: K,
|
||||
): Protobuf.LocalOnly.LocalConfig[K] | undefined {
|
||||
@@ -437,11 +330,13 @@ function deviceFactory(
|
||||
return;
|
||||
}
|
||||
|
||||
const workingValue = device.changeRegistry.changes.get(
|
||||
serializeKey({ type: "config", variant: payloadVariant }),
|
||||
)?.value as Protobuf.LocalOnly.LocalConfig[K] | undefined;
|
||||
|
||||
return {
|
||||
...device.config[payloadVariant],
|
||||
...device.workingConfig.find(
|
||||
(c) => c.payloadVariant.case === payloadVariant,
|
||||
)?.payloadVariant.value,
|
||||
...workingValue,
|
||||
};
|
||||
},
|
||||
getEffectiveModuleConfig<K extends ValidModuleConfigType>(
|
||||
@@ -452,69 +347,16 @@ function deviceFactory(
|
||||
return;
|
||||
}
|
||||
|
||||
const workingValue = device.changeRegistry.changes.get(
|
||||
serializeKey({ type: "moduleConfig", variant: payloadVariant }),
|
||||
)?.value as Protobuf.LocalOnly.LocalModuleConfig[K] | undefined;
|
||||
|
||||
return {
|
||||
...device.moduleConfig[payloadVariant],
|
||||
...device.workingModuleConfig.find(
|
||||
(c) => c.payloadVariant.case === payloadVariant,
|
||||
)?.payloadVariant.value,
|
||||
...workingValue,
|
||||
};
|
||||
},
|
||||
|
||||
setWorkingChannelConfig: (config: Protobuf.Channel.Channel) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const index = device.workingChannelConfig.findIndex(
|
||||
(wcc) => wcc.index === config.index,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
device.workingChannelConfig[index] = config;
|
||||
} else {
|
||||
device.workingChannelConfig.push(config);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
getWorkingChannelConfig: (channelNum: Types.ChannelNumber) => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workingChannelConfig = device.workingChannelConfig.find(
|
||||
(c) => c.index === channelNum,
|
||||
);
|
||||
|
||||
return workingChannelConfig;
|
||||
},
|
||||
removeWorkingChannelConfig: (channelNum?: Types.ChannelNumber) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (channelNum === undefined) {
|
||||
device.workingChannelConfig = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const index = device.workingChannelConfig.findIndex(
|
||||
(wcc: Protobuf.Channel.Channel) => wcc.index === channelNum,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
device.workingChannelConfig.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
@@ -855,6 +697,195 @@ function deviceFactory(
|
||||
}
|
||||
return device.neighborInfo.get(nodeNum);
|
||||
},
|
||||
|
||||
// New unified change tracking methods
|
||||
setChange: (
|
||||
key: ConfigChangeKey,
|
||||
value: unknown,
|
||||
originalValue?: unknown,
|
||||
) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyStr = serializeKey(key);
|
||||
device.changeRegistry.changes.set(keyStr, {
|
||||
key,
|
||||
value,
|
||||
originalValue,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
removeChange: (key: ConfigChangeKey) => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
device.changeRegistry.changes.delete(serializeKey(key));
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
hasChange: (key: ConfigChangeKey) => {
|
||||
const device = get().devices.get(id);
|
||||
return device?.changeRegistry.changes.has(serializeKey(key)) ?? false;
|
||||
},
|
||||
|
||||
getChange: (key: ConfigChangeKey) => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
return device.changeRegistry.changes.get(serializeKey(key))?.value;
|
||||
},
|
||||
|
||||
clearAllChanges: () => {
|
||||
set(
|
||||
produce<PrivateDeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
device.changeRegistry.changes.clear();
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
hasConfigChange: (variant: ValidConfigType) => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasConfigChange(device.changeRegistry, variant);
|
||||
},
|
||||
|
||||
hasModuleConfigChange: (variant: ValidModuleConfigType) => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasModuleConfigChange(device.changeRegistry, variant);
|
||||
},
|
||||
|
||||
hasChannelChange: (index: Types.ChannelNumber) => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasChannelChange(device.changeRegistry, index);
|
||||
},
|
||||
|
||||
hasUserChange: () => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasUserChange(device.changeRegistry);
|
||||
},
|
||||
|
||||
getConfigChangeCount: () => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getConfigChangeCount(device.changeRegistry);
|
||||
},
|
||||
|
||||
getModuleConfigChangeCount: () => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getModuleConfigChangeCount(device.changeRegistry);
|
||||
},
|
||||
|
||||
getChannelChangeCount: () => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getChannelChangeCount(device.changeRegistry);
|
||||
},
|
||||
|
||||
getAllConfigChanges: () => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const changes = getAllConfigChanges(device.changeRegistry);
|
||||
return changes
|
||||
.map((entry) => {
|
||||
if (entry.key.type !== "config") {
|
||||
return null;
|
||||
}
|
||||
if (!entry.value) {
|
||||
return null;
|
||||
}
|
||||
return create(Protobuf.Config.ConfigSchema, {
|
||||
payloadVariant: {
|
||||
case: entry.key.variant,
|
||||
value: entry.value,
|
||||
},
|
||||
});
|
||||
})
|
||||
.filter((c): c is Protobuf.Config.Config => c !== null);
|
||||
},
|
||||
|
||||
getAllModuleConfigChanges: () => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const changes = getAllModuleConfigChanges(device.changeRegistry);
|
||||
return changes
|
||||
.map((entry) => {
|
||||
if (entry.key.type !== "moduleConfig") {
|
||||
return null;
|
||||
}
|
||||
if (!entry.value) {
|
||||
return null;
|
||||
}
|
||||
return create(Protobuf.ModuleConfig.ModuleConfigSchema, {
|
||||
payloadVariant: {
|
||||
case: entry.key.variant,
|
||||
value: entry.value,
|
||||
},
|
||||
});
|
||||
})
|
||||
.filter((c): c is Protobuf.ModuleConfig.ModuleConfig => c !== null);
|
||||
},
|
||||
|
||||
getAllChannelChanges: () => {
|
||||
const device = get().devices.get(id);
|
||||
if (!device) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const changes = getAllChannelChanges(device.changeRegistry);
|
||||
return changes
|
||||
.map((entry) => entry.value as Protobuf.Channel.Channel)
|
||||
.filter((c): c is Protobuf.Channel.Channel => c !== undefined);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import type {
|
||||
ValidConfigType,
|
||||
ValidModuleConfigType,
|
||||
} from "./changeRegistry.ts";
|
||||
|
||||
interface Dialogs {
|
||||
import: boolean;
|
||||
@@ -22,16 +26,7 @@ interface Dialogs {
|
||||
|
||||
type DialogVariant = keyof Dialogs;
|
||||
|
||||
type ValidConfigType = Exclude<
|
||||
Protobuf.Config.Config["payloadVariant"]["case"],
|
||||
"deviceUi" | "sessionkey" | undefined
|
||||
>;
|
||||
type ValidModuleConfigType = Exclude<
|
||||
Protobuf.ModuleConfig.ModuleConfig["payloadVariant"]["case"],
|
||||
undefined
|
||||
>;
|
||||
|
||||
type Page = "messages" | "map" | "config" | "channels" | "nodes";
|
||||
type Page = "messages" | "map" | "settings" | "channels" | "nodes";
|
||||
|
||||
type WaypointWithMetadata = Protobuf.Mesh.Waypoint & {
|
||||
metadata: {
|
||||
|
||||
Reference in New Issue
Block a user