From 6d1c9431f23ac9be75ca8ca5056ac46e67a9e476 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 18 Jun 2026 10:54:14 -0600 Subject: [PATCH 01/12] setting up the framework for rebuilding the bin modes --- src/bin/binModes/BinModeController.tsx | 54 ++++++++++++++++++++++++++ src/bin/binModes/CooldownMode.tsx | 9 +++++ src/bin/binModes/DryingMode.tsx | 9 +++++ src/bin/binModes/HydratingMode.tsx | 9 +++++ src/bin/binModes/StorageMode.tsx | 9 +++++ 5 files changed, 90 insertions(+) create mode 100644 src/bin/binModes/BinModeController.tsx create mode 100644 src/bin/binModes/CooldownMode.tsx create mode 100644 src/bin/binModes/DryingMode.tsx create mode 100644 src/bin/binModes/HydratingMode.tsx create mode 100644 src/bin/binModes/StorageMode.tsx diff --git a/src/bin/binModes/BinModeController.tsx b/src/bin/binModes/BinModeController.tsx new file mode 100644 index 0000000..90aa3d1 --- /dev/null +++ b/src/bin/binModes/BinModeController.tsx @@ -0,0 +1,54 @@ +import { Box, Button, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { Bin } from "models"; +import { pond } from "protobuf-ts/pond"; +import React from "react"; +import StorageMode from "./StorageMode"; +import DryingMode from "./DryingMode"; +import HydratingMode from "./HydratingMode"; +import CooldownMode from "./CooldownMode"; + +interface Props { + newMode: pond.BinMode //used to control which dialog to open + open: boolean + onClose: () => void + bin: Bin +} + +export default function BinModeControl(props: Props) { + const {open, onClose, newMode} = props + + //the mode controller could handle the actual api call for adding the interactions etc but the dialogs would create them and pass them up + + //steps involved for devices when changing the bin mode: + //remove conflicting interactions + //add new interactions + //updating the controllers + + const renderContent = () => { + switch(newMode){ + case pond.BinMode.BIN_MODE_DRYING: + return + case pond.BinMode.BIN_MODE_HYDRATING: + return + case pond.BinMode.BIN_MODE_COOLDOWN: + return + default: + return + } + } + + return ( + + Change Bin Mode + + {renderContent()} + + + + + + ) +} \ No newline at end of file diff --git a/src/bin/binModes/CooldownMode.tsx b/src/bin/binModes/CooldownMode.tsx new file mode 100644 index 0000000..173cf15 --- /dev/null +++ b/src/bin/binModes/CooldownMode.tsx @@ -0,0 +1,9 @@ +import { Box } from "@mui/material"; + +export default function CooldownMode(){ + return ( + + + + ) +} \ No newline at end of file diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx new file mode 100644 index 0000000..7dd57a5 --- /dev/null +++ b/src/bin/binModes/DryingMode.tsx @@ -0,0 +1,9 @@ +import { Box } from "@mui/material"; + +export default function DryingMode(){ + return ( + + + + ) +} \ No newline at end of file diff --git a/src/bin/binModes/HydratingMode.tsx b/src/bin/binModes/HydratingMode.tsx new file mode 100644 index 0000000..f87ca98 --- /dev/null +++ b/src/bin/binModes/HydratingMode.tsx @@ -0,0 +1,9 @@ +import { Box } from "@mui/material"; + +export default function HydratingMode(){ + return ( + + + + ) +} \ No newline at end of file diff --git a/src/bin/binModes/StorageMode.tsx b/src/bin/binModes/StorageMode.tsx new file mode 100644 index 0000000..67f82dc --- /dev/null +++ b/src/bin/binModes/StorageMode.tsx @@ -0,0 +1,9 @@ +import { Box } from "@mui/material"; + +export default function StorageMode(){ + return ( + + + + ) +} \ No newline at end of file From 19eb2a7faf589c8bddba65f7a553e6670a768c7f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 29 Jun 2026 15:30:25 -0600 Subject: [PATCH 02/12] proto update --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19352f1..b3671a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bin_modes_v2", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -11752,7 +11752,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#100126cc7f3cdf18f68af6372e5db4113db012b7", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#aa9a59d8cc0b7ba1ee27d9bdcdade6136a00d1fb", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index 3a76c43..07cb34d 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bin_modes_v2", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", From bced38d45475b78066dadb9ce7b7eff73f31084f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 6 Jul 2026 16:41:46 -0600 Subject: [PATCH 03/12] nailed down the overall structure for the new bin modes now --- src/bin/BinConditioningInteraction.tsx | 39 +- src/bin/bin3dVisualizer.tsx | 26 +- src/bin/binModes/BinModeController.tsx | 158 +++++++- src/bin/binModes/DryingMode.tsx | 520 ++++++++++++++++++++++++- src/bin/binModes/conditionDisplay.tsx | 313 +++++++++++++++ 5 files changed, 1011 insertions(+), 45 deletions(-) create mode 100644 src/bin/binModes/conditionDisplay.tsx diff --git a/src/bin/BinConditioningInteraction.tsx b/src/bin/BinConditioningInteraction.tsx index d729462..92a4f31 100644 --- a/src/bin/BinConditioningInteraction.tsx +++ b/src/bin/BinConditioningInteraction.tsx @@ -85,10 +85,14 @@ interface Props { sink: Component; grain?: pond.Grain; customGrain?: pond.GrainSettings + /** + * if true, the button to update the interaction will be hidden because it will be assumed that the parent will handle the interaction + */ + parentUpdate?: boolean } export default function BinConditioningInteraction(props: Props) { - const { interaction, source, sink, grain, deviceId, customGrain } = props; + const { interaction, source, sink, grain, deviceId, customGrain, parentUpdate} = props; const [sliderVals, setSliderVals] = useState>(new Map()); const [sliderMarks, setSliderMarks] = useState>(new Map()); //this is the emc value calculated from the interactions temp and humidity conditions @@ -143,13 +147,13 @@ export default function BinConditioningInteraction(props: Props) { const updateInteraction = () => { interactionAPI - .updateInteraction(deviceId, interaction.settings, as) - .then(resp => { - openSnack("Updated Interaction Conditions"); - }) - .catch(err => { - openSnack("Failed to Update Interaction Conditions"); - }); + .updateInteraction(deviceId, interaction.settings, as) + .then(resp => { + openSnack("Updated Interaction Conditions"); + }) + .catch(err => { + openSnack("Failed to Update Interaction Conditions"); + }); }; const customMark = (val: string, arrowColor: string) => { @@ -238,6 +242,7 @@ export default function BinConditioningInteraction(props: Props) { max={describer.max()} value={sliderVals.get(condition.measurementType) ?? describer.min()} onChange={(_, val) => { + //note that changing it here like this is what is changing it in the interaction itself condition.value = Math.round(describer.toStored(val as number)); let sliders = cloneDeep(sliderVals); sliders.set(condition.measurementType, val as number); @@ -249,14 +254,16 @@ export default function BinConditioningInteraction(props: Props) { ); })} - + {!parentUpdate && + + } ); diff --git a/src/bin/bin3dVisualizer.tsx b/src/bin/bin3dVisualizer.tsx index 9bdfd0c..55316b8 100644 --- a/src/bin/bin3dVisualizer.tsx +++ b/src/bin/bin3dVisualizer.tsx @@ -11,6 +11,7 @@ import { CableData } from "bin/3dView/Data/BuildCableData"; import ModeChangeDialog from "bin/conditioning/modeChangeDialog"; import { cloneDeep } from "lodash"; import { useMobile } from "hooks"; +import BinModeController from "./binModes/BinModeController"; interface Props { bin: Bin @@ -35,6 +36,7 @@ export default function bin3dVisualizer(props: Props){ const [showMoistureHeatmap, setShowMoistureHeatmap] = useState(false) const [binDisplay, setBinDisplay] = useState("temp") const [binMode, setBinMode] = useState(pond.BinMode.BIN_MODE_NONE) + const [newMode, setNewMode] = useState(pond.BinMode.BIN_MODE_NONE) const [openModeChange, setOpenModeChange] = useState(false) const [selectedCable, setSelectedCable] = useState(undefined) const [selectedNode, setSelectedNode] = useState(undefined) @@ -58,10 +60,10 @@ export default function bin3dVisualizer(props: Props){ setBinMode(bin.settings.mode) },[bin]) - const updateBin = () => { + const updateBin = (newMode: pond.BinMode) => { if(updateBinCallback){ let clone = cloneDeep(bin) - clone.settings.mode = binMode + clone.settings.mode = newMode updateBinCallback(clone) } }; @@ -86,8 +88,10 @@ export default function bin3dVisualizer(props: Props){ '&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' }, }} onChange={event => { - setBinMode(event.target.value as pond.BinMode) + setNewMode(event.target.value as pond.BinMode) setOpenModeChange(true) + // for the re-built modes, each one will have its own dialog box and this will just control which one to open rather than the single dialog for all of them + // and having the mode determine what to show in the dialog }} > Select Mode.. @@ -327,7 +331,7 @@ export default function bin3dVisualizer(props: Props){ }} /> } - { setModeChangeInProgress(false) }} - + /> */} + { + updateBin(newMode) + }} + onClose={()=>{setOpenModeChange(false)}} /> void bin: Bin + binPrefs?: Map + devices: Device[] + componentDevices: Map + componentMap: Map + updateMode: (newMode: pond.BinMode) => void } -export default function BinModeControl(props: Props) { - const {open, onClose, newMode} = props +export default function BinModeController(props: Props) { + const {open, onClose, newMode, updateMode, devices, componentDevices, componentMap, binPrefs, bin} = props + const [showProgress, setShowProgress] = useState(false) + const [deviceComponents, setDeviceComponents] = useState>(new Map()) + const [promiseStages, setPromiseStages] = useState([]) + const interactionAPI = useInteractionsAPI() + const componentAPI = useComponentAPI() - //the mode controller could handle the actual api call for adding the interactions etc but the dialogs would create them and pass them up + //the use effects for the initial setup of what will be needed + useEffect(()=>{ + let newMap:Map = new Map() + componentMap.forEach((comp, key) => { + //first we need to get the device id this component belongs to + let dev = componentDevices.get(key) + if(dev){ + //check if the key exists in the new map yet + if(newMap.has(dev)){ + newMap.get(dev)?.push(comp) + }else{ + newMap.set(dev, [comp]) + } + } + }) + setDeviceComponents(newMap) + },[componentDevices, componentMap]) //steps involved for devices when changing the bin mode: //remove conflicting interactions //add new interactions //updating the controllers - const renderContent = () => { + const closeDialog = () => { + setShowProgress(false) + onClose() + } + + const buildStages = (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => { + //build the stages to pass into the PromiseProgress + //stage one is to remove conflicting interactions + let stages: Stage[] = [] + let stage1: Stage = { + title: "Remove Conflicting Interactions", + steps: [] + } + conflictingInteractions.forEach(i => { + let newStep: Step = { + title: "Removing Interaction", + promise: interactionAPI.removeInteraction(deviceId, i.key()) + } + stage1.steps.push(newStep); + }); + if(stage1.steps.length > 0){ + stages.push(stage1) + } + //stage two is to add the new interactions + let stage2: Stage = { + title: "Add New Interactions", + steps: [ + { + title: "Adding Interactions", + promise: interactionAPI.addMultiInteractions(deviceId, newInteractions) + } + ] + } + if(stage2.steps.length > 0){ + stages.push(stage2) + } + + //stage three is to update the controller components + let stage3: Stage = { + title: "Update Controllers", + steps: [] + } + componentSets.forEach(set => { + set.controllers.forEach(controller => { + let newStep: Step = { + title: "Updating " + controller.name(), + promise: componentAPI.update(deviceId, controller.settings) + } + stage3.steps.push(newStep) + }) + }) + if(stage3.steps.length > 0){ + stages.push(stage3) + } + //set those stages to a state variable + setPromiseStages(stages) + } + + /** + * the progress step is the last step of each mode change that shows what is being done and what succeeded/failed + * initially i was going to put it here because it would effectively be the same thing for each mode, however depending + * on how i decide to handle the api calls, and since i am thinking about passing the functions into the children it may go into each mode component + * + */ + const progressContent = () => { + return ( + + Change Mode + + { + //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 + }} + onComplete={()=>{ + closeDialog() + }} + /> + + + + + + ) + } + + const modeContent = () => { switch(newMode){ case pond.BinMode.BIN_MODE_DRYING: - return + //setNumSteps(DryingStepCount) + return { + buildStages(conflicting, device, newInteractions, components) + setShowProgress(true) + }} + /> case pond.BinMode.BIN_MODE_HYDRATING: return case pond.BinMode.BIN_MODE_COOLDOWN: @@ -39,16 +167,8 @@ export default function BinModeControl(props: Props) { } return ( - - Change Bin Mode - - {renderContent()} - - - - + + {showProgress ? progressContent() : modeContent()} ) } \ No newline at end of file diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index 7dd57a5..10895a1 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -1,9 +1,519 @@ -import { Box } from "@mui/material"; +import { ArrowBack, ArrowForward } from "@mui/icons-material"; +import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Grid2, 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, useMemo, useState } from "react"; +import { ComponentSet } from "bin/conditioning/pickComponentSet" +import { componentIDToString, sameComponentID } from "pbHelpers/Component"; +import moment from "moment"; +import { lowerCase } from "lodash"; +import { GetGrainExtensionMap } from "grain"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import { useGlobalState, useInteractionsAPI } from "providers"; +import BinConditioningInteraction from "bin/BinConditioningInteraction"; +import ConditionDisplay from "./conditionDisplay"; +import React from "react"; -export default function DryingMode(){ - return ( - + +const useStyles = makeStyles((theme: Theme) => { + return ({ + stepper: { + padding: theme.spacing(0.5) + }, + }) +}); + +interface Step { + label: string; + completed?: boolean; +} + +export const DryingBaseStepCount = 3 + +interface Option { + label: string; + device: Device; + icon?: string; + } + +interface Props { + devices: Device[] + deviceComponents: Map + binPrefs?: Map + grain?: pond.Grain + cancel: () => void + confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void +} + +const steps = [{label: "Style"}, {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 DryingMode(props: Props){ + const {devices, deviceComponents, binPrefs, grain, cancel, confirm} = props + const grainExtensionMap = GetGrainExtensionMap(); + const classes = useStyles() + const [{user}] = useGlobalState() + const interactionAPI = useInteractionsAPI() + const [dryingMethod, setDryingMethod] = useState("air")//whether they are using natural air or a heater + const [options, setOptions] = useState([]) + const [deviceOption, setDeviceOption] = useState + if(grain){ + //get the values using the grain type + let ext = grainExtensionMap.get(grain) + if (ext) { + temp = ext.setTempC + hum = ext.targetMC + } + }else{//otherwise use the default drying interaction + temp = 40 + hum = 20 + } + + let conditions = []; + let humidityCondition = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, + value: Math.round( + describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(hum) + ) + }); + conditions.push(humidityCondition); + + //since the measurement describers function to stored does a conversion into celsius if the users pref is fahrenheit for temperature we need to convert the preset value into fahrenheit + if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + temp = Math.round((temp * (9 / 5) + 32) * 100) / 100; + } + let tempVal = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(temp); + + let tempCondition = 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 interaction conditions wont take floats + }); + conditions.push(tempCondition); + interaction.conditions = conditions; + //the the heater output to auto so that when the components are update it uses the new mode + heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO + return interaction; + }; + + const buildFanInteraction = ( + sensor: Plenum | Ambient, + fan: Controller, + tempComparison: "greater" | "less", + humidityComparison: "greater" | "less", + ) => { + 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 tempPreset = 0; + let humidityPreset = 0; + if(grain){ + //get the values using the grain type + let ext = grainExtensionMap.get(grain) + if (ext) { + tempPreset = ext.setTempC + humidityPreset = ext.targetMC + } + }else{//otherwise use the default drying interaction + tempPreset = 40 + humidityPreset = 20 + } + + + let conditions = []; + 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(humidityPreset) + }); + 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) { + tempPreset = Math.round((tempPreset * (9 / 5) + 32) * 100) / 100; + } + let tempVal = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(tempPreset); + + let fanConditionTwo = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + comparison: + 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 + }); + 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 + 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) + // if the controller is a heater create heater interaction + if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){ + multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller)) + }else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN || + controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){// if the controller is a fan + //if it is a combination set using a heater as well just turn the fan on + if(set.combo){ + controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON + }else{ + //otherwise make a fan interaction + multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less")) + } + } + }) + }) + setComponentSets([...sets]) + return { + conflicting: conflictingInteractions, + toAdd: multiInteractionSettings + } + } + + const stepper = () => { + return ( + + {steps.map((s, i) => ( + + {s.label} + + ))} + + ); + } + + const styleStep = () => { + return ( + + Select your drying method + { + setDryingMethod(value) + }}> + } + value={"air"} + label={"Natural Air"} + /> + } + value={"heat"} + label={"Supplemental Heat"} + /> + + + ) + } + + const deviceSelector = () => { + return ( + option.label || ""} + onChange={(_, newValue) => { + if(newValue){ + setSelectedDevice(newValue.device); + } + }} + renderInput={params => } + /> + ) + } + + const deviceStep = () => { + return ( + + Devices + {deviceSelector()} + {interactionLoading ? : + { + //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) + } + }} + /> + } + + ) + } + + //this step will use the conflicting interactions and interactions to add to display them and allow users to make changes + const interactionStep = () => { + console.log(interactionsToAdd) + return ( + + Interactions + {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){ + return ( + { + console.log(newSettings) + }} + /> + ) + } + })} + + ) + } + + const stepperContent = () => { + switch(currentStep){ + case 1: + return deviceStep() + case 2: + return interactionStep() + default: + return styleStep() + } + } + + const actions = () => { + return ( + + {/* close - tells the parent to close the dialog without doing anything, always visible */} + + {/* back - goes back to the previous step, hidden on the forst step */} + {currentStep !== 0 && } + {/* next - goes to the next step, hidden on the last step */} + {currentStep !== steps.length - 1 && } + {/* confirm - tells the parent to build the stages using the data, only visible on the last step */} + {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } + + ) + } + + + + return ( + + Bin Drying + + {stepper()} + {stepperContent()} + + + {actions()} + + ) } \ No newline at end of file diff --git a/src/bin/binModes/conditionDisplay.tsx b/src/bin/binModes/conditionDisplay.tsx new file mode 100644 index 0000000..d44c3ba --- /dev/null +++ b/src/bin/binModes/conditionDisplay.tsx @@ -0,0 +1,313 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + darken, + Grid, + Slider, + Theme, + Typography + } from "@mui/material"; + import { ExpandMore } from "@mui/icons-material"; + import { ExtractMoisture } from "grain"; + import { cloneDeep } from "lodash"; + import { Component, Interaction } from "models"; + import { interactionConditionText, interactionResultText } from "pbHelpers/Interaction"; + import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; + import { pond, quack } from "protobuf-ts/pond"; + import { useGlobalState } from "providers"; + import React, { useEffect, useState } from "react"; + import { avg, fahrenheitToCelsius } from "utils"; + import { makeStyles } from "@mui/styles"; + import { Mark } from "@mui/material/Slider/useSlider.types"; + + const useStyles = makeStyles((theme: Theme) => { + return ({ + displayBG: { + marginTop: 10, + background: darken(theme.palette.background.default, 0.05), + borderRadius: 5, + padding: 10 + }, + markContainer: { + zIndex: 2 + }, + arrowDown: { + width: 0, + height: 0, + borderLeft: "10px solid transparent", + borderRight: "10px solid transparent" + //borderTop: "10px solid yellow" + }, + sliderRoot: {}, + sliderThumb: { + height: 15, + width: 15, + backgroundColor: "yellow" + }, + sliderTrack: { + height: 3, + backgroundColor: "white" + }, + sliderRail: { + height: 3, + backgroundColor: "white" + }, + sliderValLabel: { + left: -20, + top: 40, + background: "transparent", + "& *": { + color: "#fff" + } + }, + sliderMark: { + visibility: "hidden" + }, + sliderMarked: { + marginTop: 25, + marginBottom: 0 + }, + sliderMarkLabel: { + top: -25 + } + }); + }); + + interface Props { + interaction: Interaction; + source: Component; + sink: Component; + grain?: pond.Grain; + customGrain?: pond.GrainSettings + changeConditions?: (newSettings: pond.InteractionSettings) => void + } + + export default function ConditionDisplay(props: Props) { + const { interaction, source, sink, grain, customGrain, changeConditions} = props; + const [sliderVals, setSliderVals] = useState>(new Map()); + const [sliderMarks, setSliderMarks] = useState>(new Map()); + //this is the emc value calculated from the interactions temp and humidity conditions + const [baseEMC, setBaseEMC] = useState(); + const [{ user }] = useGlobalState(); + const classes = useStyles(); + + useEffect(() => { + let passedInteraction = interaction; + let sliderVals: Map = new Map(); + let sliderMarks: Map = new Map(); + passedInteraction.conditions().forEach(condition => { + let describer = describeMeasurement(condition.measurementType, source.type(), undefined, undefined, user); + //NOTE: toDisplay will convert the temp value to fahrenheit + sliderVals.set(condition.measurementType, describer.toDisplay(condition.value)); + }); + + source.status.lastGoodMeasurement.forEach(measurement => { + let m = pond.UnitMeasurementsForComponent.fromObject(measurement); + if (m.values[0]) { + let markVal = avg(m.values[0].values); + //do this since this is how interactions handle the values so that the slider can use the toDisplay method of the describer for the marks + if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) markVal = markVal * 10; + if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) markVal = markVal * 100; + sliderMarks.set(m.type, markVal); + } + }); + setSliderVals(sliderVals); + setSliderMarks(sliderMarks); + }, [interaction, source]); + + useEffect(() => { + let temp = sliderVals.get(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE); + let hum = sliderVals.get(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT); + if ( + grain !== undefined && + grain !== pond.Grain.GRAIN_INVALID && + temp && + hum + ) { + if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + //the emc calc needs the temp to be in celsius + temp = fahrenheitToCelsius(temp); + } + let emc = ExtractMoisture(grain, temp, hum, customGrain) + setBaseEMC(emc === hum ? undefined : emc); + + } + }, [sliderVals, grain, user]); + + const customMark = (val: string, arrowColor: string) => { + return ( + + + + + {val} + + + + + + + + ); + }; + + + + const conditionDisplay = () => { + return ( + + {interaction.conditions().map((condition, i) => { + let describer = describeMeasurement(condition.measurementType, source?.type(), source.subType(), undefined, user); + let labelTail = describer.unit(); + let marks: Mark[] = []; + + // if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) { + // if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + // labelTail = "°F"; + // } else { + // labelTail = "°C"; + // } + // } + if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) { + labelTail = "%"; + } + + let mark = sliderMarks.get(condition.measurementType); + if (mark !== undefined) { + marks.push({ + label: customMark(describer.toDisplay(mark) + labelTail, describer.colour()), + value: describer.toDisplay(mark) + }); + } + return ( + + + {interactionConditionText(source, condition, false, user)} + + + { + if ( + condition.measurementType === + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE + ) { + if ( + user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + ) { + return value.toFixed(1) + "°F"; + } else { + return value.toFixed(1) + "°C"; + } + } + if ( + condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT + ) { + return value.toFixed(1) + "%"; + } + }} + marks={marks} + min={describer.min()} + max={describer.max()} + value={sliderVals.get(condition.measurementType) ?? describer.min()} + onChange={(_, val) => { + //note that changing it here like this is what is changing it in the interaction itself + condition.value = Math.round(describer.toStored(val as number)); + // if(changeConditions){ + // changeConditions(interaction.settings) + // } + let sliders = cloneDeep(sliderVals); + sliders.set(condition.measurementType, val as number); + setSliderVals(sliders); + }} + onChangeCommitted={(_, val) => { + if(changeConditions){ + changeConditions(interaction.settings) + } + }} + /> + + + ); + })} + + ); + }; + + const determineEMCRelation = () => { + let relation = "Approximately:"; + let tempRelation: quack.RelationalOperator | undefined = undefined; + let humidRelation: quack.RelationalOperator | undefined = undefined; + interaction.conditions().forEach(condition => { + if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) + tempRelation = condition.comparison; + if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) + humidRelation = condition.comparison; + }); + + if ( + (tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN || + tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) && + (humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN || + humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) + ) { + relation = "Less Than: "; + } + if ( + (tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN || + tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) && + (humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN || + humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) + ) { + relation = "Greater Than: "; + } + + return relation; + }; + + return ( + + + + {interactionResultText(interaction, sink)} + + {baseEMC !== undefined ? ( + + }> + + EMC {determineEMCRelation()} + + {baseEMC.toFixed(1)}% + + + + {conditionDisplay()} + + ) : ( + {conditionDisplay()} + )} + + + ); + } + \ No newline at end of file From 7e6b41273b3f95f925b36b1b51bfcf39f1d2c405 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 8 Jul 2026 10:57:23 -0600 Subject: [PATCH 04/12] defined device change data, and started working on storage --- src/bin/bin3dVisualizer.tsx | 11 --- src/bin/binModes/BinModeController.tsx | 113 ++++++++++++++-------- src/bin/binModes/DryingMode.tsx | 8 +- src/bin/binModes/StorageMode.tsx | 39 +++++++- src/bin/conditioning/modeChangeDialog.tsx | 6 +- src/common/PromiseProgress.tsx | 4 +- src/providers/pond/componentAPI.tsx | 3 +- 7 files changed, 118 insertions(+), 66 deletions(-) diff --git a/src/bin/bin3dVisualizer.tsx b/src/bin/bin3dVisualizer.tsx index 55316b8..bce1ab9 100644 --- a/src/bin/bin3dVisualizer.tsx +++ b/src/bin/bin3dVisualizer.tsx @@ -60,14 +60,6 @@ export default function bin3dVisualizer(props: Props){ setBinMode(bin.settings.mode) },[bin]) - const updateBin = (newMode: pond.BinMode) => { - if(updateBinCallback){ - let clone = cloneDeep(bin) - clone.settings.mode = newMode - updateBinCallback(clone) - } - }; - const binModeControl = () => { return ( @@ -374,9 +366,6 @@ export default function bin3dVisualizer(props: Props){ componentMap={componentMap} newMode={newMode} open={openModeChange} - updateMode={(newMode) => { - updateBin(newMode) - }} onClose={()=>{setOpenModeChange(false)}} /> diff --git a/src/bin/binModes/BinModeController.tsx b/src/bin/binModes/BinModeController.tsx index 7ae173e..f4f825e 100644 --- a/src/bin/binModes/BinModeController.tsx +++ b/src/bin/binModes/BinModeController.tsx @@ -1,15 +1,23 @@ -import { Box, Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"; +import { Box, Button, DialogActions, DialogContent, DialogTitle } from "@mui/material"; import ResponsiveDialog from "common/ResponsiveDialog"; import { Bin, Component, Device, Interaction } from "models"; import { pond } from "protobuf-ts/pond"; import React, { useEffect, useState } from "react"; import StorageMode from "./StorageMode"; -import DryingMode, { DryingBaseStepCount } from "./DryingMode"; +import DryingMode from "./DryingMode"; import HydratingMode from "./HydratingMode"; import CooldownMode from "./CooldownMode"; import { PromiseProgress, Stage, Step } from "common/PromiseProgress"; -import { useComponentAPI, useInteractionsAPI } from "providers"; +import { useBinAPI, useComponentAPI, useInteractionsAPI } from "providers"; import { ComponentSet } from "bin/conditioning/pickComponentSet"; +import { cloneDeep } from "lodash"; + +export interface DeviceChangeData { + deviceId: number + toRemove: Interaction[] + toAdd?: pond.MultiInteractionSettings + componentSets: ComponentSet[] +} interface Props { newMode: pond.BinMode //used to control which dialog to open @@ -20,16 +28,17 @@ interface Props { devices: Device[] componentDevices: Map componentMap: Map - updateMode: (newMode: pond.BinMode) => void + modeUpdated?: (mode: pond.BinMode) => void } export default function BinModeController(props: Props) { - const {open, onClose, newMode, updateMode, devices, componentDevices, componentMap, binPrefs, bin} = props + const {open, onClose, newMode, devices, componentDevices, componentMap, binPrefs, bin, modeUpdated} = props const [showProgress, setShowProgress] = useState(false) const [deviceComponents, setDeviceComponents] = useState>(new Map()) const [promiseStages, setPromiseStages] = useState([]) const interactionAPI = useInteractionsAPI() const componentAPI = useComponentAPI() + const binAPI = useBinAPI(); //the use effects for the initial setup of what will be needed useEffect(()=>{ @@ -59,55 +68,79 @@ export default function BinModeController(props: Props) { onClose() } - const buildStages = (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => { + const buildStages = (deviceData: DeviceChangeData[]) => { //build the stages to pass into the PromiseProgress //stage one is to remove conflicting interactions let stages: Stage[] = [] let stage1: Stage = { - title: "Remove Conflicting Interactions", + title: "Remove Interactions", steps: [] } - conflictingInteractions.forEach(i => { - let newStep: Step = { - title: "Removing Interaction", - promise: interactionAPI.removeInteraction(deviceId, i.key()) - } - stage1.steps.push(newStep); - }); - if(stage1.steps.length > 0){ - stages.push(stage1) - } - //stage two is to add the new interactions let stage2: Stage = { title: "Add New Interactions", - steps: [ - { - title: "Adding Interactions", - promise: interactionAPI.addMultiInteractions(deviceId, newInteractions) - } - ] + steps: [] } - if(stage2.steps.length > 0){ - stages.push(stage2) - } - - //stage three is to update the controller components let stage3: Stage = { title: "Update Controllers", steps: [] } - componentSets.forEach(set => { - set.controllers.forEach(controller => { - let newStep: Step = { - title: "Updating " + controller.name(), - promise: componentAPI.update(deviceId, controller.settings) - } - stage3.steps.push(newStep) + deviceData.forEach(data => { + data.toRemove.forEach(interaction => { + let newStep: Step = { + title: "Removing Interaction", + promise: () => interactionAPI.removeInteraction(data.deviceId, interaction.key()) + } + stage1.steps.push(newStep); }) + let newInteractions = data.toAdd + if(newInteractions){ + stage2.steps.push({ + title: "Adding Interactions", + promise: () => interactionAPI.addMultiInteractions(data.deviceId, newInteractions) + }) + } + data.componentSets.forEach(set => { + set.controllers.forEach(controller => { + let newStep: Step = { + title: "Updating " + controller.name(), + promise: () => componentAPI.update(data.deviceId, controller.settings) + } + stage3.steps.push(newStep) + }) + }) }) + if(stage1.steps.length > 0){ + stages.push(stage1) + } + if(stage2.steps.length > 0){ + stages.push(stage2) + } if(stage3.steps.length > 0){ stages.push(stage3) } + //stage 4 is to update the bin with the new mode + let stage4: Stage = { + title: "Update Bin With New Mode", + steps: [ + { + title: "Update " + bin.name(), + promise: () => { + let clone = cloneDeep(bin.settings) + clone.mode = newMode + //this will set the bin to use the auto top nodes for cables when the bin is going into storage mode, and turn them off if the bin is being conditioned + if(newMode === pond.BinMode.BIN_MODE_STORAGE){ + clone.autoGrainNode = true + }else{ + clone.autoGrainNode = false + } + return binAPI.updateBin(bin.key(), clone) + }, + onComplete: () => modeUpdated?.(newMode) + } + ] + } + stages.push(stage4) + //set those stages to a state variable setPromiseStages(stages) } @@ -136,7 +169,7 @@ export default function BinModeController(props: Props) { /> - + ) @@ -152,8 +185,8 @@ export default function BinModeController(props: Props) { binPrefs={binPrefs} deviceComponents={deviceComponents} cancel={closeDialog} - confirm={(conflicting, device, newInteractions, components) => { - buildStages(conflicting, device, newInteractions, components) + confirm={(deviceData) => { + buildStages(deviceData) setShowProgress(true) }} /> @@ -162,7 +195,7 @@ export default function BinModeController(props: Props) { case pond.BinMode.BIN_MODE_COOLDOWN: return default: - return + return {}}/> } } diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index 10895a1..40d071a 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -18,6 +18,7 @@ import { useGlobalState, useInteractionsAPI } from "providers"; import BinConditioningInteraction from "bin/BinConditioningInteraction"; import ConditionDisplay from "./conditionDisplay"; import React from "react"; +import { DeviceChangeData } from "./BinModeController"; const useStyles = makeStyles((theme: Theme) => { @@ -33,8 +34,6 @@ interface Step { completed?: boolean; } -export const DryingBaseStepCount = 3 - interface Option { label: string; device: Device; @@ -47,7 +46,7 @@ interface Props { binPrefs?: Map grain?: pond.Grain cancel: () => void - confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void + confirm: (deviceData: DeviceChangeData[]) => void } const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}] @@ -419,6 +418,7 @@ export default function DryingMode(props: Props){ } const deviceStep = () => { + return ( Devices @@ -497,7 +497,7 @@ export default function DryingMode(props: Props){ {/* next - goes to the next step, hidden on the last step */} {currentStep !== steps.length - 1 && } {/* confirm - tells the parent to build the stages using the data, only visible on the last step */} - {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } + {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } ) } diff --git a/src/bin/binModes/StorageMode.tsx b/src/bin/binModes/StorageMode.tsx index 67f82dc..6d61c3a 100644 --- a/src/bin/binModes/StorageMode.tsx +++ b/src/bin/binModes/StorageMode.tsx @@ -1,9 +1,38 @@ -import { Box } from "@mui/material"; +import { Box, DialogContent, DialogTitle, DialogActions } from "@mui/material"; +import { ComponentSet } from "bin/conditioning/pickComponentSet"; +import { Device, Interaction } from "models"; +import { pond } from "protobuf-ts/pond"; +import React from "react"; + +interface InteractionToRemove { + deviceID: number, + //interactions: +} + +interface Props { + // the devices connected to the bin + devices: Device[] + confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void +} + +/** + * when a bin goes into storage mode then the auto top node needs to be set, should be handled in the controller actually + * interactions with connected controllers as sink should be deleted, and controllers should be set to off + * @returns + */ +export default function StorageMode(props: Props){ + + //load the interactions for all of the bins connected devices + + // -export default function StorageMode(){ return ( - - - + + Storage Mode + + {/* could have the explanation of what changing to storage mode does here */} + + + ) } \ No newline at end of file diff --git a/src/bin/conditioning/modeChangeDialog.tsx b/src/bin/conditioning/modeChangeDialog.tsx index ef2dfbd..33b8b01 100644 --- a/src/bin/conditioning/modeChangeDialog.tsx +++ b/src/bin/conditioning/modeChangeDialog.tsx @@ -439,7 +439,7 @@ if (!selectedDevice) return; conflictingSetInteractions.forEach(i => { let newStep: PromiseStep = { title: "Removing Interaction", - promise: interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as) + promise: () => interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as) } stage1.steps.push(newStep); }); @@ -452,7 +452,7 @@ if (!selectedDevice) return; steps: [ { title: "Adding Interactions", - promise: interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd) + promise: () => interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd) } ] } @@ -469,7 +469,7 @@ if (!selectedDevice) return; set.controllers.forEach(controller => { let newStep: PromiseStep = { title: "Updating " + controller.name(), - promise: componentAPI.update(selectedDevice.id(), controller.settings) + promise: () => componentAPI.update(selectedDevice.id(), controller.settings) } stage3.steps.push(newStep) }) diff --git a/src/common/PromiseProgress.tsx b/src/common/PromiseProgress.tsx index 0bc39e9..0ade5f5 100644 --- a/src/common/PromiseProgress.tsx +++ b/src/common/PromiseProgress.tsx @@ -11,7 +11,7 @@ export interface Stage { export interface Step { title: string - promise: Promise + promise: () => Promise onComplete?: () => void onError?: () => void } @@ -52,7 +52,7 @@ export function PromiseProgress(props: Props){ // mark step as in progress progMap.set(i+"-"+k, progress.InProgress); setCompletion(new Map(progMap)); - return step.promise + return step.promise() .then(() => { // mark step as completed progMap.set(i+"-"+k, progress.Complete); diff --git a/src/providers/pond/componentAPI.tsx b/src/providers/pond/componentAPI.tsx index a224952..e78b75a 100644 --- a/src/providers/pond/componentAPI.tsx +++ b/src/providers/pond/componentAPI.tsx @@ -417,7 +417,8 @@ export default function ComponentProvider(props: PropsWithChildren) { "&end=" + end + (keys ? "&keys=" + keys : "&keys=" + [deviceId.toString()]) + - (types ? "&types=" + types : "&types=" + ["device"]) + (types ? "&types=" + types : "&types=" + ["device"]) + + (as ? "&as=" + as : "") ) return new Promise>((resolve,reject) => { get(url).then(resp => { From 2c6799bcc1a68e6e695a3546c226d00b8cce8255 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 9 Jul 2026 10:21:24 -0600 Subject: [PATCH 05/12] working on storage building the stuff to send up --- src/bin/binModes/StorageMode.tsx | 72 +++++++++++++++++++++++++++++--- src/models/Plenum.ts | 4 +- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/bin/binModes/StorageMode.tsx b/src/bin/binModes/StorageMode.tsx index 6d61c3a..70c4f72 100644 --- a/src/bin/binModes/StorageMode.tsx +++ b/src/bin/binModes/StorageMode.tsx @@ -1,18 +1,26 @@ -import { Box, DialogContent, DialogTitle, DialogActions } from "@mui/material"; -import { ComponentSet } from "bin/conditioning/pickComponentSet"; -import { Device, Interaction } from "models"; +import { DialogContent, DialogTitle, DialogActions } from "@mui/material"; +import { Component, Device, Interaction } from "models"; +import { Ambient } from "models/Ambient"; +import { Controller } from "models/Controller"; +import { GrainCable } from "models/GrainCable"; +import { Plenum } from "models/Plenum"; import { pond } from "protobuf-ts/pond"; -import React from "react"; +import { quack } from "protobuf-ts/quack"; +import React, { useEffect, useState } from "react"; +import { DeviceChangeData } from "./BinModeController"; +import { useInteractionsAPI } from "providers"; +import { AxiosResponse } from "axios"; interface InteractionToRemove { deviceID: number, - //interactions: } interface Props { // the devices connected to the bin devices: Device[] - confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void + deviceComponents: Map + binPrefs?: Map + confirm: (deviceData: DeviceChangeData[]) => void } /** @@ -21,6 +29,58 @@ interface Props { * @returns */ export default function StorageMode(props: Props){ + const {devices, deviceComponents, binPrefs} = props + const interactionsAPI = useInteractionsAPI() + + useEffect(()=>{ + let interactionPromises: Promise[] = [] + let deviceContext: {device: Device, plenums: Plenum[], ambients: Ambient[], cables: GrainCable[], controllers: Controller[]}[] = [] + devices.forEach(device => { + let plenums: Plenum[] = [] + let ambients: Ambient[] = [] + let cables: GrainCable[] = [] //since this mode is going to add notification interactions to cables they should be filtered as well + let controllers: Controller[] = [] //because storage is just setting things to false i dont think i need to seperate fans from heaters + //sort the components into the plenum, ambient, grain cable, and controller arrays + deviceComponents.get(device.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_HEATER || pref.type === pond.BinComponent.BIN_COMPONENT_FAN) { + let controller = Controller.create(comp); + controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF; + controllers.push(controller); + //sinkMap.set(comp.locationString(), comp) + } + if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){ + let cable = GrainCable.create(comp) + cables.push(cable) + } + } + } + }); + //after sorting it will need to load the interactions for the device + deviceContext.push({device, plenums, ambients, cables, controllers}) + interactionPromises.push(interactionsAPI.listInteractionsByDevice(device.id())) + }) + + Promise.all(interactionPromises).then(resp => { + //the index of this should match the device index in the devices array + resp.forEach((interactions,i) => { + interactions.forEach(interaction => { + + }) + }) + }) + + },[devices, binPrefs, deviceComponents]) //load the interactions for all of the bins connected devices diff --git a/src/models/Plenum.ts b/src/models/Plenum.ts index b88b8a0..5ec161f 100644 --- a/src/models/Plenum.ts +++ b/src/models/Plenum.ts @@ -108,7 +108,9 @@ export class Plenum { return quack.ComponentID.fromObject({ type: this.settings.type, addressType: this.settings.addressType, - address: this.settings.address + address: this.settings.address, + expansionLine: this.settings.expansionLine, + muxLine: this.settings.muxLine }); } From 208a657528b863ae22f6dafe65c0785bdc7b0676 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 10 Jul 2026 11:59:42 -0600 Subject: [PATCH 06/12] finished the functional part of storage as well as modified the drying dialog to remove notification interactions from the cables --- src/bin/binModes/BinModeController.tsx | 43 +++++++--- src/bin/binModes/DryingMode.tsx | 29 +++++-- src/bin/binModes/StorageMode.tsx | 112 +++++++++++++++++++++---- src/models/Ambient.ts | 15 +++- src/models/Controller.ts | 15 +++- src/models/GrainCable.ts | 15 +++- src/models/Plenum.ts | 11 ++- 7 files changed, 193 insertions(+), 47 deletions(-) diff --git a/src/bin/binModes/BinModeController.tsx b/src/bin/binModes/BinModeController.tsx index f4f825e..32dd6af 100644 --- a/src/bin/binModes/BinModeController.tsx +++ b/src/bin/binModes/BinModeController.tsx @@ -11,12 +11,25 @@ import { PromiseProgress, Stage, Step } from "common/PromiseProgress"; import { useBinAPI, useComponentAPI, useInteractionsAPI } from "providers"; import { ComponentSet } from "bin/conditioning/pickComponentSet"; import { cloneDeep } from "lodash"; +import { Controller } from "models/Controller"; export interface DeviceChangeData { + /** + * the id of the device changes are being made to + */ deviceId: number + /** + * the array of interactions to remove from the device + */ toRemove: Interaction[] + /** + * the new interactions to add to the device + */ toAdd?: pond.MultiInteractionSettings - componentSets: ComponentSet[] + /** + * the controllers that need to have there state updated + */ + controllers: Controller[] } interface Props { @@ -99,16 +112,16 @@ export default function BinModeController(props: Props) { promise: () => interactionAPI.addMultiInteractions(data.deviceId, newInteractions) }) } - data.componentSets.forEach(set => { - set.controllers.forEach(controller => { - let newStep: Step = { - title: "Updating " + controller.name(), - promise: () => componentAPI.update(data.deviceId, controller.settings) - } - stage3.steps.push(newStep) - }) + + data.controllers.forEach(controller => { + let newStep: Step = { + title: "Updating " + controller.name(), + promise: () => componentAPI.update(data.deviceId, controller.settings) + } + stage3.steps.push(newStep) }) - }) + }) + if(stage1.steps.length > 0){ stages.push(stage1) } @@ -195,7 +208,15 @@ export default function BinModeController(props: Props) { case pond.BinMode.BIN_MODE_COOLDOWN: return default: - return {}}/> + return { + buildStages(deviceData) + setShowProgress(true) + }}/> } } diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index 40d071a..d0f7bb0 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -19,6 +19,7 @@ import BinConditioningInteraction from "bin/BinConditioningInteraction"; import ConditionDisplay from "./conditionDisplay"; import React from "react"; import { DeviceChangeData } from "./BinModeController"; +import { GrainCable } from "models/GrainCable"; const useStyles = makeStyles((theme: Theme) => { @@ -66,6 +67,7 @@ export default function DryingMode(props: Props){ const [ambients, setAmbients] = useState([]) const [heaters, setHeaters] = useState([]) const [fans, setFans] = useState([]) + const [cables, setCables] = useState([]) const [currentStep, setCurrentStep] = useState(0) const [interactionLoading,setInteractionsLoading] = useState(false) @@ -87,7 +89,7 @@ export default function DryingMode(props: Props){ if (!deviceComponents.get(selectedDevice.id())) return; var plenums: Plenum[] = []; var ambients: Ambient[] = []; - //var grainCables: GrainCable[] = []; + var grainCables: GrainCable[] = []; var heaters: Controller[] = []; var fans: Controller[] = []; var sinkMap: Map = new Map() @@ -105,8 +107,10 @@ export default function DryingMode(props: Props){ ambients.push(Ambient.create(comp)); sourceMap.set(comp.locationString(), comp) } - // if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE) - // grainCables.push(GrainCable.create(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_HEATER) { let heater = Controller.create(comp); heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF; @@ -128,7 +132,7 @@ export default function DryingMode(props: Props){ 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 => { @@ -315,11 +319,12 @@ export default function DryingMode(props: Props){ //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[]) => { + console.log("create interactions") 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 + //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 => { @@ -335,6 +340,7 @@ export default function DryingMode(props: Props){ } return conflicting; }); + conflictingInteractions = conflictingInteractions.concat(c) // if the controller is a heater create heater interaction if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){ @@ -351,6 +357,17 @@ export default function DryingMode(props: Props){ } }) }) + console.log(cables) + 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, @@ -497,7 +514,7 @@ export default function DryingMode(props: Props){ {/* next - goes to the next step, hidden on the last step */} {currentStep !== steps.length - 1 && } {/* confirm - tells the parent to build the stages using the data, only visible on the last step */} - {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } + {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } ) } diff --git a/src/bin/binModes/StorageMode.tsx b/src/bin/binModes/StorageMode.tsx index 70c4f72..e9a0926 100644 --- a/src/bin/binModes/StorageMode.tsx +++ b/src/bin/binModes/StorageMode.tsx @@ -1,5 +1,5 @@ -import { DialogContent, DialogTitle, DialogActions } from "@mui/material"; -import { Component, Device, Interaction } from "models"; +import { DialogContent, DialogTitle, DialogActions, Button } from "@mui/material"; +import { Bin, Component, Device, Interaction } from "models"; import { Ambient } from "models/Ambient"; import { Controller } from "models/Controller"; import { GrainCable } from "models/GrainCable"; @@ -10,15 +10,16 @@ import React, { useEffect, useState } from "react"; import { DeviceChangeData } from "./BinModeController"; import { useInteractionsAPI } from "providers"; import { AxiosResponse } from "axios"; - -interface InteractionToRemove { - deviceID: number, -} +import { componentIDToString } from "pbHelpers/Component"; +import { ComponentSet } from "bin/conditioning/pickComponentSet"; +import moment from "moment"; +import { lowerCase } from "lodash"; interface Props { // the devices connected to the bin devices: Device[] deviceComponents: Map + bin: Bin binPrefs?: Map confirm: (deviceData: DeviceChangeData[]) => void } @@ -29,13 +30,17 @@ interface Props { * @returns */ export default function StorageMode(props: Props){ - const {devices, deviceComponents, binPrefs} = props + const {devices, deviceComponents, bin, binPrefs, confirm} = props const interactionsAPI = useInteractionsAPI() + const [data, setData] = useState([]) + const [loading, setLoading] = useState(false) + const [loadErr, setLoadErr] = useState(false) useEffect(()=>{ let interactionPromises: Promise[] = [] let deviceContext: {device: Device, plenums: Plenum[], ambients: Ambient[], cables: GrainCable[], controllers: Controller[]}[] = [] devices.forEach(device => { + if (!deviceComponents.get(device.id())) return; let plenums: Plenum[] = [] let ambients: Ambient[] = [] let cables: GrainCable[] = [] //since this mode is going to add notification interactions to cables they should be filtered as well @@ -70,29 +75,106 @@ export default function StorageMode(props: Props){ deviceContext.push({device, plenums, ambients, cables, controllers}) interactionPromises.push(interactionsAPI.listInteractionsByDevice(device.id())) }) - + setLoading(true) + let deviceChangeData: DeviceChangeData[] = [] Promise.all(interactionPromises).then(resp => { //the index of this should match the device index in the devices array resp.forEach((interactions,i) => { + let toRemove: Interaction[] = [] + let ctx = deviceContext[i] + let sensorsAddr = ctx.plenums.concat(ctx.ambients).map(sensor => sensor.locationString()) + let controllerAddr = ctx.controllers.map(cont => cont.locationString()) + let cableAddr = ctx.cables.map(cab => cab.locationString()) + let cablesWithInteraction = new Set() + interactions.forEach(interaction => { - + //first check if the interaction has a sink in the devices fans or heaters and is being controlled by a plenum or ambient + //this should make it so that it only removes conditioning interactions and not all interactions on the device + let source = interaction.settings.source + let sink = interaction.settings.sink + //this checks if the interaction is a 'conditioning' interaction that needs to be removed + if(source && sink){ + if(controllerAddr.includes(componentIDToString(sink)) && sensorsAddr.includes(componentIDToString(source))){ + toRemove.push(interaction) + } + } + //need to make sure that it is a notification only interaction and not a controlling one + //it is possible to control things with cables and we want to leave those alone and not consider them notification interactions + //the reason we check that there is no sink is because even control interactions can still send notifications + //we want interactions that effectively ONLY send notifications + if (source && !sink && cableAddr.includes(componentIDToString(source))) { + //check if notifications is true + if(interaction.settings.notifications?.notify){ + cablesWithInteraction.add(componentIDToString(source)) + } + } }) + let cablesNeedingNotification = ctx.cables.filter( + cable => !cablesWithInteraction.has(cable.locationString()) + ) + + let dd: DeviceChangeData = { + deviceId: ctx.device.id(), + toRemove: toRemove, + controllers: ctx.controllers, + + } + + if(cablesNeedingNotification.length > 0){ + let toAdd: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() + cablesNeedingNotification.forEach(cable => { + //create the new interaction here + let notification: pond.InteractionSettings = pond.InteractionSettings.create({ + source: cable.location(), + schedule: pond.InteractionSchedule.create({ + timeOfDayStart: "00:00", + timeOfDayEnd: "24:00", + timezone: moment.tz.guess(), + weekdays: moment.weekdays().map(d => lowerCase(d)) + }), + conditions: [pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, + value: bin.settings.highTemp + })], + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT + }), + notifications: pond.InteractionNotifications.create({ + notify: true + }), + }) + + //push it to toAdd.interactions + toAdd.interactions.push(notification) + + }) + //add toAdd to dd + dd.toAdd = toAdd + } + deviceChangeData.push(dd) }) + setData(deviceChangeData) + }).catch(err => { + //TODO: display a snackBar message saying there was an issue loading the data for one or more devices + setData([]) + setLoadErr(true) + }).finally(() => { + setLoading(false) }) - },[devices, binPrefs, deviceComponents]) - //load the interactions for all of the bins connected devices - - // - return ( Storage Mode {/* could have the explanation of what changing to storage mode does here */} - + + + ) } \ No newline at end of file diff --git a/src/models/Ambient.ts b/src/models/Ambient.ts index d2c7577..4fde1ea 100644 --- a/src/models/Ambient.ts +++ b/src/models/Ambient.ts @@ -108,18 +108,25 @@ export class Ambient { return quack.ComponentID.fromObject({ type: this.settings.type, addressType: this.settings.addressType, - address: this.settings.address + address: this.settings.address, + expansionLine: this.settings.expansionLine, + muxLine: this.settings.muxLine }); } public locationString(): string { - return ( - or(this.settings.type, 0).toString() + + let compositeLocation = or(this.settings.type, 0).toString() + "-" + or(this.settings.addressType, 0).toString() + "-" + or(this.settings.address, 0).toString() - ); + if(this.settings.expansionLine){ + compositeLocation = compositeLocation + "-" + this.settings.expansionLine + } + if(this.settings.muxLine){ + compositeLocation = compositeLocation + ":" + this.settings.muxLine + } + return compositeLocation; } public type(): quack.ComponentType { diff --git a/src/models/Controller.ts b/src/models/Controller.ts index 4e7170c..c29ba52 100644 --- a/src/models/Controller.ts +++ b/src/models/Controller.ts @@ -82,18 +82,25 @@ export class Controller { return quack.ComponentID.fromObject({ type: this.settings.type, addressType: this.settings.addressType, - address: this.settings.address + address: this.settings.address, + expansionLine: this.settings.expansionLine, + muxLine: this.settings.muxLine }); } public locationString(): string { - return ( - or(this.settings.type, 0).toString() + + let compositeLocation = or(this.settings.type, 0).toString() + "-" + or(this.settings.addressType, 0).toString() + "-" + or(this.settings.address, 0).toString() - ); + if(this.settings.expansionLine){ + compositeLocation = compositeLocation + "-" + this.settings.expansionLine + } + if(this.settings.muxLine){ + compositeLocation = compositeLocation + ":" + this.settings.muxLine + } + return compositeLocation; } public type(): quack.ComponentType { diff --git a/src/models/GrainCable.ts b/src/models/GrainCable.ts index 4de1ae4..e3e988b 100644 --- a/src/models/GrainCable.ts +++ b/src/models/GrainCable.ts @@ -230,18 +230,25 @@ export class GrainCable { return quack.ComponentID.fromObject({ type: this.settings.type, addressType: this.settings.addressType, - address: this.settings.address + address: this.settings.address, + expansionLine: this.settings.expansionLine, + muxLine: this.settings.muxLine }); } public locationString(): string { - return ( - or(this.settings.type, 0).toString() + + let compositeLocation = or(this.settings.type, 0).toString() + "-" + or(this.settings.addressType, 0).toString() + "-" + or(this.settings.address, 0).toString() - ); + if(this.settings.expansionLine){ + compositeLocation = compositeLocation + "-" + this.settings.expansionLine + } + if(this.settings.muxLine){ + compositeLocation = compositeLocation + ":" + this.settings.muxLine + } + return compositeLocation; } public type(): quack.ComponentType { diff --git a/src/models/Plenum.ts b/src/models/Plenum.ts index 5ec161f..15f99bc 100644 --- a/src/models/Plenum.ts +++ b/src/models/Plenum.ts @@ -115,13 +115,18 @@ export class Plenum { } public locationString(): string { - return ( - or(this.settings.type, 0).toString() + + let compositeLocation = or(this.settings.type, 0).toString() + "-" + or(this.settings.addressType, 0).toString() + "-" + or(this.settings.address, 0).toString() - ); + if(this.settings.expansionLine){ + compositeLocation = compositeLocation + "-" + this.settings.expansionLine + } + if(this.settings.muxLine){ + compositeLocation = compositeLocation + ":" + this.settings.muxLine + } + return compositeLocation; } public type(): quack.ComponentType { From aa346a6a5a03694820af59935ec5a375b39dd7e2 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 10 Jul 2026 15:27:47 -0600 Subject: [PATCH 07/12] set up the bones for the humidity mode --- src/bin/binModes/BinModeController.tsx | 14 +- src/bin/binModes/DryingMode.tsx | 11 +- src/bin/binModes/HydratingMode.tsx | 401 ++++++++++++++++++++++++- 3 files changed, 415 insertions(+), 11 deletions(-) diff --git a/src/bin/binModes/BinModeController.tsx b/src/bin/binModes/BinModeController.tsx index 32dd6af..6c4fead 100644 --- a/src/bin/binModes/BinModeController.tsx +++ b/src/bin/binModes/BinModeController.tsx @@ -194,6 +194,7 @@ export default function BinModeController(props: Props) { //setNumSteps(DryingStepCount) return case pond.BinMode.BIN_MODE_HYDRATING: - return + return { + buildStages(deviceData) + setShowProgress(true) + }} + /> case pond.BinMode.BIN_MODE_COOLDOWN: return default: diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index d0f7bb0..716e726 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -1,5 +1,4 @@ -import { ArrowBack, ArrowForward } from "@mui/icons-material"; -import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Grid2, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material"; +import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, 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"; @@ -7,7 +6,7 @@ import { Ambient } from "models/Ambient"; import { Controller } from "models/Controller"; import { Plenum } from "models/Plenum"; import { pond, quack } from "protobuf-ts/pond"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { ComponentSet } from "bin/conditioning/pickComponentSet" import { componentIDToString, sameComponentID } from "pbHelpers/Component"; import moment from "moment"; @@ -15,7 +14,6 @@ import { lowerCase } from "lodash"; import { GetGrainExtensionMap } from "grain"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { useGlobalState, useInteractionsAPI } from "providers"; -import BinConditioningInteraction from "bin/BinConditioningInteraction"; import ConditionDisplay from "./conditionDisplay"; import React from "react"; import { DeviceChangeData } from "./BinModeController"; @@ -46,6 +44,7 @@ interface Props { deviceComponents: Map binPrefs?: Map grain?: pond.Grain + customGrain?: pond.GrainSettings cancel: () => void confirm: (deviceData: DeviceChangeData[]) => void } @@ -54,7 +53,7 @@ const steps = [{label: "Style"}, {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 DryingMode(props: Props){ - const {devices, deviceComponents, binPrefs, grain, cancel, confirm} = props + const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props const grainExtensionMap = GetGrainExtensionMap(); const classes = useStyles() const [{user}] = useGlobalState() @@ -478,6 +477,8 @@ export default function DryingMode(props: Props){ if(sink && source){ return ( { + 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 + binPrefs?: Map + grain?: pond.Grain + customGrain?: pond.GrainSettings + cancel: () => void + confirm: (deviceData: DeviceChangeData[]) => void +} + +const steps = [{label: "Style"}, {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 DryingMode(props: Props){ + const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props + const grainExtensionMap = GetGrainExtensionMap(); + const classes = useStyles() + const [{user}] = useGlobalState() + const interactionAPI = useInteractionsAPI() + const [options, setOptions] = useState([]) + const [deviceOption, setDeviceOption] = useState