finished the bin sensor view to display connected components for a bin

This commit is contained in:
csawatzky 2026-05-28 11:18:40 -06:00
parent 67b251d865
commit b00f10b831
16 changed files with 983 additions and 232 deletions

105
src/bin/BinSensorCard.tsx Normal file
View file

@ -0,0 +1,105 @@
import { AccessTime, MoreVert } from "@mui/icons-material";
import { Avatar, Box, Card, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Typography, useTheme } from "@mui/material";
import moment from "moment";
import React from "react";
interface SensorRow {
label: string;
data: string;
}
interface CableRow {
label: string;
min: string;
avg: string;
max: string;
}
interface Props {
name: string;
icon?: string;
tag: string;
lastReading: string;
onMenuClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
rows?: SensorRow[];
cableRows?: CableRow[];
/**
* the time in hours that a reading is considered 'good'
* defaults to 6
*/
staleLimit?: number;
}
export default function BinSensorCard(props: Props) {
const { name, icon, tag, lastReading, onMenuClick, rows, cableRows, staleLimit = 6 } = props
const theme = useTheme()
const isStale = moment().diff(moment(lastReading), "hours") > staleLimit
return (
<Card raised sx={{ height: "100%" }}>
<Box padding={1.5} height="100%" display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={1}>
<Box display="flex" alignItems="center" gap={1}>
{icon &&
<Avatar
variant="square"
src={icon}
alt={name + " icon"}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
}
<Typography fontWeight={500}>{name}</Typography>
<Typography variant="caption" color="textSecondary">- {tag}</Typography>
</Box>
<IconButton size="small" onClick={onMenuClick}>
<MoreVert fontSize="small" />
</IconButton>
</Box>
<Table size="small">
{cableRows ? (
<>
<TableHead>
<TableRow>
<TableCell sx={{ color: "text.secondary", fontSize: 11, fontWeight: 500, border: 0, pb: 0.5, pl: 0 }} />
<TableCell align="right" sx={{ color: "text.secondary", fontSize: 11, fontWeight: 500, border: 0, pb: 0.5 }}>Min</TableCell>
<TableCell align="right" sx={{ color: "primary.main", fontSize: 11, fontWeight: 500, border: 0, pb: 0.5 }}>Avg</TableCell>
<TableCell align="right" sx={{ color: "text.secondary", fontSize: 11, fontWeight: 500, border: 0, pb: 0.5, pr: 0 }}>Max</TableCell>
</TableRow>
</TableHead>
<TableBody>
{cableRows.map((row) => (
<TableRow key={row.label}>
<TableCell sx={{ color: "text.secondary", fontSize: 11, pl: 0, py: 0.5 }}>{row.label}</TableCell>
<TableCell align="right" sx={{ fontSize: 12, py: 0.5 }}>{row.min}</TableCell>
<TableCell align="right" sx={{ color: "primary.main", fontWeight: 500, fontSize: 12, py: 0.5 }}>{row.avg}</TableCell>
<TableCell align="right" sx={{ fontSize: 12, py: 0.5, pr: 0 }}>{row.max}</TableCell>
</TableRow>
))}
</TableBody>
</>
) : (
<TableBody>
{rows?.map((row) => (
<TableRow key={row.label}>
<TableCell sx={{ color: "text.secondary", fontSize: 11, pl: 0, py: 0.5 }}>{row.label}</TableCell>
<TableCell align="right" sx={{ fontSize: 12, py: 0.5 }}>{row.data}</TableCell>
</TableRow>
))}
</TableBody>
)}
</Table>
<Box display="flex" alignItems="center" gap={0.5} marginTop="auto">
<AccessTime sx={{ fontSize: 12, color: isStale ? "warning.main" : "text.disabled" }} />
<Typography variant="caption" color={isStale ? "warning.main" : "text.secondary"}>
{moment(lastReading).fromNow()}
</Typography>
</Box>
</Box>
</Card>
)
}

View file

