DM support
This commit is contained in:
@@ -122,9 +122,8 @@ export const CommandPalette = (): JSX.Element => {
|
||||
subItems: getDevices().map((device) => {
|
||||
return {
|
||||
label:
|
||||
device.nodes.find(
|
||||
(n) => n.data.num === device.hardware.myNodeNum
|
||||
)?.data.user?.longName ?? device.hardware.myNodeNum.toString(),
|
||||
device.nodes.get(device.hardware.myNodeNum)?.user?.longName ??
|
||||
device.hardware.myNodeNum.toString(),
|
||||
icon: (
|
||||
<Hashicon
|
||||
size={16}
|
||||
|
||||
@@ -42,9 +42,8 @@ export const Dashboard = () => {
|
||||
<div className="px-4 py-4 sm:px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="truncate text-sm font-medium text-accent">
|
||||
{device.nodes.filter(
|
||||
(n) => n.data.num === device.hardware.myNodeNum
|
||||
)[0]?.data.user?.longName ?? "UNK"}
|
||||
{device.nodes.get(device.hardware.myNodeNum)?.user
|
||||
?.longName ?? "UNK"}
|
||||
</p>
|
||||
<div className="inline-flex w-24 justify-center gap-2 rounded-full bg-slate-100 py-1 text-xs font-semibold text-slate-900 transition-colors hover:bg-slate-700 hover:text-slate-50">
|
||||
{device.connection?.connType === "ble" && (
|
||||
@@ -74,9 +73,7 @@ export const Dashboard = () => {
|
||||
className="text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{device.nodes.length === 0
|
||||
? 0
|
||||
: device.nodes.length - 1}
|
||||
{device.nodes.size === 0 ? 0 : device.nodes.size - 1}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,19 +29,19 @@ export const DeviceNameDialog = ({
|
||||
}: DeviceNameDialogProps): JSX.Element => {
|
||||
const { hardware, nodes, connection } = useDevice();
|
||||
|
||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
||||
const myNode = nodes.get(hardware.myNodeNum);
|
||||
|
||||
const { register, handleSubmit } = useForm<User>({
|
||||
values: {
|
||||
longName: myNode?.data.user?.longName ?? "Unknown",
|
||||
shortName: myNode?.data.user?.shortName ?? "Unknown"
|
||||
longName: myNode?.user?.longName ?? "Unknown",
|
||||
shortName: myNode?.user?.shortName ?? "Unknown"
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
connection?.setOwner(
|
||||
new Protobuf.User({
|
||||
...myNode?.data.user,
|
||||
...myNode?.user,
|
||||
...data
|
||||
})
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const DialogManager = (): JSX.Element => {
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen("QR", open);
|
||||
}}
|
||||
channels={channels.map((ch) => ch.config)}
|
||||
channels={channels}
|
||||
loraConfig={config.lora}
|
||||
/>
|
||||
<ImportDialog
|
||||
@@ -22,7 +22,7 @@ export const DialogManager = (): JSX.Element => {
|
||||
onOpenChange={(open) => {
|
||||
setDialogOpen("import", open);
|
||||
}}
|
||||
channels={channels.map((ch) => ch.config)}
|
||||
channels={channels}
|
||||
loraConfig={config.lora}
|
||||
/>
|
||||
<ShutdownDialog
|
||||
|
||||
@@ -1,33 +1,46 @@
|
||||
import { Subtle } from "@app/components/UI/Typography/Subtle.js";
|
||||
import { MessageWithState, useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import { Message } from "@components/PageComponents/Messages/Message.js";
|
||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { Channel } from "@core/stores/deviceStore.js";
|
||||
import type { Types } from "@meshtastic/meshtasticjs";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
|
||||
export interface ChannelChatProps {
|
||||
channel: Channel;
|
||||
messages?: MessageWithState[];
|
||||
channel: Types.ChannelNumber;
|
||||
to: Types.Destination;
|
||||
}
|
||||
|
||||
export const ChannelChat = ({ channel }: ChannelChatProps): JSX.Element => {
|
||||
export const ChannelChat = ({
|
||||
messages,
|
||||
channel,
|
||||
to
|
||||
}: ChannelChatProps): JSX.Element => {
|
||||
const { nodes } = useDevice();
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="flex flex-grow flex-col">
|
||||
{channel.messages.map((message, index) => (
|
||||
<Message
|
||||
key={index}
|
||||
message={message}
|
||||
lastMsgSameUser={
|
||||
index === 0
|
||||
? false
|
||||
: channel.messages[index - 1].from === message.from
|
||||
}
|
||||
sender={nodes.find((node) => node.data.num === message.from)?.data}
|
||||
/>
|
||||
))}
|
||||
{messages ? (
|
||||
messages.map((message, index) => (
|
||||
<Message
|
||||
key={index}
|
||||
message={message}
|
||||
lastMsgSameUser={
|
||||
index === 0 ? false : messages[index - 1].from === message.from
|
||||
}
|
||||
sender={nodes.get(message.from)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="m-auto">
|
||||
<InboxIcon className="m-auto" />
|
||||
<Subtle>No Messages</Subtle>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<MessageInput channel={channel} />
|
||||
<MessageInput to={to} channel={channel} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { WaypointMessage } from "@components/PageComponents/Messages/WaypointMessage.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { AllMessageTypes } from "@core/stores/deviceStore.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import {
|
||||
CircleEllipsisIcon,
|
||||
@@ -8,10 +5,11 @@ import {
|
||||
CheckCircle2Icon
|
||||
} from "lucide-react";
|
||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
import type { MessageWithState } from "@app/core/stores/deviceStore.js";
|
||||
|
||||
export interface MessageProps {
|
||||
lastMsgSameUser: boolean;
|
||||
message: AllMessageTypes;
|
||||
message: MessageWithState;
|
||||
sender?: Protobuf.NodeInfo;
|
||||
}
|
||||
|
||||
@@ -20,13 +18,6 @@ export const Message = ({
|
||||
message,
|
||||
sender
|
||||
}: MessageProps): JSX.Element => {
|
||||
const { setPeerInfoOpen, setActivePeer, connection } = useDevice();
|
||||
|
||||
const openPeer = (): void => {
|
||||
setActivePeer(message.from);
|
||||
setPeerInfoOpen(true);
|
||||
};
|
||||
|
||||
return lastMsgSameUser ? (
|
||||
<div className="ml-5 flex">
|
||||
{message.state === "ack" ? (
|
||||
@@ -36,28 +27,21 @@ export const Message = ({
|
||||
) : (
|
||||
<AlertCircleIcon size={16} className="my-auto text-textSecondary" />
|
||||
)}
|
||||
{"waypointID" in message ? (
|
||||
<WaypointMessage waypointID={message.waypointID} />
|
||||
) : (
|
||||
<span
|
||||
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${
|
||||
message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
|
||||
}`}
|
||||
>
|
||||
{message.data}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${
|
||||
message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
|
||||
}`}
|
||||
>
|
||||
{message.data}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-4 mt-2 gap-2">
|
||||
<div className="flex gap-2">
|
||||
<div className="w-6 cursor-pointer" onClick={openPeer}>
|
||||
<div className="w-6 cursor-pointer">
|
||||
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
|
||||
</div>
|
||||
<span
|
||||
className="cursor-pointer font-medium text-textPrimary"
|
||||
onClick={openPeer}
|
||||
>
|
||||
<span className="cursor-pointer font-medium text-textPrimary">
|
||||
{sender?.user?.longName ?? "UNK"}
|
||||
</span>
|
||||
<span className="mt-1 font-mono text-xs text-textSecondary">
|
||||
@@ -78,19 +62,13 @@ export const Message = ({
|
||||
) : (
|
||||
<AlertCircleIcon size={16} className="my-auto text-textSecondary" />
|
||||
)}
|
||||
{"waypointID" in message ? (
|
||||
<WaypointMessage waypointID={message.waypointID} />
|
||||
) : (
|
||||
<span
|
||||
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${
|
||||
message.state === "ack"
|
||||
? "text-textPrimary"
|
||||
: "text-textSecondary"
|
||||
}`}
|
||||
>
|
||||
{message.data}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`ml-4 border-l-2 border-l-backgroundPrimary pl-2 ${
|
||||
message.state === "ack" ? "text-textPrimary" : "text-textSecondary"
|
||||
}`}
|
||||
>
|
||||
{message.data}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
import { Input } from "@components/UI/Input.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import type { Channel } from "@core/stores/deviceStore.js";
|
||||
import { SendIcon } from "lucide-react";
|
||||
import type { Types } from "@meshtastic/meshtasticjs";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
|
||||
export interface MessageInputProps {
|
||||
channel: Channel;
|
||||
to: Types.Destination;
|
||||
channel: Types.ChannelNumber;
|
||||
}
|
||||
|
||||
export const MessageInput = ({ channel }: MessageInputProps): JSX.Element => {
|
||||
const { connection, setMessageState, messageDraft, setMessageDraft } =
|
||||
useDevice();
|
||||
export const MessageInput = ({
|
||||
to,
|
||||
channel
|
||||
}: MessageInputProps): JSX.Element => {
|
||||
const {
|
||||
connection,
|
||||
setMessageState,
|
||||
messageDraft,
|
||||
setMessageDraft,
|
||||
hardware
|
||||
} = useDevice();
|
||||
|
||||
const myNodeNum = hardware.myNodeNum;
|
||||
|
||||
const sendText = async (message: string) => {
|
||||
await connection
|
||||
?.sendText(
|
||||
message,
|
||||
"broadcast",
|
||||
true,
|
||||
channel.config.index as Types.ChannelNumber
|
||||
?.sendText(message, to, true, channel)
|
||||
.then((id) =>
|
||||
setMessageState(
|
||||
to === "broadcast" ? "broadcast" : "direct",
|
||||
channel,
|
||||
to as number,
|
||||
myNodeNum,
|
||||
id,
|
||||
"ack"
|
||||
)
|
||||
)
|
||||
.then((id) => setMessageState(channel.config.index, id, "ack"))
|
||||
.catch((e: Types.PacketError) =>
|
||||
setMessageState(channel.config.index, e.id, e.error)
|
||||
setMessageState(
|
||||
to === "broadcast" ? "broadcast" : "direct",
|
||||
channel,
|
||||
to as number,
|
||||
myNodeNum,
|
||||
e.id,
|
||||
e.error
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import { toMGRS } from "@core/utils/toMGRS.js";
|
||||
import { MapPinIcon } from "lucide-react";
|
||||
|
||||
export interface WaypointMessageProps {
|
||||
waypointID: number;
|
||||
}
|
||||
|
||||
export const WaypointMessage = ({
|
||||
waypointID
|
||||
}: WaypointMessageProps): JSX.Element => {
|
||||
const { waypoints } = useDevice();
|
||||
const waypoint = waypoints.find((wp) => wp.id === waypointID);
|
||||
|
||||
return (
|
||||
<div className="ml-4 border-l-2 border-l-slate-200 pl-2">
|
||||
<div className="flex gap-2 rounded-md p-2">
|
||||
<MapPinIcon size={16} className="m-auto text-slate-600" />
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<div className="font-bold">{waypoint?.name}</div>
|
||||
<span className="font-mono text-sm text-slate-500">
|
||||
{toMGRS(waypoint?.latitudeI, waypoint?.longitudeI)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm">{waypoint?.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export interface SidebarProps {
|
||||
|
||||
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
const { hardware, nodes } = useDevice();
|
||||
const myNode = nodes.find((n) => n.data.num === hardware.myNodeNum);
|
||||
const myNode = nodes.get(hardware.myNodeNum);
|
||||
const { activePage, setActivePage, setDialogOpen } = useDevice();
|
||||
|
||||
interface NavLink {
|
||||
@@ -62,9 +62,9 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
||||
<div className="flex justify-between px-8 py-6">
|
||||
<div>
|
||||
<span className="text-lg font-medium">
|
||||
{myNode?.data.user?.shortName ?? "UNK"}
|
||||
{myNode?.user?.shortName ?? "UNK"}
|
||||
</span>
|
||||
<Subtle>{myNode?.data.user?.longName ?? "UNK"}</Subtle>
|
||||
<Subtle>{myNode?.user?.longName ?? "UNK"}</Subtle>
|
||||
</div>
|
||||
<button
|
||||
className="transition-all hover:text-accent"
|
||||
|
||||
+153
-187
@@ -4,6 +4,7 @@ import { produce } from "immer";
|
||||
import { create } from "zustand";
|
||||
|
||||
import { Protobuf, Types } from "@meshtastic/meshtasticjs";
|
||||
import { channel } from "diagnostics_channel";
|
||||
|
||||
export type Page = "messages" | "map" | "config" | "channels" | "peers";
|
||||
|
||||
@@ -11,35 +12,8 @@ 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;
|
||||
@@ -55,22 +29,24 @@ export type DialogVariant =
|
||||
|
||||
export interface Device {
|
||||
id: number;
|
||||
ready: boolean;
|
||||
status: Types.DeviceStatusEnum;
|
||||
channels: Channel[];
|
||||
channels: Protobuf.Channel[];
|
||||
config: Protobuf.LocalConfig;
|
||||
moduleConfig: Protobuf.LocalModuleConfig;
|
||||
workingConfig: Protobuf.Config[];
|
||||
workingModuleConfig: Protobuf.ModuleConfig[];
|
||||
hardware: Protobuf.MyNodeInfo;
|
||||
nodes: Node[];
|
||||
nodes: Map<number, Protobuf.NodeInfo>;
|
||||
metadata: Map<number, Protobuf.DeviceMetadata>;
|
||||
messages: {
|
||||
direct: Map<number, MessageWithState[]>;
|
||||
broadcast: Map<Types.ChannelNumber, MessageWithState[]>;
|
||||
};
|
||||
connection?: Types.ConnectionType;
|
||||
activePage: Page;
|
||||
peerInfoOpen: boolean;
|
||||
activePeer: number;
|
||||
waypoints: Protobuf.Waypoint[];
|
||||
regionUnset: boolean;
|
||||
currentMetrics: Protobuf.DeviceMetrics;
|
||||
// currentMetrics: Protobuf.DeviceMetrics;
|
||||
pendingSettingsChanges: boolean;
|
||||
messageDraft: string;
|
||||
dialog: {
|
||||
@@ -81,31 +57,29 @@ export interface Device {
|
||||
deviceName: boolean;
|
||||
};
|
||||
|
||||
setReady(ready: boolean): void;
|
||||
setStatus: (status: Types.DeviceStatusEnum) => void;
|
||||
setConfig: (config: Protobuf.Config) => void;
|
||||
setModuleConfig: (config: Protobuf.ModuleConfig) => void;
|
||||
setWorkingConfig: (config: Protobuf.Config) => void;
|
||||
setWorkingModuleConfig: (config: Protobuf.ModuleConfig) => void;
|
||||
setHardware: (hardware: Protobuf.MyNodeInfo) => void;
|
||||
setMetrics: (metrics: Types.PacketMetadata<Protobuf.Telemetry>) => 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;
|
||||
addChannel: (channel: Protobuf.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;
|
||||
addMetadata: (from: number, metadata: Protobuf.DeviceMetadata) => void;
|
||||
setMessageState: (
|
||||
channelIndex: number,
|
||||
type: "direct" | "broadcast",
|
||||
channelIndex: Types.ChannelNumber,
|
||||
to: number,
|
||||
from: number,
|
||||
messageId: number,
|
||||
state: MessageState
|
||||
) => void;
|
||||
@@ -133,7 +107,6 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
produce<DeviceState>((draft) => {
|
||||
draft.devices.set(id, {
|
||||
id,
|
||||
ready: false,
|
||||
status: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
|
||||
channels: [],
|
||||
config: new Protobuf.LocalConfig(),
|
||||
@@ -141,14 +114,17 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
workingConfig: [],
|
||||
workingModuleConfig: [],
|
||||
hardware: new Protobuf.MyNodeInfo(),
|
||||
nodes: [],
|
||||
nodes: new Map(),
|
||||
metadata: new Map(),
|
||||
messages: {
|
||||
direct: new Map(),
|
||||
broadcast: new Map()
|
||||
},
|
||||
connection: undefined,
|
||||
activePage: "messages",
|
||||
peerInfoOpen: false,
|
||||
activePeer: 0,
|
||||
waypoints: [],
|
||||
regionUnset: false,
|
||||
currentMetrics: new Protobuf.DeviceMetrics(),
|
||||
// currentMetrics: new Protobuf.DeviceMetrics(),
|
||||
dialog: {
|
||||
import: false,
|
||||
QR: false,
|
||||
@@ -159,16 +135,6 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
pendingSettingsChanges: false,
|
||||
messageDraft: "",
|
||||
|
||||
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) => {
|
||||
@@ -203,9 +169,6 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
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;
|
||||
@@ -305,50 +268,50 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
})
|
||||
);
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
// 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) => {
|
||||
@@ -369,18 +332,16 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
})
|
||||
);
|
||||
},
|
||||
addChannel: (channel: Channel) => {
|
||||
addChannel: (channel: Protobuf.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
|
||||
(c) => c.index === channel.index
|
||||
);
|
||||
if (channelIndex !== -1) {
|
||||
const messages = device.channels[channelIndex].messages;
|
||||
device.channels[channelIndex] = channel;
|
||||
device.channels[channelIndex].messages = messages;
|
||||
} else {
|
||||
device.channels.push(channel);
|
||||
}
|
||||
@@ -410,29 +371,10 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
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;
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
device.nodes.set(nodeInfo.num, nodeInfo);
|
||||
})
|
||||
);
|
||||
},
|
||||
@@ -450,12 +392,13 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
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;
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const currentNode =
|
||||
device.nodes.get(user.from) ?? new Protobuf.NodeInfo();
|
||||
currentNode.user = user.data;
|
||||
device.nodes.set(user.from, currentNode);
|
||||
})
|
||||
);
|
||||
},
|
||||
@@ -463,12 +406,13 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
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;
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const currentNode =
|
||||
device.nodes.get(position.from) ?? new Protobuf.NodeInfo();
|
||||
currentNode.position = position.data;
|
||||
device.nodes.set(position.from, currentNode);
|
||||
})
|
||||
);
|
||||
},
|
||||
@@ -486,53 +430,78 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
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;
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const messageGroup = device.messages[message.type];
|
||||
const messageIndex =
|
||||
message.type === "direct"
|
||||
? message.from === device.hardware.myNodeNum
|
||||
? message.to
|
||||
: message.from
|
||||
: message.channel;
|
||||
const messages = messageGroup.get(messageIndex);
|
||||
|
||||
if (messages) {
|
||||
messages.push(message);
|
||||
messageGroup.set(messageIndex, messages);
|
||||
} else {
|
||||
messageGroup.set(messageIndex, [message]);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
addMetadata: (from, metadata) => {
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
const device = draft.devices.get(id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
device.metadata.set(from, metadata);
|
||||
})
|
||||
);
|
||||
},
|
||||
setMessageState: (
|
||||
channelIndex: number,
|
||||
type: "direct" | "broadcast",
|
||||
channelIndex: Types.ChannelNumber,
|
||||
to: number,
|
||||
from: number,
|
||||
messageId: number,
|
||||
state: MessageState
|
||||
) => {
|
||||
set(
|
||||
produce<DeviceState>((draft) => {
|
||||
console.log("setMessageState called");
|
||||
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;
|
||||
if (!device) {
|
||||
console.log("no device found for id");
|
||||
return;
|
||||
}
|
||||
const messageGroup = device.messages[type];
|
||||
|
||||
const messageIndex =
|
||||
type === "direct"
|
||||
? from === device.hardware.myNodeNum
|
||||
? to
|
||||
: from
|
||||
: channelIndex;
|
||||
const messages = messageGroup.get(messageIndex);
|
||||
|
||||
if (!messages) {
|
||||
console.log("no messages found for id");
|
||||
return;
|
||||
}
|
||||
|
||||
messageGroup.set(
|
||||
messageIndex,
|
||||
messages.map((msg) => {
|
||||
if (msg.id === messageId) {
|
||||
msg.state = state;
|
||||
}
|
||||
return msg;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
@@ -554,25 +523,22 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const node = device.nodes.find((n) => n.data.num === data.from);
|
||||
const node = device.nodes.get(data.from);
|
||||
if (!node) {
|
||||
device.nodes.push({
|
||||
data: new Protobuf.NodeInfo({
|
||||
device.nodes.set(
|
||||
data.from,
|
||||
new Protobuf.NodeInfo({
|
||||
num: data.from,
|
||||
lastHeard: data.time,
|
||||
snr: data.snr
|
||||
}),
|
||||
metadata: undefined,
|
||||
deviceMetrics: [],
|
||||
environmentMetrics: []
|
||||
});
|
||||
return;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
node.data = {
|
||||
...node.data,
|
||||
device.nodes.set(data.from, {
|
||||
...node,
|
||||
lastHeard: data.time,
|
||||
snr: data.snr
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ export const subscribeAll = (
|
||||
// onMeshHeartbeat
|
||||
|
||||
connection.events.onDeviceMetadataPacket.subscribe((metadataPacket) => {
|
||||
device.addDeviceMetadataMessage(metadataPacket);
|
||||
device.addMetadata(metadataPacket.from, metadataPacket.data);
|
||||
});
|
||||
|
||||
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||
@@ -34,27 +34,16 @@ export const subscribeAll = (
|
||||
});
|
||||
|
||||
connection.events.onTelemetryPacket.subscribe((telemetryPacket) => {
|
||||
device.setMetrics(telemetryPacket);
|
||||
// device.setMetrics(telemetryPacket);
|
||||
});
|
||||
|
||||
connection.events.onDeviceStatus.subscribe((status) => {
|
||||
device.setStatus(status);
|
||||
|
||||
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
|
||||
// device.setReady(true);
|
||||
} else if (status === Types.DeviceStatusEnum.DEVICE_DISCONNECTED) {
|
||||
device.setReady(false);
|
||||
}
|
||||
});
|
||||
|
||||
connection.events.onWaypointPacket.subscribe((waypoint) => {
|
||||
const { data, ...rest } = waypoint;
|
||||
device.addWaypoint(data);
|
||||
device.addWaypointMessage({
|
||||
waypointID: data.id,
|
||||
state: rest.from !== myNodeNum ? "ack" : "waiting",
|
||||
...rest
|
||||
});
|
||||
});
|
||||
|
||||
connection.events.onMyNodeInfo.subscribe((nodeInfo) => {
|
||||
@@ -78,11 +67,7 @@ export const subscribeAll = (
|
||||
});
|
||||
|
||||
connection.events.onChannelPacket.subscribe((channel) => {
|
||||
device.addChannel({
|
||||
config: channel,
|
||||
lastInterraction: new Date(),
|
||||
messages: []
|
||||
});
|
||||
device.addChannel(channel);
|
||||
});
|
||||
connection.events.onConfigPacket.subscribe((config) => {
|
||||
device.setConfig(config);
|
||||
|
||||
+5
-14
@@ -5,10 +5,7 @@ import { Channel } from "@components/PageComponents/Channel.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import { QrCodeIcon, ImportIcon } from "lucide-react";
|
||||
import { Protobuf, Types } from "@meshtastic/meshtasticjs";
|
||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@components/UI/Button.js";
|
||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
@@ -35,7 +32,7 @@ export const ChannelsPage = (): JSX.Element => {
|
||||
<PageLayout
|
||||
label={`Channel: ${
|
||||
channels[activeChannel]
|
||||
? getChannelName(channels[activeChannel].config)
|
||||
? getChannelName(channels[activeChannel])
|
||||
: "Loading..."
|
||||
}`}
|
||||
actions={[
|
||||
@@ -58,20 +55,14 @@ export const ChannelsPage = (): JSX.Element => {
|
||||
<Tabs defaultValue="0">
|
||||
<TabsList>
|
||||
{channels.map((channel) => (
|
||||
<TabsTrigger
|
||||
key={channel.config.index}
|
||||
value={channel.config.index.toString()}
|
||||
>
|
||||
{getChannelName(channel.config)}
|
||||
<TabsTrigger key={channel.index} value={channel.index.toString()}>
|
||||
{getChannelName(channel)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{channels.map((channel) => (
|
||||
<TabsContent
|
||||
key={channel.config.index}
|
||||
value={channel.config.index.toString()}
|
||||
>
|
||||
<Channel key={channel.config.index} channel={channel.config} />
|
||||
<TabsContent key={channel.index} value={channel.index.toString()}>
|
||||
<Channel key={channel.index} channel={channel} />
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
+15
-11
@@ -21,13 +21,17 @@ export const MapPage = (): JSX.Element => {
|
||||
const { rasterSources } = useAppStore();
|
||||
const { default: map } = useMap();
|
||||
|
||||
const allNodes = Array.from(nodes.values());
|
||||
|
||||
const getBBox = () => {
|
||||
const nodesWithPosition = nodes.filter((n) => n.data.position?.latitudeI);
|
||||
const nodesWithPosition = allNodes.filter(
|
||||
(node) => node.position?.latitudeI
|
||||
);
|
||||
if (!nodesWithPosition.length) return;
|
||||
const line = lineString(
|
||||
nodesWithPosition.map((n) => [
|
||||
(n.data.position?.latitudeI ?? 0) / 1e7,
|
||||
(n.data.position?.longitudeI ?? 0) / 1e7
|
||||
(n.position?.latitudeI ?? 0) / 1e7,
|
||||
(n.position?.longitudeI ?? 0) / 1e7
|
||||
])
|
||||
);
|
||||
const bounds = bbox(line);
|
||||
@@ -43,8 +47,8 @@ export const MapPage = (): JSX.Element => {
|
||||
map?.easeTo({
|
||||
zoom: 12,
|
||||
center: [
|
||||
(nodesWithPosition[0].data.position?.longitudeI ?? 0) / 1e7,
|
||||
(nodesWithPosition[0].data.position?.latitudeI ?? 0) / 1e7
|
||||
(nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
|
||||
(nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7
|
||||
]
|
||||
});
|
||||
};
|
||||
@@ -123,16 +127,16 @@ export const MapPage = (): JSX.Element => {
|
||||
<Layer type="raster" />
|
||||
</Source>
|
||||
))} */}
|
||||
{nodes.map((n) => {
|
||||
if (n.data.position?.latitudeI) {
|
||||
{allNodes.map((node) => {
|
||||
if (node.position?.latitudeI) {
|
||||
return (
|
||||
<Marker
|
||||
key={n.data.num}
|
||||
longitude={n.data.position.longitudeI / 1e7}
|
||||
latitude={n.data.position.latitudeI / 1e7}
|
||||
key={node.num}
|
||||
longitude={node.position.longitudeI / 1e7}
|
||||
latitude={node.position.latitudeI / 1e7}
|
||||
anchor="bottom"
|
||||
>
|
||||
<Hashicon value={n.data.num.toString()} size={32} />
|
||||
<Hashicon value={node.num.toString()} size={32} />
|
||||
</Marker>
|
||||
);
|
||||
}
|
||||
|
||||
+53
-26
@@ -6,63 +6,90 @@ import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { HashIcon } from "lucide-react";
|
||||
import { Protobuf, Types } from "@meshtastic/meshtasticjs";
|
||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { getChannelName } from "./Channels.js";
|
||||
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
|
||||
|
||||
export const MessagesPage = (): JSX.Element => {
|
||||
const { channels, nodes, hardware } = useDevice();
|
||||
const [activeChannel, setActiveChannel] = useState<Types.ChannelNumber>(
|
||||
const { channels, nodes, hardware, messages } = useDevice();
|
||||
const [chatType, setChatType] =
|
||||
useState<Types.PacketDestination>("broadcast");
|
||||
const [activeChat, setActiveChat] = useState<number>(
|
||||
Types.ChannelNumber.PRIMARY
|
||||
);
|
||||
const filteredNodes = Array.from(nodes.values()).filter(
|
||||
(n) => n.num !== hardware.myNodeNum
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar>
|
||||
<SidebarSection label="Channels">
|
||||
{channels
|
||||
.filter((ch) => ch.config.role !== Protobuf.Channel_Role.DISABLED)
|
||||
.filter((ch) => ch.role !== Protobuf.Channel_Role.DISABLED)
|
||||
.map((channel) => (
|
||||
<SidebarButton
|
||||
key={channel.config.index}
|
||||
key={channel.index}
|
||||
label={
|
||||
channel.config.settings?.name.length
|
||||
? channel.config.settings?.name
|
||||
: channel.config.index === 0
|
||||
channel.settings?.name.length
|
||||
? channel.settings?.name
|
||||
: channel.index === 0
|
||||
? "Primary"
|
||||
: `Ch ${channel.config.index}`
|
||||
: `Ch ${channel.index}`
|
||||
}
|
||||
active={channel.config.index === activeChannel}
|
||||
onClick={() => setActiveChannel(channel.config.index)}
|
||||
active={activeChat === channel.index}
|
||||
onClick={() => {
|
||||
setChatType("broadcast");
|
||||
setActiveChat(channel.index);
|
||||
}}
|
||||
element={<HashIcon size={16} className="mr-2" />}
|
||||
/>
|
||||
))}
|
||||
</SidebarSection>
|
||||
<SidebarSection label="Peers">
|
||||
{nodes
|
||||
.filter((n) => n.data.num !== hardware.myNodeNum)
|
||||
.map((node) => (
|
||||
<SidebarButton
|
||||
key={node.data.num}
|
||||
label={node.data.user?.longName ?? "Unknown"}
|
||||
element={
|
||||
<Hashicon size={20} value={node.data.num.toString()} />
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{filteredNodes.map((node) => (
|
||||
<SidebarButton
|
||||
key={node.num}
|
||||
label={node.user?.longName ?? "Unknown"}
|
||||
active={activeChat === node.num}
|
||||
onClick={() => {
|
||||
setChatType("direct");
|
||||
setActiveChat(node.num);
|
||||
}}
|
||||
element={<Hashicon size={20} value={node.num.toString()} />}
|
||||
/>
|
||||
))}
|
||||
</SidebarSection>
|
||||
</Sidebar>
|
||||
<PageLayout
|
||||
label={`Messages: ${
|
||||
channels[activeChannel]
|
||||
? getChannelName(channels[activeChannel].config)
|
||||
chatType === "broadcast" && channels[activeChat]
|
||||
? getChannelName(channels[activeChat])
|
||||
: chatType === "direct" && nodes.get(activeChat)
|
||||
? nodes.get(activeChat)?.user?.longName ?? "Unknown"
|
||||
: "Loading..."
|
||||
}`}
|
||||
>
|
||||
{channels.map(
|
||||
(channel) =>
|
||||
channel.config.index === activeChannel && (
|
||||
<ChannelChat key={channel.config.index} channel={channel} />
|
||||
activeChat === channel.index && (
|
||||
<ChannelChat
|
||||
key={channel.index}
|
||||
to="broadcast"
|
||||
messages={messages.broadcast.get(channel.index)}
|
||||
channel={channel.index}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{filteredNodes.map(
|
||||
(node) =>
|
||||
activeChat === node.num && (
|
||||
<ChannelChat
|
||||
key={node.num}
|
||||
to={activeChat}
|
||||
messages={messages.direct.get(node.num)}
|
||||
channel={Types.ChannelNumber.PRIMARY}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</PageLayout>
|
||||
|
||||
+17
-13
@@ -8,7 +8,11 @@ import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||
import { Sidebar } from "@components/Sidebar.js";
|
||||
|
||||
export const PeersPage = (): JSX.Element => {
|
||||
const { connection, nodes } = useDevice();
|
||||
const { nodes, hardware } = useDevice();
|
||||
|
||||
const filteredNodes = Array.from(nodes.values()).filter(
|
||||
(n) => n.num !== hardware.myNodeNum
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -23,32 +27,32 @@ export const PeersPage = (): JSX.Element => {
|
||||
{ title: "Last Heard", type: "normal", sortable: true },
|
||||
{ title: "SNR", type: "normal", sortable: true }
|
||||
]}
|
||||
rows={nodes.map((node) => [
|
||||
<Hashicon size={24} value={node.data.num.toString()} />,
|
||||
rows={filteredNodes.map((node) => [
|
||||
<Hashicon size={24} value={node.num.toString()} />,
|
||||
<h1>
|
||||
{node.data.user?.longName ?? node.data.user?.macaddr
|
||||
{node.user?.longName ?? node.user?.macaddr
|
||||
? `Meshtastic_${base16
|
||||
.stringify(node.data.user?.macaddr.subarray(4, 6) ?? [])
|
||||
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
||||
.toLowerCase()}`
|
||||
: `UNK: ${node.data.num}`}
|
||||
: `UNK: ${node.num}`}
|
||||
</h1>,
|
||||
|
||||
<Mono>{Protobuf.HardwareModel[node.data.user?.hwModel ?? 0]}</Mono>,
|
||||
<Mono>{Protobuf.HardwareModel[node.user?.hwModel ?? 0]}</Mono>,
|
||||
<Mono>
|
||||
{base16
|
||||
.stringify(node.data.user?.macaddr ?? [])
|
||||
.stringify(node.user?.macaddr ?? [])
|
||||
.match(/.{1,2}/g)
|
||||
?.join(":") ?? "UNK"}
|
||||
</Mono>,
|
||||
node.data.lastHeard === 0 ? (
|
||||
node.lastHeard === 0 ? (
|
||||
<p>Never</p>
|
||||
) : (
|
||||
<TimeAgo timestamp={node.data.lastHeard * 1000} />
|
||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||
),
|
||||
<Mono>
|
||||
{node.data.snr}db/
|
||||
{Math.min(Math.max((node.data.snr + 10) * 5, 0), 100)}%/
|
||||
{(node.data.snr + 10) * 5}raw
|
||||
{node.snr}db/
|
||||
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
||||
{(node.snr + 10) * 5}raw
|
||||
</Mono>
|
||||
])}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user