Skip to content

Commit c1fb03d

Browse files
authored
Merge pull request #63 from ProjectLighthouseCAU/monitor-filter
Add support for monitoring arbitrary properties visually
2 parents 503be8e + 4d75490 commit c1fb03d

File tree

6 files changed

+243
-37
lines changed

6 files changed

+243
-37
lines changed
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
import { LampV2Metrics } from '@luna/contexts/api/model/types';
22
import { FlatRoomV2Metrics } from '@luna/screens/home/admin/helpers/FlatRoomV2Metrics';
3+
import { MonitorCriterion } from '@luna/screens/home/admin/helpers/MonitorCriterion';
34
import { MonitorInspectorLampsCard } from '@luna/screens/home/admin/MonitorInspectorLampsCard';
45
import { MonitorInspectorRoomCard } from '@luna/screens/home/admin/MonitorInspectorRoomCard';
56

67
export interface MonitorInspectorProps {
8+
criterion?: MonitorCriterion;
9+
setCriterion: (criterion?: MonitorCriterion) => void;
710
flatRoomMetrics?: FlatRoomV2Metrics;
811
lampMetrics?: LampV2Metrics[];
912
}
1013

1114
export function MonitorInspector({
15+
criterion,
16+
setCriterion,
1217
flatRoomMetrics,
1318
lampMetrics,
1419
}: MonitorInspectorProps) {
1520
return (
1621
<div className="flex flex-col space-y-3">
17-
<MonitorInspectorRoomCard metrics={flatRoomMetrics} />
18-
<MonitorInspectorLampsCard metrics={lampMetrics ?? []} />
22+
<MonitorInspectorRoomCard
23+
criterion={criterion?.type === 'room' ? criterion : undefined}
24+
setCriterion={setCriterion}
25+
metrics={flatRoomMetrics}
26+
/>
27+
<MonitorInspectorLampsCard
28+
criterion={criterion?.type === 'lamp' ? criterion : undefined}
29+
setCriterion={setCriterion}
30+
metrics={lampMetrics ?? []}
31+
/>
1932
</div>
2033
);
2134
}

src/screens/home/admin/MonitorInspectorLampsCard.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { TitledCard } from '@luna/components/TitledCard';
22
import { LampV2Metrics } from '@luna/contexts/api/model/types';
3+
import { MonitorLampCriterion } from '@luna/screens/home/admin/helpers/MonitorCriterion';
34
import { MonitorInspectorTable } from '@luna/screens/home/admin/MonitorInspectorTable';
45
import { MonitorInspectorValue } from '@luna/screens/home/admin/MonitorInspectorValue';
56
import { IconCheck, IconLamp } from '@tabler/icons-react';
67

78
export interface MonitorInspectorLampsCardProps {
9+
criterion?: MonitorLampCriterion;
10+
setCriterion: (criterion?: MonitorLampCriterion) => void;
811
metrics: LampV2Metrics[];
912
}
1013

@@ -19,6 +22,8 @@ const names: { [Property in keyof LampV2Metrics]: string } = {
1922
};
2023