@ -0,0 +1,780 @@
import { AccessTime, MoreVert } from "@mui/icons-material";
import { Avatar, Box, Button, Card, Grid2, IconButton, Menu, MenuItem, Typography, useTheme } from "@mui/material";
import BinSensorCard from "bin/BinSensorCard";
import { ExtractMoisture } from "grain";
import { useThemeType } from "hooks";
import { Bin, Component } from "models";
import { Ambient } from "models/Ambient";
import { CO2 } from "models/CO2";
import { Controller } from "models/Controller";
import { GrainCable } from "models/GrainCable";
import { Headspace } from "models/Headspace";
import { Plenum } from "models/Plenum";
import { Pressure } from "models/Pressure";
import moment from "moment";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useBinAPI, useComponentAPI, useGlobalState, useSnackbar } from "providers";
import React, { useEffect, useState } from "react";
import { avg } from "utils";
interface Props {
bin: Bin
components: Map<string, Component>;
componentDevices: Map<string, number>;
preferences?: Map<string, pond.BinComponentPreferences>;
setPreferences: React.Dispatch<
React.SetStateAction<Map<string, pond.BinComponentPreferences> | undefined>
>;
}
export default function BinSensorsDisplay(props: Props){
const {bin, components, componentDevices, preferences, setPreferences} = props
const themeType = useThemeType()
const [{user, as}] = useGlobalState()
const binAPI = useBinAPI()
const componentAPI = useComponentAPI()
const theme = useTheme()
const snackbar = useSnackbar()
const [cables, setCables] = useState<GrainCable[]>([])
const [plenums, setPlenums] = useState<Plenum[]>([])
const [ambient, setAmbient] = useState<Ambient[]>([])
const [pressures, setPressures] = useState<Pressure[]>([])
const [fans, setFans] = useState<Controller[]>([])
const [heaters, setHeaters] = useState<Controller[]>([])
//the above component preferences all have only components that are one type of sensor, however headspace has components of three different sensors T/H, CO2, and Lidar
const [headspaceTH, setHeadspaceTH] = useState<Headspace[]>([]) //the components in the headspace that measure temp/humidity
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]) //the CO2 components in the headspace
const [headspaceLidar, setHeadspaceLidar] = useState<Component[]>([]) //the lidar components in the headspace
const [unassignedComponents, setUnassignedComponents] = useState<Component[]>([])
//using counts so i dont have to have to check multiple arrays for sensors and controllers
const [sensorCount, setSensorCount] = useState(0)
const [controllerCount, setControllerCount] = useState(0)
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
//only used to determine what preferences are options for the selected component ie. DHT can be a plenum or a headspace
const [selectedComponentType, setSelectedComponentType] = useState<
quack.ComponentType | undefined
>(undefined);
const [componentUnassigned, setComponentUnnassigned] = useState(false);
const [selectedComponentKey, setSelectedComponentKey] = useState("");
const [selectedComponentSubtype, setSelectedComponentSubtype] = useState(0);
const [selectedComponentTopNode, setSelectedComponentTopNode] = useState(0);
const [showGraphs, setShowGraphs] = useState(false);
//organize the components into the correct 'positions' using the bins preferences
useEffect(()=>{
let unassigned: Component[] = []
let cables: GrainCable[] = []
let plenums: Plenum[] = []
let ambients: Ambient[] = []
let pressures: Pressure[] = []
let fans: Controller[] = []
let heaters: Controller[] = []
let headspaceTH: Headspace[] = []
let headspaceCO2: CO2[] = []
let headspaceLidar: Component[] = []
let sensorCount = 0
let controllerCount = 0
components.forEach(comp => {
if(preferences){
let pref = preferences.get(comp.key())
switch(pref?.type){
case pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE:
cables.push(GrainCable.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_PLENUM:
plenums.push(Plenum.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_AMBIENT:
ambients.push(Ambient.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_PRESSURE:
pressures.push(Pressure.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_FAN:
fans.push(Controller.create(comp))
controllerCount++
break;
case pond.BinComponent.BIN_COMPONENT_HEATER:
heaters.push(Controller.create(comp))
controllerCount++
break;
case pond.BinComponent.BIN_COMPONENT_HEADSPACE:
if (comp.type() === quack.ComponentType.COMPONENT_TYPE_DHT) {
headspaceTH.push(Headspace.create(comp));
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) {
headspaceLidar.push(comp);
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) {
let coComp = CO2.create(comp)
headspaceCO2.push(coComp);
}
sensorCount++
break;
default:
unassigned.push(comp)
}
}else{
unassigned.push(comp)
}
})
// console.log(cables)
// console.log(unassigned)
setSensorCount(sensorCount)
setControllerCount(controllerCount)
setCables(cables)
setPlenums(plenums)
setPressures(pressures)
setAmbient(ambients)
setFans(fans)
setHeaters(heaters)
setHeadspaceTH(headspaceTH)
setHeadspaceCO2(headspaceCO2)
setHeadspaceLidar(headspaceLidar)
setUnassignedComponents(unassigned)
},[components, preferences])
const selectType = (component: string, type: pond.BinComponent, topNode?: number) => {
setAnchorEl(null);
if (!preferences) return
let p = preferences.get(component);
if (p) {
p.type = type;
p.node = topNode ?? 0;
binAPI
.updateComponentPreferences(bin.key(), component, p, as)
.then(() => {
snackbar.success("Component preferences updated");
preferences.set(component, p!);
setPreferences(new Map(preferences));
})
.catch(() => {
snackbar.error("Component preference update failed");
});
}
};
const getOptions = () => {
let options: JSX.Element[] = [];
if (!componentUnassigned) {
options.push(
<MenuItem
key="remove"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_UNKNOWN);
}}>
Remove
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_DHT ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE
) {
options.push(
<MenuItem
key="ambientOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_AMBIENT);
}}>
Ambient
</MenuItem>
);
options.push(
<MenuItem
key="plenumOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_PLENUM);
}}>
Plenum
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_DHT ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_LIDAR ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_CO2
) {
options.push(
<MenuItem
key="headspaceOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_HEADSPACE);
}}>
Headspace
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_PRESSURE ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE
) {
options.push(
<MenuItem
key="pressureOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_PRESSURE);
}}>
Plenum Pressure
</MenuItem>
);
}
if (selectedComponentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
options.push(
<MenuItem
key="cableOption"
onClick={() => {
selectType(
selectedComponentKey,
pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE,
selectedComponentTopNode
);
}}>
Grain Cable
</MenuItem>
);
}
if (selectedComponentType === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT) {
options.push(
<MenuItem
key="controlOption"
onClick={() => {
if (
selectedComponentSubtype === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER
) {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_HEATER);
} else if (
selectedComponentSubtype ===
quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN
) {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_FAN);
}
}}>
Heaters & Fans
</MenuItem>
);
}
if (options.length === 0) {
return <MenuItem>No Available Options</MenuItem>;
} else {
return options;
}
};
const assignmentMenu = () => {
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{getOptions()}
</Menu>
);
};
const cableCard = (cable: GrainCable) => {
const component = cable.asComponent()
let icon = GetComponentIcon(component.type(), component.subType(), themeType)
let unit = user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "°F" : "°C"
const hasHumidity = cable.aveHumidity() !== 0
const hasMoisture = cable.grainMoistures.length > 0
const rows: { label: string; min: string; avg: string; max: string }[] = [
{
label: `Temp ${unit}`,
min: cable.minTemp(user.tempUnit()).toFixed(2),
avg: cable.aveTemp(user.tempUnit()).toFixed(2),
max: cable.maxTemp(user.tempUnit()).toFixed(2),
},
...(hasHumidity ? [{
label: "RH %",
min: cable.minHumidity().toFixed(2),
avg: cable.aveHumidity().toFixed(2),
max: cable.maxHumidity().toFixed(2),
}] : []),
...(hasMoisture ? [{
label: "EMC %",
min: cable.minMoisture().toFixed(2),
avg: cable.aveMoisture().toFixed(2),
max: cable.maxMoisture().toFixed(2),
}] : []),
]
return (
<BinSensorCard
icon={icon}
lastReading={cable.lastReading}
name={cable.name()}
tag={"cable"}
cableRows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(cable.type());
setSelectedComponentKey(cable.key());
setSelectedComponentTopNode(cable.settings.grainFilledTo);
setAnchorEl(event.currentTarget);
}}/>
)
}
const tempHumidCard = (sensor: Plenum | Ambient | Headspace, showEMC?: boolean) => {
let icon = GetComponentIcon(sensor.settings.type, sensor.settings.subtype, themeType)
let prefDisplay = ""
switch (preferences?.get(sensor.key())?.type){
case pond.BinComponent.BIN_COMPONENT_PLENUM:
prefDisplay = "Plenum"
break;
case pond.BinComponent.BIN_COMPONENT_AMBIENT:
prefDisplay = "Ambient"
break;
case pond.BinComponent.BIN_COMPONENT_HEADSPACE:
prefDisplay = "Headspace"
}
let rows: {label: string, data: string}[] = [
// build temperature row data
{
label: "Temp " + (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "°F" : "°C"),
data: (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? sensor.temperature * (9 / 5) + 32 : sensor.temperature).toFixed(2)
},
// build humidity row
{
label: "RH %",
data: sensor.humidity.toFixed(2)
}
]
// build moisture row assuming the bin has a grain type to use to calculatee the moisture
if(showEMC && bin.grain() !== pond.Grain.GRAIN_INVALID && bin.grain() !== pond.Grain.GRAIN_NONE){
let emc = ExtractMoisture(
bin.grain(),
sensor.temperature,
sensor.humidity,
bin.customGrain()
)
rows.push(
{
label: "EMC %",
data: emc.toFixed(2)
}
)
}
return (
<BinSensorCard
lastReading={sensor.lastReading}
name={sensor.name()}
tag={prefDisplay}
icon={icon}
rows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(sensor.type());
setSelectedComponentKey(sensor.key());
setAnchorEl(event.currentTarget);
}}
/>
)
}
const pressureCard = (pressure: Pressure) => {
let icon = GetComponentIcon(pressure.settings.type, pressure.settings.subtype, themeType)
let rows: {label: string, data: string}[] = [
{
label: "Pressure " + (user.pressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? "iwg" : "kPa"),
data: (user.pressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? pressure.pascals/249 : pressure.pascals/1000).toFixed(2)
}
]
if(pressure.airflow){
rows.push({
label: "Airflow CFM",
data: pressure.airflow.toFixed(2)
})
}
return (
<BinSensorCard
lastReading={pressure.lastReading}
name={pressure.name()}
tag={"Pressure"}
icon={icon}
rows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(pressure.type());
setSelectedComponentKey(pressure.key());
setAnchorEl(event.currentTarget);
}}
/>
)
}
const co2Card = (sensor: CO2) => {
let icon = GetComponentIcon(sensor.settings.type, sensor.settings.subtype, themeType)
let rows: {label: string, data: string}[] = [
{
label: "CO2 ppm",
data: sensor.ppm.toFixed(2)
}
]
return (
<BinSensorCard
lastReading={sensor.lastReading}
name={sensor.name()}
tag={"Headspace"}
icon={icon}
rows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(sensor.type());
setSelectedComponentKey(sensor.key());
setAnchorEl(event.currentTarget);
}}
/>
)
}
const lidarCard = (sensor: Component) => {
let icon = GetComponentIcon(sensor.settings.type, sensor.settings.subtype, themeType)
let rows: {label: string, data: string}[] = []
let lastReading = moment.now().toString()
sensor.lastMeasurement.forEach(unitMeasurement => {
let label = ""
let value = 0
if(sensor.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR){
if(unitMeasurement.type === quack.MeasurementType.MEASUREMENT_TYPE_DISTANCE_CM){
label = "Lidar " + (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? " ft" : " M")
if(unitMeasurement.timestamps.length > 0){
lastReading = unitMeasurement.timestamps[0]
}
if (unitMeasurement.values.length > 0){
//distance is stored in cm so needs to be converted
value = avg(unitMeasurement.values[0].values)
value = user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? value/30.48 : value/100
}
}
}
rows.push({label: label, data: value.toFixed(2)})
})
return (
<BinSensorCard
lastReading={lastReading}
name={sensor.name()}
tag={"Headspace"}
icon={icon}
rows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(sensor.type());
setSelectedComponentKey(sensor.key());
setAnchorEl(event.currentTarget);
}}
/>
)
}
const controllerCard = (controller: Controller) => {
const component = controller.asComponent()
let icon = GetComponentIcon(component.type(), component.subType(), themeType)
const isStale = moment().diff(moment(controller.lastReading), "hours") > 6
const isAuto = controller.mode === quack.OutputMode.OUTPUT_MODE_AUTO
const isOn = controller.mode === quack.OutputMode.OUTPUT_MODE_ON
const isOff = controller.mode === quack.OutputMode.OUTPUT_MODE_OFF
const prefType = preferences?.get(controller.key())?.type
const tag = prefType === pond.BinComponent.BIN_COMPONENT_FAN ? "Fan" : "Heater"
const segStyle = (active: boolean, activeColor?: string): React.CSSProperties => ({
flex: 1,
padding: "5px 0",
fontSize: 11,
fontWeight: 500,
textAlign: "center",
cursor: "pointer",
borderRadius: 0,
backgroundColor: active ? (activeColor ?? theme.palette.success.main) : "transparent",
color: active ? "#fff" : theme.palette.text.secondary,
border: "none",
transition: "background-color 0.15s",
})
return (
<Card raised sx={{ height: "100%" }}>
<Box padding={1.5} height="100%" display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={1}>
<Box display="flex" alignItems="center" gap={1}>
<Avatar
variant="square"
src={icon}
alt={controller.name() + " icon"}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
<Typography fontWeight={500}>{controller.name()}</Typography>
<Typography variant="caption" color="textSecondary">- {tag}</Typography>
</Box>
<IconButton
size="small"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false)
setSelectedComponentType(controller.type())
setSelectedComponentKey(controller.key())
setAnchorEl(event.currentTarget)
}}
>
<MoreVert fontSize="small" />
</IconButton>
</Box>
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={1.5}>
<Typography variant="caption" color="textSecondary">Current state</Typography>
<Box display="flex" alignItems="center" gap={0.5}>
<Box
sx={{
width: 7,
height: 7,
borderRadius: "50%",
backgroundColor: controller.on
? theme.palette.success.main
: theme.palette.text.disabled,
}}
/>
<Typography
variant="caption"
fontWeight={500}
color={controller.on ? "success.main" : "text.secondary"}
>
{controller.on ? "Running" : isAuto ? "Standby" : "Off"}
</Typography>
</Box>
</Box>
<Box
display="flex"
sx={{
border: `0.5px solid ${theme.palette.divider}`,
borderRadius: 1,
overflow: "hidden",
}}
>
{[
{ label: "Auto", mode: quack.OutputMode.OUTPUT_MODE_AUTO, active: isAuto },
{ label: "On", mode: quack.OutputMode.OUTPUT_MODE_ON, active: isOn },
{ label: "Off", mode: quack.OutputMode.OUTPUT_MODE_OFF, active: isOff },
].map(({ label, mode, active }, i) => (
<Box
key={label}
component="button"
onClick={() => {
//this will update the components settings here to change the mode
console.log("changing mode: " + mode)
controller.settings.defaultOutputState = mode
let device = componentDevices.get(controller.key())
if(device){
componentAPI.update(device, controller.settings).then(resp => {
console.log("updated controller")
if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) {
setFans(prev => [...prev])
} else {
setHeaters(prev => [...prev])
}
}).catch(err => {
console.log("there was a problem updating the controller")
})
}
}}
sx={{
...segStyle(active, label === "Off" ? theme.palette.action.selected : undefined),
borderLeft: i > 0 ? `0.5px solid ${theme.palette.divider}` : "none",
}}
>
{label}
</Box>
))}
</Box>
<Box display="flex" alignItems="center" gap={0.5} marginTop="auto" paddingTop={1}>
<AccessTime sx={{ fontSize: 12, color: isStale ? "warning.main" : "text.disabled" }} />
<Typography variant="caption" color={isStale ? "warning.main" : "text.secondary"}>
{moment(controller.lastReading).fromNow()}
</Typography>
</Box>
</Box>
</Card>
)
}
const unassignedComponentCard = (component: Component) => {
let icon = GetComponentIcon(component.type(), component.subType(), themeType)
const lastReading = component.lastMeasurement.length > 0
? component.lastMeasurement[0].timestamps[0] ?? ""
: ""
const isStale = moment().diff(moment(lastReading), "hours") > 6
return (
<Card raised sx={{ height: "100%" }}>
<Box padding={1.5} height="100%" display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box display="flex" alignItems="center" gap={1}>
<Avatar
variant="square"
src={icon}
alt={component.name() + " icon"}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
<Typography fontWeight={500}>{component.name()}</Typography>
</Box>
<IconButton
size="small"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(true)
setSelectedComponentType(component.type())
setSelectedComponentKey(component.key())
setSelectedComponentSubtype(component.subType())
setSelectedComponentTopNode(0)
setAnchorEl(event.currentTarget)
}}
>
<MoreVert fontSize="small" />
</IconButton>
</Box>
<Box display="flex" alignItems="center" gap={0.5} marginTop="auto">
<AccessTime sx={{ fontSize: 12, color: isStale ? "warning.main" : "text.disabled" }} />
<Typography variant="caption" color={isStale ? "warning.main" : "text.secondary"}>
{lastReading ? moment(lastReading).fromNow() : "No readings yet"}
</Typography>
</Box>
</Box>
</Card>
)
}
return (
<Box padding={1}>
{assignmentMenu()}
{sensorCount > 0 &&
<Box>
<Box display="flex" gap={2} alignItems="center">
<Typography sx={{fontWeight: 650, fontSize: 25}}>Sensors</Typography>
<Button variant="contained" color="primary" onClick={() => {
setShowGraphs(true)
}}>
Show Graphs
</Button>
</Box>
<Grid2 container spacing={2}>
{cables.map(cable => {
return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{cableCard(cable)}
</Grid2>
)
}
)}
{plenums.map(plenum => {
return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(plenum)}
</Grid2>
)
}
)}
{pressures.map(pressure => {
return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{pressureCard(pressure)}
</Grid2>
)
})}
{ambient.map(ambient => {
return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(ambient)}
</Grid2>
)
}
)}
{headspaceTH.map(h => {
return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(h)}
</Grid2>
)
})}
{headspaceCO2.map(co2 => {
return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{co2Card(co2)}
</Grid2>
)
})}
{headspaceLidar.map(lidar => {
return (
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{lidarCard(lidar)}
</Grid2>
)
})}
</Grid2>
</Box>
}
{controllerCount > 0 &&
<Box>
<Typography sx={{fontWeight: 650, fontSize: 25}}>Controllers</Typography>
<Grid2 container spacing={2}>
{heaters.map(heater => {
return (
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{controllerCard(heater)}
</Grid2>
)
})}
{fans.map(fan => {
return (
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{controllerCard(fan)}
</Grid2>
)
})}
</Grid2>
</Box>
}
{unassignedComponents.length > 0 &&
<Box>
<Typography sx={{ fontWeight: 650, fontSize: 25 }}>Unassigned Components</Typography>
<Grid2 container spacing={2}>
{unassignedComponents.map(comp => (
<Grid2 size={{ xs: 6, sm: 6, md: 4, lg: 3 }}>
{unassignedComponentCard(comp)}
</Grid2>
))}
</Grid2>
</Box>
}
</Box>
)
}

