import { createContext } from "react"; import { produce } from "immer"; import { create } from "zustand"; import { Protobuf, Types } from "@meshtastic/meshtasticjs"; export type Page = | "messages" | "map" | "extensions" | "config" | "channels" | "peers"; export interface MessageWithState extends Types.PacketMetadata { state: MessageState; } export interface WaypointIDWithState extends Omit, "data"> { waypointID: number; state: MessageState; } export type AllMessageTypes = MessageWithState | WaypointIDWithState; export type MessageState = "ack" | "waiting" | Protobuf.Routing_Error; export interface Channel { config: Protobuf.Channel; lastInterraction: Date; messages: AllMessageTypes[]; } export interface Node { deviceMetrics: { metric: Protobuf.DeviceMetrics; timestamp: Date; }[]; environmentMetrics: { metric: Protobuf.EnvironmentMetrics; timestamp: Date; }[]; metadata?: Protobuf.DeviceMetadata; data: Protobuf.NodeInfo; } export interface processPacketParams { from: number; snr: number; time: number; } export type WorkingConfig = [ Protobuf.Config_DeviceConfig?, Protobuf.Config_PositionConfig?, Protobuf.Config_PowerConfig?, Protobuf.Config_NetworkConfig?, Protobuf.Config_DisplayConfig?, Protobuf.Config_LoRaConfig?, Protobuf.Config_BluetoothConfig? ]; export type DialogVariant = "import" | "QR" | "shutdown" | "reboot"; export interface Device { id: number; ready: boolean; status: Types.DeviceStatusEnum; channels: Channel[]; config: Protobuf.LocalConfig; moduleConfig: Protobuf.LocalModuleConfig; hardware: Protobuf.MyNodeInfo; nodes: Node[]; connection?: Types.ConnectionType; activePage: Page; peerInfoOpen: boolean; activePeer: number; waypoints: Protobuf.Waypoint[]; regionUnset: boolean; currentMetrics: Protobuf.DeviceMetrics; pendingSettingsChanges: boolean; messageDraft: string; workingConfig: WorkingConfig; dialog: { import: boolean; QR: boolean; shutdown: boolean; reboot: boolean; }; setReady(ready: boolean): void; setStatus: (status: Types.DeviceStatusEnum) => void; setConfig: (config: Protobuf.Config) => void; setModuleConfig: (config: Protobuf.ModuleConfig) => void; setHardware: (hardware: Protobuf.MyNodeInfo) => void; setMetrics: (metrics: Types.PacketMetadata) => void; setActivePage: (page: Page) => void; setPeerInfoOpen: (open: boolean) => void; setActivePeer: (peer: number) => void; setPendingSettingsChanges: (state: boolean) => void; addChannel: (channel: Channel) => void; addWaypoint: (waypoint: Protobuf.Waypoint) => void; addNodeInfo: (nodeInfo: Protobuf.NodeInfo) => void; addUser: (user: Types.PacketMetadata) => void; addPosition: (position: Types.PacketMetadata) => void; addConnection: (connection: Types.ConnectionType) => void; addMessage: (message: MessageWithState) => void; addWaypointMessage: (message: WaypointIDWithState) => void; addDeviceMetadataMessage: ( metadata: Types.PacketMetadata ) => void; setMessageState: ( channelIndex: number, messageId: number, state: MessageState ) => void; setDialogOpen: (dialog: DialogVariant, open: boolean) => void; processPacket: (data: processPacketParams) => void; setMessageDraft: (message: string) => void; } export interface DeviceState { devices: Map; remoteDevices: Map; addDevice: (id: number) => Device; removeDevice: (id: number) => void; getDevices: () => Device[]; getDevice: (id: number) => Device | undefined; } export const useDeviceStore = create((set, get) => ({ devices: new Map(), remoteDevices: new Map(), addDevice: (id: number) => { set( produce((draft) => { draft.devices.set(id, { id, ready: false, status: Types.DeviceStatusEnum.DEVICE_DISCONNECTED, channels: [], config: new Protobuf.LocalConfig(), moduleConfig: new Protobuf.LocalModuleConfig(), hardware: new Protobuf.MyNodeInfo(), nodes: [], connection: undefined, activePage: "messages", peerInfoOpen: false, activePeer: 0, waypoints: [], regionUnset: false, currentMetrics: new Protobuf.DeviceMetrics(), dialog: { import: false, QR: false, shutdown: false, reboot: false }, pendingSettingsChanges: false, messageDraft: "", workingConfig: [], setReady: (ready: boolean) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.ready = ready; } }) ); }, setStatus: (status: Types.DeviceStatusEnum) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.status = status; } }) ); }, setConfig: (config: Protobuf.Config) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { switch (config.payloadVariant.case) { case "device": device.config.device = config.payloadVariant.value; break; case "position": device.config.position = config.payloadVariant.value; break; case "power": device.config.power = config.payloadVariant.value; break; case "network": device.config.network = config.payloadVariant.value; break; case "display": device.config.display = config.payloadVariant.value; break; case "lora": device.config.lora = config.payloadVariant.value; device.regionUnset = config.payloadVariant.value.region === Protobuf.Config_LoRaConfig_RegionCode.UNSET; break; case "bluetooth": device.config.bluetooth = config.payloadVariant.value; break; } } }) ); }, setModuleConfig: (config: Protobuf.ModuleConfig) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { switch (config.payloadVariant.case) { case "mqtt": device.moduleConfig.mqtt = config.payloadVariant.value; break; case "serial": device.moduleConfig.serial = config.payloadVariant.value; break; case "externalNotification": device.moduleConfig.externalNotification = config.payloadVariant.value; break; case "storeForward": device.moduleConfig.storeForward = config.payloadVariant.value; break; case "rangeTest": device.moduleConfig.rangeTest = config.payloadVariant.value; break; case "telemetry": device.moduleConfig.telemetry = config.payloadVariant.value; break; case "cannedMessage": device.moduleConfig.cannedMessage = config.payloadVariant.value; break; case "audio": device.moduleConfig.audio = config.payloadVariant.value; } } }) ); }, setHardware: (hardware: Protobuf.MyNodeInfo) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.hardware = hardware; } }) ); }, setMetrics: (metrics: Types.PacketMetadata) => { set( produce((draft) => { const device = draft.devices.get(id); let node = device?.nodes.find( (n) => n.data.num === metrics.from ); if (node) { switch (metrics.data.variant.case) { case "deviceMetrics": if (device) { if (metrics.data.variant.value.batteryLevel) { device.currentMetrics.batteryLevel = metrics.data.variant.value.batteryLevel; } if (metrics.data.variant.value.voltage) { device.currentMetrics.voltage = metrics.data.variant.value.voltage; } if (metrics.data.variant.value.airUtilTx) { device.currentMetrics.airUtilTx = metrics.data.variant.value.airUtilTx; } if (metrics.data.variant.value.channelUtilization) { device.currentMetrics.channelUtilization = metrics.data.variant.value.channelUtilization; } } node.deviceMetrics.push({ metric: metrics.data.variant.value, timestamp: metrics.rxTime }); break; case "environmentMetrics": node.environmentMetrics.push({ metric: metrics.data.variant.value, timestamp: metrics.rxTime }); break; } } }) ); }, setActivePage: (page) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.activePage = page; } }) ); }, setPendingSettingsChanges: (state) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.pendingSettingsChanges = state; } }) ); }, addChannel: (channel: Channel) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { const channelIndex = device.channels.findIndex( (c) => c.config.index === channel.config.index ); if (channelIndex !== -1) { const messages = device.channels[channelIndex].messages; device.channels[channelIndex] = channel; device.channels[channelIndex].messages = messages; } else { device.channels.push(channel); } } }) ); }, addWaypoint: (waypoint: Protobuf.Waypoint) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { const waypointIndex = device.waypoints.findIndex( (wp) => wp.id === waypoint.id ); if (waypointIndex !== -1) { device.waypoints[waypointIndex] = waypoint; } else { device.waypoints.push(waypoint); } } }) ); }, addNodeInfo: (nodeInfo) => { set( produce((draft) => { const device = draft.devices.get(id); const node = device?.nodes.find( (node) => node.data.num === nodeInfo.num ); if (node) { node.data = nodeInfo; } else { device?.nodes.push({ data: nodeInfo, metadata: undefined, deviceMetrics: [], environmentMetrics: [] }); } }) ); }, setPeerInfoOpen: (open) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.peerInfoOpen = open; } }) ); }, setActivePeer: (peer) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.activePeer = peer; } }) ); }, addUser: (user) => { set( produce((draft) => { const device = draft.devices.get(id); const node = device?.nodes.find( (node) => node.data.num === user.from ); if (node) { node.data.user = user.data; } }) ); }, addPosition: (position) => { set( produce((draft) => { const device = draft.devices.get(id); const node = device?.nodes.find( (node) => node.data.num === position.from ); if (node) { node.data.position = position.data; } }) ); }, addConnection: (connection) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.connection = connection; } }) ); }, addMessage: (message) => { set( produce((draft) => { const device = draft.devices.get(id); device?.channels .find((ch) => ch.config.index === message.channel) ?.messages.push(message); }) ); }, addWaypointMessage: (waypointID) => { set( produce((draft) => { const device = draft.devices.get(id); device?.channels .find((ch) => ch.config.index === waypointID.channel) ?.messages.push(waypointID); }) ); }, addDeviceMetadataMessage: (metadata) => { set( produce((draft) => { const device = draft.devices.get(id); const node = device?.nodes.find( (n) => n.data.num === metadata.from ); if (node) { node.metadata = metadata.data; } }) ); }, setMessageState: ( channelIndex: number, messageId: number, state: MessageState ) => { set( produce((draft) => { const device = draft.devices.get(id); const channel = device?.channels.find( (ch) => ch.config.index === channelIndex ); const message = channel?.messages.find( (msg) => msg.id === messageId ); if (message) { message.state = state; } }) ); }, setDialogOpen: (dialog: DialogVariant, open: boolean) => { set( produce((draft) => { const device = draft.devices.get(id); if (!device) { return; } device.dialog[dialog] = open; }) ); }, processPacket(data: processPacketParams) { set( produce((draft) => { const device = draft.devices.get(id); if (!device) { return; } const node = device.nodes.find((n) => n.data.num === data.from); if (!node) { device.nodes.push({ data: new Protobuf.NodeInfo({ num: data.from, lastHeard: data.time, snr: data.snr }), metadata: undefined, deviceMetrics: [], environmentMetrics: [] }); return; } else { node.data = { ...node.data, lastHeard: data.time, snr: data.snr }; } }) ); }, setMessageDraft: (message: string) => { set( produce((draft) => { const device = draft.devices.get(id); if (device) { device.messageDraft = message; } }) ); } }); }) ); const device = get().devices.get(id); if (!device) { throw new Error("Device not found"); } return device; }, removeDevice: (id) => { set( produce((draft) => { draft.devices.delete(id); }) ); }, getDevices: () => Array.from(get().devices.values()), getDevice: (id) => get().devices.get(id) })); export const DeviceContext = createContext(undefined);