2124
export function MonitorInspectorLampsCard({
25+
criterion,
26+
setCriterion,
2227
metrics,
2328
}: MonitorInspectorLampsCardProps) {
2429
return (
@@ -27,6 +32,10 @@ export function MonitorInspectorLampsCard({
2732
<MonitorInspectorTable
2833
metrics={metrics}
2934
names={names}
35+
selection={criterion?.key}
36+
onSelect={key =>
37+
setCriterion(key ? { type: 'lamp', key } : undefined)
38+
}
3039
render={(value, prop) => (
3140
<MonitorInspectorLampValue value={value} prop={prop} />
3241
)}

src/screens/home/admin/MonitorInspectorRoomCard.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { TitledCard } from '@luna/components/TitledCard';
22
import { FlatRoomV2Metrics } from '@luna/screens/home/admin/helpers/FlatRoomV2Metrics';
3+
import { MonitorRoomCriterion } from '@luna/screens/home/admin/helpers/MonitorCriterion';
34
import { MonitorInspectorTable } from '@luna/screens/home/admin/MonitorInspectorTable';
45
import { MonitorInspectorValue } from '@luna/screens/home/admin/MonitorInspectorValue';
56
import { IconDoor } from '@tabler/icons-react';
6-
import { useMemo, useState } from 'react';
7+
import { useMemo } from 'react';
78

89
export interface MonitorInspectorRoomCardProps {
10+
criterion?: MonitorRoomCriterion;
11+
setCriterion: (criterion?: MonitorRoomCriterion) => void;
912
metrics?: FlatRoomV2Metrics;
1013
}
1114

@@ -37,9 +40,10 @@ const units: { [Property in keyof FlatRoomV2Metrics]?: string } = {
3740
};
3841

3942
export function MonitorInspectorRoomCard({
43+
criterion,
44+
setCriterion,
4045
metrics,
4146
}: MonitorInspectorRoomCardProps) {
42-
const [selection, setSelection] = useState<keyof FlatRoomV2Metrics>();
4347
const renderedMetrics = useMemo<FlatRoomV2Metrics[]>(
4448
() => (metrics ? [metrics] : []),
4549
[metrics]
@@ -51,8 +55,10 @@ export function MonitorInspectorRoomCard({
5155
<MonitorInspectorTable
5256
metrics={renderedMetrics}
5357
names={names}
54-
selection={selection}
55-
onSelect={setSelection}
58+
selection={criterion?.key}
59+
onSelect={key =>
60+
setCriterion(key ? { type: 'room', key } : undefined)
61+
}
5662
render={(value, prop) => (
5763
<MonitorInspectorRoomValue value={value} prop={prop} />
5864
)}

src/screens/home/admin/MonitorView.tsx

Lines changed: 104 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@ import { LaserMetrics, RoomV2Metrics } from '@luna/contexts/api/model/types';
44
import { Breakpoint, useBreakpoint } from '@luna/hooks/useBreakpoint';
55
import { useEventListener } from '@luna/hooks/useEventListener';
66
import { flattenRoomV2Metrics } from '@luna/screens/home/admin/helpers/FlatRoomV2Metrics';
7+
import { MonitorCriterion } from '@luna/screens/home/admin/helpers/MonitorCriterion';
78
import { MonitorInspector } from '@luna/screens/home/admin/MonitorInspector';
89
import { HomeContent } from '@luna/screens/home/HomeContent';
10+
import * as rgb from '@luna/utils/rgb';
911
import { throttle } from '@luna/utils/schedule';
1012
import { Vec2 } from '@luna/utils/vec2';
1113
import { Button } from '@heroui/react';
1214
import { IconRefresh } from '@tabler/icons-react';
1315
import { Set } from 'immutable';
14-
import {
15-
LIGHTHOUSE_COLOR_CHANNELS,
16-
LIGHTHOUSE_COLS,
17-
LIGHTHOUSE_FRAME_BYTES,
18-
} from 'nighthouse/browser';
16+
import { LIGHTHOUSE_COLS, LIGHTHOUSE_FRAME_BYTES } from 'nighthouse/browser';
1917
import {
2018
useCallback,
2119
useContext,
@@ -24,6 +22,7 @@ import {
2422
useRef,
2523
useState,
2624
} from 'react';
25+
import { Bounded, isBounded } from '@luna/utils/bounded';
2726

2827
export function MonitorView() {
2928
const [maxSize, setMaxSize] = useState({ width: 0, height: 0 });
@@ -58,6 +57,7 @@ export function MonitorView() {
5857

5958
const [focusedRoom, setSelectedRoom] = useState<number>();
6059
const [hoveredRoom, setHoveredRoom] = useState<number>();
60+
const [criterion, setCriterion] = useState<MonitorCriterion>();
6161

6262
const getLatestMetrics = useCallback(async () => {
6363
// setMetrics(testMetrics); // TODO: change back from test data to fetched data
@@ -75,7 +75,10 @@ export function MonitorView() {
7575
}, [getLatestMetrics]);
7676

7777
const roomMetrics = useMemo(
78-
() => (metrics?.rooms ?? []) as RoomV2Metrics[],
78+
() =>
79+
(metrics?.rooms ?? []).filter(
80+
room => room.api_version === 2
81+
) as RoomV2Metrics[],
7982
[metrics?.rooms]
8083
);
8184

@@ -84,40 +87,108 @@ export function MonitorView() {
8487
[roomMetrics]
8588
);
8689

90+
const valueToNumberOrNull = useCallback(
91+
(value: number | string | boolean | Bounded<number> | null | undefined) => {
92+
if (value === null || value === undefined) {
93+
return null;
94+
}
95+
if (typeof value === 'object' && isBounded(value)) {
96+
return value.value;
97+
}
98+
return +value;
99+
},
100+
[]
101+
);
102+
103+
const criterionValues = useMemo(() => {
104+
if (criterion === undefined) return undefined;
105+
switch (criterion.type) {
106+
case 'room':
107+
return flatRoomMetrics.flatMap(room =>
108+
[...Array(room.responsive_lamps.total)].map(() =>
109+
valueToNumberOrNull(room[criterion.key])
110+
)
111+
);
112+
case 'lamp':
113+
return roomMetrics.flatMap(room =>
114+
room.lamp_metrics.map(lamp =>
115+
valueToNumberOrNull(lamp[criterion.key])
116+
)
117+
);
118+
}
119+
}, [criterion, flatRoomMetrics, roomMetrics, valueToNumberOrNull]);
120+
121+
const normalizedCriterionValues = useMemo(() => {
122+
if (criterionValues === undefined) return undefined;
123+
if (criterionValues.length === 0) return [];
124+
const nonNulls = criterionValues.filter(v => v !== null) as number[];
125+
const min = nonNulls.reduce((x, y) => Math.min(x, y));
126+
const max = nonNulls.reduce((x, y) => Math.max(x, y));
127+
return criterionValues.map(x => (x === null ? 0 : (x - min) / (max - min)));
128+
}, [criterionValues]);
129+
130+
const criterionColormap = useMemo(() => {
131+
if (criterion !== undefined) {
132+
switch (criterion.type) {
133+
case 'room':
134+
switch (criterion.key) {
135+
case 'board_temperature':
136+
case 'core_temperature':
137+
return [rgb.BLUE, rgb.RED];
138+
case 'responding':
139+
return [rgb.GREEN, rgb.RED];
140+
}
141+
break;
142+
case 'lamp':
143+
switch (criterion.key) {
144+
case 'responding':
145+
case 'fuse_tripped':
146+
return [rgb.GREEN, rgb.RED];
147+
}
148+
break;
149+
}
150+
}
151+
return [rgb.BLACK, rgb.RED, rgb.YELLOW, rgb.WHITE];
152+
}, [criterion]);
153+
87154
// fill the frame with colors according to the metrics data
88155
const frame = useMemo(() => {
89156
const frame = new Uint8Array(LIGHTHOUSE_FRAME_BYTES);
90-
// alternate between light and dark color to visualize room borders
91-
let parity = false;
92-
let i = 0;
93-
for (const room of roomMetrics) {
94-
if (room.api_version !== 2) continue;
95-
const endIdx = i + LIGHTHOUSE_COLOR_CHANNELS * room.lamp_metrics.length;
96-
// controller works?
97-
if (room.controller_metrics.responding) {
98-
let lampIdx = 0;
99-
for (; i < endIdx; i += LIGHTHOUSE_COLOR_CHANNELS) {
100-
// lamp works?
101-
if (room.lamp_metrics[lampIdx].responding) {
102-
frame[i + 1] = parity ? 255 : 128; // green
103-
} else {
104-
// lamp down -> magenta
105-
frame[i] = parity ? 255 : 128;
106-
frame[i + 2] = parity ? 255 : 128;
157+
let windowIdx = 0;
158+
159+
const hasActiveCriterion = normalizedCriterionValues !== undefined;
160+
if (hasActiveCriterion) {
161+
for (const value of normalizedCriterionValues as number[]) {
162+
const color = rgb.lerpMultiple(criterionColormap, value);
163+
rgb.setAt(windowIdx, color, frame);
164+
windowIdx++;
165+
}
166+
} else {
167+
// alternate between light and dark color to visualize room borders
168+
let parity = false;
169+
const parityDim = (c: rgb.Color) => rgb.scale(c, parity ? 1 : 0.6);
170+
for (const room of roomMetrics) {
171+
const lampCount = room.lamp_metrics.length;
172+
// controller works?
173+
if (room.controller_metrics.responding) {
174+
for (let lampIdx = 0; lampIdx < lampCount; lampIdx++) {
175+
// lamp works?
176+
const color = parityDim(
177+
room.lamp_metrics[lampIdx].responding ? rgb.GREEN : rgb.MAGENTA
178+
);
179+
rgb.setAt(windowIdx + lampIdx, color, frame);
107180
}
108-
lampIdx++;
109-
}
110-
} else {
111-
// controller down
112-
for (; i < endIdx; i += 3) {
113-
frame[i] = parity ? 255 : 128; // red
181+
} else {
182+
// controller down
183+
rgb.fillAt(windowIdx, lampCount, parityDim(rgb.RED), frame);
114184
}
185+
windowIdx += lampCount;
186+
parity = !parity;
115187
}
116-
parity = !parity;
117188
}
118189

119190
return frame;
120-
}, [roomMetrics]);
191+
}, [criterionColormap, normalizedCriterionValues, roomMetrics]);
121192

122193
const [roomsByWindow, windowsByRoom] = useMemo<[number[], number[][]]>(() => {
123194
const roomsByWindow: number[] = [];
@@ -207,6 +278,8 @@ export function MonitorView() {
207278
className={isCompact ? '' : 'flex flex-row justify-end grow-0 w-1/3'}
208279
>
209280
<MonitorInspector
281+
criterion={criterion}
282+
setCriterion={setCriterion}
210283
flatRoomMetrics={focusedFlatRoomMetrics}
211284
lampMetrics={focusedLampMetrics}
212285
/>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { LampV2Metrics } from '@luna/contexts/api/model/types';
2+
import { FlatRoomV2Metrics } from '@luna/screens/home/admin/helpers/FlatRoomV2Metrics';
3+
4+
export interface MonitorRoomCriterion {
5+
type: 'room';
6+
key: keyof FlatRoomV2Metrics;
7+
}
8+
9+
export interface MonitorLampCriterion {
10+
type: 'lamp';
11+
key: keyof LampV2Metrics;
12+
}
13+
14+
export type MonitorCriterion = MonitorRoomCriterion | MonitorLampCriterion;

0 commit comments

Comments
 (0)