Skip to content

Commit 3d1a6d1

Browse files
esc widget
1 parent 5ee5b68 commit 3d1a6d1

3 files changed

Lines changed: 222 additions & 54 deletions

File tree

gcs/src/components/dashboard/EscTelemetryWidget.jsx

Lines changed: 211 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,43 @@
11
/*
22
Floating ESC telemetry widget (row-positioned like VideoWidget)
3+
4+
Notes:
5+
- Uses fake ESC data for now so layout/thresholds can be tested easily.
6+
- Thresholds are user-configurable and stored in localStorage.
7+
- Only the numeric values change colour.
38
*/
49
import { useMemo, useState } from "react"
5-
import { ActionIcon, Text } from "@mantine/core"
6-
import { IconBolt, IconMaximize, IconMinus, IconResize } from "@tabler/icons-react"
10+
import { ActionIcon, NumberInput, Popover, Stack, Text } from "@mantine/core"
11+
import { useLocalStorage } from "@mantine/hooks"
12+
import {
13+
IconBolt,
14+
IconMaximize,
15+
IconMinus,
16+
IconResize,
17+
IconSettings,
18+
} from "@tabler/icons-react"
719
import { useSelector } from "react-redux"
820
import GetOutsideVisibilityColor from "../../helpers/outsideVisibility"
921
import { selectEscTelemetry } from "../../redux/slices/droneInfoSlice"
1022

