import { useCallback, useEffect, useRef, useState } from "react"; import PageContainer from "./PageContainer"; import { DevicePreset } from "models/DevicePreset"; import { Controller } from "models/Controller"; import { useNavigate, useParams } from "react-router-dom"; import { useMobile, useSnackbar } from "hooks"; import { Component, Device, Bin as IBin, Interaction } from "models"; import { useBinAPI, useGlobalState } from "providers"; import { pond } from "protobuf-ts/pond"; import { Plenum } from "models/Plenum"; import { Ambient } from "models/Ambient"; import { GrainCable } from "models/GrainCable"; import { Pressure } from "models/Pressure"; import { CO2 } from "models/CO2"; import moment from "moment"; import { quack } from "protobuf-ts/quack"; import { Box, Drawer, Grid2 as Grid, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tab, Tabs, Theme, Tooltip } from "@mui/material"; import { makeStyles } from "@mui/styles"; import NotesIcon from "@mui/icons-material/Notes"; import TasksIcon from "products/Construction/TasksIcon"; import BindaptIcon from "products/Bindapt/BindaptIcon"; import FieldsIcon from "products/AgIcons/FieldsIcon"; import BinActions from "bin/BinActions"; import BinSummary from "bin/binSummary/BinSummary"; import { Close } from "@mui/icons-material"; import React from "react"; import BinHistory from "bin/BinHistory"; import Chat from "chat/Chat"; import TaskViewer from "tasks/TaskViewer"; interface TabPanelProps { children?: React.ReactNode; index: any; value: any; } function TabPanelMine(props: TabPanelProps) { const { children, value, index, ...other } = props; return ( ); } const useStyles = makeStyles((theme: Theme) => { const themeType = theme.palette.mode; return ({ spacer: { width: "32px", height: "32px", padding: "auto", display: "flex", justifyContent: "center", alignItems: "center" }, avatar: { color: themeType === "light" ? theme.palette.common.black : theme.palette.common.white, backgroundColor: "transparent", width: theme.spacing(5), height: theme.spacing(5), border: "1px solid", borderColor: theme.palette.divider }, menuPaper: { borderRadius: "20px" }, drawerPaperDesktop: { height: "100%", width: "30%" }, drawerPaperMobile: { height: "50%", width: "100%" }, }) }) export default function BinV2(){ const warningThreshold = 6; const isMobile = useMobile(); const binID = useParams<{ binID: string }>()?.binID ?? ""; const binAPI = useBinAPI(); const classes = useStyles() const navigate = useNavigate() const [{ user, as, showErrors }] = useGlobalState(); const [binLoading, setBinLoading] = useState(false); const loadRef = useRef(false); const [bin, setBin] = useState(IBin.create()); const [devices, setDevices] = useState([]); const [components, setComponents] = useState>(new Map()); const [interactions, setInteractions] = useState([]); const [permissions, setPermissions] = useState([]); const [preferences, setPreferences] = useState>(); const [plenums, setPlenums] = useState([]); const [ambients, setAmbients] = useState([]); const [grainCables, setGrainCables] = useState([]); const [pressures, setPressures] = useState([]); const [headspaceCO2, setHeadspaceCO2] = useState([]); const [heaters, setHeaters] = useState([]); const {openSnack} = useSnackbar() const [fans, setFans] = useState([]); const [compositionNameMap, setCompositionNameMap] = useState>(new Map()); const [anchorEl, setAnchorEl] = useState(null); const [noteTab, setNoteTab] = useState(0) const [componentDevices, setComponentDevices] = useState>( new Map() ); const [interactionDevices, setInteractionDevices] = useState>( new Map() ); const [binPresets, setBinPresets] = useState([]); const [missedReadings, setMissedReadings] = useState(0); //Drawer states const [noteDrawerOpen, setNoteDrawerOpen] = useState(false) const [taskDrawerOpen, setTaskDrawerOpen] = useState(false) //DATA LOAD/UPDATE FUNCTIONS const load = useCallback(() => { if (loadRef.current || user.id() === "") return; setBinLoading(true); loadRef.current = true; //add the presets to the bin page data load binAPI .getBinPageData(binID, user.id(), showErrors, as) .then(resp => { if (resp.data.grainCompositionNames) { let tempMap: Map = new Map(); Object.keys(resp.data.grainCompositionNames).forEach(key => { tempMap.set(key, resp.data.grainCompositionNames[key]); }); tempMap.set("correction", "Unknown Source"); setCompositionNameMap(tempMap); } let devs: Device[] = []; let p = new Map(); Object.keys(resp.data.preferences).forEach(k => { let prefKey = k.split(":")[1] ? k.split(":")[1] : k; p.set(prefKey, pond.BinComponentPreferences.fromObject(resp.data.preferences[k])); }); setPreferences(p); let r: pond.GetBinPageDataResponse = pond.GetBinPageDataResponse.fromObject(resp.data); setBin(IBin.any(resp.data.bin)); let newComponentDevices = new Map(); let attachedDevIds: number[] = []; if (resp.data.componentDevices) Object.keys(resp.data.componentDevices).forEach(k => { newComponentDevices.set(k, resp.data.componentDevices[k]); //make a list of all of the ids for devices that components are currently attached if (!attachedDevIds.includes(resp.data.componentDevices[k])) { attachedDevIds.push(resp.data.componentDevices[k]); } }); setComponentDevices(newComponentDevices); let newInteractionDevices = new Map(); if (r.interactionDevices) Object.keys(r.interactionDevices).forEach(k => { newInteractionDevices.set(k, r.interactionDevices[k]); }); setInteractionDevices(newInteractionDevices); if (resp.data.interactions) { setInteractions(resp.data.interactions.map((i: pond.Interaction) => Interaction.any(i))); } else { setInteractions([]); } //go through the devices and if the device has an attached component include it in the device list if (resp.data.devices && resp.data.devices[0]) { resp.data.devices.forEach((dev: any) => { let device = Device.create(dev); if (attachedDevIds.includes(device.id())) { devs.push(device); } }); setDevices(devs); } if (resp.data.components) { let compMap: Map = new Map(); resp.data.components.forEach((comp: pond.Component) => { if (comp && comp.settings) { let c = Component.any(comp); compMap.set(comp.settings.key, c); } }); setComponents(compMap); } if (resp.data.presets) { setBinPresets( resp.data.presets.map((preset: pond.DevicePreset) => DevicePreset.create(preset)) ); } setPermissions(r.permissions ? r.permissions : []); setBinLoading(false); loadRef.current = false; }) .finally(() => { setBinLoading(false); loadRef.current = false; }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [binID, user.id(), binAPI, showErrors, as]); useEffect(() => { load(); }, [load]); const setBinComponents = useCallback(() => { if (!preferences) return; var unassigned: Component[] = []; var plenums: Plenum[] = []; var ambients: Ambient[] = []; var grainCables: GrainCable[] = []; var fans: Controller[] = []; var heaters: Controller[] = []; var pressures: Pressure[] = []; var headspaceCo2: CO2[] = []; var mostMissed: number = 0; let now = moment(); components.forEach(comp => { let pref = preferences.get(comp.key()); if (pref) { if (pref.type) { if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM) plenums.push(Plenum.create(comp)); if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT) ambients.push(Ambient.create(comp)); if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE) { //check cable for missed measurements let cable = GrainCable.create(comp); let lastRead = moment(cable.lastReading); let elapsedMS = now.diff(lastRead); if (elapsedMS > cable.settings.measurementPeriodMs) { let missedReadings = Math.floor(elapsedMS / cable.settings.measurementPeriodMs); if (missedReadings > mostMissed) { mostMissed = missedReadings; } } grainCables.push(cable); } if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) heaters.push(Controller.create(comp)); if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) fans.push(Controller.create(comp)); if (pref.type === pond.BinComponent.BIN_COMPONENT_PRESSURE) pressures.push(Pressure.create(comp)); if (pref.type === pond.BinComponent.BIN_COMPONENT_HEADSPACE) { if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) { //check if there are missed readings from the co2 and compare them to what we already had let coComp = CO2.create(comp) let lastRead = moment(coComp.lastReading); let elapsedMS = now.diff(lastRead); if (elapsedMS > coComp.settings.measurementPeriodMs) { let missedReadings = Math.floor(elapsedMS / coComp.settings.measurementPeriodMs); if (missedReadings > mostMissed) { mostMissed = missedReadings; } } headspaceCo2.push(coComp); } } } else { unassigned.push(comp); } } }); setMissedReadings(mostMissed); setPlenums(plenums); setGrainCables(grainCables); setPressures(pressures); setAmbients(ambients); setHeaters(heaters); setFans(fans); setHeadspaceCO2(headspaceCo2); }, [components, preferences]); useEffect(() => { setBinComponents(); }, [setBinComponents]); const updateStatus = (componentKeys: string[], removed?: boolean) => { componentKeys.forEach(compKey => { let comp = components.get(compKey); if (comp) { //determine what part of the status to update based on the component //for lidar if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) { //determine how to update the status based on if added or removed if (removed) { bin.status.distance = 0; } else { if ( comp.lastMeasurement[0] && comp.lastMeasurement[0].values[0] && comp.lastMeasurement[0].values[0].values[0] ) { bin.status.distance = comp.lastMeasurement[0].values[0].values[0]; } } } } }); //update the bins status with the new values based on the components changed binAPI.updateBinStatus(bin.key(), bin.status, as); }; //NAVIGATION FUNCTIONS const goToDevice = (dev: Device) => { navigate(window.location.pathname + "/devices/" + dev.id(), { replace: true }); }; const goToYard = (bin: IBin) => { navigate("/visualFarm", { state: { long: bin.getLocation()?.longitude, lat: bin.getLocation()?.latitude } }); }; /** * UI STUFF STARTS HERE */ const deviceMenu = () => { return ( setAnchorEl(null)} keepMounted classes={{ paper: classes.menuPaper }} disableAutoFocusItem> {devices.map(dev => ( goToDevice(dev)}> ))} ); }; /** * Header - consists of the actions and things that are always visible like * opening the note/history and tasks drawers, navigating to the device and map pages, opening binsensors and settings etc. * also has the bin name/model and activity and the dropdown for changing sections */ const pageHeader = () => { return ( setNoteDrawerOpen(true) } size="small" style={{ marginRight: "0.5rem" }} className={classes.avatar} component="span">
setTaskDrawerOpen(true) } size="small" style={{ marginRight: "0.5rem" }} className={classes.avatar} component="span">
{devices.length > 1 ? ( ) => setAnchorEl(event.currentTarget) } size="small" style={{ marginRight: "0.5rem" }} className={classes.avatar} component="span"> ) : ( devices.map(dev => ( goToDevice(dev)} size="small" style={{ marginRight: "0.5rem" }} className={classes.avatar} component="span">
)) )} {bin.binMapped() && ( { goToYard(bin); }} size="small" style={{ marginRight: "0.5rem" }} className={classes.avatar} component="span"> )}
{ load(); }} userID={user.id()} components={components} setComponents={setComponents} componentDevices={componentDevices} setComponentDevices={setComponentDevices} updateBinStatus={updateStatus} />
) } const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { setNoteTab(newValue); }; //DRAWERS const noteDrawer = () => { return ( setNoteDrawerOpen(false)}> setNoteDrawerOpen(false)}> {/* */} {/* */} ) } const taskDrawer = () => { return ( setTaskDrawerOpen(false)}> setTaskDrawerOpen(false)}> ) } return ( {deviceMenu()} {pageHeader()} { setBin(bin) binAPI.updateBin(bin.key(), bin.settings).then(resp => { openSnack("Bin has Been Updated") }) }} setBin={setBin} /> {/* render drawers */} {noteDrawer()} {taskDrawer()} ) }