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*/
49import { 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"
719import { useSelector } from "react-redux"
820import GetOutsideVisibilityColor from "../../helpers/outsideVisibility"
921import { 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+
1141function 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 >
0 commit comments