23+
const DEFAULT_ESC_THRESHOLDS = {
24+
rpm: {
25+
warning: 2000,
26+
danger: 1000,
27+
higherIsBetter: true,
28+
},
29+
current: {
30+
warning: 15,
31+
danger: 20,
32+
higherIsBetter: false,
33+
},
34+
temperature: {
35+
warning: 60,
36+
danger: 80,
37+
higherIsBetter: false,
38+
},
39+
}
40+
1141
function fmt(value, decimals = 0) {
1242
if (value === null || value === undefined) return "—"
1343
const n = Number(value)
@@ -23,59 +53,77 @@ function fmtTemp(value) {
2353
return degC.toFixed(0)
2454
}
2555

26-
function EscTile({ esc }) {
56+
function getThresholdColor(value, config) {
57+
if (value === null || value === undefined) return "text-slate-200"
58+
59+
const n = Number(value)
60+
if (!Number.isFinite(n)) return "text-slate-200"
61+
62+
if (config.higherIsBetter) {
63+
if (n <= config.danger) return "text-red-400"
64+
if (n <= config.warning) return "text-yellow-400"
65+
return "text-green-400"
66+
}
67+
68+
if (n >= config.danger) return "text-red-400"
69+
if (n >= config.warning) return "text-yellow-400"
70+
return "text-green-400"
71+
}
72+
73+
function EscTile({ esc, thresholds }) {
74+
const rpmClass = getThresholdColor(esc.rpm, thresholds.rpm)
75+
const currentClass = getThresholdColor(esc.current, thresholds.current)
76+
const temperatureClass = getThresholdColor(
77+
esc.temperature,
78+
thresholds.temperature,
79+
)
80+
2781
return (
2882
<div className="rounded-md border border-falcongrey-700 bg-falcongrey-900 p-2">
2983
<div className="flex flex-row items-center justify-between mb-1">
30-
<div className="text-slate-200 text-xs font-semibold">ESC {esc.escId}</div>
84+
<div className="text-slate-200 text-xs font-semibold">
85+
ESC {esc.escId}
86+
</div>
3187
</div>
3288

3389
<div className="flex flex-col gap-y-0.5">
3490
<div className="flex flex-row items-center justify-between">
3591
<div className="text-slate-500 text-[10px]">RPM</div>
36-
<div className="text-slate-200 text-xs">{fmt(esc.rpm, 0)}</div>
92+
<div className={`text-xs ${rpmClass}`}>{fmt(esc.rpm, 0)}</div>
3793
</div>
94+
3895
<div className="flex flex-row items-center justify-between">
3996
<div className="text-slate-500 text-[10px]">A</div>
40-
<div className="text-slate-200 text-xs">{fmt(esc.current, 2)}</div>
97+
<div className={`text-xs ${currentClass}`}>{fmt(esc.current, 2)}</div>
4198
</div>
99+
42100
<div className="flex flex-row items-center justify-between">
43101
<div className="text-slate-500 text-[10px]">°C</div>
44-
<div className="text-slate-200 text-xs">{fmtTemp(esc.temperature)}</div>
102+
<div className={`text-xs ${temperatureClass}`}>
103+
{fmtTemp(esc.temperature)}
104+
</div>
45105
</div>
46106
</div>
47107
</div>
48108
)
49109
}
50110

51-
export default function EscTelemetryWidget({
52-
telemetryPanelWidth, // unused now, kept so your callsites don’t break
53-
onMaximizedChange,
54-
}) {
55-
//const escs = useSelector(selectEscTelemetry)
56-
const realEscs = useSelector(selectEscTelemetry)
57-
58-
const escs = Array.from({ length: 8 }).map((_, i) => ({
59-
escId: i + 1,
60-
rpm: Math.floor(Math.random() * 6000),
61-
current: (Math.random() * 20).toFixed(2),
62-
temperature: Math.floor(30 + Math.random() * 20)
63-
}))
111+
export default function EscTelemetryWidget() {
112+
const escs = useSelector(selectEscTelemetry)
64113

65114
const [isMaximized, setIsMaximized] = useState(false)
66115
const [scale, setScale] = useState(1)
116+
const [settingsOpened, setSettingsOpened] = useState(false)
67117

68-
const setMaximized = (next) => {
69-
setIsMaximized(next)
70-
onMaximizedChange?.(next)
71-
}
118+
const [thresholds, setThresholds] = useLocalStorage({
119+
key: "escTelemetryThresholds",
120+
defaultValue: DEFAULT_ESC_THRESHOLDS,
121+
})
72122

73123
const hasAnyData =
74124
Array.isArray(escs) &&
75125
escs.some(
76-
(e) =>
77-
e &&
78-
(e.rpm != null || e.current != null || e.temperature != null),
126+
(e) => e && (e.rpm != null || e.current != null || e.temperature != null),
79127
)
80128

81129
const dimensions = useMemo(() => {
@@ -84,7 +132,7 @@ export default function EscTelemetryWidget({
84132

85133
const cols = 4
86134
const count = Array.isArray(escs) ? escs.length : 0
87-
const rows = Math.max(1, Math.ceil(count / cols))
135+
const rows = Math.max(1, Math.ceil(Math.min(count, 8) / cols))
88136

89137
const tileH = 74 * scale
90138
const gapH = 8 * scale
@@ -117,26 +165,46 @@ export default function EscTelemetryWidget({
117165
document.addEventListener("mouseup", handleMouseUp)
118166
}
119167

168+
function updateThreshold(metric, field, value) {
169+
const numericValue = Number(value)
170+
171+
setThresholds((prev) => ({
172+
...prev,
173+
[metric]: {
174+
...prev[metric],
175+
[field]: Number.isFinite(numericValue)
176+
? numericValue
177+
: prev[metric][field],
178+
},
179+
}))
180+
}
181+
182+
function resetThresholds() {
183+
setThresholds(DEFAULT_ESC_THRESHOLDS)
184+
}
185+
120186
// Minimized view
121187
if (!isMaximized) {
122188
return (
123189
<div
124190
className="rounded-md"
125191
style={{ background: GetOutsideVisibilityColor() }}
126192
>
127-
<div className="p-2 flex items-center gap-2">
128-
<IconBolt
129-
size={16}
130-
className={hasAnyData ? "text-slate-200" : "text-slate-500"}
131-
/>
132-
<Text size="sm" className="truncate max-w-[150px]">
133-
{hasAnyData ? "ESC telemetry" : "No ESC telemetry"}
134-
</Text>
193+
<div className="p-2 flex items-center justify-between">
194+
<div className="flex items-center gap-2">
195+
<IconBolt
196+
size={16}
197+
className={hasAnyData ? "text-slate-200" : "text-slate-500"}
198+
/>
199+
<Text size="sm" className="truncate max-w-[150px]">
200+
{hasAnyData ? "ESC telemetry" : "No ESC telemetry"}
201+
</Text>
202+
</div>
135203

136204
<ActionIcon
137205
size="sm"
138206
variant="subtle"
139-
onClick={() => setMaximized(true)}
207+
onClick={() => setIsMaximized(true)}
140208
className="text-slate-400 hover:text-slate-200"
141209
title="Maximize ESC widget"
142210
>
@@ -147,9 +215,12 @@ export default function EscTelemetryWidget({
147215
)
148216
}
149217

150-
// Full view (make it stretch nicely when parent uses items-stretch)
218+
// Full view
151219
return (
152-
<div className="min-w-[350px] min-h-[253px] rounded-md flex flex-col" style={{ background: GetOutsideVisibilityColor() }}>
220+
<div
221+
className="min-w-[350px] min-h-[253px] rounded-md flex flex-col"
222+
style={{ background: GetOutsideVisibilityColor() }}
223+
>
153224
<div className="p-2 h-full flex flex-col">
154225
<div className="flex items-center justify-between mb-2">
155226
<Text>ESC telemetry</Text>
@@ -158,7 +229,7 @@ export default function EscTelemetryWidget({
158229
<ActionIcon
159230
size="sm"
160231
variant="subtle"
161-
onClick={() => setMaximized(false)}
232+
onClick={() => setIsMaximized(false)}
162233
className="text-slate-400 hover:text-slate-200"
163234
title="Minimize ESC widget"
164235
>
@@ -174,6 +245,104 @@ export default function EscTelemetryWidget({
174245
>
175246
<IconResize size={16} />
176247
</ActionIcon>
248+
249+
<Popover
250+
opened={settingsOpened}
251+
onChange={setSettingsOpened}
252+
position="top"
253+
withArrow
254+
shadow="md"
255+
width={260}
256+
>
257+
<Popover.Target>
258+
<ActionIcon
259+
size="sm"
260+
variant="subtle"
261+
onClick={() => setSettingsOpened((o) => !o)}
262+
className="text-slate-400 hover:text-slate-200"
263+
title="ESC threshold settings"
264+
>
265+
<IconSettings size={16} />
266+
</ActionIcon>
267+
</Popover.Target>
268+
269+
<Popover.Dropdown>
270+
<Stack gap="xs">
271+
<Text size="sm" fw={600}>
272+
ESC Thresholds
273+
</Text>
274+
275+
<Text size="xs" fw={600}>
276+
RPM
277+
</Text>
278+
<NumberInput
279+
label="Warning"
280+
value={thresholds.rpm.warning}
281+
onChange={(value) =>
282+
updateThreshold("rpm", "warning", value)
283+
}
284+
allowDecimal={false}
285+
/>
286+
<NumberInput
287+
label="Danger"
288+
value={thresholds.rpm.danger}
289+
onChange={(value) =>
290+
updateThreshold("rpm", "danger", value)
291+
}
292+
allowDecimal={false}
293+
/>
294+
295+
<Text size="xs" fw={600}>
296+
Current (A)
297+
</Text>
298+
<NumberInput
299+
label="Warning"
300+
value={thresholds.current.warning}
301+
onChange={(value) =>
302+
updateThreshold("current", "warning", value)
303+
}
304+
decimalScale={2}
305+
/>
306+
<NumberInput
307+
label="Danger"
308+
value={thresholds.current.danger}
309+
onChange={(value) =>
310+
updateThreshold("current", "danger", value)
311+
}
312+
decimalScale={2}
313+
/>
314+
315+
<Text size="xs" fw={600}>
316+
Temperature (°C)
317+
</Text>
318+
<NumberInput
319+
label="Warning"
320+
value={thresholds.temperature.warning}
321+
onChange={(value) =>
322+
updateThreshold("temperature", "warning", value)
323+
}
324+
allowDecimal={false}
325+
/>
326+
<NumberInput
327+
label="Danger"
328+
value={thresholds.temperature.danger}
329+
onChange={(value) =>
330+
updateThreshold("temperature", "danger", value)
331+
}
332+
allowDecimal={false}
333+
/>
334+
335+
<Text
336+
size="xs"
337+
c="blue"
338+
className="cursor-pointer text-center w-full"
339+
onClick={resetThresholds}
340+
>
341+
Reset thresholds
342+
</Text>
343+
</Stack>
344+
</Popover.Dropdown>
345+
</Popover>
177346
</div>
178347
</div>
179348

@@ -192,9 +361,9 @@ export default function EscTelemetryWidget({
192361
</div>
193362
) : (
194363
<div className="w-full h-full overflow-auto p-2">
195-
<div className="grid grid-cols-4 gap-2">
364+
<div className="grid grid-cols-4 gap-2 justify-items-center">
196365
{escs.map((esc) => (
197-
<EscTile key={esc.escId} esc={esc} />
366+
<EscTile key={esc.escId} esc={esc} thresholds={thresholds} />
198367
))}
199368
</div>
200369
</div>

gcs/src/components/dashboard/videoWidget.jsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
} from "../../redux/slices/droneConnectionSlice"
3434
import VideoWidgetSourceSelectModal from "./videoWidgetSourceSelectModal"
3535

36-
export default function VideoWidget({ telemetryPanelWidth }) {
36+
export default function VideoWidget() {
3737
const videoSource = useSelector(selectVideoSource)
3838
const isMaximized = useSelector(selectVideoMaximized)
3939
const scale = useSelector(selectVideoScale) // Scale factor for resizing
@@ -366,7 +366,10 @@ export default function VideoWidget({ telemetryPanelWidth }) {
366366

367367
{/* Minimized view */}
368368
{!isMaximized && !isPoppedOut && (
369-
<div className="rounded-md" style={{ background: GetOutsideVisibilityColor() }}>
369+
<div
370+
className="rounded-md"
371+
style={{ background: GetOutsideVisibilityColor() }}
372+
>
370373
<div className="p-2 flex items-center gap-2">
371374
{videoSource && error === null ? (
372375
<div
@@ -396,7 +399,10 @@ export default function VideoWidget({ telemetryPanelWidth }) {
396399

397400
{/* Full view */}
398401
{isMaximized && (
399-
<div className="min-w-[350px] min-h-[253px] rounded-md flex flex-col" style={{ background: GetOutsideVisibilityColor() }}>
402+
<div
403+
className="min-w-[350px] min-h-[253px] rounded-md flex flex-col"
404+
style={{ background: GetOutsideVisibilityColor() }}
405+
>
400406
<div className="p-2">
401407
<div className="flex items-center justify-between mb-2">
402408
<Text>{videoSource?.name || "No video selected"}</Text>

0 commit comments

Comments
 (0)