diff --git a/web/fixtures/telemetry.json b/web/fixtures/telemetry1.json
similarity index 72%
rename from web/fixtures/telemetry.json
rename to web/fixtures/telemetry1.json
index 0277223..82eac2e 100644
--- a/web/fixtures/telemetry.json
+++ b/web/fixtures/telemetry1.json
@@ -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
}
-}
\ No newline at end of file
+}
diff --git a/web/fixtures/telemetry2.json b/web/fixtures/telemetry2.json
new file mode 100644
index 0000000..27c47df
--- /dev/null
+++ b/web/fixtures/telemetry2.json
@@ -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
+ }
+}
diff --git a/web/src/components/PacketList.tsx b/web/src/components/PacketList.tsx
index 29eef17..16559d4 100644
--- a/web/src/components/PacketList.tsx
+++ b/web/src/components/PacketList.tsx
@@ -131,7 +131,7 @@ export const PacketList: React.FC = () => {
-
+
{currentPackets.map((packet, index) => (
-
diff --git a/web/src/components/packets/DeviceMetricsPacket.tsx b/web/src/components/packets/DeviceMetricsPacket.tsx
new file mode 100644
index 0000000..a1e23db
--- /dev/null
+++ b/web/src/components/packets/DeviceMetricsPacket.tsx
@@ -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 = ({
+ 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 (
+ }
+ iconBgColor="bg-amber-500"
+ label="Device Telemetry"
+ backgroundColor="bg-amber-950/5"
+ >
+
+
+ {metrics.batteryLevel !== undefined && (
+
+
Battery
+
+
75
+ ? "bg-green-500"
+ : metrics.batteryLevel > 40
+ ? "bg-amber-500"
+ : "bg-red-500"
+ }`}
+ style={{ width: `${Math.min(100, metrics.batteryLevel)}%` }}
+ >
+
+
+ 75
+ ? "text-green-600"
+ : metrics.batteryLevel > 40
+ ? "text-amber-400"
+ : "text-red-400"
+ }
+ >
+ {metrics.batteryLevel}%
+
+ {metrics.voltage !== undefined && (
+
+ {metrics.voltage.toFixed(2)}V
+
+ )}
+
+
+ )}
+
+ {metrics.channelUtilization !== undefined && (
+
+
+
+ Channel
+
+
+ {metrics.channelUtilization.toFixed(1)}%
+
+
+ )}
+
+ {metrics.airUtilTx !== undefined && (
+
+
+
+ Air TX
+
+
{metrics.airUtilTx.toFixed(1)}%
+
+ )}
+
+ {metrics.uptimeSeconds !== undefined && (
+
+
+
+ Uptime
+
+
+ {formatUptime(metrics.uptimeSeconds)}
+
+
+ )}
+
+ {formattedTime && (
+
+
+
+ Reported
+
+
{formattedTime}
+
+ )}
+
+
+
+ );
+};
diff --git a/web/src/components/packets/EnvironmentMetricsPacket.tsx b/web/src/components/packets/EnvironmentMetricsPacket.tsx
new file mode 100644
index 0000000..2980382
--- /dev/null
+++ b/web/src/components/packets/EnvironmentMetricsPacket.tsx
@@ -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 ;
+ case "relativeHumidity":
+ case "soilMoisture":
+ case "rainfall1h":
+ case "rainfall24h":
+ return ;
+ case "windSpeed":
+ case "windDirection":
+ case "windGust":
+ case "windLull":
+ return ;
+ case "lux":
+ case "whiteLux":
+ case "irLux":
+ case "uvLux":
+ return ;
+ case "distance":
+ return ;
+ 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 (
+
+
+ {getMetricIcon(key)}
+ {displayName}
+
+
+ {formatMetricValue(key, value as number)}
+
+
+ );
+ });
+ };
+
+ return (
+ }
+ iconBgColor="bg-emerald-700"
+ label="Environment Telemetry"
+ backgroundColor="bg-green-950/5"
+ >
+
+
+ {renderMetrics()}
+
+ {formattedTime && (
+
+
+
+ Reported
+
+
{formattedTime}
+
+ )}
+
+
+
+ );
+};
diff --git a/web/src/components/packets/PacketRenderer.tsx b/web/src/components/packets/PacketRenderer.tsx
index c4b17ce..2b0da51 100644
--- a/web/src/components/packets/PacketRenderer.tsx
+++ b/web/src/components/packets/PacketRenderer.tsx
@@ -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";
diff --git a/web/src/components/packets/TelemetryPacket.tsx b/web/src/components/packets/TelemetryPacket.tsx
index 2e43c6b..aec4013 100644
--- a/web/src/components/packets/TelemetryPacket.tsx
+++ b/web/src/components/packets/TelemetryPacket.tsx
@@ -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 = ({ 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 (
-
-
Device
-
- {metrics.batteryLevel !== undefined && (
-
- )}
- {metrics.voltage !== undefined && (
-
- )}
- {metrics.channelUtilization !== undefined && (
-
- )}
- {metrics.uptimeSeconds !== undefined && (
-
- )}
-
-
+
);
- };
+ }
- // Helper function to render environment metrics
- const renderEnvironmentMetrics = () => {
- if (!telemetry.environmentMetrics) return null;
-
- const metrics = telemetry.environmentMetrics;
+ if (hasEnvironmentMetrics && telemetry.environmentMetrics) {
return (
-
-
Environment
-
- {metrics.temperature !== undefined && (
-
- )}
- {metrics.relativeHumidity !== undefined && (
-
- )}
- {metrics.barometricPressure !== undefined && (
-
- )}
- {metrics.lux !== undefined && (
-
- )}
-
-
+
);
- };
-
- // 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 (
}
- iconBgColor="bg-amber-500"
- label="Telemetry"
- backgroundColor="bg-amber-950/5"
+ iconBgColor="bg-neutral-500"
+ label="Unknown Telemetry"
+ backgroundColor="bg-neutral-950/5"
>
-
- {telemetry.time && (
-
-
-
- )}
-
- {renderDeviceMetrics()}
- {renderEnvironmentMetrics()}
+
+ Unknown telemetry data received at{' '}
+ {telemetry.time
+ ? new Date(telemetry.time * 1000).toLocaleTimeString()
+ : 'unknown time'
+ }
);
diff --git a/web/src/routes/demo.tsx b/web/src/routes/demo.tsx
index a418dd5..7daaef6 100644
--- a/web/src/routes/demo.tsx
+++ b/web/src/routes/demo.tsx
@@ -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() {
- Telemetry Packet
+ Telemetry Packet (Device Metrics)
-
+
-
+
+
+
+ Telemetry Packet (Environmental Data)
+
+
+
+
-
+