580 lines
19 KiB
TypeScript
580 lines
19 KiB
TypeScript
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<string> {
|
|
state: MessageState;
|
|
}
|
|
|
|
export interface WaypointIDWithState
|
|
extends Omit<Types.PacketMetadata<Protobuf.Waypoint>, "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<Protobuf.Telemetry>) => 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<Protobuf.User>) => void;
|
|
addPosition: (position: Types.PacketMetadata<Protobuf.Position>) => void;
|
|
addConnection: (connection: Types.ConnectionType) => void;
|
|
addMessage: (message: MessageWithState) => void;
|
|
addWaypointMessage: (message: WaypointIDWithState) => void;
|
|
addDeviceMetadataMessage: (
|
|
metadata: Types.PacketMetadata<Protobuf.DeviceMetadata>
|
|
) => 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<number, Device>;
|
|
remoteDevices: Map<number, undefined>;
|
|
|
|
addDevice: (id: number) => Device;
|
|
removeDevice: (id: number) => void;
|
|
getDevices: () => Device[];
|
|
getDevice: (id: number) => Device | undefined;
|
|
}
|
|
|
|
export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|
devices: new Map(),
|
|
remoteDevices: new Map(),
|
|
|
|
addDevice: (id: number) => {
|
|
set(
|
|
produce<DeviceState>((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<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (device) {
|
|
device.ready = ready;
|
|
}
|
|
})
|
|
);
|
|
},
|
|
setStatus: (status: Types.DeviceStatusEnum) => {
|
|
set(
|
|
produce<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (device) {
|
|
device.status = status;
|
|
}
|
|
})
|
|
);
|
|
},
|
|
setConfig: (config: Protobuf.Config) => {
|
|
set(
|
|
produce<DeviceState>((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<DeviceState>((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<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (device) {
|
|
device.hardware = hardware;
|
|
}
|
|
})
|
|
);
|
|
},
|
|
setMetrics: (metrics: Types.PacketMetadata<Protobuf.Telemetry>) => {
|
|
set(
|
|
produce<DeviceState>((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<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (device) {
|
|
device.activePage = page;
|
|
}
|
|
})
|
|
);
|
|
},
|
|
setPendingSettingsChanges: (state) => {
|
|
set(
|
|
produce<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (device) {
|
|
device.pendingSettingsChanges = state;
|
|
}
|
|
})
|
|
);
|
|
},
|
|
addChannel: (channel: Channel) => {
|
|
set(
|
|
produce<DeviceState>((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<DeviceState>((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<DeviceState>((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<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (device) {
|
|
device.peerInfoOpen = open;
|
|
}
|
|
})
|
|
);
|
|
},
|
|
setActivePeer: (peer) => {
|
|
set(
|
|
produce<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (device) {
|
|
device.activePeer = peer;
|
|
}
|
|
})
|
|
);
|
|
},
|
|
addUser: (user) => {
|
|
set(
|
|
produce<DeviceState>((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<DeviceState>((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<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (device) {
|
|
device.connection = connection;
|
|
}
|
|
})
|
|
);
|
|
},
|
|
addMessage: (message) => {
|
|
set(
|
|
produce<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
device?.channels
|
|
.find((ch) => ch.config.index === message.channel)
|
|
?.messages.push(message);
|
|
})
|
|
);
|
|
},
|
|
addWaypointMessage: (waypointID) => {
|
|
set(
|
|
produce<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
device?.channels
|
|
.find((ch) => ch.config.index === waypointID.channel)
|
|
?.messages.push(waypointID);
|
|
})
|
|
);
|
|
},
|
|
addDeviceMetadataMessage: (metadata) => {
|
|
set(
|
|
produce<DeviceState>((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<DeviceState>((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<DeviceState>((draft) => {
|
|
const device = draft.devices.get(id);
|
|
if (!device) {
|
|
return;
|
|
}
|
|
device.dialog[dialog] = open;
|
|
})
|
|
);
|
|
},
|
|
processPacket(data: processPacketParams) {
|
|
set(
|
|
produce<DeviceState>((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<DeviceState>((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<DeviceState>((draft) => {
|
|
draft.devices.delete(id);
|
|
})
|
|
);
|
|
},
|
|
|
|
getDevices: () => Array.from(get().devices.values()),
|
|
|
|
getDevice: (id) => get().devices.get(id)
|
|
}));
|
|
|
|
export const DeviceContext = createContext<Device | undefined>(undefined);
|