changing the visualizer to use the bins status controllers rather than controllers being passed in
This commit is contained in:
parent
a6b31c30c9
commit
cd5fbb29b7
4 changed files with 418 additions and 224 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue