changing the visualizer to use the bins status controllers rather than controllers being passed in

This commit is contained in:
csawatzky 2026-06-12 14:05:32 -06:00
parent a6b31c30c9
commit cd5fbb29b7
4 changed files with 418 additions and 224 deletions

View file

@ -1,8 +1,8 @@
import { DateRange, PlayArrow, Start, Stop } from "@mui/icons-material"; import { DateRange, PlayArrow, Stop } from "@mui/icons-material";
import { Box, Button, darken, DialogActions, DialogContent, FormControl, IconButton, LinearProgress, MenuItem, Select, TextField, Typography } from "@mui/material"; import { Box, Button, DialogActions, DialogContent, FormControl, IconButton, LinearProgress, MenuItem, Select, TextField, Typography } from "@mui/material";
import { grey } from "@mui/material/colors"; import { grey } from "@mui/material/colors";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { GetDefaultDateRange } from "common/time/DateRange"; import { useMobile } from "hooks";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { Bin } from "models"; import { Bin } from "models";
import moment, { Moment } from "moment"; import moment, { Moment } from "moment";
@ -17,6 +17,8 @@ interface StatusCheckpoint {
time: Moment; time: Moment;
timeString: string; timeString: string;
cables: pond.GrainCable[]; cables: pond.GrainCable[];
fans: pond.BinFan[];
heaters: pond.BinHeater[];
} }
interface Props { interface Props {
@ -36,9 +38,20 @@ type CableReadingsAtCheckpoint = {
moisture?: ReadingSlot; moisture?: ReadingSlot;
}; };
/**
* Per-checkpoint bucket for boolean output components (fans, heaters).
* Only the most recent boolean reading in the time slice is kept.
*/
type BooleanStateAtCheckpoint = {
state?: ReadingSlot; // values[0] is the boolean (1 = on, 0 = off)
};
/** Per-checkpoint bucket: cable key → latest temp/humidity/moisture readings in that time slice. */ /** Per-checkpoint bucket: cable key → latest temp/humidity/moisture readings in that time slice. */
type CheckpointReadings = Map<string, CableReadingsAtCheckpoint>; type CheckpointReadings = Map<string, CableReadingsAtCheckpoint>;
/** Per-checkpoint bucket: component key → latest boolean state in that time slice. */
type CheckpointBooleanStates = Map<string, BooleanStateAtCheckpoint>;
/** /**
* Returns true when `candidate` should replace `existing` in a bucket. * Returns true when `candidate` should replace `existing` in a bucket.
* Used so multiple samples in the same checkpoint keep only the newest reading per type. * Used so multiple samples in the same checkpoint keep only the newest reading per type.
@ -133,6 +146,20 @@ function emptyCheckpointReadings(cableKeys: string[], count: number): Checkpoint
}); });
} }
/**
* Creates `count` empty boolean state buckets, each pre-populated per component key.
*/
function emptyCheckpointBooleanStates(
componentKeys: string[],
count: number
): CheckpointBooleanStates[] {
return Array.from({ length: count }, () => {
const map: CheckpointBooleanStates = new Map();
componentKeys.forEach(key => map.set(key, {}));
return map;
});
}
/** /**
* Walks every sampled measurement from each cable response and places it into a time bucket. * Walks every sampled measurement from each cable response and places it into a time bucket.
* `measurementResponses[i]` corresponds to `cableKeys[i]` (same order as the sample requests). * `measurementResponses[i]` corresponds to `cableKeys[i]` (same order as the sample requests).
@ -169,6 +196,48 @@ function assignMeasurementsToCheckpoints(
return buckets; return buckets;
} }
/**
* Walks every sampled measurement from each boolean output component (fan/heater) response
* and places the boolean state into a time bucket. Only the newest MEASUREMENT_TYPE_BOOLEAN
* reading per component per bucket is kept.
*/
function assignBooleanStatesToCheckpoints(
componentKeys: string[],
measurementResponses: pond.SampleUnitMeasurementsResponse[],
start: Moment,
end: Moment,
count: number
): CheckpointBooleanStates[] {
const buckets = emptyCheckpointBooleanStates(componentKeys, count);
measurementResponses.forEach((response, componentIndex) => {
const componentKey = componentKeys[componentIndex];
if (!componentKey) {
return;
}
response.measurements.forEach(um => {
if (um.type !== quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN) {
return;
}
um.timestamps.forEach((ts, i) => {
const valueArray = um.values[i];
if (!ts || !valueArray || valueArray.error || valueArray.values.length === 0) {
return;
}
const slot: ReadingSlot = { timestamp: ts, values: valueArray.values };
const idx = checkpointIndexForTimestamp(moment(ts), start, end, count);
const entry = buckets[idx].get(componentKey)!;
if (readingIsNewer(slot, entry.state)) {
entry.state = slot;
}
});
});
});
return buckets;
}
/** /**
* Picks the newest timestamp across temp, humidity, and moisture slots for a cable. * Picks the newest timestamp across temp, humidity, and moisture slots for a cable.
* Used as `lastRead` on the reconstructed `pond.GrainCable`. * Used as `lastRead` on the reconstructed `pond.GrainCable`.
@ -203,6 +272,20 @@ function mergeCarriedReadings(
}; };
} }
/**
* Combines boolean states carried forward from earlier checkpoints with the current bucket.
* If no reading exists in the current bucket the last known state is preserved, so fans/heaters
* don't flicker back to their template default between sparse samples.
*/
function mergeCarriedBooleanState(
carried: BooleanStateAtCheckpoint,
current: BooleanStateAtCheckpoint
): BooleanStateAtCheckpoint {
return {
state: current.state ?? carried.state
};
}
/** /**
* Clones a live `pond.GrainCable` template (name, key, device, top node, etc.) and overwrites * Clones a live `pond.GrainCable` template (name, key, device, top node, etc.) and overwrites
* node arrays from reconciled readings. `lastRead` is the newest measurement time, or the * node arrays from reconciled readings. `lastRead` is the newest measurement time, or the
@ -235,50 +318,136 @@ function buildGrainCableFromReadings(
} }
/** /**
* End-to-end pipeline: evenly spaced times bucket measurements carry-forward merge cables. * Clones a `pond.BinFan` template and overwrites its `state` from the reconciled boolean
* Returns one `StatusCheckpoint` per frame with `time` (playback moment) and `cables` * reading. If no reading has been seen yet the template state is left as-is (carry-forward
* (full `grainCables` array as it would have appeared at that moment). * will eventually set it once the first sample arrives).
*/
function buildBinFanFromState(
template: pond.BinFan,
booleanState: BooleanStateAtCheckpoint
): pond.BinFan {
const fan = pond.BinFan.fromObject(cloneDeep(template));
if (booleanState.state !== undefined) {
fan.state = booleanState.state.values[0] !== 0;
}
return fan;
}
/**
* Clones a `pond.BinHeater` template and overwrites its `state` from the reconciled boolean
* reading. Same carry-forward semantics as `buildBinFanFromState`.
*/
function buildBinHeaterFromState(
template: pond.BinHeater,
booleanState: BooleanStateAtCheckpoint
): pond.BinHeater {
const heater = pond.BinHeater.fromObject(cloneDeep(template));
if (booleanState.state !== undefined) {
heater.state = booleanState.state.values[0] !== 0;
}
return heater;
}
/**
* End-to-end pipeline: evenly spaced times bucket measurements carry-forward merge cables + fans + heaters.
* Returns one `StatusCheckpoint` per frame with `time` (playback moment), `cables`
* (full `grainCables` array), `fans`, and `heaters` as they would have appeared at that moment.
*/ */
function buildStatusCheckpoints( function buildStatusCheckpoints(
templates: pond.GrainCable[], cableTemplates: pond.GrainCable[],
measurementResponses: pond.SampleUnitMeasurementsResponse[], fanTemplates: pond.BinFan[],
heaterTemplates: pond.BinHeater[],
cableMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
fanMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
heaterMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
start: Moment, start: Moment,
end: Moment, end: Moment,
count: number = CHECKPOINT_COUNT count: number = CHECKPOINT_COUNT
): StatusCheckpoint[] { ): StatusCheckpoint[] {
const times = buildCheckpointTimes(start, end, count); const times = buildCheckpointTimes(start, end, count);
console.log(times) console.log(times);
const cableKeys = templates.map(c => c.key);
const buckets = assignMeasurementsToCheckpoints( // --- cables ---
const cableKeys = cableTemplates.map(c => c.key);
const cableBuckets = assignMeasurementsToCheckpoints(
cableKeys, cableKeys,
measurementResponses, cableMeasurementResponses,
start, start,
end, end,
count count
); );
// --- fans ---
const fanKeys = fanTemplates.map(f => f.key);
const fanBuckets = assignBooleanStatesToCheckpoints(
fanKeys,
fanMeasurementResponses,
start,
end,
count
);
// --- heaters ---
const heaterKeys = heaterTemplates.map(h => h.key);
const heaterBuckets = assignBooleanStatesToCheckpoints(
heaterKeys,
heaterMeasurementResponses,
start,
end,
count
);
// Carry-forward state per component
const carriedByCable = new Map<string, CableReadingsAtCheckpoint>(); const carriedByCable = new Map<string, CableReadingsAtCheckpoint>();
cableKeys.forEach(key => carriedByCable.set(key, {})); cableKeys.forEach(key => carriedByCable.set(key, {}));
return buckets.map((bucket, i) => { const carriedByFan = new Map<string, BooleanStateAtCheckpoint>();
fanKeys.forEach(key => carriedByFan.set(key, {}));
const carriedByHeater = new Map<string, BooleanStateAtCheckpoint>();
heaterKeys.forEach(key => carriedByHeater.set(key, {}));
return cableBuckets.map((cableBucket, i) => {
const time = times[i]; const time = times[i];
const cables = templates.map(template => {
// Build cables
const cables = cableTemplates.map(template => {
const key = template.key; const key = template.key;
const bucketReadings = bucket.get(key) ?? {}; const bucketReadings = cableBucket.get(key) ?? {};
const merged = mergeCarriedReadings(carriedByCable.get(key)!, bucketReadings); const merged = mergeCarriedReadings(carriedByCable.get(key)!, bucketReadings);
carriedByCable.set(key, merged); carriedByCable.set(key, merged);
return buildGrainCableFromReadings(template, merged, time); return buildGrainCableFromReadings(template, merged, time);
}); });
return { time, cables, timeString: time.toISOString() };
}).filter(checkpoint => //filter the checkpoints to only include ones that have readings // Build fans
const fans = fanTemplates.map(template => {
const key = template.key;
const bucketState = fanBuckets[i].get(key) ?? {};
const merged = mergeCarriedBooleanState(carriedByFan.get(key)!, bucketState);
carriedByFan.set(key, merged);
return buildBinFanFromState(template, merged);
});
// Build heaters
const heaters = heaterTemplates.map(template => {
const key = template.key;
const bucketState = heaterBuckets[i].get(key) ?? {};
const merged = mergeCarriedBooleanState(carriedByHeater.get(key)!, bucketState);
carriedByHeater.set(key, merged);
return buildBinHeaterFromState(template, merged);
});
return { time, cables, fans, heaters, timeString: time.toISOString() };
}).filter(checkpoint =>
// Keep checkpoints that have at least some cable data
checkpoint.cables.some(cable => checkpoint.cables.some(cable =>
cable.celcius.length > 0 || cable.celcius.length > 0 ||
cable.relativeHumidity.length > 0 || cable.relativeHumidity.length > 0 ||
cable.moisture.length > 0 cable.moisture.length > 0
)); )
);
} }
/** /**
* Calculates the number of checkpoints to create based on the start and end dates. * Calculates the number of checkpoints to create based on the start and end dates.
* Is hard coded to space the checkpoints 6 hours apart. * Is hard coded to space the checkpoints 6 hours apart.
@ -293,27 +462,28 @@ function checkpointCountFromRange(start: Moment, end: Moment): number {
/** /**
* This component is used to reconcile measurements with an array of bin objects in order to see how the bin has progressed over a set period. * This component is used to reconcile measurements with an array of bin objects in order to see how the bin has progressed over a set period.
* It gets the measurments for connected components, and uses those measurements to create a bin and simulate what its status would have been * It gets the measurements for connected components, and uses those measurements to create a bin and simulate what its status would have been
* at that time, creates an array of bins doing this and then uses a timer in a loop to change the bin in the status * at that time, creates an array of bins doing this and then uses a timer in a loop to change the bin in the status.
* Fans and heaters are included so playback also reflects which equipment was running at each checkpoint.
* @returns a JSX Element * @returns a JSX Element
*/ */
export default function BinPlayer(props: Props) { export default function BinPlayer(props: Props) {
const { bin, componentDevices, setBin } = props; const { bin, componentDevices, setBin } = props;
const isMobile = useMobile();
const componentAPI = useComponentAPI(); const componentAPI = useComponentAPI();
//the default start and end dates are 1 week ago and now
const [dateSelect, setDateSelect] = useState<number>(0); const [dateSelect, setDateSelect] = useState<number>(0);
const [startDate, setStartDate] = useState<Moment>(moment().subtract(1, 'week')); const [startDate, setStartDate] = useState<Moment>(moment().subtract(1, 'week'));
const [endDate, setEndDate] = useState<Moment>(moment); const [endDate, setEndDate] = useState<Moment>(moment);
const [dateRangeDialog, setDateRangeDialog] = useState(false); const [dateRangeDialog, setDateRangeDialog] = useState(false);
const [tempStartDate, setTempStartDate] = useState<Moment>(moment().subtract(1, 'week')); const [tempStartDate, setTempStartDate] = useState<Moment>(moment().subtract(1, 'week'));
const [tempEndDate, setTempEndDate] = useState<Moment>(moment()); const [tempEndDate, setTempEndDate] = useState<Moment>(moment());
const [currentCheckpointTime, setCurrentCheckpointTime] = useState<Moment | undefined>(undefined) const [currentCheckpointTime, setCurrentCheckpointTime] = useState<Moment | undefined>(undefined);
const [displayProgress, setDisplayProgress] = useState(0); const [displayProgress, setDisplayProgress] = useState(0);
const totalCheckpoints = useRef(0); const totalCheckpoints = useRef(0);
const checkpointStartTime = useRef(0); const checkpointStartTime = useRef(0);
const currentCheckpointIndex = useRef(0); const currentCheckpointIndex = useRef(0);
const isPlaying = useRef(false); const isPlaying = useRef(false);
const PLAYBACK_INTERVAL = 1000 //playback time between checkpoints in ms const PLAYBACK_INTERVAL = 1000;
const [isPlayingState, setIsPlayingState] = useState(false); const [isPlayingState, setIsPlayingState] = useState(false);
const stopPlayer = () => { isPlaying.current = false; }; const stopPlayer = () => { isPlaying.current = false; };
const originalBin = useRef<Bin | null>(null); const originalBin = useRef<Bin | null>(null);
@ -323,7 +493,7 @@ export default function BinPlayer(props: Props) {
const fps = 30; const fps = 30;
const interval = 1000 / fps; const interval = 1000 / fps;
const stepDuration = PLAYBACK_INTERVAL; // must match your setTimeout delay const stepDuration = PLAYBACK_INTERVAL;
const timer = setInterval(() => { const timer = setInterval(() => {
if (!isPlayingState) { if (!isPlayingState) {
@ -335,35 +505,55 @@ export default function BinPlayer(props: Props) {
if (total === 0) return; if (total === 0) return;
const idx = currentCheckpointIndex.current; const idx = currentCheckpointIndex.current;
// and change the progress formula to account for the last checkpoint reaching 100%:
const elapsed = Date.now() - checkpointStartTime.current; const elapsed = Date.now() - checkpointStartTime.current;
const stepProgress = Math.min(elapsed / stepDuration, 1); const stepProgress = Math.min(elapsed / stepDuration, 1);
// use (total - 1) as denominator so the last checkpoint goes from ~end to 100%
const raw = total <= 1 ? 100 : ((idx + stepProgress) / (total - 1)) * 100; const raw = total <= 1 ? 100 : ((idx + stepProgress) / (total - 1)) * 100;
setDisplayProgress(Math.min(raw, 100)); setDisplayProgress(Math.min(raw, 100));
}, interval); }, interval);
return () => clearInterval(timer); return () => clearInterval(timer);
}, [isPlayingState]); // re-run when playing state changes }, [isPlayingState]);
/** /**
* Fetches sampled unit measurements for every grain cable in the current bin status, * Fetches sampled unit measurements for every grain cable, fan, and heater in the current
* then builds `statusCheckpoints` for playback over [startDate, endDate]. * bin status, then builds `statusCheckpoints` for playback over [startDate, endDate].
*/ */
const startPlayer = async () => { const startPlayer = async () => {
originalBin.current = cloneDeep(bin); // capture before async work begins originalBin.current = cloneDeep(bin);
isPlaying.current = true; isPlaying.current = true;
setIsPlayingState(true) setIsPlayingState(true);
const sampleRequests = bin.status.grainCables.map(cable => { // Cable requests
const cableSampleRequests = bin.status.grainCables.map(cable => {
const deviceID = componentDevices.get(cable.key) ?? 0; const deviceID = componentDevices.get(cable.key) ?? 0;
return componentAPI.sampleUnitMeasurements(deviceID, cable.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true); return componentAPI.sampleUnitMeasurements(deviceID, cable.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
}); });
const responses = await Promise.all(sampleRequests); // Fan requests
const fanSampleRequests = bin.status.fans.map(fan => {
const deviceID = componentDevices.get(fan.key) ?? 0;
return componentAPI.sampleUnitMeasurements(deviceID, fan.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
});
// Heater requests
const heaterSampleRequests = bin.status.heaters.map(heater => {
const deviceID = componentDevices.get(heater.key) ?? 0;
return componentAPI.sampleUnitMeasurements(deviceID, heater.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
});
const [cableResponses, fanResponses, heaterResponses] = await Promise.all([
Promise.all(cableSampleRequests),
Promise.all(fanSampleRequests),
Promise.all(heaterSampleRequests),
]);
const checkpoints = buildStatusCheckpoints( const checkpoints = buildStatusCheckpoints(
bin.status.grainCables, bin.status.grainCables,
responses.map(r => r.data), bin.status.fans,
bin.status.heaters,
cableResponses.map(r => r.data),
fanResponses.map(r => r.data),
heaterResponses.map(r => r.data),
startDate, startDate,
endDate, endDate,
checkpointCountFromRange(startDate, endDate) checkpointCountFromRange(startDate, endDate)
@ -373,20 +563,25 @@ const startPlayer = async () => {
if (!isPlaying.current) { if (!isPlaying.current) {
setBin(originalBin.current!); setBin(originalBin.current!);
setCurrentCheckpointTime(undefined); setCurrentCheckpointTime(undefined);
setIsPlayingState(false) setIsPlayingState(false);
return; return;
} }
currentCheckpointIndex.current = i; currentCheckpointIndex.current = i;
totalCheckpoints.current = checkpoints.length; totalCheckpoints.current = checkpoints.length;
checkpointStartTime.current = Date.now(); checkpointStartTime.current = Date.now();
const newBin = cloneDeep(originalBin.current!); const newBin = cloneDeep(originalBin.current!);
newBin.status.grainCables = checkpoints[i].cables; newBin.status.grainCables = checkpoints[i].cables;
newBin.status.fans = checkpoints[i].fans;
newBin.status.heaters = checkpoints[i].heaters;
setBin(newBin); setBin(newBin);
setCurrentCheckpointTime(checkpoints[i].time) setCurrentCheckpointTime(checkpoints[i].time);
await new Promise(res => setTimeout(res, PLAYBACK_INTERVAL)); await new Promise(res => setTimeout(res, PLAYBACK_INTERVAL));
} }
isPlaying.current = false; isPlaying.current = false;
setIsPlayingState(false) setIsPlayingState(false);
setBin(originalBin.current!); setBin(originalBin.current!);
setCurrentCheckpointTime(undefined); setCurrentCheckpointTime(undefined);
}; };
@ -476,7 +671,6 @@ const dateSelector = () => {
case 2: case 2:
setStartDate(currentDate.clone().subtract(4, 'weeks')); setStartDate(currentDate.clone().subtract(4, 'weeks'));
break; break;
} }
}} }}
> >
@ -490,8 +684,8 @@ const dateSelector = () => {
<Button onClick={() => setDateRangeDialog(true)} color="primary"><DateRange /></Button> <Button onClick={() => setDateRangeDialog(true)} color="primary"><DateRange /></Button>
)} )}
</Box> </Box>
) );
} };
return ( return (
<React.Fragment> <React.Fragment>
@ -500,14 +694,14 @@ const dateSelector = () => {
{/* start/stop button */} {/* start/stop button */}
<Box> <Box>
{isPlayingState ? ( {isPlayingState ? (
<IconButton sx={{ border: "1px solid", borderRadius: "50%", padding: 1, height: 60, width: 60}} onClick={stopPlayer}><Stop fontSize="large" /></IconButton> <IconButton sx={{ border: "1px solid", borderRadius: "50%", padding: 1, height: isMobile ? 40 : 60, width: isMobile ? 40 : 60 }} onClick={stopPlayer}><Stop fontSize="large" /></IconButton>
) : ( ) : (
<IconButton sx={{ border: "1px solid", borderRadius: "50%", padding: 1, height: 60, width: 60 }} onClick={startPlayer}><PlayArrow fontSize="large" /></IconButton> <IconButton sx={{ border: "1px solid", borderRadius: "50%", padding: 1, height: isMobile ? 40 : 60, width: isMobile ? 40 : 60 }} onClick={startPlayer}><PlayArrow fontSize="large" /></IconButton>
)} )}
</Box> </Box>
{/* progress bar and date display */} {/* progress bar and date display */}
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Typography textAlign={"center"}>{currentCheckpointTime ? currentCheckpointTime.format("MMM D, YYYY h:mm A") : moment().format("MMM D, YYYY h:mm A")}</Typography> <Typography fontSize={isMobile ? 15 : 17} textAlign={"center"}>{currentCheckpointTime ? currentCheckpointTime.format("MMM D, YYYY h:mm A") : moment().format("MMM D, YYYY h:mm A")}</Typography>
<LinearProgress value={displayProgress} variant="determinate" color="inherit" /> <LinearProgress value={displayProgress} variant="determinate" color="inherit" />
<Box display="flex" flexDirection="row" justifyContent="space-between"> <Box display="flex" flexDirection="row" justifyContent="space-between">
<Typography variant="caption">{startDate.format("MMM D, YYYY")}</Typography> <Typography variant="caption">{startDate.format("MMM D, YYYY")}</Typography>

View file

@ -16,8 +16,8 @@ interface Props {
bin: Bin bin: Bin
// cables?: GrainCable[] // cables?: GrainCable[]
devices: Device[] devices: Device[]
fans?: Controller[] // fans?: Controller[]
heaters?: Controller[] // heaters?: Controller[]
permissions: pond.Permission[] permissions: pond.Permission[]
componentDevices: Map<string, number> componentDevices: Map<string, number>
componentMap: Map<string, Component> componentMap: Map<string, Component>
@ -25,7 +25,7 @@ interface Props {
updateBinCallback?: (bin: Bin) => void updateBinCallback?: (bin: Bin) => void
} }
export default function bin3dVisualizer(props: Props){ export default function bin3dVisualizer(props: Props){
const {bin, fans, heaters, permissions, devices, componentDevices, componentMap, binPrefs, updateBinCallback} = props const {bin, permissions, devices, componentDevices, componentMap, binPrefs, updateBinCallback} = props
const theme = useTheme() const theme = useTheme()
const [showHeatmap, setShowHeatmap] = useState(true) const [showHeatmap, setShowHeatmap] = useState(true)
// const [showHotspots, setShowHotspots] = useState(false) // const [showHotspots, setShowHotspots] = useState(false)
@ -86,7 +86,6 @@ export default function bin3dVisualizer(props: Props){
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' }, '&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' },
}} }}
onChange={event => { onChange={event => {
console.log("change bin mode")
setBinMode(event.target.value as pond.BinMode) setBinMode(event.target.value as pond.BinMode)
setOpenModeChange(true) setOpenModeChange(true)
}} }}
@ -159,38 +158,37 @@ export default function bin3dVisualizer(props: Props){
) )
} }
const controllerState = (controller: Controller) => { // const controllerState = (controller: Controller) => {
let state = false // let state = false
controller.status.lastGoodMeasurement.forEach(um => { // controller.status.lastGoodMeasurement.forEach(um => {
if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN){ // if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN){
if(um.values.length > 0){ // if(um.values.length > 0){
let readings = um.values // let readings = um.values
if(readings[readings.length-1].values.length > 0){ // if(readings[readings.length-1].values.length > 0){
let nodes = readings[readings.length-1].values // let nodes = readings[readings.length-1].values
if (nodes[nodes.length -1] === 1){ // if (nodes[nodes.length -1] === 1){
state = true // state = true
} // }
} // }
} // }
} // }
}) // })
return state // return state
} // }
const controllerDisplay = () => { const controllerDisplay = () => {
if (fans && fans.length > 1 || heaters && heaters.length > 1){ if ((bin.status.fans && bin.status.fans.length > 0) || (bin.status.heaters && bin.status.heaters.length > 0)){
return ( return (
<Box width="100%"> <Box width="100%">
<Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 17}}> <Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 17}}>
Controllers Controllers
</Typography> </Typography>
{fans && fans.map(fan => { {bin.status.fans && bin.status.fans.map(fan => {
let isOn = controllerState(fan) // let isOn = controllerState(fan)
return ( return (
<Box <Box
key={fan.key()} key={fan.key}
sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -202,25 +200,25 @@ export default function bin3dVisualizer(props: Props){
py: 1, py: 1,
}} }}
> >
<Typography sx={{fontSize: 13}}>{fan.name()}</Typography> <Typography sx={{fontSize: 13}}>{fan.name}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography sx={{fontWeight: 650, fontSize: 13}}> <Typography sx={{fontWeight: 650, fontSize: 13}}>
{isOn ? "ON" : "OFF"} {fan.state ? "ON" : "OFF"}
</Typography> </Typography>
<Box sx={{ <Box sx={{
width: 8, width: 8,
height: 8, height: 8,
borderRadius: '50%', borderRadius: '50%',
bgcolor: isOn ? 'success.main' : 'error.main' bgcolor: fan.state ? 'success.main' : 'error.main'
}} /> }} />
</Box> </Box>
</Box> </Box>
)})} )})}
{heaters && heaters.map(heater => { {bin.status.heaters && bin.status.heaters.map(heater => {
let isOn = controllerState(heater) // let isOn = controllerState(heater)
return ( return (
<Box <Box
key={heater.key()} key={heater.key}
sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -232,16 +230,16 @@ export default function bin3dVisualizer(props: Props){
py: 1, py: 1,
}} }}
> >
<Typography sx={{fontSize: 13}}>{heater.name()}</Typography> <Typography sx={{fontSize: 13}}>{heater.name}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography sx={{fontWeight: 650, fontSize: 13}}> <Typography sx={{fontWeight: 650, fontSize: 13}}>
{isOn ? "ON" : "OFF"} {heater.state ? "ON" : "OFF"}
</Typography> </Typography>
<Box sx={{ <Box sx={{
width: 8, width: 8,
height: 8, height: 8,
borderRadius: '50%', borderRadius: '50%',
bgcolor: isOn ? 'success.main' : 'error.main' bgcolor: heater.state ? 'success.main' : 'error.main'
}} /> }} />
</Box> </Box>
</Box> </Box>
@ -382,17 +380,23 @@ export default function bin3dVisualizer(props: Props){
//filter the controls so that only components on THIS device are options for new interactions, a side note of this using the components that are in the bins status //filter the controls so that only components on THIS device are options for new interactions, a side note of this using the components that are in the bins status
//is that it will indirectly also filter by bin as well so only components that are both on the bin and the same device as the cable will appear //is that it will indirectly also filter by bin as well so only components that are both on the bin and the same device as the cable will appear
let filtered: Component[] = [] let filtered: Component[] = []
if(fans){ if(bin.status.fans){
fans.forEach((f) => { bin.status.fans.forEach((f) => {
if(componentDevices.get(f.key()) === d.id()){ if(componentDevices.get(f.key) === d.id()){
filtered.push(f.asComponent()) let comp = componentMap.get(f.key)
if(comp){
filtered.push(comp)
}
} }
}) })
} }
if(heaters){ if(bin.status.heaters){
heaters.forEach((h) => { bin.status.heaters.forEach((h) => {
if(componentDevices.get(h.key()) === d.id()){ if(componentDevices.get(h.key) === d.id()){
filtered.push(h.asComponent()) let comp = componentMap.get(h.key)
if(comp){
filtered.push(comp)
}
} }
}) })
} }

View file

@ -342,8 +342,6 @@ export default function BinSummary(props: Props){
<Card raised sx={{height: 600, position: "relative", borderRadius: 4}}> <Card raised sx={{height: 600, position: "relative", borderRadius: 4}}>
<Bin3dVisualizer <Bin3dVisualizer
bin={bin} bin={bin}
fans={fans}
heaters={heaters}
permissions={permissions} permissions={permissions}
devices={devices} devices={devices}
componentDevices={componentDevices} componentDevices={componentDevices}

View file

@ -93,8 +93,6 @@ export default function BinDetails (props: Props) {
<TabPanelMine value={currentTab} index={"bin"}> <TabPanelMine value={currentTab} index={"bin"}>
<Bin3dVisualizer <Bin3dVisualizer
bin={bin} bin={bin}
fans={fans}
heaters={heaters}
permissions={permissions} permissions={permissions}
devices={devices} devices={devices}
componentDevices={componentDevices} componentDevices={componentDevices}