mirror of
https://github.com/dpup/meshstream.git
synced 2026-06-28 22:11:47 +02:00
Update visualization of Telemetry Packets
This commit is contained in:
@@ -24,23 +24,15 @@
|
||||
"telemetry": {
|
||||
"time": 1745300043,
|
||||
"deviceMetrics": {
|
||||
"batteryLevel": 100,
|
||||
"batteryLevel": 88,
|
||||
"voltage": 4.19,
|
||||
"channelUtilization": 13.37,
|
||||
"airUtilTx": 2.58,
|
||||
"uptimeSeconds": 771327
|
||||
},
|
||||
"environmentMetrics": {
|
||||
"temperature": 22.5,
|
||||
"relativeHumidity": 58.2,
|
||||
"barometricPressure": 101325,
|
||||
"gasResistance": 250000,
|
||||
"voltage": 3.3,
|
||||
"current": 0.01
|
||||
"uptimeSeconds": 189327
|
||||
}
|
||||
},
|
||||
"requestId": 0,
|
||||
"replyId": 0,
|
||||
"wantResponse": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"info": {
|
||||
"fullTopic": "msh/US/bayarea/2/e/LongFast/!defg4567",
|
||||
"regionPath": "US/bayarea",
|
||||
"version": "2",
|
||||
"format": "e",
|
||||
"channel": "LongFast",
|
||||
"userId": "!defg4567"
|
||||
},
|
||||
"data": {
|
||||
"channelId": "LongFast",
|
||||
"gatewayId": "!defg4567",
|
||||
"id": 5567890123,
|
||||
"from": 3949439480,
|
||||
"to": 4294967295,
|
||||
"hopLimit": 3,
|
||||
"hopStart": 3,
|
||||
"wantAck": false,
|
||||
"priority": "UNSET",
|
||||
"viaMqtt": true,
|
||||
"nextHop": 0,
|
||||
"relayNode": 0,
|
||||
"portNum": "TELEMETRY_APP",
|
||||
"telemetry": {
|
||||
"time": 1745300047,
|
||||
"environmentMetrics": {
|
||||
"temperature": 22.5,
|
||||
"relativeHumidity": 58.2,
|
||||
"barometricPressure": 101325,
|
||||
"lux": 450,
|
||||
"windSpeed": 3.2,
|
||||
"rainfall1h": 0.5,
|
||||
"soilMoisture": 65,
|
||||
"soilTemperature": 18.5
|
||||
}
|
||||
},
|
||||
"requestId": 0,
|
||||
"replyId": 0,
|
||||
"wantResponse": false
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export const PacketList: React.FC = () => {
|
||||
</div>
|
||||
<Separator className="mx-0" />
|
||||
|
||||
<ul className="space-y-3">
|
||||
<ul className="space-y-12">
|
||||
{currentPackets.map((packet, index) => (
|
||||
<li key={getPacketKey(packet, index)}>
|
||||
<PacketRenderer packet={packet} />
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React from "react";
|
||||
import { Packet, DeviceMetrics } from "../../lib/types";
|
||||
import { Gauge, Wifi, Clock } from "lucide-react";
|
||||
import { PacketCard } from "./PacketCard";
|
||||
|
||||
interface DeviceMetricsPacketProps {
|
||||
packet: Packet;
|
||||
metrics: DeviceMetrics;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export const DeviceMetricsPacket: React.FC<DeviceMetricsPacketProps> = ({
|
||||
packet,
|
||||
metrics,
|
||||
timestamp,
|
||||
}) => {
|
||||
// Format uptime in a readable way
|
||||
const formatUptime = (seconds: number): string => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
return `${mins}m`;
|
||||
};
|
||||
|
||||
// Format time without seconds
|
||||
const formattedTime = timestamp
|
||||
? new Date(timestamp * 1000).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<PacketCard
|
||||
packet={packet}
|
||||
icon={<Gauge />}
|
||||
iconBgColor="bg-amber-500"
|
||||
label="Device Telemetry"
|
||||
backgroundColor="bg-amber-950/5"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{metrics.batteryLevel !== undefined && (
|
||||
<div className="col-span-2">
|
||||
<div className="text-xs text-neutral-400 mb-1">Battery</div>
|
||||
<div className="w-full h-3 bg-neutral-700/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${
|
||||
metrics.batteryLevel > 75
|
||||
? "bg-green-500"
|
||||
: metrics.batteryLevel > 40
|
||||
? "bg-amber-500"
|
||||
: "bg-red-500"
|
||||
}`}
|
||||
style={{ width: `${Math.min(100, metrics.batteryLevel)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs mt-1">
|
||||
<span
|
||||
className={
|
||||
metrics.batteryLevel > 75
|
||||
? "text-green-600"
|
||||
: metrics.batteryLevel > 40
|
||||
? "text-amber-400"
|
||||
: "text-red-400"
|
||||
}
|
||||
>
|
||||
{metrics.batteryLevel}%
|
||||
</span>
|
||||
{metrics.voltage !== undefined && (
|
||||
<span className="text-neutral-500">
|
||||
{metrics.voltage.toFixed(2)}V
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{metrics.channelUtilization !== undefined && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Wifi className="h-3 w-3 text-neutral-500" />
|
||||
<span className="text-xs text-neutral-400">Channel</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{metrics.channelUtilization.toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{metrics.airUtilTx !== undefined && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Wifi className="h-3 w-3 text-neutral-500" />
|
||||
<span className="text-xs text-neutral-400">Air TX</span>
|
||||
</div>
|
||||
<div className="text-sm">{metrics.airUtilTx.toFixed(1)}%</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{metrics.uptimeSeconds !== undefined && (
|
||||
<div className="">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="h-3 w-3 text-neutral-500" />
|
||||
<span className="text-xs text-neutral-400">Uptime</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{formatUptime(metrics.uptimeSeconds)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formattedTime && (
|
||||
<div className="">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="h-3 w-3 text-neutral-500" />
|
||||
<span className="text-xs text-neutral-400">Reported</span>
|
||||
</div>
|
||||
<div className="text-sm">{formattedTime}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PacketCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
import React from "react";
|
||||
import { Packet, EnvironmentMetrics } from "../../lib/types";
|
||||
import { Thermometer, Droplets, Wind, Sun, Ruler, Clock } from "lucide-react";
|
||||
import { PacketCard } from "./PacketCard";
|
||||
|
||||
interface EnvironmentMetricsPacketProps {
|
||||
packet: Packet;
|
||||
metrics: EnvironmentMetrics;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export const EnvironmentMetricsPacket: React.FC<
|
||||
EnvironmentMetricsPacketProps
|
||||
> = ({ packet, metrics, timestamp }) => {
|
||||
// Format time without seconds
|
||||
const formattedTime = timestamp
|
||||
? new Date(timestamp * 1000).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: null;
|
||||
|
||||
// Get appropriate icon for a metric
|
||||
const getMetricIcon = (metricName: string) => {
|
||||
switch (metricName) {
|
||||
case "temperature":
|
||||
case "soilTemperature":
|
||||
return <Thermometer className="h-3 w-3 text-neutral-500" />;
|
||||
case "relativeHumidity":
|
||||
case "soilMoisture":
|
||||
case "rainfall1h":
|
||||
case "rainfall24h":
|
||||
return <Droplets className="h-3 w-3 text-neutral-500" />;
|
||||
case "windSpeed":
|
||||
case "windDirection":
|
||||
case "windGust":
|
||||
case "windLull":
|
||||
return <Wind className="h-3 w-3 text-neutral-500" />;
|
||||
case "lux":
|
||||
case "whiteLux":
|
||||
case "irLux":
|
||||
case "uvLux":
|
||||
return <Sun className="h-3 w-3 text-neutral-500" />;
|
||||
case "distance":
|
||||
return <Ruler className="h-3 w-3 text-neutral-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Format metric value with appropriate unit
|
||||
const formatMetricValue = (name: string, value: number): string => {
|
||||
switch (name) {
|
||||
case "temperature":
|
||||
case "soilTemperature":
|
||||
return `${value.toFixed(1)}°C`;
|
||||
case "relativeHumidity":
|
||||
case "soilMoisture":
|
||||
return `${value.toFixed(1)}%`;
|
||||
case "barometricPressure":
|
||||
return `${(value / 100).toFixed(1)} hPa`;
|
||||
case "lux":
|
||||
case "whiteLux":
|
||||
case "irLux":
|
||||
case "uvLux":
|
||||
return `${value.toFixed(0)} lux`;
|
||||
case "windSpeed":
|
||||
case "windGust":
|
||||
case "windLull":
|
||||
return `${value.toFixed(1)} m/s`;
|
||||
case "windDirection":
|
||||
return `${value.toFixed(0)}°`;
|
||||
case "rainfall1h":
|
||||
case "rainfall24h":
|
||||
return `${value.toFixed(1)} mm`;
|
||||
case "distance":
|
||||
return `${value.toFixed(0)} mm`;
|
||||
case "radiation":
|
||||
return `${value.toFixed(1)} µR/h`;
|
||||
case "weight":
|
||||
return `${value.toFixed(2)} kg`;
|
||||
default:
|
||||
return `${value}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Create metrics grid items from available metrics
|
||||
const renderMetrics = () => {
|
||||
const entries = Object.entries(metrics).filter(
|
||||
([key]) => key !== "time" && key !== "voltage" && key !== "current"
|
||||
);
|
||||
|
||||
return entries.map(([key, value]) => {
|
||||
if (value === undefined) return null;
|
||||
|
||||
// Format the name for display
|
||||
const displayName = key
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{getMetricIcon(key)}
|
||||
<span className="text-xs text-neutral-400">{displayName}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{formatMetricValue(key, value as number)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PacketCard
|
||||
packet={packet}
|
||||
icon={<Thermometer />}
|
||||
iconBgColor="bg-emerald-700"
|
||||
label="Environment Telemetry"
|
||||
backgroundColor="bg-green-950/5"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{renderMetrics()}
|
||||
|
||||
{formattedTime && (
|
||||
<div className="">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="h-3 w-3 text-neutral-500" />
|
||||
<span className="text-xs text-neutral-400">Reported</span>
|
||||
</div>
|
||||
<div className="text-sm">{formattedTime}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PacketCard>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,8 @@ import { TextMessagePacket } from "./TextMessagePacket";
|
||||
import { PositionPacket } from "./PositionPacket";
|
||||
import { NodeInfoPacket } from "./NodeInfoPacket";
|
||||
import { TelemetryPacket } from "./TelemetryPacket";
|
||||
import { DeviceMetricsPacket } from "./DeviceMetricsPacket";
|
||||
import { EnvironmentMetricsPacket } from "./EnvironmentMetricsPacket";
|
||||
import { ErrorPacket } from "./ErrorPacket";
|
||||
import { WaypointPacket } from "./WaypointPacket";
|
||||
import { MapReportPacket } from "./MapReportPacket";
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from "react";
|
||||
import { Packet } from "../../lib/types";
|
||||
import { BarChart } from "lucide-react";
|
||||
import { PacketCard } from "./PacketCard";
|
||||
import { KeyValueGrid, KeyValuePair } from "./KeyValuePair";
|
||||
import { DeviceMetricsPacket } from "./DeviceMetricsPacket";
|
||||
import { EnvironmentMetricsPacket } from "./EnvironmentMetricsPacket";
|
||||
|
||||
interface TelemetryPacketProps {
|
||||
packet: Packet;
|
||||
@@ -16,117 +17,51 @@ export const TelemetryPacket: React.FC<TelemetryPacketProps> = ({ packet }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to render device metrics
|
||||
const renderDeviceMetrics = () => {
|
||||
if (!telemetry.deviceMetrics) return null;
|
||||
|
||||
const metrics = telemetry.deviceMetrics;
|
||||
// Determine which type of telemetry we're dealing with
|
||||
const hasDeviceMetrics = telemetry.deviceMetrics && Object.keys(telemetry.deviceMetrics).some(
|
||||
key => telemetry.deviceMetrics![key as keyof typeof telemetry.deviceMetrics] !== undefined
|
||||
);
|
||||
|
||||
const hasEnvironmentMetrics = telemetry.environmentMetrics && Object.keys(telemetry.environmentMetrics).some(
|
||||
key => telemetry.environmentMetrics![key as keyof typeof telemetry.environmentMetrics] !== undefined
|
||||
);
|
||||
|
||||
// Return the appropriate component based on telemetry type
|
||||
if (hasDeviceMetrics && telemetry.deviceMetrics) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-neutral-300 mb-2">Device</h4>
|
||||
<KeyValueGrid>
|
||||
{metrics.batteryLevel !== undefined && (
|
||||
<KeyValuePair
|
||||
label="Battery"
|
||||
value={`${metrics.batteryLevel}%`}
|
||||
/>
|
||||
)}
|
||||
{metrics.voltage !== undefined && (
|
||||
<KeyValuePair
|
||||
label="Voltage"
|
||||
value={`${metrics.voltage.toFixed(2)}V`}
|
||||
/>
|
||||
)}
|
||||
{metrics.channelUtilization !== undefined && (
|
||||
<KeyValuePair
|
||||
label="Channel Util."
|
||||
value={`${metrics.channelUtilization.toFixed(1)}%`}
|
||||
/>
|
||||
)}
|
||||
{metrics.uptimeSeconds !== undefined && (
|
||||
<KeyValuePair
|
||||
label="Uptime"
|
||||
value={formatUptime(metrics.uptimeSeconds)}
|
||||
/>
|
||||
)}
|
||||
</KeyValueGrid>
|
||||
</div>
|
||||
<DeviceMetricsPacket
|
||||
packet={packet}
|
||||
metrics={telemetry.deviceMetrics}
|
||||
timestamp={telemetry.time}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to render environment metrics
|
||||
const renderEnvironmentMetrics = () => {
|
||||
if (!telemetry.environmentMetrics) return null;
|
||||
|
||||
const metrics = telemetry.environmentMetrics;
|
||||
if (hasEnvironmentMetrics && telemetry.environmentMetrics) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-neutral-300 mb-2">Environment</h4>
|
||||
<KeyValueGrid>
|
||||
{metrics.temperature !== undefined && (
|
||||
<KeyValuePair
|
||||
label="Temperature"
|
||||
value={`${metrics.temperature.toFixed(1)}°C`}
|
||||
/>
|
||||
)}
|
||||
{metrics.relativeHumidity !== undefined && (
|
||||
<KeyValuePair
|
||||
label="Humidity"
|
||||
value={`${metrics.relativeHumidity.toFixed(1)}%`}
|
||||
/>
|
||||
)}
|
||||
{metrics.barometricPressure !== undefined && (
|
||||
<KeyValuePair
|
||||
label="Pressure"
|
||||
value={`${(metrics.barometricPressure/100).toFixed(1)} hPa`}
|
||||
/>
|
||||
)}
|
||||
{metrics.lux !== undefined && (
|
||||
<KeyValuePair
|
||||
label="Light"
|
||||
value={`${metrics.lux.toFixed(0)} lux`}
|
||||
/>
|
||||
)}
|
||||
</KeyValueGrid>
|
||||
</div>
|
||||
<EnvironmentMetricsPacket
|
||||
packet={packet}
|
||||
metrics={telemetry.environmentMetrics}
|
||||
timestamp={telemetry.time}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Format uptime in a readable way
|
||||
const formatUptime = (seconds: number): string => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
return `${mins}m`;
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback for unknown telemetry type
|
||||
return (
|
||||
<PacketCard
|
||||
packet={packet}
|
||||
icon={<BarChart />}
|
||||
iconBgColor="bg-amber-500"
|
||||
label="Telemetry"
|
||||
backgroundColor="bg-amber-950/5"
|
||||
iconBgColor="bg-neutral-500"
|
||||
label="Unknown Telemetry"
|
||||
backgroundColor="bg-neutral-950/5"
|
||||
>
|
||||
<div className="max-w-md">
|
||||
{telemetry.time && (
|
||||
<div className="mb-3">
|
||||
<KeyValuePair
|
||||
label="Time"
|
||||
value={new Date(telemetry.time * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderDeviceMetrics()}
|
||||
{renderEnvironmentMetrics()}
|
||||
<div className="text-neutral-400 text-sm">
|
||||
Unknown telemetry data received at{' '}
|
||||
{telemetry.time
|
||||
? new Date(telemetry.time * 1000).toLocaleTimeString()
|
||||
: 'unknown time'
|
||||
}
|
||||
</div>
|
||||
</PacketCard>
|
||||
);
|
||||
|
||||
+13
-5
@@ -14,7 +14,8 @@ import { GenericPacket } from "../components/packets/GenericPacket";
|
||||
import textMessageData from "../../fixtures/text_message.json";
|
||||
import positionData from "../../fixtures/position.json";
|
||||
import nodeInfoData from "../../fixtures/nodeinfo.json";
|
||||
import telemetryData from "../../fixtures/telemetry.json";
|
||||
import telemetryData1 from "../../fixtures/telemetry1.json";
|
||||
import telemetryData2 from "../../fixtures/telemetry2.json";
|
||||
import decodeErrorData from "../../fixtures/decode_error.json";
|
||||
import waypointData from "../../fixtures/waypoint.json";
|
||||
import mapReportData from "../../fixtures/map_report.json";
|
||||
@@ -60,18 +61,25 @@ export function DemoPage() {
|
||||
|
||||
<section>
|
||||
<h3 className="text-md font-medium mb-4 text-neutral-300">
|
||||
Telemetry Packet
|
||||
Telemetry Packet (Device Metrics)
|
||||
</h3>
|
||||
<TelemetryPacket packet={telemetryData} />
|
||||
<TelemetryPacket packet={telemetryData1} />
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<h3 className="text-md font-medium mb-4 text-neutral-300">
|
||||
Telemetry Packet (Environmental Data)
|
||||
</h3>
|
||||
<TelemetryPacket packet={telemetryData2} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="text-md font-medium mb-4 text-neutral-300">
|
||||
Waypoint Packet
|
||||
</h3>
|
||||
<WaypointPacket packet={waypointData} />
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<h3 className="text-md font-medium mb-4 text-neutral-300">
|
||||
Map Report Packet
|
||||
|
||||
Reference in New Issue
Block a user