mirror of
https://github.com/dpup/meshstream.git
synced 2026-07-07 02:11:20 +02:00
Fix types and render packets in a custom style
This commit is contained in:
@@ -1,36 +1,221 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { useAppSelector } from "../hooks";
|
import { useAppSelector, useAppDispatch } from "../hooks";
|
||||||
|
import { PacketRenderer } from "./packets/PacketRenderer";
|
||||||
|
import { StreamControl } from "./StreamControl";
|
||||||
|
import { Trash2, RefreshCw, Archive } from "lucide-react";
|
||||||
|
import { clearPackets, toggleStreamPause } from "../store/slices/packetSlice";
|
||||||
|
import { Packet } from "../lib/types";
|
||||||
|
|
||||||
|
// Number of packets to show per page
|
||||||
|
const PACKETS_PER_PAGE = 10;
|
||||||
|
|
||||||
export const PacketList: React.FC = () => {
|
export const PacketList: React.FC = () => {
|
||||||
const { packets, loading, error } = useAppSelector((state) => state.packets);
|
const { packets, bufferedPackets, loading, error, streamPaused } = useAppSelector(
|
||||||
|
(state) => state.packets
|
||||||
|
);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
// A unique ID is attached to each rendered packet element
|
||||||
|
const [packetKeys, setPacketKeys] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// Generate a reproducible hash code for a string
|
||||||
|
const hashString = useCallback((str: string): string => {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
// Make sure hash is always positive and convert to string
|
||||||
|
return Math.abs(hash).toString(36);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Create a consistent, unique fingerprint for a packet
|
||||||
|
const createPacketFingerprint = useCallback((packet: Packet): string => {
|
||||||
|
// Combine multiple fields to maximize uniqueness
|
||||||
|
const parts = [
|
||||||
|
packet.data.from?.toString() ?? 'unknown',
|
||||||
|
packet.data.id?.toString() ?? 'noid',
|
||||||
|
packet.data.portNum?.toString() ?? 'noport',
|
||||||
|
packet.info.channel ?? 'nochannel',
|
||||||
|
packet.info.userId ?? 'nouser',
|
||||||
|
// Include a hash of raw JSON data for extra uniqueness
|
||||||
|
hashString(JSON.stringify(packet)),
|
||||||
|
];
|
||||||
|
return parts.join('_');
|
||||||
|
}, [hashString]);
|
||||||
|
|
||||||
|
// Update the packet keys whenever the packets change
|
||||||
|
useEffect(() => {
|
||||||
|
// Store new packet keys
|
||||||
|
const newKeys: Record<string, string> = {...packetKeys};
|
||||||
|
|
||||||
|
// Assign keys to any packets that don't already have them
|
||||||
|
packets.forEach((packet) => {
|
||||||
|
const fingerprint = createPacketFingerprint(packet);
|
||||||
|
|
||||||
|
// Only create new keys for packets we haven't seen before
|
||||||
|
if (!newKeys[fingerprint]) {
|
||||||
|
const randomPart = Math.random().toString(36).substring(2, 10);
|
||||||
|
newKeys[fingerprint] = `${fingerprint}_${randomPart}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only update state if we actually added new keys
|
||||||
|
if (Object.keys(newKeys).length !== Object.keys(packetKeys).length) {
|
||||||
|
setPacketKeys(newKeys);
|
||||||
|
}
|
||||||
|
}, [packets, createPacketFingerprint]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="p-4 text-neutral-300">Loading...</div>;
|
return (
|
||||||
|
<div className="p-4 flex items-center justify-center h-40 text-neutral-300">
|
||||||
|
<RefreshCw className="h-5 w-5 mr-2 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div className="p-4 text-red-400">Error: {error}</div>;
|
return (
|
||||||
|
<div className="p-4 text-red-400 border border-red-800 rounded bg-neutral-900">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packets.length === 0) {
|
if (packets.length === 0 && bufferedPackets.length === 0) {
|
||||||
return <div className="p-4 text-neutral-400">No packets received yet</div>;
|
return (
|
||||||
|
<div className="p-6 text-neutral-400 text-center border border-neutral-700 rounded bg-neutral-900">
|
||||||
|
No packets received yet
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate pagination
|
||||||
|
const totalPages = Math.ceil(packets.length / PACKETS_PER_PAGE);
|
||||||
|
const startIndex = (currentPage - 1) * PACKETS_PER_PAGE;
|
||||||
|
const endIndex = startIndex + PACKETS_PER_PAGE;
|
||||||
|
const currentPackets = packets.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const handleClearPackets = () => {
|
||||||
|
if (window.confirm("Are you sure you want to clear all packets?")) {
|
||||||
|
dispatch(clearPackets());
|
||||||
|
setCurrentPage(1);
|
||||||
|
// Clear the packet keys too
|
||||||
|
setPacketKeys({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleStream = () => {
|
||||||
|
dispatch(toggleStreamPause());
|
||||||
|
// When unpausing with buffered packets, reset to first page to see new content
|
||||||
|
if (streamPaused && bufferedPackets.length > 0) {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the key for a packet
|
||||||
|
const getPacketKey = (packet: Packet, index: number): string => {
|
||||||
|
const fingerprint = createPacketFingerprint(packet);
|
||||||
|
// Fallback to index-based key if no fingerprint key exists
|
||||||
|
return packetKeys[fingerprint] || `packet_${index}_${Date.now()}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div>
|
||||||
<h2 className="text-xl font-bold mb-4 text-neutral-200">
|
<div className="flex justify-between items-center mb-4">
|
||||||
Received Packets
|
<h2 className="text-xl font-normal text-neutral-200">
|
||||||
</h2>
|
Packets{" "}
|
||||||
<ul className="space-y-2">
|
<span className="text-sm text-neutral-400">
|
||||||
{packets.map((packet) => (
|
({packets.length} total)
|
||||||
<li
|
</span>
|
||||||
key={packet.id}
|
</h2>
|
||||||
className="p-3 border border-neutral-700 rounded bg-neutral-900 shadow-inner text-neutral-300 hover:bg-neutral-800 transition-colors"
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{/* Show buffered count when paused */}
|
||||||
|
{streamPaused && bufferedPackets.length > 0 && (
|
||||||
|
<div className="flex items-center text-sm text-amber-400">
|
||||||
|
<Archive className="h-4 w-4 mr-1.5" />
|
||||||
|
{bufferedPackets.length} new
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stream control toggle */}
|
||||||
|
<StreamControl
|
||||||
|
isPaused={streamPaused}
|
||||||
|
onToggle={handleToggleStream}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Clear button */}
|
||||||
|
<button
|
||||||
|
onClick={handleClearPackets}
|
||||||
|
className="flex items-center px-3 py-1.5 text-sm bg-neutral-700 hover:bg-neutral-600 rounded transition-colors text-neutral-200"
|
||||||
>
|
>
|
||||||
{packet.id}
|
<Trash2 className="h-4 w-4 mr-1.5" />
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{currentPackets.map((packet, index) => (
|
||||||
|
<li key={getPacketKey(packet, index)}>
|
||||||
|
<PacketRenderer packet={packet} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{/* Empty state when no packets are visible but stream is paused */}
|
||||||
|
{packets.length === 0 && streamPaused && bufferedPackets.length > 0 && (
|
||||||
|
<div className="p-6 text-amber-400 text-center border border-amber-900 bg-neutral-800 rounded my-4">
|
||||||
|
Stream is paused with {bufferedPackets.length} buffered messages.
|
||||||
|
<button
|
||||||
|
onClick={handleToggleStream}
|
||||||
|
className="block mx-auto mt-2 px-3 py-1.5 text-sm bg-neutral-700 hover:bg-neutral-600 rounded transition-colors"
|
||||||
|
>
|
||||||
|
Resume to view
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex justify-between items-center mt-6 text-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(currentPage > 1 ? currentPage - 1 : 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className={`px-3 py-1.5 rounded ${
|
||||||
|
currentPage === 1
|
||||||
|
? "text-neutral-500 cursor-not-allowed"
|
||||||
|
: "bg-neutral-700 text-neutral-200 hover:bg-neutral-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-neutral-400">
|
||||||
|
Page {currentPage} of {totalPages}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage(
|
||||||
|
currentPage < totalPages ? currentPage + 1 : totalPages
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className={`px-3 py-1.5 rounded ${
|
||||||
|
currentPage === totalPages
|
||||||
|
? "text-neutral-500 cursor-not-allowed"
|
||||||
|
: "bg-neutral-700 text-neutral-200 hover:bg-neutral-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Play, Pause } from "lucide-react";
|
||||||
|
|
||||||
|
interface StreamControlProps {
|
||||||
|
isPaused: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StreamControl: React.FC<StreamControlProps> = ({
|
||||||
|
isPaused,
|
||||||
|
onToggle,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className={`flex items-center space-x-2 px-3 py-1.5 rounded-md transition-colors ${
|
||||||
|
isPaused
|
||||||
|
? "bg-neutral-700 text-amber-400 hover:bg-neutral-600"
|
||||||
|
: "bg-neutral-700 text-green-400 hover:bg-neutral-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{isPaused ? "Paused" : "Streaming"}
|
||||||
|
</span>
|
||||||
|
{isPaused ? (
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,4 +5,5 @@ export * from './Filter';
|
|||||||
export * from './InfoMessage';
|
export * from './InfoMessage';
|
||||||
export * from './ConnectionStatus';
|
export * from './ConnectionStatus';
|
||||||
export * from './Nav';
|
export * from './Nav';
|
||||||
export * from './Separator';
|
export * from './Separator';
|
||||||
|
export * from './StreamControl';
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Packet } from "../../lib/types";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
|
||||||
|
interface ErrorPacketProps {
|
||||||
|
packet: Packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorPacket: React.FC<ErrorPacketProps> = ({ packet }) => {
|
||||||
|
const { data } = packet;
|
||||||
|
|
||||||
|
if (!data.decodeError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-red-900 rounded bg-neutral-800 shadow-inner">
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="rounded-md bg-red-500 p-1.5 mr-3">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-neutral-100" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-neutral-200">
|
||||||
|
Decode Error
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-neutral-400 text-sm">
|
||||||
|
ID: {data.id || "No ID"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-red-400 pl-9">
|
||||||
|
{data.decodeError}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex justify-between items-center text-xs text-neutral-500">
|
||||||
|
<span>Channel: {packet.info.channel}</span>
|
||||||
|
<span>Error</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Packet, PortNum, PortNumByName } from "../../lib/types";
|
||||||
|
import { Package } from "lucide-react";
|
||||||
|
|
||||||
|
interface GenericPacketProps {
|
||||||
|
packet: Packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GenericPacket: React.FC<GenericPacketProps> = ({ packet }) => {
|
||||||
|
const { data } = packet;
|
||||||
|
|
||||||
|
// Helper to get port name from enum
|
||||||
|
const getPortName = (portNum: PortNum | string): string => {
|
||||||
|
if (typeof portNum === 'string') {
|
||||||
|
return portNum;
|
||||||
|
}
|
||||||
|
return PortNum[portNum] || "Unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to get simple payload description
|
||||||
|
const getPayloadDescription = () => {
|
||||||
|
if (data.decodeError) {
|
||||||
|
return `Error: ${data.decodeError}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine what type of payload is present
|
||||||
|
if (data.binaryData) return "Binary data";
|
||||||
|
if (data.waypoint) return "Waypoint";
|
||||||
|
if (data.compressedText) return "Compressed text";
|
||||||
|
if (data.mapReport) return "Map report";
|
||||||
|
if (data.remoteHardware) return "Remote hardware";
|
||||||
|
if (data.routing) return "Routing";
|
||||||
|
if (data.admin) return "Admin";
|
||||||
|
if (data.audioData) return "Audio";
|
||||||
|
if (data.alert) return `Alert: ${data.alert}`;
|
||||||
|
if (data.reply) return `Reply: ${data.reply}`;
|
||||||
|
|
||||||
|
return "Unknown data";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="rounded-md bg-neutral-500 p-1.5 mr-3">
|
||||||
|
<Package className="h-4 w-4 text-neutral-100" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-neutral-200">
|
||||||
|
From: {data.from || "Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-neutral-400 text-sm">
|
||||||
|
ID: {data.id || "No ID"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-neutral-300 pl-9">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Port</div>
|
||||||
|
<div>{getPortName(data.portNum)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Payload</div>
|
||||||
|
<div>{getPayloadDescription()}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">To</div>
|
||||||
|
<div>{data.to || "Broadcast"}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Hop Limit</div>
|
||||||
|
<div>{data.hopLimit}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex justify-between items-center text-xs text-neutral-500">
|
||||||
|
<span>Channel: {packet.info.channel}</span>
|
||||||
|
<span>{getPortName(data.portNum)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Packet } from "../../lib/types";
|
||||||
|
import { User } from "lucide-react";
|
||||||
|
|
||||||
|
interface NodeInfoPacketProps {
|
||||||
|
packet: Packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeInfoPacket: React.FC<NodeInfoPacketProps> = ({ packet }) => {
|
||||||
|
const { data } = packet;
|
||||||
|
const nodeInfo = data.nodeInfo;
|
||||||
|
|
||||||
|
if (!nodeInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="rounded-md bg-purple-500 p-1.5 mr-3">
|
||||||
|
<User className="h-4 w-4 text-neutral-100" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-neutral-200">
|
||||||
|
From: {data.from || "Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-neutral-400 text-sm">
|
||||||
|
ID: {data.id || "No ID"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-neutral-300 pl-9">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Long Name</div>
|
||||||
|
<div>{nodeInfo.longName || "—"}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Short Name</div>
|
||||||
|
<div>{nodeInfo.shortName || "—"}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">ID</div>
|
||||||
|
<div className="font-mono">{nodeInfo.id || "—"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex justify-between items-center text-xs text-neutral-500">
|
||||||
|
<span>Channel: {packet.info.channel}</span>
|
||||||
|
<span>Node Info</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Packet, PortNum, PortNumByName } from "../../lib/types";
|
||||||
|
import { TextMessagePacket } from "./TextMessagePacket";
|
||||||
|
import { PositionPacket } from "./PositionPacket";
|
||||||
|
import { NodeInfoPacket } from "./NodeInfoPacket";
|
||||||
|
import { TelemetryPacket } from "./TelemetryPacket";
|
||||||
|
import { ErrorPacket } from "./ErrorPacket";
|
||||||
|
import { GenericPacket } from "./GenericPacket";
|
||||||
|
|
||||||
|
interface PacketRendererProps {
|
||||||
|
packet: Packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PacketRenderer: React.FC<PacketRendererProps> = ({ packet }) => {
|
||||||
|
const { data } = packet;
|
||||||
|
|
||||||
|
// If there's a decode error, show the error packet
|
||||||
|
if (data.decodeError) {
|
||||||
|
return <ErrorPacket packet={packet} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the PortNum enum value
|
||||||
|
const portNumValue = typeof data.portNum === 'string'
|
||||||
|
? PortNumByName[data.portNum]
|
||||||
|
: data.portNum;
|
||||||
|
|
||||||
|
// Determine which component to use based on portNum
|
||||||
|
switch (portNumValue) {
|
||||||
|
case PortNum.TEXT_MESSAGE_APP:
|
||||||
|
return <TextMessagePacket packet={packet} />;
|
||||||
|
|
||||||
|
case PortNum.POSITION_APP:
|
||||||
|
return <PositionPacket packet={packet} />;
|
||||||
|
|
||||||
|
case PortNum.NODEINFO_APP:
|
||||||
|
return <NodeInfoPacket packet={packet} />;
|
||||||
|
|
||||||
|
case PortNum.TELEMETRY_APP:
|
||||||
|
return <TelemetryPacket packet={packet} />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <GenericPacket packet={packet} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Packet } from "../../lib/types";
|
||||||
|
import { MapPin } from "lucide-react";
|
||||||
|
|
||||||
|
interface PositionPacketProps {
|
||||||
|
packet: Packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PositionPacket: React.FC<PositionPacketProps> = ({ packet }) => {
|
||||||
|
const { data } = packet;
|
||||||
|
const position = data.position;
|
||||||
|
|
||||||
|
if (!position) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert latitudeI and longitudeI to degrees (multiply by 1e-7)
|
||||||
|
const latitude = position.latitudeI ? position.latitudeI * 1e-7 : undefined;
|
||||||
|
const longitude = position.longitudeI ? position.longitudeI * 1e-7 : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="rounded-md bg-emerald-500 p-1.5 mr-3">
|
||||||
|
<MapPin className="h-4 w-4 text-neutral-100" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-neutral-200">
|
||||||
|
From: {data.from || "Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-neutral-400 text-sm">
|
||||||
|
ID: {data.id || "No ID"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-neutral-300 pl-9">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Latitude</div>
|
||||||
|
<div>{latitude !== undefined ? latitude.toFixed(6) : 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Longitude</div>
|
||||||
|
<div>{longitude !== undefined ? longitude.toFixed(6) : 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
{position.altitude && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Altitude</div>
|
||||||
|
<div>{position.altitude.toFixed(1)}m</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{position.time && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Time</div>
|
||||||
|
<div>{new Date(position.time * 1000).toLocaleTimeString()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{position.locationSource && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Source</div>
|
||||||
|
<div>{position.locationSource.replace('LOC_', '')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{position.satsInView && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Satellites</div>
|
||||||
|
<div>{position.satsInView}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex justify-between items-center text-xs text-neutral-500">
|
||||||
|
<span>Channel: {packet.info.channel}</span>
|
||||||
|
<span>Position</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Packet } from "../../lib/types";
|
||||||
|
import { BarChart } from "lucide-react";
|
||||||
|
|
||||||
|
interface TelemetryPacketProps {
|
||||||
|
packet: Packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TelemetryPacket: React.FC<TelemetryPacketProps> = ({ packet }) => {
|
||||||
|
const { data } = packet;
|
||||||
|
const telemetry = data.telemetry;
|
||||||
|
|
||||||
|
if (!telemetry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to display telemetry fields
|
||||||
|
const renderTelemetryFields = () => {
|
||||||
|
const entries = Object.entries(telemetry).filter(([key]) => key !== 'time');
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return <div>No telemetry data available</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{entries.map(([key, value]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<div className="text-xs text-neutral-400">{key.charAt(0).toUpperCase() + key.slice(1)}</div>
|
||||||
|
<div>{typeof value === 'number' ? value.toFixed(2) : String(value)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{telemetry.time && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<div className="text-xs text-neutral-400">Time</div>
|
||||||
|
<div>{new Date(telemetry.time * 1000).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="rounded-md bg-amber-500 p-1.5 mr-3">
|
||||||
|
<BarChart className="h-4 w-4 text-neutral-100" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-neutral-200">
|
||||||
|
From: {data.from || "Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-neutral-400 text-sm">
|
||||||
|
ID: {data.id || "No ID"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-neutral-300 pl-9">
|
||||||
|
{renderTelemetryFields()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex justify-between items-center text-xs text-neutral-500">
|
||||||
|
<span>Channel: {packet.info.channel}</span>
|
||||||
|
<span>Telemetry</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Packet } from "../../lib/types";
|
||||||
|
import { MessageSquareText } from "lucide-react";
|
||||||
|
|
||||||
|
interface TextMessagePacketProps {
|
||||||
|
packet: Packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextMessagePacket: React.FC<TextMessagePacketProps> = ({ packet }) => {
|
||||||
|
const { data } = packet;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="rounded-md bg-blue-500 p-1.5 mr-3">
|
||||||
|
<MessageSquareText className="h-4 w-4 text-neutral-100" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-neutral-200">
|
||||||
|
From: {data.from || "Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-neutral-400 text-sm">
|
||||||
|
ID: {data.id || "No ID"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-neutral-300 pl-9">
|
||||||
|
{data.textMessage || "Empty message"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex justify-between items-center text-xs text-neutral-500">
|
||||||
|
<span>Channel: {packet.info.channel}</span>
|
||||||
|
<span>Text Message</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './TextMessagePacket';
|
||||||
|
export * from './PositionPacket';
|
||||||
|
export * from './NodeInfoPacket';
|
||||||
|
export * from './TelemetryPacket';
|
||||||
|
export * from './GenericPacket';
|
||||||
|
export * from './ErrorPacket';
|
||||||
|
export * from './PacketRenderer';
|
||||||
+201
-59
@@ -35,135 +35,277 @@ export enum PortNum {
|
|||||||
RETICULUM_TUNNEL_APP = 102,
|
RETICULUM_TUNNEL_APP = 102,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map of PortNum string names to numeric enum values
|
||||||
|
export const PortNumByName: Record<string, PortNum> = {
|
||||||
|
"UNKNOWN_APP": PortNum.UNKNOWN_APP,
|
||||||
|
"TEXT_MESSAGE_APP": PortNum.TEXT_MESSAGE_APP,
|
||||||
|
"REMOTE_HARDWARE_APP": PortNum.REMOTE_HARDWARE_APP,
|
||||||
|
"POSITION_APP": PortNum.POSITION_APP,
|
||||||
|
"NODEINFO_APP": PortNum.NODEINFO_APP,
|
||||||
|
"ROUTING_APP": PortNum.ROUTING_APP,
|
||||||
|
"ADMIN_APP": PortNum.ADMIN_APP,
|
||||||
|
"TEXT_MESSAGE_COMPRESSED_APP": PortNum.TEXT_MESSAGE_COMPRESSED_APP,
|
||||||
|
"WAYPOINT_APP": PortNum.WAYPOINT_APP,
|
||||||
|
"AUDIO_APP": PortNum.AUDIO_APP,
|
||||||
|
"REPLY_APP": PortNum.REPLY_APP,
|
||||||
|
"IP_TUNNEL_APP": PortNum.IP_TUNNEL_APP,
|
||||||
|
"SERIAL_APP": PortNum.SERIAL_APP,
|
||||||
|
"STORE_FORWARD_APP": PortNum.STORE_FORWARD_APP,
|
||||||
|
"RANGE_TEST_APP": PortNum.RANGE_TEST_APP,
|
||||||
|
"TELEMETRY_APP": PortNum.TELEMETRY_APP,
|
||||||
|
"PRIVATE_APP": PortNum.PRIVATE_APP,
|
||||||
|
"DETECTION_SENSOR_APP": PortNum.DETECTION_SENSOR_APP,
|
||||||
|
"ZPS_APP": PortNum.ZPS_APP,
|
||||||
|
"SIMULATOR_APP": PortNum.SIMULATOR_APP,
|
||||||
|
"TRACEROUTE_APP": PortNum.TRACEROUTE_APP,
|
||||||
|
"NEIGHBORINFO_APP": PortNum.NEIGHBORINFO_APP,
|
||||||
|
"PAXCOUNTER_APP": PortNum.PAXCOUNTER_APP,
|
||||||
|
"MAP_REPORT_APP": PortNum.MAP_REPORT_APP,
|
||||||
|
"ALERT_APP": PortNum.ALERT_APP,
|
||||||
|
"ATAK_PLUGIN": PortNum.ATAK_PLUGIN,
|
||||||
|
"POWERSTRESS_APP": PortNum.POWERSTRESS_APP,
|
||||||
|
"RETICULUM_TUNNEL_APP": PortNum.RETICULUM_TUNNEL_APP,
|
||||||
|
}
|
||||||
|
|
||||||
// Different payload types based on the Meshtastic protocol
|
// Different payload types based on the Meshtastic protocol
|
||||||
export interface Position {
|
export interface Position {
|
||||||
latitude: number;
|
latitudeI?: number; // multiply by 1e-7 to get degrees
|
||||||
longitude: number;
|
longitudeI?: number; // multiply by 1e-7 to get degrees
|
||||||
altitude: number;
|
altitude?: number; // in meters above MSL
|
||||||
time: number;
|
time: number; // seconds since 1970
|
||||||
[key: string]: any; // For additional properties
|
locationSource?: string; // "LOC_UNSET", "LOC_MANUAL", "LOC_INTERNAL", "LOC_EXTERNAL"
|
||||||
|
altitudeSource?: string; // "ALT_UNSET", "ALT_MANUAL", "ALT_INTERNAL", "ALT_EXTERNAL", "ALT_BAROMETRIC"
|
||||||
|
timestamp?: number; // positional timestamp in integer epoch seconds
|
||||||
|
timestampMillisAdjust?: number;
|
||||||
|
altitudeHae?: number; // HAE altitude in meters
|
||||||
|
altitudeGeoSeparation?: number;
|
||||||
|
pdop?: number; // Position Dilution of Precision, in 1/100 units
|
||||||
|
hdop?: number; // Horizontal Dilution of Precision
|
||||||
|
vdop?: number; // Vertical Dilution of Precision
|
||||||
|
gpsAccuracy?: number; // GPS accuracy in mm
|
||||||
|
groundSpeed?: number; // in m/s
|
||||||
|
groundTrack?: number; // True North track in 1/100 degrees
|
||||||
|
fixQuality?: number; // GPS fix quality
|
||||||
|
fixType?: number; // GPS fix type 2D/3D
|
||||||
|
satsInView?: number; // Satellites in view
|
||||||
|
sensorId?: number; // For multiple positioning sensors
|
||||||
|
nextUpdate?: number; // Estimated time until next update in seconds
|
||||||
|
seqNumber?: number; // Sequence number for this packet
|
||||||
|
precisionBits?: number; // Bits of precision set by the sending node
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string; // Globally unique ID for this user
|
||||||
longName: string;
|
longName?: string; // Full name for this user
|
||||||
shortName: string;
|
shortName?: string; // Very short name, ideally two characters
|
||||||
[key: string]: any; // For additional properties
|
macaddr?: string; // MAC address of the device
|
||||||
|
hwModel?: string; // Hardware model name
|
||||||
|
hasGps?: boolean; // Whether the node has GPS capability
|
||||||
|
role?: string; // User's role in the mesh (e.g., "ROUTER")
|
||||||
|
snr?: number; // Signal-to-noise ratio
|
||||||
|
batteryLevel?: number; // Battery level 0-100
|
||||||
|
voltage?: number; // Battery voltage
|
||||||
|
channelUtilization?: number; // Channel utilization percentage
|
||||||
|
airUtilTx?: number; // Air utilization for transmission
|
||||||
|
lastHeard?: number; // Last time the node was heard from
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device metrics for telemetry
|
||||||
|
export interface DeviceMetrics {
|
||||||
|
batteryLevel?: number; // 0-100 (>100 means powered)
|
||||||
|
voltage?: number; // Voltage measured
|
||||||
|
channelUtilization?: number;
|
||||||
|
airUtilTx?: number; // Percent of airtime for transmission used within the last hour
|
||||||
|
uptimeSeconds?: number; // How long the device has been running since last reboot (in seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment metrics for telemetry
|
||||||
|
export interface EnvironmentMetrics {
|
||||||
|
temperature?: number; // Temperature measured
|
||||||
|
relativeHumidity?: number; // Relative humidity percent measured
|
||||||
|
barometricPressure?: number; // Barometric pressure in hPA
|
||||||
|
gasResistance?: number; // Gas resistance in MOhm
|
||||||
|
voltage?: number; // Voltage measured (To be deprecated)
|
||||||
|
current?: number; // Current measured (To be deprecated)
|
||||||
|
iaq?: number; // IAQ value (0-500)
|
||||||
|
distance?: number; // Distance in mm (water level detection)
|
||||||
|
lux?: number; // Ambient light (Lux)
|
||||||
|
whiteLux?: number; // White light (irradiance)
|
||||||
|
irLux?: number; // Infrared lux
|
||||||
|
uvLux?: number; // Ultraviolet lux
|
||||||
|
windDirection?: number; // Wind direction in degrees (0 = North, 90 = East)
|
||||||
|
windSpeed?: number; // Wind speed in m/s
|
||||||
|
weight?: number; // Weight in KG
|
||||||
|
windGust?: number; // Wind gust in m/s
|
||||||
|
windLull?: number; // Wind lull in m/s
|
||||||
|
radiation?: number; // Radiation in µR/h
|
||||||
|
rainfall1h?: number; // Rainfall in the last hour in mm
|
||||||
|
rainfall24h?: number; // Rainfall in the last 24 hours in mm
|
||||||
|
soilMoisture?: number; // Soil moisture measured (% 1-100)
|
||||||
|
soilTemperature?: number; // Soil temperature measured (°C)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main telemetry interface
|
||||||
export interface Telemetry {
|
export interface Telemetry {
|
||||||
time: number;
|
time: number; // seconds since 1970
|
||||||
[key: string]: any; // For other telemetry fields
|
deviceMetrics?: DeviceMetrics;
|
||||||
|
environmentMetrics?: EnvironmentMetrics;
|
||||||
|
// Other telemetry types could be added as needed (PowerMetrics, AirQualityMetrics, etc.)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Waypoint {
|
export interface Waypoint {
|
||||||
id: number;
|
id: number; // ID of the waypoint
|
||||||
name: string;
|
latitudeI?: number; // multiply by 1e-7 to get degrees
|
||||||
description: string;
|
longitudeI?: number; // multiply by 1e-7 to get degrees
|
||||||
latitude: number;
|
expire?: number; // Time the waypoint is to expire (epoch)
|
||||||
longitude: number;
|
lockedTo?: number; // If greater than zero, nodenum allowed to update the waypoint
|
||||||
[key: string]: any; // For additional properties
|
name?: string; // Name of the waypoint - max 30 chars
|
||||||
|
description?: string; // Description of the waypoint - max 100 chars
|
||||||
|
icon?: number; // Designator icon for the waypoint (unicode emoji)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteDiscovery {
|
export interface RouteDiscovery {
|
||||||
[key: string]: any;
|
route?: number[]; // The list of nodenums this packet has visited
|
||||||
|
snrTowards?: number[]; // The list of SNRs (in dB, scaled by 4) in the route towards destination
|
||||||
|
routeBack?: number[]; // The list of nodenums the packet has visited on the way back
|
||||||
|
snrBack?: number[]; // The list of SNRs (in dB, scaled by 4) in the route back
|
||||||
|
}
|
||||||
|
|
||||||
|
// A neighbor in the mesh
|
||||||
|
export interface Neighbor {
|
||||||
|
nodeId: number; // Node ID of neighbor
|
||||||
|
snr: number; // SNR of last heard message
|
||||||
|
lastRxTime?: number; // Reception time of last message
|
||||||
|
nodeBroadcastIntervalSecs?: number; // Broadcast interval of this neighbor
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NeighborInfo {
|
export interface NeighborInfo {
|
||||||
[key: string]: any;
|
nodeId: number; // The node ID of the node sending info on its neighbors
|
||||||
|
lastSentById?: number; // Field to pass neighbor info for the next sending cycle
|
||||||
|
nodeBroadcastIntervalSecs?: number; // Broadcast interval of the represented node
|
||||||
|
neighbors?: Neighbor[]; // The list of out edges from this node
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MapReport {
|
export interface MapReport {
|
||||||
[key: string]: any;
|
// This would need to be defined based on the actual data structure
|
||||||
|
// Currently not defined in the proto files
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HardwareMessage {
|
export interface HardwareMessage {
|
||||||
[key: string]: any;
|
// Based on remote_hardware.proto but would need to be defined
|
||||||
|
// Currently not detailed enough in the proto files to model completely
|
||||||
|
type?: number;
|
||||||
|
gpioMask?: number;
|
||||||
|
gpioValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routing error enum
|
||||||
|
export enum RoutingError {
|
||||||
|
NONE = 0,
|
||||||
|
NO_ROUTE = 1,
|
||||||
|
GOT_NAK = 2,
|
||||||
|
TIMEOUT = 3,
|
||||||
|
NO_INTERFACE = 4,
|
||||||
|
MAX_RETRANSMIT = 5,
|
||||||
|
NO_CHANNEL = 6,
|
||||||
|
TOO_LARGE = 7,
|
||||||
|
NO_RESPONSE = 8,
|
||||||
|
DUTY_CYCLE_LIMIT = 9,
|
||||||
|
BAD_REQUEST = 32,
|
||||||
|
NOT_AUTHORIZED = 33,
|
||||||
|
PKI_FAILED = 34,
|
||||||
|
PKI_UNKNOWN_PUBKEY = 35,
|
||||||
|
ADMIN_BAD_SESSION_KEY = 36,
|
||||||
|
ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Routing {
|
export interface Routing {
|
||||||
[key: string]: any;
|
routeRequest?: RouteDiscovery; // A route request going from the requester
|
||||||
|
routeReply?: RouteDiscovery; // A route reply
|
||||||
|
errorReason?: RoutingError; // A failure in delivering a message
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminMessage {
|
export interface AdminMessage {
|
||||||
[key: string]: any;
|
// This would need to be defined based on the admin.proto
|
||||||
|
// Only include what's actually used in your application
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Paxcount {
|
export interface Paxcount {
|
||||||
[key: string]: any;
|
// This would need to be defined based on the paxcount.proto
|
||||||
|
// Only include what's actually used in your application
|
||||||
}
|
}
|
||||||
|
|
||||||
// TopicInfo contains parsed information about a Meshtastic MQTT topic
|
// TopicInfo contains parsed information about a Meshtastic MQTT topic
|
||||||
export interface TopicInfo {
|
export interface TopicInfo {
|
||||||
full_topic: string;
|
fullTopic: string;
|
||||||
region_path: string;
|
regionPath: string;
|
||||||
version: string;
|
version: string;
|
||||||
format: string;
|
format: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
user_id: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data provides a flattened structure for decoded Meshtastic packets
|
// Data provides a flattened structure for decoded Meshtastic packets
|
||||||
export interface Data {
|
export interface Data {
|
||||||
// From Service Envelope
|
// From Service Envelope
|
||||||
channel_id: string;
|
channelId: string;
|
||||||
gateway_id: string;
|
gatewayId: string;
|
||||||
|
|
||||||
// From Mesh Packet
|
// From Mesh Packet
|
||||||
id: number;
|
id: number;
|
||||||
from: number;
|
from: number;
|
||||||
to: number;
|
to: number;
|
||||||
hop_limit: number;
|
hopLimit: number;
|
||||||
hop_start: number;
|
hopStart: number;
|
||||||
want_ack: boolean;
|
wantAck: boolean;
|
||||||
priority: string;
|
priority: string;
|
||||||
via_mqtt: boolean;
|
viaMqtt: boolean;
|
||||||
next_hop: number;
|
nextHop: number;
|
||||||
relay_node: number;
|
relayNode: number;
|
||||||
|
|
||||||
// From Data
|
// From Data
|
||||||
port_num: PortNum;
|
portNum: PortNum | string; // Can be either enum value or string name
|
||||||
|
|
||||||
// Payload is one of these types, depending on port_num
|
// Payload is one of these types, depending on portNum
|
||||||
text_message?: string;
|
textMessage?: string;
|
||||||
binary_data?: string; // Base64 encoded binary data
|
binaryData?: string; // Base64 encoded binary data
|
||||||
position?: Position;
|
position?: Position;
|
||||||
node_info?: User;
|
nodeInfo?: User;
|
||||||
telemetry?: Telemetry;
|
telemetry?: Telemetry;
|
||||||
waypoint?: Waypoint;
|
waypoint?: Waypoint;
|
||||||
route_discovery?: RouteDiscovery;
|
routeDiscovery?: RouteDiscovery;
|
||||||
neighbor_info?: NeighborInfo;
|
neighborInfo?: NeighborInfo;
|
||||||
compressed_text?: string; // Base64 encoded
|
compressedText?: string; // Base64 encoded
|
||||||
map_report?: MapReport;
|
mapReport?: MapReport;
|
||||||
remote_hardware?: HardwareMessage;
|
remoteHardware?: HardwareMessage;
|
||||||
routing?: Routing;
|
routing?: Routing;
|
||||||
admin?: AdminMessage;
|
admin?: AdminMessage;
|
||||||
audio_data?: string; // Base64 encoded
|
audioData?: string; // Base64 encoded
|
||||||
alert?: string;
|
alert?: string;
|
||||||
reply?: string;
|
reply?: string;
|
||||||
ip_tunnel?: string; // Base64 encoded
|
ipTunnel?: string; // Base64 encoded
|
||||||
paxcounter?: Paxcount;
|
paxcounter?: Paxcount;
|
||||||
serial_app?: string; // Base64 encoded
|
serialApp?: string; // Base64 encoded
|
||||||
store_forward?: string; // Base64 encoded
|
storeForward?: string; // Base64 encoded
|
||||||
range_test?: string;
|
rangeTest?: string;
|
||||||
zps_app?: string; // Base64 encoded
|
zpsApp?: string; // Base64 encoded
|
||||||
simulator?: string; // Base64 encoded
|
simulator?: string; // Base64 encoded
|
||||||
atak_plugin?: string; // Base64 encoded
|
atakPlugin?: string; // Base64 encoded
|
||||||
powerstress?: string; // Base64 encoded
|
powerstress?: string; // Base64 encoded
|
||||||
reticulum_tunnel?: string; // Base64 encoded
|
reticulumTunnel?: string; // Base64 encoded
|
||||||
private_app?: string; // Base64 encoded
|
privateApp?: string; // Base64 encoded
|
||||||
detection_sensor?: string;
|
detectionSensor?: string;
|
||||||
|
|
||||||
// Additional Data fields
|
// Additional Data fields
|
||||||
request_id?: number;
|
requestId?: number;
|
||||||
reply_id?: number;
|
replyId?: number;
|
||||||
emoji?: number;
|
emoji?: number;
|
||||||
dest?: number;
|
dest?: number;
|
||||||
source?: number;
|
source?: number;
|
||||||
want_response?: boolean;
|
wantResponse?: boolean;
|
||||||
|
|
||||||
// Error tracking
|
// Error tracking
|
||||||
decode_error?: string;
|
decodeError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Packet represents a complete decoded MQTT message
|
// Packet represents a complete decoded MQTT message
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { Packet } from '../../lib/types';
|
||||||
|
|
||||||
interface Packet {
|
// Maximum number of packets to keep in memory
|
||||||
id: string;
|
const MAX_PACKETS = 100;
|
||||||
// We'll define the full packet structure later based on the protobuf definitions
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PacketState {
|
interface PacketState {
|
||||||
packets: Packet[];
|
packets: Packet[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
streamPaused: boolean;
|
||||||
|
bufferedPackets: Packet[]; // Holds packets received while paused
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: PacketState = {
|
const initialState: PacketState = {
|
||||||
packets: [],
|
packets: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
streamPaused: false,
|
||||||
|
bufferedPackets: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const packetSlice = createSlice({
|
const packetSlice = createSlice({
|
||||||
@@ -26,7 +29,8 @@ const packetSlice = createSlice({
|
|||||||
state.error = null;
|
state.error = null;
|
||||||
},
|
},
|
||||||
fetchPacketsSuccess(state, action: PayloadAction<Packet[]>) {
|
fetchPacketsSuccess(state, action: PayloadAction<Packet[]>) {
|
||||||
state.packets = action.payload;
|
// Limit initial load to MAX_PACKETS
|
||||||
|
state.packets = action.payload.slice(-MAX_PACKETS);
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
},
|
},
|
||||||
fetchPacketsFailure(state, action: PayloadAction<string>) {
|
fetchPacketsFailure(state, action: PayloadAction<string>) {
|
||||||
@@ -34,8 +38,54 @@ const packetSlice = createSlice({
|
|||||||
state.loading = false;
|
state.loading = false;
|
||||||
},
|
},
|
||||||
addPacket(state, action: PayloadAction<Packet>) {
|
addPacket(state, action: PayloadAction<Packet>) {
|
||||||
state.packets.push(action.payload);
|
if (state.streamPaused) {
|
||||||
|
// When paused, add to buffer instead of main list
|
||||||
|
state.bufferedPackets.unshift(action.payload);
|
||||||
|
|
||||||
|
// Ensure buffer doesn't grow too large
|
||||||
|
if (state.bufferedPackets.length > MAX_PACKETS) {
|
||||||
|
state.bufferedPackets = state.bufferedPackets.slice(0, MAX_PACKETS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal flow - add to main list
|
||||||
|
state.packets.unshift(action.payload);
|
||||||
|
|
||||||
|
// Remove oldest packets if we exceed the limit
|
||||||
|
if (state.packets.length > MAX_PACKETS) {
|
||||||
|
state.packets = state.packets.slice(0, MAX_PACKETS);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
clearPackets(state) {
|
||||||
|
state.packets = [];
|
||||||
|
state.bufferedPackets = [];
|
||||||
|
},
|
||||||
|
toggleStreamPause(state) {
|
||||||
|
state.streamPaused = !state.streamPaused;
|
||||||
|
|
||||||
|
// If unpausing, prepend buffered packets to the main list
|
||||||
|
if (!state.streamPaused && state.bufferedPackets.length > 0) {
|
||||||
|
state.packets = [...state.bufferedPackets, ...state.packets]
|
||||||
|
.slice(0, MAX_PACKETS);
|
||||||
|
state.bufferedPackets = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Explicitly set the pause state
|
||||||
|
setPauseState(state, action: PayloadAction<boolean>) {
|
||||||
|
const newPausedState = action.payload;
|
||||||
|
|
||||||
|
// Only process if state is actually changing
|
||||||
|
if (state.streamPaused !== newPausedState) {
|
||||||
|
state.streamPaused = newPausedState;
|
||||||
|
|
||||||
|
// If unpausing, prepend buffered packets to the main list
|
||||||
|
if (!newPausedState && state.bufferedPackets.length > 0) {
|
||||||
|
state.packets = [...state.bufferedPackets, ...state.packets]
|
||||||
|
.slice(0, MAX_PACKETS);
|
||||||
|
state.bufferedPackets = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,6 +94,9 @@ export const {
|
|||||||
fetchPacketsSuccess,
|
fetchPacketsSuccess,
|
||||||
fetchPacketsFailure,
|
fetchPacketsFailure,
|
||||||
addPacket,
|
addPacket,
|
||||||
|
clearPackets,
|
||||||
|
toggleStreamPause,
|
||||||
|
setPauseState,
|
||||||
} = packetSlice.actions;
|
} = packetSlice.actions;
|
||||||
|
|
||||||
export default packetSlice.reducer;
|
export default packetSlice.reducer;
|
||||||
Reference in New Issue
Block a user