mirror of
https://github.com/dpup/meshstream.git
synced 2026-05-07 05:44:34 +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 { useAppSelector } from "../hooks";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
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 = () => {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
return <div className="p-4 text-neutral-400">No packets received yet</div>;
|
||||
if (packets.length === 0 && bufferedPackets.length === 0) {
|
||||
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 (
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-bold mb-4 text-neutral-200">
|
||||
Received Packets
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{packets.map((packet) => (
|
||||
<li
|
||||
key={packet.id}
|
||||
className="p-3 border border-neutral-700 rounded bg-neutral-900 shadow-inner text-neutral-300 hover:bg-neutral-800 transition-colors"
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-normal text-neutral-200">
|
||||
Packets{" "}
|
||||
<span className="text-sm text-neutral-400">
|
||||
({packets.length} total)
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -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 './ConnectionStatus';
|
||||
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';
|
||||
Reference in New Issue
Block a user