started work on the bin summary part of the new bin page, built the framework and the new 3d visualizer, still doing some work on the camera to create an overlay with a reset button

This commit is contained in:
csawatzky 2026-05-01 13:11:12 -06:00
parent b3cf5b5e83
commit 4b2c67dfd9
11 changed files with 974 additions and 220 deletions

View file

@ -816,53 +816,6 @@ export default function Bin(props: Props) {
</Box>
{overview()}
{preferences && binComponents(preferences)}
<Card>
<Box height={1000}>
<TextField
type="number"
onChange={e => {
setFillPercent(+e.target.value)
}}
/>
<FormControlLabel
control={
<Checkbox
value={showGrain}
checked={showGrain}
onChange={(_, checked) => {
setShowGrain(checked);
}}
/>
}
label={"Inventory Toggle"}
/>
<FormControlLabel
control={
<Checkbox
value={showHotspots}
checked={showHotspots}
onChange={(_, checked) => {
setShowHotspots(checked);
}}
/>
}
label={"Hot Spots"}
/>
<FormControlLabel
control={
<Checkbox
value={showHeatmap}
checked={showHeatmap}
onChange={(_, checked) => {
setShowHeatmap(checked);
}}
/>
}
label={"Heatmap"}
/>
<Bin3dView bin={bin} scale={100} fillPercent={fillPercent/100} showGrain={showGrain} showHeatmap={showHeatmap} showHotspots={showHotspots}/>
</Box>
</Card>
</Grid>
<Grid id="tour-conditions" size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||

482
src/pages/BinV2.tsx Normal file
View file

@ -0,0 +1,482 @@
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 } 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, 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";
const useStyles = makeStyles((theme: Theme) => {
const themeType = theme.palette.mode;
const activeBG = theme.palette.secondary.main;
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
},
})
})
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<boolean>(false);
const [bin, setBin] = useState<IBin>(IBin.create());
const [devices, setDevices] = useState<Device[]>([]);
const [components, setComponents] = useState<Map<string, Component>>(new Map());
const [interactions, setInteractions] = useState<Interaction[]>([]);
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [preferences, setPreferences] = useState<Map<string, pond.BinComponentPreferences>>();
const [plenums, setPlenums] = useState<Plenum[]>([]);
const [ambients, setAmbients] = useState<Ambient[]>([]);
const [grainCables, setGrainCables] = useState<GrainCable[]>([]);
const [pressures, setPressures] = useState<Pressure[]>([]);
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]);
const [heaters, setHeaters] = useState<Controller[]>([]);
const [fans, setFans] = useState<Controller[]>([]);
const [compositionNameMap, setCompositionNameMap] = useState<Map<string, string>>(new Map());
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [componentDevices, setComponentDevices] = useState<Map<string, number>>(
new Map<string, number>()
);
const [interactionDevices, setInteractionDevices] = useState<Map<string, number>>(
new Map<string, number>()
);
const [binPresets, setBinPresets] = useState<DevicePreset[]>([]);
const [missedReadings, setMissedReadings] = useState(0);
const [currentSection, setCurrentSection] = useState<"binSum">("binSum")
//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<string, string> = 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<string, pond.BinComponentPreferences>();
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<string, number>();
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<string, number>();
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<string, Component> = 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
*/
/**
* 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 (
<Box marginTop={1} marginBottom={1}>
<Grid
container
direction="row"
justifyContent="space-between"
alignContent="center"
alignItems="center">
<Grid >
<IconButton
onClick={() =>
setNoteDrawerOpen(true)
}
size="small"
style={{
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<div className={classes.spacer}>
<NotesIcon />
</div>
</IconButton>
<IconButton
onClick={() =>
setTaskDrawerOpen(true)
}
size="small"
style={{
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<div className={classes.spacer}>
<TasksIcon />
</div>
</IconButton>
{devices.length > 1 ? (
<Tooltip key="devices" title="devices">
<IconButton
key="devButton"
onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
setAnchorEl(event.currentTarget)
}
size="small"
style={{
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<BindaptIcon />
</IconButton>
</Tooltip>
) : (
devices.map(dev => (
<Tooltip key={dev.id()} title={dev.name()}>
<IconButton
key={dev.id()}
onClick={() => goToDevice(dev)}
size="small"
style={{
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<div className={classes.spacer}>
<BindaptIcon />
</div>
</IconButton>
</Tooltip>
))
)}
{bin.binMapped() && (
<Tooltip key={"goToMap"} title={"Go to Yard"}>
<IconButton
onClick={() => {
goToYard(bin);
}}
size="small"
style={{
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<FieldsIcon />
</IconButton>
</Tooltip>
)}
</Grid>
<Grid>
<BinActions
bin={bin}
permissions={permissions}
refreshCallback={() => {
load();
}}
userID={user.id()}
components={components}
setComponents={setComponents}
componentDevices={componentDevices}
setComponentDevices={setComponentDevices}
updateBinStatus={updateStatus}
/>
</Grid>
</Grid>
</Box>
)
}
//PAGE SECTIONS
/**
* Contains the main summary of your bin as it currently is
* has the 3D bin model, a switch to go to the table view
* the bin conditions if it is in a mode other than storage (the do nothing mode)
* a box underneath to display sensor data, for the plenums and such
* as well as the controls for attached controllers
*/
const binSummarySection = () => {
return (
<BinSummary bin={bin}/>
)
}
/**
* this sections is for showing attached bin components and assigning them to bin positions (see old pages bin components)
* as well as seeing the graph data for attached sensors
*/
const sensorsSection = () => {}
//Inventory
/**
* this section is for seeing information relating to the bins inventory for its fill level over time and transactions related to inventory of the bin
*/
const InventorySection = () => {}
//Analysis
/**
* this section is for the analytical charts (drying/hydrating, Moisture trending, Grain Water Content)
*/
const analysisSection = () => {}
//Interactions
/**
* this is the section for alerts and controls on the bin, if presets get re-implemented they can go here too
*/
const interactionsSection = () => {}
//DRAWERS
const noteDrawer = () => {
return (
<Drawer open={noteDrawerOpen}>
</Drawer>
)
}
const taskDrawer = () => {
return (
<Drawer open={taskDrawerOpen}>
</Drawer>
)
}
return (
<PageContainer>
{pageHeader()}
{currentSection === "binSum" && binSummarySection()}
{/* render drawers */}
{noteDrawer()}
{taskDrawer()}
</PageContainer>
)
}