updated the colldown mode, fixed a bug where the bin mode on the page was not changing in the dropdown after completing the mode change and some UI tweaks like text and descriptions
This commit is contained in:
parent
e91d2f6005
commit
1ac937e7db
9 changed files with 500 additions and 101 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
import React from "react";
|
import { useThree, useFrame } from "@react-three/fiber";
|
||||||
import { useThree } from "@react-three/fiber";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { Vector2, Vector3 } from "three";
|
import { Vector2, Vector3 } from "three";
|
||||||
|
|
||||||
interface CameraTarget {
|
interface CameraTarget {
|
||||||
|
|
@ -45,6 +44,18 @@ interface Props {
|
||||||
* Wire it by passing a ref setter: onReset={fn => resetFn.current = fn}
|
* Wire it by passing a ref setter: onReset={fn => resetFn.current = fn}
|
||||||
*/
|
*/
|
||||||
onReset?: (resetFn: () => void) => void;
|
onReset?: (resetFn: () => void) => void;
|
||||||
|
/**
|
||||||
|
* Automatically spin the camera around the target on mount.
|
||||||
|
* Any user interaction (drag, pan, zoom, pinch) stops it permanently
|
||||||
|
* until the camera is reset (reset restarts it if this is still true).
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
autoRotate?: boolean;
|
||||||
|
/**
|
||||||
|
* Auto-rotate speed in radians per second.
|
||||||
|
* @default 0.3
|
||||||
|
*/
|
||||||
|
autoRotateSpeed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OrbitCameraControls(props: Props) {
|
export default function OrbitCameraControls(props: Props) {
|
||||||
|
|
@ -58,6 +69,8 @@ export default function OrbitCameraControls(props: Props) {
|
||||||
initialRadius,
|
initialRadius,
|
||||||
maxRadius,
|
maxRadius,
|
||||||
onReset,
|
onReset,
|
||||||
|
autoRotate = false,
|
||||||
|
autoRotateSpeed = 0.3,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { camera, gl } = useThree();
|
const { camera, gl } = useThree();
|
||||||
|
|
@ -75,6 +88,9 @@ export default function OrbitCameraControls(props: Props) {
|
||||||
const panCameraPosition = useRef(new Vector3());
|
const panCameraPosition = useRef(new Vector3());
|
||||||
const lastPinchDist = useRef(0);
|
const lastPinchDist = useRef(0);
|
||||||
|
|
||||||
|
// Auto-rotate: active until the user does *anything* (drag/pan/zoom/pinch)
|
||||||
|
const autoRotateActive = useRef(autoRotate);
|
||||||
|
|
||||||
// Spherical coords
|
// Spherical coords
|
||||||
const spherical = useRef({
|
const spherical = useRef({
|
||||||
radius: initialRadius ?? 10,
|
radius: initialRadius ?? 10,
|
||||||
|
|
@ -82,14 +98,9 @@ export default function OrbitCameraControls(props: Props) {
|
||||||
phi: Math.PI / 2, // vertical angle
|
phi: Math.PI / 2, // vertical angle
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// Pulled out of the effect so both the DOM-event effect and the
|
||||||
// Sync radius whenever initialRadius prop changes (e.g. after bin data loads)
|
// useFrame auto-rotate loop below can call it.
|
||||||
spherical.current.radius = initialRadius ?? 10;
|
const updateCamera = useCallback(() => {
|
||||||
|
|
||||||
const canvas = gl.domElement;
|
|
||||||
canvas.addEventListener("contextmenu", e => e.preventDefault());
|
|
||||||
|
|
||||||
const updateCamera = () => {
|
|
||||||
const { radius, theta, phi } = spherical.current;
|
const { radius, theta, phi } = spherical.current;
|
||||||
|
|
||||||
const x = radius * Math.sin(phi) * Math.sin(theta);
|
const x = radius * Math.sin(phi) * Math.sin(theta);
|
||||||
|
|
@ -110,7 +121,22 @@ export default function OrbitCameraControls(props: Props) {
|
||||||
// Shift the projected image without affecting orbit
|
// Shift the projected image without affecting orbit
|
||||||
const { width, height } = gl.domElement.getBoundingClientRect();
|
const { width, height } = gl.domElement.getBoundingClientRect();
|
||||||
(camera as any).setViewOffset(width, height, viewOffset ?? 0, 0, width, height);
|
(camera as any).setViewOffset(width, height, viewOffset ?? 0, 0, width, height);
|
||||||
};
|
}, [camera, gl, viewOffset]);
|
||||||
|
|
||||||
|
// Drives the auto-spin every frame while it's active. Runs inside the
|
||||||
|
// R3F render loop, so it's cheap and stays in sync with everything else.
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
if (!autoRotateActive.current) return;
|
||||||
|
spherical.current.theta += autoRotateSpeed * delta;
|
||||||
|
updateCamera();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Sync radius whenever initialRadius prop changes (e.g. after bin data loads)
|
||||||
|
spherical.current.radius = initialRadius ?? 10;
|
||||||
|
|
||||||
|
const canvas = gl.domElement;
|
||||||
|
canvas.addEventListener("contextmenu", e => e.preventDefault());
|
||||||
|
|
||||||
// Hand the reset function to the caller so they can trigger it
|
// Hand the reset function to the caller so they can trigger it
|
||||||
// (e.g. from a button in CameraOverlay) without needing an external ref.
|
// (e.g. from a button in CameraOverlay) without needing an external ref.
|
||||||
|
|
@ -124,15 +150,25 @@ export default function OrbitCameraControls(props: Props) {
|
||||||
y: (target?.y ?? 0) + (offset?.y ?? 0),
|
y: (target?.y ?? 0) + (offset?.y ?? 0),
|
||||||
z: (target?.z ?? 0) + (offset?.z ?? 0),
|
z: (target?.z ?? 0) + (offset?.z ?? 0),
|
||||||
};
|
};
|
||||||
|
// Resuming auto-rotate on reset feels natural — remove this
|
||||||
|
// line if you'd rather it stay stopped once the user has
|
||||||
|
// touched the camera.
|
||||||
|
autoRotateActive.current = autoRotate;
|
||||||
updateCamera();
|
updateCamera();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCamera();
|
updateCamera();
|
||||||
|
|
||||||
|
const stopAutoRotate = () => {
|
||||||
|
autoRotateActive.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
if (e.target !== canvas) return;
|
if (e.target !== canvas) return;
|
||||||
|
|
||||||
|
stopAutoRotate();
|
||||||
|
|
||||||
lastPos.current = { x: e.clientX, y: e.clientY };
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||||
panCameraPosition.current.copy(camera.position);
|
panCameraPosition.current.copy(camera.position);
|
||||||
|
|
||||||
|
|
@ -213,6 +249,7 @@ export default function OrbitCameraControls(props: Props) {
|
||||||
if (e.target !== canvas) return;
|
if (e.target !== canvas) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
stopAutoRotate();
|
||||||
|
|
||||||
spherical.current.radius += e.deltaY * 0.01;
|
spherical.current.radius += e.deltaY * 0.01;
|
||||||
spherical.current.radius = Math.max(
|
spherical.current.radius = Math.max(
|
||||||
|
|
@ -226,6 +263,7 @@ export default function OrbitCameraControls(props: Props) {
|
||||||
const handleTouchStart = (e: TouchEvent) => {
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
if (e.target !== canvas) return;
|
if (e.target !== canvas) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
stopAutoRotate();
|
||||||
|
|
||||||
if (e.touches.length === 1) {
|
if (e.touches.length === 1) {
|
||||||
isDragging.current = true;
|
isDragging.current = true;
|
||||||
|
|
@ -323,7 +361,7 @@ export default function OrbitCameraControls(props: Props) {
|
||||||
canvas.removeEventListener("touchmove", handleTouchMove);
|
canvas.removeEventListener("touchmove", handleTouchMove);
|
||||||
canvas.removeEventListener("touchend", handleTouchEnd);
|
canvas.removeEventListener("touchend", handleTouchEnd);
|
||||||
};
|
};
|
||||||
}, [camera, gl, target, clampVerticalRotation, minPhi, maxPhi, initialRadius, maxRadius]);
|
}, [camera, gl, target, offset, clampVerticalRotation, minPhi, maxPhi, initialRadius, maxRadius, onReset, autoRotate, updateCamera]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +145,8 @@ export default function Bin3dView(props: Props) {
|
||||||
<Canvas style={{position: "absolute", inset: 0}}>
|
<Canvas style={{position: "absolute", inset: 0}}>
|
||||||
<OrbitCameraControls
|
<OrbitCameraControls
|
||||||
clampVerticalRotation
|
clampVerticalRotation
|
||||||
|
autoRotate
|
||||||
|
autoRotateSpeed={0.5}
|
||||||
initialRadius={initialCameraRadius}
|
initialRadius={initialCameraRadius}
|
||||||
maxRadius={initialCameraRadius * 2}
|
maxRadius={initialCameraRadius * 2}
|
||||||
onReset={fn => { resetCameraFn.current = fn; }}
|
onReset={fn => { resetCameraFn.current = fn; }}
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,9 @@ export default function bin3dVisualizer(props: Props){
|
||||||
newMode={newMode}
|
newMode={newMode}
|
||||||
open={openModeChange}
|
open={openModeChange}
|
||||||
onClose={()=>{setOpenModeChange(false)}}
|
onClose={()=>{setOpenModeChange(false)}}
|
||||||
|
modeUpdated={(newMode) => {
|
||||||
|
setBinMode(newMode)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Box position={"relative"} height={600}>
|
<Box position={"relative"} height={600}>
|
||||||
<Bin3dView
|
<Bin3dView
|
||||||
|
|
|
||||||
|
|
@ -171,10 +171,12 @@ export default function BinModeController(props: Props) {
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<PromiseProgress
|
<PromiseProgress
|
||||||
stages={promiseStages}
|
stages={promiseStages}
|
||||||
description="These are the changes that will occur in order to change your bin mode with the given options. Press start to begin."
|
description="These are the changes that will occur in order to change your bin mode with the given options.
|
||||||
|
It will allow the device complete control of the selected components. Your grain is still your responsibility.
|
||||||
|
Press start to begin."
|
||||||
failFast
|
failFast
|
||||||
onStart={() => {
|
onStart={() => {
|
||||||
//this is just a function that is rin as soo as the start button is clicked it can be used to disable things while the change is in progress if we want to
|
//this is just a function that is rin as soon as the start button is clicked it can be used to disable things while the change is in progress if we want to
|
||||||
}}
|
}}
|
||||||
onComplete={()=>{
|
onComplete={()=>{
|
||||||
closeDialog()
|
closeDialog()
|
||||||
|
|
@ -218,13 +220,25 @@ export default function BinModeController(props: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
case pond.BinMode.BIN_MODE_COOLDOWN:
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
||||||
return <CooldownMode />
|
return <CooldownMode
|
||||||
|
grain={bin.settings.inventory?.grainType}
|
||||||
|
customGrain={bin.settings.inventory?.customGrain ?? undefined}
|
||||||
|
devices={devices}
|
||||||
|
binPrefs={binPrefs}
|
||||||
|
deviceComponents={deviceComponents}
|
||||||
|
cancel={closeDialog}
|
||||||
|
confirm={(deviceData) => {
|
||||||
|
buildStages(deviceData)
|
||||||
|
setShowProgress(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
default:
|
default:
|
||||||
return <StorageMode
|
return <StorageMode
|
||||||
devices={devices}
|
devices={devices}
|
||||||
binPrefs={binPrefs}
|
binPrefs={binPrefs}
|
||||||
bin={bin}
|
bin={bin}
|
||||||
deviceComponents={deviceComponents}
|
deviceComponents={deviceComponents}
|
||||||
|
cancel={closeDialog}
|
||||||
confirm={(deviceData)=>{
|
confirm={(deviceData)=>{
|
||||||
buildStages(deviceData)
|
buildStages(deviceData)
|
||||||
setShowProgress(true)
|
setShowProgress(true)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,394 @@
|
||||||
import { Box } from "@mui/material";
|
import { Box, Theme, Stepper, Step, StepLabel, Typography, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import ConditioningSelector from "bin/conditioning/conditioningSelector";
|
||||||
|
import { Component, Device, Interaction } from "models";
|
||||||
|
import { Ambient } from "models/Ambient";
|
||||||
|
import { Controller } from "models/Controller";
|
||||||
|
import { Plenum } from "models/Plenum";
|
||||||
|
import { pond, quack } from "protobuf-ts/pond";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ComponentSet } from "bin/conditioning/pickComponentSet"
|
||||||
|
import { componentIDToString, sameComponentID } from "pbHelpers/Component";
|
||||||
|
import moment from "moment";
|
||||||
|
import { lowerCase } from "lodash";
|
||||||
|
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||||
|
import { useGlobalState, useInteractionsAPI } from "providers";
|
||||||
|
import ConditionDisplay from "./conditionDisplay";
|
||||||
|
import React from "react";
|
||||||
|
import { DeviceChangeData } from "./BinModeController";
|
||||||
|
import { GrainCable } from "models/GrainCable";
|
||||||
|
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
return ({
|
||||||
|
stepper: {
|
||||||
|
padding: theme.spacing(0.5)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Step {
|
||||||
|
label: string;
|
||||||
|
completed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string;
|
||||||
|
device: Device;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
devices: Device[]
|
||||||
|
deviceComponents: Map<number, Component[]>
|
||||||
|
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||||
|
grain?: pond.Grain
|
||||||
|
customGrain?: pond.GrainSettings
|
||||||
|
cancel: () => void
|
||||||
|
confirm: (deviceData: DeviceChangeData[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = [{label: "Device"}, {label: "Interaction"}]
|
||||||
|
|
||||||
|
//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing
|
||||||
|
export default function CooldownMode(props: Props){
|
||||||
|
const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props
|
||||||
|
const classes = useStyles()
|
||||||
|
const [{user}] = useGlobalState()
|
||||||
|
const interactionAPI = useInteractionsAPI()
|
||||||
|
const [options, setOptions] = useState<Option[]>([])
|
||||||
|
const [deviceOption, setDeviceOption] = useState<Option>({device: Device.create(), label: "Select Device"})
|
||||||
|
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
||||||
|
const [plenums, setPlenums] = useState<Plenum[]>([])
|
||||||
|
const [ambients, setAmbients] = useState<Ambient[]>([])
|
||||||
|
const [fans, setFans] = useState<Controller[]>([])
|
||||||
|
const [cables, setCables] = useState<GrainCable[]>([])
|
||||||
|
const [currentStep, setCurrentStep] = useState(0)
|
||||||
|
const [interactionLoading,setInteractionsLoading] = useState(false)
|
||||||
|
|
||||||
|
// possibly all 4 of these will need to be passed up, at least the conflicting, toAdd, and sets will
|
||||||
|
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
|
||||||
|
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
|
||||||
|
const [conflictingInteractions, setConflictingInteractions] = useState<Interaction[]>([])
|
||||||
|
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
|
||||||
|
|
||||||
|
|
||||||
|
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
|
||||||
|
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//sort the components according to the bin preferences
|
||||||
|
useEffect(()=>{
|
||||||
|
if (!selectedDevice) return;
|
||||||
|
if (!deviceComponents.get(selectedDevice.id())) return;
|
||||||
|
var plenums: Plenum[] = [];
|
||||||
|
var ambients: Ambient[] = [];
|
||||||
|
var grainCables: GrainCable[] = [];
|
||||||
|
var fans: Controller[] = [];
|
||||||
|
var sinkMap: Map<string, Component> = new Map()
|
||||||
|
var sourceMap: Map<string, Component> = new Map()
|
||||||
|
|
||||||
|
deviceComponents.get(selectedDevice.id())!.forEach(comp => {
|
||||||
|
let pref = binPrefs?.get(comp.key());
|
||||||
|
if (pref) {
|
||||||
|
if (pref.type) {
|
||||||
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM){
|
||||||
|
plenums.push(Plenum.create(comp));
|
||||||
|
sourceMap.set(comp.locationString(), comp)
|
||||||
|
}
|
||||||
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT){
|
||||||
|
ambients.push(Ambient.create(comp));
|
||||||
|
sourceMap.set(comp.locationString(), comp)
|
||||||
|
}
|
||||||
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){
|
||||||
|
grainCables.push(GrainCable.create(comp));
|
||||||
|
sourceMap.set(comp.locationString(), comp)
|
||||||
|
}
|
||||||
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
|
||||||
|
let fan = Controller.create(comp)
|
||||||
|
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
||||||
|
fans.push(fan);
|
||||||
|
sinkMap.set(comp.locationString(), comp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setPlenums(plenums);
|
||||||
|
setAmbients(ambients);
|
||||||
|
setFans(fans);
|
||||||
|
setSourceMap(sourceMap)
|
||||||
|
setSinkMap(sinkMap)
|
||||||
|
setCables(grainCables)
|
||||||
|
//also load the interactions for the selected device so that we can find any conflicting ones
|
||||||
|
setInteractionsLoading(true)
|
||||||
|
interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => {
|
||||||
|
setExistingInteractions(resp)
|
||||||
|
}).finally(() => {
|
||||||
|
setInteractionsLoading(false)
|
||||||
|
})
|
||||||
|
}, [deviceComponents, selectedDevice, binPrefs]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let o: Option[] = [];
|
||||||
|
devices.forEach((device, i) => {
|
||||||
|
let newOption: Option = {device: device, label: device.name()}
|
||||||
|
if(i === 0) {
|
||||||
|
setDeviceOption(newOption)
|
||||||
|
setSelectedDevice(device)
|
||||||
|
}
|
||||||
|
o.push(newOption);
|
||||||
|
});
|
||||||
|
setOptions(o);
|
||||||
|
}, [devices, setOptions]);
|
||||||
|
|
||||||
|
const buildFanInteraction = (
|
||||||
|
sensor: Plenum | Ambient,
|
||||||
|
fan: Controller,
|
||||||
|
) => {
|
||||||
|
let interaction = pond.InteractionSettings.create({
|
||||||
|
source: sensor.location(),
|
||||||
|
sink: fan.location(),
|
||||||
|
schedule: pond.InteractionSchedule.create({
|
||||||
|
timeOfDayStart: "00:00",
|
||||||
|
timeOfDayEnd: "24:00",
|
||||||
|
timezone: moment.tz.guess(),
|
||||||
|
weekdays: moment.weekdays().map(d => lowerCase(d))
|
||||||
|
}),
|
||||||
|
notifications: pond.InteractionNotifications.create({
|
||||||
|
reports: true
|
||||||
|
}),
|
||||||
|
result: pond.InteractionResult.create({
|
||||||
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||||
|
value: 1
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let tempDefault = 15;
|
||||||
|
|
||||||
|
|
||||||
|
let conditions = [];
|
||||||
|
//may not actually need a condition on the humidity for cooldown
|
||||||
|
// let humidityDefault = 60;
|
||||||
|
// let fanConditionOne = pond.InteractionCondition.create({
|
||||||
|
// measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||||
|
// comparison:
|
||||||
|
// humidityComparison === "greater"
|
||||||
|
// ? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
||||||
|
// : quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||||
|
// value: describeMeasurement(
|
||||||
|
// quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||||
|
// sensor.settings.type,
|
||||||
|
// sensor.settings.subtype,
|
||||||
|
// undefined,
|
||||||
|
// user
|
||||||
|
// ).toStored(humidityDefault)
|
||||||
|
// });
|
||||||
|
// conditions.push(fanConditionOne);
|
||||||
|
|
||||||
|
//since the measurement describers function toStored does a conversion into celsius for temperature if the users pref is fahrenheit we need to convert the preset value into fahrenheit
|
||||||
|
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||||
|
tempDefault = Math.round((tempDefault * (9 / 5) + 32) * 100) / 100;
|
||||||
|
}
|
||||||
|
let tempVal = describeMeasurement(
|
||||||
|
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||||
|
sensor.settings.type,
|
||||||
|
sensor.settings.subtype,
|
||||||
|
undefined,
|
||||||
|
user
|
||||||
|
).toStored(tempDefault);
|
||||||
|
|
||||||
|
let fanConditionTwo = pond.InteractionCondition.create({
|
||||||
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||||
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||||
|
value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals
|
||||||
|
});
|
||||||
|
conditions.push(fanConditionTwo);
|
||||||
|
|
||||||
|
interaction.conditions = conditions;
|
||||||
|
//set the output mode to auto in the fan so that when the components are updated it uses the new mode
|
||||||
|
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
|
||||||
|
return interaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
//this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them
|
||||||
|
const createInteractions = (sets: ComponentSet[]) => {
|
||||||
|
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
|
||||||
|
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
|
||||||
|
let conflictingInteractions: Interaction[] = []
|
||||||
|
let linkedComponents = deviceComponents.get(selectedDevice.id());
|
||||||
|
//loop through the sets to find interactions to remove as well as set the new ones
|
||||||
|
sets.forEach((set) => {
|
||||||
|
//loop through the controllers
|
||||||
|
set.controllers.forEach(controller => {
|
||||||
|
//filter the conflicting interactions for that controller
|
||||||
|
let c = existingInteractions.filter(i => {
|
||||||
|
let conflicting = false;
|
||||||
|
if (linkedComponents) {
|
||||||
|
linkedComponents.forEach(comp => {
|
||||||
|
if (sameComponentID(comp.location(), i.settings.sink)) {
|
||||||
|
conflicting = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return conflicting;
|
||||||
|
});
|
||||||
|
|
||||||
|
conflictingInteractions = conflictingInteractions.concat(c)
|
||||||
|
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
cables.forEach(cable => {
|
||||||
|
console.log(cable.name())
|
||||||
|
let c = existingInteractions.filter(i => {
|
||||||
|
if (sameComponentID(cable.location(), i.settings.source) && !i.settings.sink && i.settings.notifications?.notify) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
conflictingInteractions = conflictingInteractions.concat(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
setComponentSets([...sets])
|
||||||
|
return {
|
||||||
|
conflicting: conflictingInteractions,
|
||||||
|
toAdd: multiInteractionSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepper = () => {
|
||||||
|
return (
|
||||||
|
<Stepper
|
||||||
|
activeStep={currentStep}
|
||||||
|
alternativeLabel
|
||||||
|
classes={{
|
||||||
|
root: classes.stepper
|
||||||
|
}}>
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<Step key={i}>
|
||||||
|
<StepLabel>{s.label}</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const deviceSelector = () => {
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
disablePortal
|
||||||
|
options={options}
|
||||||
|
value={deviceOption}
|
||||||
|
fullWidth
|
||||||
|
getOptionLabel={option => option.label || ""}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
if(newValue){
|
||||||
|
setSelectedDevice(newValue.device);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceStep = () => {
|
||||||
|
|
||||||
export default function CooldownMode(){
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
<Typography>Select the device to condition the bin</Typography>
|
||||||
|
{deviceSelector()}
|
||||||
|
{interactionLoading ? <CircularProgress /> :
|
||||||
|
<ConditioningSelector
|
||||||
|
plenums={plenums}
|
||||||
|
ambients={ambients}
|
||||||
|
fans={fans}
|
||||||
|
heaters={[]}
|
||||||
|
//presets={presets}
|
||||||
|
binMode={pond.BinMode.BIN_MODE_COOLDOWN}
|
||||||
|
updateSets={(sets) => {
|
||||||
|
//update the component sets that will be used for the interactions
|
||||||
|
//when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
|
||||||
|
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
|
||||||
|
let i = createInteractions(sets)
|
||||||
|
if(i !== undefined){
|
||||||
|
setConflictingInteractions(i.conflicting)
|
||||||
|
setInteractionsToAdd(i.toAdd)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//this step will use the conflicting interactions and interactions to add to display them and allow users to make changes
|
||||||
|
const interactionStep = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography>Interactions</Typography>
|
||||||
|
{interactionsToAdd?.interactions.map((interaction, index) => {
|
||||||
|
let temp = Interaction.create()
|
||||||
|
temp.settings = interaction
|
||||||
|
let sink = sinkMap.get(componentIDToString(interaction.sink))
|
||||||
|
let source = sourceMap.get(componentIDToString(interaction.source))
|
||||||
|
if(sink && source && selectedDevice){
|
||||||
|
return (
|
||||||
|
<ConditionDisplay
|
||||||
|
key={index}
|
||||||
|
grain={grain}
|
||||||
|
customGrain={customGrain}
|
||||||
|
interaction={temp}
|
||||||
|
device={selectedDevice}
|
||||||
|
sink={sink}
|
||||||
|
source={source}
|
||||||
|
changeConditions={(newSettings) => {
|
||||||
|
console.log(newSettings)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepperContent = () => {
|
||||||
|
switch(currentStep){
|
||||||
|
case 1:
|
||||||
|
return interactionStep()
|
||||||
|
default:
|
||||||
|
return deviceStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{/* close - tells the parent to close the dialog without doing anything, always visible */}
|
||||||
|
<Button onClick={cancel}>Close</Button>
|
||||||
|
{/* back - goes back to the previous step, hidden on the forst step */}
|
||||||
|
{currentStep !== 0 && <Button onClick={() => {setCurrentStep(currentStep-1)}}>Back</Button>}
|
||||||
|
{/* next - goes to the next step, hidden on the last step */}
|
||||||
|
{currentStep !== steps.length - 1 && <Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>}
|
||||||
|
{/* confirm - tells the parent to build the stages using the data, only visible on the last step */}
|
||||||
|
{currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && <Button onClick={() => {confirm([{ toRemove: conflictingInteractions, deviceId: selectedDevice.id(), toAdd: interactionsToAdd, controllers: componentSets.flatMap(set => set.controllers)}])}}>Confirm</Button>}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DialogTitle>Bin Cooldown</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{stepper()}
|
||||||
|
{stepperContent()}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{actions()}
|
||||||
|
</DialogActions>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +437,7 @@ export default function DryingMode(props: Props){
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography>Devices</Typography>
|
<Typography>Select the device to condition the bin</Typography>
|
||||||
{deviceSelector()}
|
{deviceSelector()}
|
||||||
{interactionLoading ? <CircularProgress /> :
|
{interactionLoading ? <CircularProgress /> :
|
||||||
<ConditioningSelector
|
<ConditioningSelector
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material";
|
import { Box, Theme, Stepper, Step, StepLabel, Typography, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import ConditioningSelector from "bin/conditioning/conditioningSelector";
|
import ConditioningSelector from "bin/conditioning/conditioningSelector";
|
||||||
import { Component, Device, Interaction } from "models";
|
import { Component, Device, Interaction } from "models";
|
||||||
|
|
@ -49,7 +49,7 @@ interface Props {
|
||||||
confirm: (deviceData: DeviceChangeData[]) => void
|
confirm: (deviceData: DeviceChangeData[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}]
|
const steps = [{label: "Device"}, {label: "Interaction"}]
|
||||||
|
|
||||||
//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing
|
//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing
|
||||||
export default function DryingMode(props: Props){
|
export default function DryingMode(props: Props){
|
||||||
|
|
@ -59,7 +59,7 @@ export default function DryingMode(props: Props){
|
||||||
const [{user}] = useGlobalState()
|
const [{user}] = useGlobalState()
|
||||||
const interactionAPI = useInteractionsAPI()
|
const interactionAPI = useInteractionsAPI()
|
||||||
const [options, setOptions] = useState<Option[]>([])
|
const [options, setOptions] = useState<Option[]>([])
|
||||||
const [deviceOption, setDeviceOption] = useState<Option>()
|
const [deviceOption, setDeviceOption] = useState<Option>({device: Device.create(), label: "Select Device"})
|
||||||
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
||||||
const [plenums, setPlenums] = useState<Plenum[]>([])
|
const [plenums, setPlenums] = useState<Plenum[]>([])
|
||||||
const [ambients, setAmbients] = useState<Ambient[]>([])
|
const [ambients, setAmbients] = useState<Ambient[]>([])
|
||||||
|
|
@ -134,9 +134,9 @@ export default function DryingMode(props: Props){
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let o: Option[] = [];
|
let o: Option[] = [];
|
||||||
devices.forEach(device => {
|
devices.forEach((device, i) => {
|
||||||
let newOption: Option = {device: device, label: device.name()}
|
let newOption: Option = {device: device, label: device.name()}
|
||||||
if(deviceOption === undefined) {
|
if(i === 0) {
|
||||||
setDeviceOption(newOption)
|
setDeviceOption(newOption)
|
||||||
setSelectedDevice(device)
|
setSelectedDevice(device)
|
||||||
}
|
}
|
||||||
|
|
@ -148,8 +148,6 @@ export default function DryingMode(props: Props){
|
||||||
const buildFanInteraction = (
|
const buildFanInteraction = (
|
||||||
sensor: Plenum | Ambient,
|
sensor: Plenum | Ambient,
|
||||||
fan: Controller,
|
fan: Controller,
|
||||||
tempComparison: "greater" | "less",
|
|
||||||
humidityComparison: "greater" | "less",
|
|
||||||
) => {
|
) => {
|
||||||
let interaction = pond.InteractionSettings.create({
|
let interaction = pond.InteractionSettings.create({
|
||||||
source: sensor.location(),
|
source: sensor.location(),
|
||||||
|
|
@ -176,10 +174,7 @@ export default function DryingMode(props: Props){
|
||||||
let conditions = [];
|
let conditions = [];
|
||||||
let fanConditionOne = pond.InteractionCondition.create({
|
let fanConditionOne = pond.InteractionCondition.create({
|
||||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||||
comparison:
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||||
humidityComparison === "greater"
|
|
||||||
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
|
||||||
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
|
||||||
value: describeMeasurement(
|
value: describeMeasurement(
|
||||||
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||||
sensor.settings.type,
|
sensor.settings.type,
|
||||||
|
|
@ -204,10 +199,7 @@ export default function DryingMode(props: Props){
|
||||||
|
|
||||||
let fanConditionTwo = pond.InteractionCondition.create({
|
let fanConditionTwo = pond.InteractionCondition.create({
|
||||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||||
comparison:
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||||
tempComparison === "greater"
|
|
||||||
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
|
||||||
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
|
||||||
value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals
|
value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals
|
||||||
});
|
});
|
||||||
conditions.push(fanConditionTwo);
|
conditions.push(fanConditionTwo);
|
||||||
|
|
@ -242,7 +234,7 @@ export default function DryingMode(props: Props){
|
||||||
});
|
});
|
||||||
|
|
||||||
conflictingInteractions = conflictingInteractions.concat(c)
|
conflictingInteractions = conflictingInteractions.concat(c)
|
||||||
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less"))
|
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
cables.forEach(cable => {
|
cables.forEach(cable => {
|
||||||
|
|
@ -302,7 +294,7 @@ export default function DryingMode(props: Props){
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography>Devices</Typography>
|
<Typography>Select the device to condition the bin</Typography>
|
||||||
{deviceSelector()}
|
{deviceSelector()}
|
||||||
{interactionLoading ? <CircularProgress /> :
|
{interactionLoading ? <CircularProgress /> :
|
||||||
<ConditioningSelector
|
<ConditioningSelector
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ interface Props {
|
||||||
bin: Bin
|
bin: Bin
|
||||||
binPrefs?: Map<string, pond.BinComponentPreferences>
|
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||||
confirm: (deviceData: DeviceChangeData[]) => void
|
confirm: (deviceData: DeviceChangeData[]) => void
|
||||||
|
cancel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,7 +31,7 @@ interface Props {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function StorageMode(props: Props){
|
export default function StorageMode(props: Props){
|
||||||
const {devices, deviceComponents, bin, binPrefs, confirm} = props
|
const {devices, deviceComponents, bin, binPrefs, confirm, cancel} = props
|
||||||
const interactionsAPI = useInteractionsAPI()
|
const interactionsAPI = useInteractionsAPI()
|
||||||
const [data, setData] = useState<DeviceChangeData[]>([])
|
const [data, setData] = useState<DeviceChangeData[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
@ -169,8 +170,10 @@ export default function StorageMode(props: Props){
|
||||||
<DialogTitle>Storage Mode</DialogTitle>
|
<DialogTitle>Storage Mode</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{/* could have the explanation of what changing to storage mode does here */}
|
{/* could have the explanation of what changing to storage mode does here */}
|
||||||
|
Setting the bin to storage mode will remove any interactions for controllers connected to this bin and will set alerts on the cables that dont have any alerts set.
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
<Button onClick={() => cancel}>Cancel</Button>
|
||||||
<Button onClick={()=>{
|
<Button onClick={()=>{
|
||||||
confirm(data)
|
confirm(data)
|
||||||
}} disabled={loading || loadErr}>Confirm</Button>
|
}} disabled={loading || loadErr}>Confirm</Button>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import {
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { avg, fahrenheitToCelsius } from "utils";
|
import { avg, fahrenheitToCelsius } from "utils";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import { Mark } from "@mui/material/Slider/useSlider.types";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
return ({
|
return ({
|
||||||
|
|
@ -300,29 +299,6 @@ import {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const customMark = (val: string, arrowColor: string) => {
|
|
||||||
return (
|
|
||||||
<Box className={classes.markContainer}>
|
|
||||||
<Grid container direction="column" alignContent="center" alignItems="center" spacing={1}>
|
|
||||||
<Grid item>
|
|
||||||
<Typography style={{ color: "white", fontSize: 15, fontWeight: 650, lineHeight: 1 }}>
|
|
||||||
{val}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Box className={classes.arrowDown} style={{ borderTop: "10px solid " + arrowColor }} />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Renders a single condition's slider. Pulled out so both the grouped
|
|
||||||
// (paired) rendering path and the fallback flat rendering path can share it.
|
|
||||||
// onRemove/removeTooltip, when provided, render a small remove icon inline
|
|
||||||
// with this specific slider -- used when two pairs exist, so each row's
|
|
||||||
// control is unambiguous about which single condition it sits on, even
|
|
||||||
// though clicking it removes that condition's whole paired counterpart too.
|
|
||||||
const renderConditionSlider = (
|
const renderConditionSlider = (
|
||||||
condition: pond.InteractionCondition,
|
condition: pond.InteractionCondition,
|
||||||
key: React.Key,
|
key: React.Key,
|
||||||
|
|
@ -330,16 +306,6 @@ import {
|
||||||
removeTooltip?: string
|
removeTooltip?: string
|
||||||
) => {
|
) => {
|
||||||
let describer = describeMeasurement(condition.measurementType, source?.type(), source.subType(), undefined, user);
|
let describer = describeMeasurement(condition.measurementType, source?.type(), source.subType(), undefined, user);
|
||||||
let labelTail = describer.unit();
|
|
||||||
let marks: Mark[] = [];
|
|
||||||
|
|
||||||
let mark = sliderMarks.get(condition.measurementType);
|
|
||||||
if (mark !== undefined) {
|
|
||||||
marks.push({
|
|
||||||
label: customMark(describer.toDisplay(mark) + labelTail, describer.colour()),
|
|
||||||
value: describer.toDisplay(mark)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid key={key} container item xs={12} alignItems="center" wrap="nowrap">
|
<Grid key={key} container item xs={12} alignItems="center" wrap="nowrap">
|
||||||
|
|
@ -351,9 +317,6 @@ import {
|
||||||
track: classes.sliderTrack,
|
track: classes.sliderTrack,
|
||||||
thumb: classes.sliderThumb,
|
thumb: classes.sliderThumb,
|
||||||
valueLabel: classes.sliderValLabel,
|
valueLabel: classes.sliderValLabel,
|
||||||
marked: classes.sliderMarked,
|
|
||||||
mark: classes.sliderMark,
|
|
||||||
markLabel: classes.sliderMarkLabel
|
|
||||||
}}
|
}}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
valueLabelDisplay="on"
|
valueLabelDisplay="on"
|
||||||
|
|
@ -369,7 +332,6 @@ import {
|
||||||
return value.toFixed(1) + "%";
|
return value.toFixed(1) + "%";
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
marks={marks}
|
|
||||||
min={describer.min()}
|
min={describer.min()}
|
||||||
max={describer.max()}
|
max={describer.max()}
|
||||||
value={sliderVals.get(condition) ?? describer.min()}
|
value={sliderVals.get(condition) ?? describer.min()}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue