diff --git a/package.json b/package.json index efd5257a..05e34b50 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "immer": "^9.0.15", "mapbox-gl": "npm:empty-npm-package@^1.0.0", "maplibre-gl": "^2.4.0", + "pretty-ms": "^8.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.37.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 324649fd..758a9a75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,7 @@ specifiers: postcss: ^8.4.17 prettier: ^2.7.1 prettier-plugin-tailwindcss: ^0.1.13 + pretty-ms: ^8.0.0 react: ^18.2.0 react-dom: ^18.2.0 react-hook-form: ^7.37.0 @@ -65,6 +66,7 @@ dependencies: immer: 9.0.15 mapbox-gl: /empty-npm-package/1.0.0 maplibre-gl: 2.4.0 + pretty-ms: 8.0.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-hook-form: 7.37.0_react@18.2.0 @@ -3150,6 +3152,11 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse-ms/3.0.0: + resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==} + engines: {node: '>=12'} + dev: false + /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3287,6 +3294,13 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + /pretty-ms/8.0.0: + resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==} + engines: {node: '>=14.16'} + dependencies: + parse-ms: 3.0.0 + dev: false + /process-nextick-args/2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true diff --git a/src/components/Widgets/BatteryWidget.tsx b/src/components/Widgets/BatteryWidget.tsx index 02b06a78..1700e9f5 100644 --- a/src/components/Widgets/BatteryWidget.tsx +++ b/src/components/Widgets/BatteryWidget.tsx @@ -1,10 +1,10 @@ import type React from "react"; +import { useEffect, useState } from "react"; -import { Battery100Icon, BoltIcon } from "@heroicons/react/24/outline"; +import prettyMilliseconds from "pretty-ms"; -import { Card } from "../Card.js"; -import { Dropdown } from "../Dropdown.js"; -import { Mono } from "../Mono.js"; +import { useDevice } from "@app/core/providers/useDevice.js"; +import { Battery100Icon, ClockIcon } from "@heroicons/react/24/outline"; export interface BatteryWidgetProps { batteryLevel: number; @@ -15,23 +15,73 @@ export const BatteryWidget = ({ batteryLevel, voltage, }: BatteryWidgetProps): JSX.Element => { + const { nodes, hardware } = useDevice(); + + const [timeRemaining, setTimeRemaining] = useState("Unknown"); + + useEffect(() => { + const stats = nodes.find( + (n) => n.data.num === hardware.myNodeNum + )?.deviceMetrics; + + if (stats) { + let currentStat: number | undefined = undefined; + let currentTime = new Date(); + let previousStat: number | undefined = undefined; + let previousTime = new Date(); + for (const stat of [...stats].reverse()) { + if (stat.batteryLevel) { + if (!currentStat) { + currentStat = stat.batteryLevel; + currentTime = stat.timestamp; + } else { + previousStat = stat.batteryLevel; + previousTime = stat.timestamp; + } + break; + } + } + + if (currentStat && previousStat) { + const timeDiff = currentTime.getTime() - previousTime.getTime(); + const statDiff = Math.abs(currentStat - previousStat); + // + console.log(`timeDiff: ${timeDiff}`); + console.log(`statDiff: ${statDiff}`); + + //convert to ms/% + const msPerPercent = timeDiff / statDiff; + console.log(`msPerPercent: ${msPerPercent}`); + const formatted = prettyMilliseconds( + (100 - currentStat) * msPerPercent + ); + setTimeRemaining(formatted); + } else setTimeRemaining("Unknown"); + } + }, [hardware.myNodeNum, nodes]); + return ( - - }> -
-
- -
- - {batteryLevel} - % - - - {voltage.toPrecision(2)} - v - +
+
+
+
- - +

+ Battery State +

+
+
+

{batteryLevel}%

+

+

+
+
); }; diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index 020507d0..5c3126d0 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -32,8 +32,9 @@ export interface Channel { } export interface Node { - deviceMetrics: Protobuf.DeviceMetrics[]; - environmentMetrics: Protobuf.EnvironmentMetrics[]; + deviceMetrics: (Protobuf.DeviceMetrics & { timestamp: Date })[]; + environmentMetrics: (Protobuf.EnvironmentMetrics & { timestamp: Date })[]; + metadata?: Protobuf.DeviceMetadata; data: Protobuf.NodeInfo; } @@ -72,6 +73,7 @@ export interface Device { addConnection: (connection: Types.ConnectionType) => void; addMessage: (message: MessageWithAck) => void; addWaypointMessage: (message: WaypointIDWithAck) => void; + addDeviceMetadataMessage: (metadata: Types.DeviceMetadataPacket) => void; ackMessage: (channelIndex: number, messageId: number) => void; setQRDialogOpen: (open: boolean) => void; } @@ -226,6 +228,7 @@ export const useDeviceStore = create((set, get) => ({ snr: metrics.packet.rxSnr, lastHeard: new Date().getSeconds(), }), + metadata: undefined, deviceMetrics: [], environmentMetrics: [], }; @@ -256,14 +259,16 @@ export const useDeviceStore = create((set, get) => ({ } } - node.deviceMetrics.push( - metrics.data.variant.deviceMetrics - ); + node.deviceMetrics.push({ + ...metrics.data.variant.deviceMetrics, + timestamp: new Date(metrics.packet.rxTime), + }); break; case "environmentMetrics": - node.environmentMetrics.push( - metrics.data.variant.environmentMetrics - ); + node.environmentMetrics.push({ + ...metrics.data.variant.environmentMetrics, + timestamp: new Date(metrics.packet.rxTime), + }); break; } } @@ -335,6 +340,7 @@ export const useDeviceStore = create((set, get) => ({ } else { device.nodes.push({ data: Protobuf.NodeInfo.create(nodeInfo.data), + metadata: undefined, deviceMetrics: [], environmentMetrics: [], }); @@ -385,6 +391,7 @@ export const useDeviceStore = create((set, get) => ({ snr: user.packet.rxSnr, user: user.data, }), + metadata: undefined, deviceMetrics: [], environmentMetrics: [], }); @@ -414,6 +421,7 @@ export const useDeviceStore = create((set, get) => ({ num: position.packet.from, position: position.data, }), + metadata: undefined, deviceMetrics: [], environmentMetrics: [], }); @@ -456,6 +464,23 @@ export const useDeviceStore = create((set, get) => ({ }) ); }, + addDeviceMetadataMessage: (metadata) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (device) { + const node = device.nodes.find( + (n) => n.data.num === metadata.packet.from + ); + if (node) { + node.metadata = metadata.data; + } else { + console.log("Node not found!"); + } + } + }) + ); + }, ackMessage: (channelIndex: number, messageId: number) => { console.log("ack called"); diff --git a/src/core/subscriptions.ts b/src/core/subscriptions.ts index 92881664..cc5c6f7c 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -13,6 +13,10 @@ export const subscribeAll = ( // onLogEvent // onMeshHeartbeat + connection.onDeviceMetadataPacket.subscribe((metadataPacket) => { + device.addDeviceMetadataMessage(metadataPacket); + }); + connection.onRoutingPacket.subscribe((routingPacket) => { console.log(routingPacket); });