View file

@ -2,18 +2,16 @@ import { Box, Card, Grid2, LinearProgress, Typography } from "@mui/material";
import { useMobile } from "hooks";
import { Bin, Component, Device } from "models";
import Bin3dVisualizer from "./components/bin3dVisualizer";
import BinTableView from "./components/binTableView";
// import GrassIcon from "@mui/icons-material/Grass"
import { grey, orange, red } from "@mui/material/colors";
import { AccessTime, Spa } from "@mui/icons-material";
import moment from "moment";
import { useEffect, useState } from "react";
import { Controller } from "models/Controller";
import { GrainCable } from "models/GrainCable";
import { pond } from "protobuf-ts/pond";
import BinDetails from "./components/binDetails";
import { Plenum } from "models/Plenum";
import BinSensors from "./components/binSensors";
import BinSensorsDisplay from "../binSensorsDisplay";
interface Props {
bin: Bin
@ -73,20 +71,6 @@ export default function BinSummary(props: Props){
)
}
const sensorData = () => {
return (
<Box>sensor data</Box>
)
}
const controllerStatus = () => {
return (
<Box>controller status</Box>
)
}
return (
<Box padding={2}>
<Card raised sx={{marginBottom: 2}}>
@ -191,7 +175,7 @@ export default function BinSummary(props: Props){
</Grid2>
<Grid2 size={12}>
<Card>
<BinSensors components={componentMap} bin={bin} preferences={binPrefs} setPreferences={setPreferences}/>
<BinSensorsDisplay components={componentMap} bin={bin} preferences={binPrefs} setPreferences={setPreferences} componentDevices={componentDevices}/>
</Card>
</Grid2>
</Grid2>

View file

@ -1,7 +1,7 @@
import { Box, Tab, Tabs } from "@mui/material";
import React from "react";
import { useState } from "react";
import BinTableView from "./binTableView";
import BinTableView from "../../binTableView";
import { Bin, Component, Device } from "models";
import Alerts from "objects/objectInteractions/Alerts";
import BinAlerts from "./binAlerts";

View file

@ -1,156 +0,0 @@
import { Box, Grid2, Typography } from "@mui/material";
import { Bin, Component } from "models";
import { Ambient } from "models/Ambient";
import { CO2 } from "models/CO2";
import { Controller } from "models/Controller";
import { GrainCable } from "models/GrainCable";
import { Headspace } from "models/Headspace";
import { Plenum } from "models/Plenum";
import { Pressure } from "models/Pressure";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react";
interface Props {
bin: Bin
components: Map<string, Component>;
preferences?: Map<string, pond.BinComponentPreferences>;
setPreferences: React.Dispatch<
React.SetStateAction<Map<string, pond.BinComponentPreferences> | undefined>
>;
}
export default function BinSensors(props: Props){
const {bin, components, preferences, setPreferences} = props
const [cables, setCables] = useState<GrainCable[]>([])
const [plenums, setPlenums] = useState<Plenum[]>([])
const [ambient, setAmbient] = useState<Ambient[]>([])
const [pressures, setPressures] = useState<Pressure[]>([])
const [fans, setFans] = useState<Controller[]>([])
const [heaters, setHeaters] = useState<Controller[]>([])
//the above component preferences all have only components that are one type of sensor, however headspace has components of three different sensors T/H, CO2, and Lidar
const [headspaceTH, setHeadspaceTH] = useState<Headspace[]>([]) //the components in the headspace that measure temp/humidity
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]) //the CO2 components in the headspace
const [headspaceLidar, setHeadspaceLidar] = useState<Component[]>([]) //the lidar components in the headspace
const [unassignedComponents, setUnassignedComponents] = useState<Component[]>([])
//using counts so i dont have to have to check multiple arrays for sensors and controllers
const [sensorCount, setSensorCount] = useState(0)
const [controllerCount, setControllerCount] = useState(0)
//organize the components into the correct 'positions' using the bins preferences
useEffect(()=>{
let unassigned: Component[] = []
let cables: GrainCable[] = []
let plenums: Plenum[] = []
let ambients: Ambient[] = []
let pressures: Pressure[] = []
let fans: Controller[] = []
let heaters: Controller[] = []
let headspaceTH: Headspace[] = []
let headspaceCO2: CO2[] = []
let headspaceLidar: Component[] = []
let sensorCount = 0
let controllerCount = 0
components.forEach(comp => {
if(preferences){
let pref = preferences.get(comp.key())
switch(pref?.type){
case pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE:
cables.push(GrainCable.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_PLENUM:
plenums.push(Plenum.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_AMBIENT:
ambients.push(Ambient.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_PRESSURE:
pressures.push(Pressure.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_FAN:
fans.push(Controller.create(comp))
controllerCount++
break;
case pond.BinComponent.BIN_COMPONENT_HEATER:
heaters.push(Controller.create(comp))
controllerCount++
break;
case pond.BinComponent.BIN_COMPONENT_HEADSPACE:
if (comp.type() === quack.ComponentType.COMPONENT_TYPE_DHT) {
headspaceTH.push(Headspace.create(comp));
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) {
headspaceLidar.push(comp);
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) {
let coComp = CO2.create(comp)
headspaceCO2.push(coComp);
}
sensorCount++
break;
default:
unassigned.push(comp)
}
}else{
unassigned.push(comp)
}
})
// console.log(cables)
// console.log(unassigned)
setCables(cables)
setPlenums(plenums)
setPressures(pressures)
setAmbient(ambients)
setFans(fans)
setHeaters(heaters)
setHeadspaceTH(headspaceTH)
setHeadspaceCO2(headspaceCO2)
setHeadspaceLidar(headspaceLidar)
setUnassignedComponents(unassigned)
},[components, preferences])
const cableDisplay = (cable: GrainCable) => {
return (
<Box>
</Box>
)
}
return (
<Box>
{sensorCount > 0 &&
<React.Fragment>
<Typography>Sensors</Typography>
{cables.length > 0 &&
<React.Fragment>
<Typography>Cables</Typography>
<Grid2 container>
{cables.map(cable => {
return(
<Grid2 size={{xs: 6, xl: 4}}>
{cableDisplay(cable)}
</Grid2>
)
})}
</Grid2>
</React.Fragment>
}
{plenums.length }
</React.Fragment>
}
<Typography>Controllers</Typography>
{/* do all of the fans and heaters here */}
<Typography>Unassigned Components</Typography>
{/* list the components that have no assignment on the bin */}
</Box>
)
}

View file

@ -14,12 +14,11 @@ import WheatImg from "assets/grain/wheat.jpg";
import { Option } from "common/SearchSelect";
import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond";
import { Equation } from "./GrainMoisture";
export interface GrainExtension {
name: string;
group: string;
equation: Equation;
equation: pond.MoistureEquation;
a: number;
b: number;
c: number;
@ -38,7 +37,7 @@ const defaultSetTemp = 30.0;
const defaultGrain: GrainExtension = {
name: "None",
group: "",
equation: Equation.none,
equation: pond.MoistureEquation.MOISTURE_EQUATION_NONE,
a: 0,
b: 0,
c: 0,
@ -58,7 +57,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Custom Type",
group: "",
equation: Equation.none,
equation: pond.MoistureEquation.MOISTURE_EQUATION_NONE,
a: 0,
b: 0,
c: 0,
@ -75,7 +74,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Barley",
group: "Barley",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 475.12,
b: 0.14843,
c: 71.996,
@ -93,7 +92,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Buckwheat",
group: "Buckwheat",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 103540000,
b: 0.1646,
c: 15853000,
@ -111,7 +110,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Canola",
group: "Canola",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 3.489,
b: -0.010553,
c: 1.86,
@ -130,7 +129,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Rapeseed",
group: "Canola",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 3.0026,
b: -0.0048967,
c: 1.7607,
@ -148,7 +147,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Corn (Henderson)",
group: "Corn",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.000066612,
b: 1.9677,
c: 42.143,
@ -166,7 +165,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Corn (Chung-Pfost)",
group: "Corn",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 374.34,
b: 0.18662,
c: 31.696,
@ -184,7 +183,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Corn (Oswin)",
group: "Corn",
equation: Equation.oswin,
equation: pond.MoistureEquation.MOISTURE_EQUATION_OSWIN,
a: 15.303,
b: -0.10164,
c: 3.0358,
@ -202,7 +201,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Maize White",
group: "Corn",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.000066612,
b: 1.9677,
c: 70.143,
@ -220,7 +219,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Maize Yellow",
group: "Corn",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.000066612,
b: 1.9677,
c: 65.143,
@ -238,7 +237,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Oats (Henderson)",
group: "Oats",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.000085511,
b: 2.0087,
c: 37.811,
@ -256,7 +255,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Oats (Chung-Pfost)",
group: "Oats",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 442.85,
b: 0.21228,
c: 35.803,
@ -274,7 +273,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Oats (Oswin)",
group: "Oats",
equation: Equation.oswin,
equation: pond.MoistureEquation.MOISTURE_EQUATION_OSWIN,
a: 12.412,
b: -0.060707,
c: 2.9397,
@ -292,7 +291,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Peanuts",
group: "Peanuts",
equation: Equation.oswin,
equation: pond.MoistureEquation.MOISTURE_EQUATION_OSWIN,
a: 8.6588,
b: -0.057904,
c: 2.6204,
@ -310,7 +309,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Long Grain Rice",
group: "Rice",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.000041276,
b: 2.1191,
c: 49.828,
@ -328,7 +327,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Medium Grain Rice",
group: "Rice",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.000035502,
b: 2.31,
c: 27.396,
@ -346,7 +345,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Short Grain Rice",
group: "Rice",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.000048524,
b: 2.0794,
c: 45.646,
@ -364,7 +363,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Sorghum",
group: "Sorghum",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 797.33,
b: 0.18159,
c: 52.238,
@ -382,7 +381,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Soybeans",
group: "Soybeans",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 228.2,
b: 0.2072,
c: 30,
@ -400,7 +399,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Sunflower",
group: "Sunflower",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.00031,
b: 1.7459,
c: 66.603,
@ -418,7 +417,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Durum Wheat",
group: "Wheat",
equation: Equation.oswin,
equation: pond.MoistureEquation.MOISTURE_EQUATION_OSWIN,
a: 13.101,
b: -0.052626,
c: 2.9987,
@ -436,7 +435,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Hard Red Wheat",
group: "Wheat",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 610.34,
b: 0.15526,
c: 93.213,
@ -454,7 +453,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Wheat SAWOS",
group: "Wheat",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 610.34,
b: 0.15526,
c: 93.213,
@ -472,7 +471,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Un-retted Flax",
group: "Flax",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 5.11,
b: Math.pow(-8.46 * 10, -3),
c: 2.26,
@ -490,7 +489,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Dew-retted Flax",
group: "Flax",
equation: Equation.oswin,
equation: pond.MoistureEquation.MOISTURE_EQUATION_OSWIN,
a: 6.5,
b: Math.pow(-1.68 * 10, -2),
c: 3.2,
@ -508,7 +507,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Yellow Peas",
group: "Peas",
equation: Equation.oswin,
equation: pond.MoistureEquation.MOISTURE_EQUATION_OSWIN,
a: 14.81,
b: -0.109,
c: 3.019,
@ -526,7 +525,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Blaze Lentils",
group: "Lentils",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 5.39,
b: -0.015,
c: 2.273,
@ -544,7 +543,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Redberry Lentils",
group: "Lentils",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 4.749,
b: -0.0116,
c: 2.066,
@ -562,7 +561,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Robin Lentils",
group: "Lentils",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 5.176,
b: -0.0065,
c: 2.337,
@ -580,7 +579,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Dry Beans Red",
group: "Dry Beans",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 4.2669,
b: -0.013382,
c: 1.6933,
@ -596,7 +595,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Dry Beans Black",
group: "Dry Beans",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 5.2003,
b: -0.022685,
c: 1.9656,
@ -612,7 +611,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Rye (Henderson)",
group: "Rye",
equation: Equation.henderson,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON,
a: 0.00006343,
b: 2.2060,
c: 13.1810,
@ -628,7 +627,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Rye (Chung-Pfost)",
group: "Rye",
equation: Equation.chungPfost,
equation: pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST,
a: 461.0230,
b: 0.1840,
c: 36.7410,
@ -644,7 +643,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Rye (Halsey)",
group: "Rye",
equation: Equation.halsey,
equation: pond.MoistureEquation.MOISTURE_EQUATION_HALSEY,
a: 4.2970,
b: 0.380,
c: 2.2710,
@ -660,7 +659,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
{
name: "Rye (Oswin)",
group: "Rye",
equation: Equation.oswin,
equation: pond.MoistureEquation.MOISTURE_EQUATION_OSWIN,
a: 11.8870,
b: 0.0210,
c: 3.2620,

View file

@ -1,14 +1,6 @@
import { pond } from "protobuf-ts/pond";
import GrainDescriber from "./GrainDescriber";
export enum Equation {
"none",
"oswin",
"halsey",
"henderson",
"chungPfost"
}
const toERH = (humidity: number): number => {
return humidity >= 100 ? 0.99999 : humidity / 100;
};

View file

@ -10,6 +10,7 @@ export class Ambient {
public status: pond.ComponentStatus = pond.ComponentStatus.create();
public temperature: number = -127;
public humidity: number = 200;
public lastReading: string = ""
public static create(comp: Component): Ambient {
let my = new Ambient();
@ -18,6 +19,9 @@ export class Ambient {
if (comp.status.measurement.length > 0) {
comp.status.measurement.forEach(um => {
if (um.timestamps[0]){
my.lastReading = um.timestamps[0]
}
if (um.values[0]) {
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
my.temperature = or(um.values[0].values[0], -127);
@ -39,6 +43,7 @@ export class Ambient {
hum = humAndTemp["relativeHumidityTimes100"];
}
}
my.lastReading = comp.status.lastMeasurement?.timestamp ?? ""
my.temperature = or(temp, -1270) / 10;
my.humidity = or(hum, 20000) / 100;
}

View file

@ -9,13 +9,30 @@ export class Controller {
public settings: pond.ComponentSettings = pond.ComponentSettings.create();
public status: pond.ComponentStatus = pond.ComponentStatus.create();
public on = false;
public mode: quack.OutputMode | undefined
public lastReading: string = "";
public static create(comp: Component): Controller {
let my = new Controller();
my.settings = comp.settings;
my.status = comp.status;
my.mode = comp.settings.defaultOutputState
my.on = Boolean(my.status.lastMeasurement?.measurement?.booleanOutput?.value);
//this is deprecated because it is our old measurement structure
// my.on = Boolean(my.status.lastMeasurement?.measurement?.booleanOutput?.value);
if (comp.status.lastGoodMeasurement.length > 0) {
comp.status.measurement.forEach(um => {
if (um.timestamps[0]){
my.lastReading = um.timestamps[0]
}
if (um.values[0]) {
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN) {
my.on = Boolean(um.values[0].values[0]);
}
}
});
}
return my;
}
@ -25,6 +42,7 @@ export class Controller {
my.settings = comp.settings ? comp.settings : pond.ComponentSettings.create();
my.status = comp.status ? comp.status : pond.ComponentStatus.create();
my.on = Boolean(my.status.lastMeasurement?.measurement?.booleanOutput?.value);
return my;

View file

@ -158,6 +158,10 @@ export class GrainCable {
return this.humidities.reduce((p: any, c: any) => p + c, 0) / this.humidities.length;
}
public aveMoisture() {
return this.grainMoistures.reduce((p: any, c: any) => p + c, 0) / this.grainMoistures.length;
}
public maxTemp(unit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS) {
if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT)
return this.farenheit(Math.max(...this.temperatures));

View file

@ -10,6 +10,7 @@ export class Headspace {
public status: pond.ComponentStatus = pond.ComponentStatus.create();
public temperature: number = -127;
public humidity: number = 200;
public lastReading: string = "";
public static create(comp: Component): Headspace {
let my = new Headspace();
@ -18,6 +19,9 @@ export class Headspace {
if (comp.status.measurement.length > 0) {
comp.status.measurement.forEach(um => {
if (um.timestamps[0]){
my.lastReading = um.timestamps[0]
}
if (um.values[0]) {
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
my.temperature = um.values[0].values[0];
@ -39,6 +43,7 @@ export class Headspace {
hum = humAndTemp["relativeHumidityTimes100"];
}
}
my.lastReading = comp.status.lastMeasurement?.timestamp ?? ""
my.temperature = or(temp, -1270) / 10;
my.humidity = or(hum, 20000) / 100;
}

View file

@ -10,6 +10,7 @@ export class Plenum {
public status: pond.ComponentStatus = pond.ComponentStatus.create();
public temperature: number = -127;
public humidity: number = 200;
public lastReading: string = ""
public static create(comp: Component): Plenum {
let my = new Plenum();
@ -18,6 +19,9 @@ export class Plenum {
if (comp.status.measurement.length > 0) {
comp.status.measurement.forEach(um => {
if (um.timestamps[0]) {
my.lastReading = um.timestamps[0];
}
if (um.values[0]) {
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
my.temperature = or(um.values[0].values[0], -127);
@ -41,6 +45,7 @@ export class Plenum {
}
my.temperature = or(temp, -1270) / 10;
my.humidity = or(hum, 20000) / 100;
my.lastReading = comp.status.lastMeasurement?.timestamp ?? ""
}
return my;

View file

@ -9,7 +9,9 @@ export class Pressure {
public settings: pond.ComponentSettings = pond.ComponentSettings.create();
public status: pond.ComponentStatus = pond.ComponentStatus.create();
public pascals: number = 0;
public airflow: number | undefined
public fanId: number = 0;
public lastReading: string = ""
public static create(comp: Component): Pressure {
let my = new Pressure();
@ -19,19 +21,23 @@ export class Pressure {
//getting the value from the unitmeasurements in status instead of the old style measurements in the status
if (comp.status.measurement.length > 0) {
comp.status.measurement.forEach(um => {
if (um.timestamps[0]){
my.lastReading = um.timestamps[0]
}
if (um.values[0] && um.values[0].values.length > 0) {
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE) {
my.pascals = um.values[0].values[0];
}
//TODO-CS: could expand this to have the fan cfm in the pressure model as well
// if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_CFM){
// my.fanCFM = um.values[0].values[0]
// }
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_CFM){
my.airflow = um.values[0].values[0]
}
}
});
} else {
//if no unit measurements in status use the old measurements in status
let pre = comp.status?.lastMeasurement?.measurement?.pressure?.pascals;
my.lastReading = comp.status.lastMeasurement?.timestamp ?? ""
if (pre) {
my.pascals = pre;
}

View file

@ -215,11 +215,6 @@ export default function Bin(props: Props) {
const [binPresets, setBinPresets] = useState<DevicePreset[]>([]);
const [missedReadings, setMissedReadings] = useState(0);
//3d bin variables/toggle
const [showGrain, setShowGrain] = useState(false)
const [showHotspots, setShowHotspots] = useState(false)
const [showHeatmap, setShowHeatmap] = useState(false)
const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};

View file

@ -48,6 +48,15 @@ function makePaletteColor(name: keyof typeof Colours) {
export const getTheme = (mode: 'light' | 'dark') =>
createTheme({
components: {
MuiPaper: {
styleOverrides: {
root: {
backgroundImage: 'none', // disables MUI's elevation overlay in dark mode
},
},
},
},
...baseTheme,
palette: {
...baseTheme.palette,
@ -57,8 +66,8 @@ export const getTheme = (mode: 'light' | 'dark') =>
...(mode === 'dark'
? {
background: {
default: '#121212',
paper: '#1e1e1e',
default: '#0A1118', // was #0F1923 — push it darker
paper: '#141F2A', // was #1A2530 — slightly darker, less grey/blue
},
}
: {