Merge branch 'master' of gitlab.com:brandx/bxt-app

This commit is contained in:
csawatzky 2026-01-15 15:52:43 -06:00
commit 7892bbfe1d
28 changed files with 2092 additions and 659 deletions

View file

@ -1,6 +1,6 @@
// import { useAuth0 } from "@auth0/auth0-react";
import { Typography } from "@mui/material";
import Tour from "common/Tour";
import Tour, { TourStep } from "common/Tour";
import { random } from "lodash";
import moment from "moment";
import { useGlobalState, useSnackbar, useUserAPI } from "providers";
@ -8,8 +8,13 @@ import React, { useEffect, useState } from "react";
import { Step } from "react-joyride";
// import Emoji from "react-emoji-render";
export default function BinTour() {
interface Props {
setDetailTabState?: ( detail: "inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions", buttonIndex: number) => void
}
export default function BinTour(props: Props) {
// const { userID } = useAuth0();
const {setDetailTabState} = props
const [{ user }, dispatch] = useGlobalState();
const userID = user.id()
const { error } = useSnackbar();
@ -35,8 +40,8 @@ export default function BinTour() {
}
};
const getTourSteps = (): Step[] => {
let steps: Step[] = [
const getTourSteps = (): TourStep[] => {
let steps: TourStep[] = [
{
title: (
<React.Fragment>
@ -103,24 +108,12 @@ export default function BinTour() {
disableBeacon: false
},
{
title: "Graphs",
title: "View Other Data",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
Bin related analytics are displayed here.
</Typography>
</React.Fragment>
),
target: "#tour-graphs",
placement: "left",
disableBeacon: false
},
{
title: "Choose your graphs",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
Use this tab to view other sets of data. Sensors must be attached to view sensor data.
Use these tabs to view other Bin Details such as inventory levels over time,
alerts and interactions for connected components, and data for attached sensors
</Typography>
</React.Fragment>
),
@ -128,6 +121,122 @@ export default function BinTour() {
placement: "bottom",
disableBeacon: false
},
{
title: "Inventory",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The inventory tab shows your bins inventory level over a specified window as well as the times the bin mode was changed.
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("sensors", 1)
}
},
{
title: "Sensors",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The sensors tab shows readings from attached sensors over time
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("analytics", 2)
}
},
{
title: "Analytics",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The analysis tab shows analytic bin data using attached sensors
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("alerts", 3)
}
},
{
title: "Alerts and Interactions",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The Alerts tab allows you to view or add any new interactions or alerts for connected components
and displays recent notifications from those alerts
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("presets", 4)
}
},
{
title: "Bin Mode Presets",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The Presets tab allows you to create custom presets for the interactions for a device when changing bin modes
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("transactions", 5)
}
},
{
title: "Bin Transactions",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The transactions tab allows view any pending transaction when using a hybrid inventory control
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("inventory", 0)
}
},
{
title: "Bin Conditions",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
This section will display conditions based on the bin mode
</Typography>
<Typography variant="body2" paragraph>
Storage Mode: simply displays the Storage conditions by itself with the target temperature and moisture set in the bin settings
</Typography>
<Typography variant="body2" paragraph>
Other Modes: will display the interactions for the bins devices that are controlling a fan or heater on the bin as well as the storage conditions
</Typography>
</React.Fragment>
),
target: "#tour-conditions",
placement: "right",
disableBeacon: false
},
{
title: "Change Mode",
content: (
@ -138,12 +247,15 @@ export default function BinTour() {
<ul>
<li>Storage mode: default</li>
<li>
Drying mode: use heat to dry grain
Cooldown mode: use fans to hold bin temperature lower
{/* <Emoji text=" ❆❅" /> */}
</li>
<li>
Drying mode: use heat to dry grain (only visible when the target moisture is lower than the starting moisture in the bin settings)
{/* <Emoji text=" ☀️" /> */}
</li>
<li>
Cooldown mode: use fans to hold bin temperature lower
{/* <Emoji text=" ❆❅" /> */}
Hydrating mode: use humid air to hydrate grain (only visible when the target moisture is higher than the starting moisture in the bin settings)
</li>
</ul>
</React.Fragment>

View file

@ -1727,32 +1727,34 @@ export default function BinVisualizer(props: Props) {
{modeChangeInProgress &&
<CircularProgress color="primary" size={25}/>
}
<ButtonGroup
disableAll={modeChangeInProgress}
buttons={[
{
title: "Storage",
function: () => {
setModeStorage()
<Box id="tour-bin-mode">
<ButtonGroup
disableAll={modeChangeInProgress}
buttons={[
{
title: "Storage",
function: () => {
setModeStorage()
}
},
{
title: "Cooldown",
function: () => {
setModeCooldown()
}
},
{
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
function: () => {
// setShowInputMoisture(true)
setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
}
}
},
{
title: "Cooldown",
function: () => {
setModeCooldown()
}
},
{
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
function: () => {
// setShowInputMoisture(true)
setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
}
}
]}
toggledButtons={determineToggle(mode)}
toggle
/>
]}
toggledButtons={determineToggle(mode)}
toggle
/>
</Box>
</Box>
);
};

View file

@ -23,6 +23,12 @@ interface Props {
//startingTranslate: number;
valDisplay?: "high" | "low" | "average";
insert?: boolean
/**
* optional function for when you want clicking the card to do something other than navigate to the bin page
* @param bin the bin model for the card that was clicked
* @returns void
*/
cardClickFunction?: (bin: Bin) => void
}
const useStyles = makeStyles((_theme) => {
@ -40,7 +46,7 @@ const useStyles = makeStyles((_theme) => {
});
export default function BinsList(props: Props) {
const { bins, duplicateBin, title, gridView, loadMore, valDisplay, insert } = props;
const { bins, duplicateBin, title, gridView, loadMore, valDisplay, insert, cardClickFunction } = props;
const classes = useStyles();
// const history = useHistory();
const navigate = useNavigate()
@ -53,6 +59,14 @@ export default function BinsList(props: Props) {
navigate(path, { state: {bin: bins[i]} });
};
const cardClicked = (index: number) => {
if(cardClickFunction) {
cardClickFunction(bins[index])
}else{
goToBin(index)
}
}
const leftArrow = () => {
const visibility = useContext(VisibilityContext);
//const isFirstVisible = visibility.useIsVisible('first', false)
@ -95,7 +109,7 @@ export default function BinsList(props: Props) {
key={i}
className={classes.gridListTile}
onClick={() => {
!duplicate && goToBin(i)
!duplicate && cardClicked(i)
}}>
<BinCardV2
bin={b}
@ -129,7 +143,7 @@ export default function BinsList(props: Props) {
key={i}
className={classes.gridListTile}
onClick={() => {
!duplicate && goToBin(i)
!duplicate && cardClicked(i)
}}>
<BinCardV2
bin={b}

View file

@ -42,6 +42,11 @@ import BinsList from "./BinsList";
interface Props {
yardKey: string;
insert?: boolean;
/**
* function to run when a bin card is clicked, when not passed in will default to attempting to navigate to the bin page by adding the bin key to the url
* @param bin the selected bin model
*/
binClicked?: (bin: Bin) => void
}
const useStyles = makeStyles((theme: Theme) => ({
@ -82,7 +87,7 @@ const useStyles = makeStyles((theme: Theme) => ({
}));
export default function BinyardDisplay(props: Props) {
const { yardKey, insert } = props;
const { yardKey, insert, binClicked } = props;
const classes = useStyles();
const binAPI = useBinAPI();
const theme = useTheme();
@ -321,6 +326,7 @@ export default function BinyardDisplay(props: Props) {
bins={grainBins}
duplicateBin={duplicateBin}
title={"Grain Bins"}
cardClickFunction={binClicked}
/>
</Grid>
</React.Fragment>
@ -338,6 +344,7 @@ export default function BinyardDisplay(props: Props) {
bins={fertBins}
duplicateBin={duplicateBin}
title={"Fertilizer Bins"}
cardClickFunction={binClicked}
/>
</Grid>
</React.Fragment>
@ -355,6 +362,7 @@ export default function BinyardDisplay(props: Props) {
bins={emptyBins}
duplicateBin={duplicateBin}
title={"Empty Bins"}
cardClickFunction={binClicked}
// loadMore={newTranslation => {
// //only triggered in the scroll view so this will never trigger in grid view
// if (yardBins.length < binTotal) {

View file

@ -57,6 +57,7 @@ interface Props {
* When true will disable all of the buttons in the group, will be overridded by individual buttons disabled state in the button data
*/
disableAll?: boolean
}
const useStyles = makeStyles((theme: Theme) => {

View file

@ -141,6 +141,28 @@ export default function DeviceWizard(props: Props) {
</MenuItem>
</Tooltip>
)}
{selectedPort &&
selectedPort.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY &&
device.featureSupported("detectOneWire") &&
selectedPort.autoDetectable && (
<Tooltip title="Used to tell the Device to scan this port for any plugged in sensors">
<MenuItem
key={"scanOneWire"}
disabled={scanInProgress || buttonClicked}
onClick={() => {
setButtonClicked(true)
console.log(selectedPort)
deviceAPI.detectOneWire(device.id(), selectedPort.address)
.then(resp => {
openSnack("Device will scan port on next check-in")
}).catch(err => {
openSnack("Command failed to send to device")
})
}}>
Scan Port
</MenuItem>
</Tooltip>
)}
{portComponents.length > 0 && (
<List>
<ListSubheader>Current Components</ListSubheader>
@ -204,7 +226,6 @@ export default function DeviceWizard(props: Props) {
*/
const adjustAvailablePositions = (port: PortInformation, availability: DeviceAvailabilityMap) => {
let currentAvailability = availability;
console.log(port)
switch (port.addressType) {
case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
currentAvailability.set(port.addressType, [{ address: port.address, label: port.label }]);

View file

@ -7,9 +7,11 @@ import { useEffect, useState } from "react";
import ScannedI2C from "./ScannedI2C";
import ComponentForm from "component/ComponentForm";
import { Component, Device } from "models";
import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useComponentAPI, useDeviceAPI, useSnackbar } from "hooks";
import { ConfigurablePin } from "pbHelpers/AddressTypes";
import ScannedOneWirePort from "./OneWire/ScannedOneWirePort";
interface Props {
scannedComponents: pond.ComponentAddressMap
@ -31,17 +33,19 @@ export default function DeviceScannedComponents(props: Props){
const compAPI = useComponentAPI();
const deviceAPI = useDeviceAPI()
const [scannedI2C, setScannedI2C] = useState<pond.DetectI2C>() //the unmodified scan of addresses
const [validCompAddresses, setValidComponentAddresses] = useState<quack.AddressData[]>([]) //the filtered array of address data
const [scannedOneWire, setScannedOneWire] = useState<pond.DetectOneWire[]>([])
const [validI2CCompAddresses, setValidI2CComponentAddresses] = useState<quack.AddressData[]>([]) //the filtered array of address data
const [components, setComponents] = useState<Component[]>([])
const [steps, setSteps] = useState<CompStep[]>([]);
const { error, success } = useSnackbar();
const [currentStep, setCurrentStep] = useState(0)
const [settingsValid, setSettingsValid] = useState(false);
const [openDialog, setOpenDialog] = useState(false);
const [clearOpen, setClearOpen] = useState(false);
useEffect(()=>{
//console.log(scannedComponents)
if (scannedComponents.i2c) setScannedI2C(scannedComponents.i2c)
if (scannedComponents.oneWire) setScannedOneWire(scannedComponents.oneWire)
//makes the array empty for testing
// if (scannedComponents.i2c) {
// let clone = cloneDeep(scannedComponents.i2c)
@ -76,7 +80,7 @@ export default function DeviceScannedComponents(props: Props){
}
})
}
setValidComponentAddresses(valid)
setValidI2CComponentAddresses(valid)
},[scannedI2C])
const stepper = () => {
@ -200,11 +204,12 @@ export default function DeviceScannedComponents(props: Props){
})
})
}
console.log(cloneList)
setComponents(cloneList)
}
const removeScan = (key: string) => {
deviceAPI.removeFoundComponents(device.settings.deviceId, key)
const removeScan = (key: string, type: pond.ObjectType) => {
deviceAPI.removeFoundComponents(device.settings.deviceId, key, type)
.then(resp => {
console.log("Cleared this scan")
}).catch(err => {
@ -212,20 +217,51 @@ export default function DeviceScannedComponents(props: Props){
})
}
const removeAllScans = () => {
deviceAPI.removeAllFoundComponents(device.settings.deviceId)
.then(resp => {
console.log("Cleared all scans")
refreshCallback()
}).catch(err => {
console.log("There was a problem clearing scans")
})
}
const clearWarning = () => {
return (
<ResponsiveDialog open={clearOpen} onClose={()=>{setClearOpen(false)}}>
<DialogTitle>Clear All Scans</DialogTitle>
<DialogContent>This action will clear all scans for this device.</DialogContent>
<DialogActions>
<Button variant="contained" onClick={()=>{setClearOpen(false)}}>Close</Button>
<Button variant="contained" color="primary" onClick={()=>{
removeAllScans()
setClearOpen(false)
}}>Continue</Button>
</DialogActions>
</ResponsiveDialog>
)
}
return (
<React.Fragment>
{clearWarning()}
<Card raised sx={{padding: 1}}>
<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Typography sx={{fontWeight: 650, fontSize: 25}}>Sensor Scan</Typography>
<Button variant="contained" color="primary" onClick={()=>{setClearOpen(true)}}>Clear</Button>
</Box>
{scannedI2C !== undefined &&
<React.Fragment>
<Box marginBottom={5}>
<Box justifyContent="space-between" display="flex">
<Typography sx={{fontWeight: 650}}>
I2C Sensors
</Typography>
<Button variant="contained" color="primary" onClick={()=>{removeScan(scannedI2C.key)}}>Clear Scan</Button>
<Button variant="contained" color="primary" onClick={()=>{removeScan(scannedI2C.key, pond.ObjectType.OBJECT_TYPE_DETECT_I2C)}}>Clear Scan</Button>
</Box>
{validCompAddresses.length > 0 ? validCompAddresses.map((addr, index) => {
{validI2CCompAddresses.length > 0 ? validI2CCompAddresses.map((addr, index) => {
return (
<ScannedI2C key={index} sensorNum={index+1} addressData={addr} deviceProduct={device.settings.product} componentSelectionCallback={componentSelection} availablePositions={availablePositions.get(quack.AddressType.ADDRESS_TYPE_I2C) ?? new Map<quack.ComponentType, number[]>()}/>
)
@ -240,7 +276,39 @@ export default function DeviceScannedComponents(props: Props){
</Typography>
</Box>
}
</React.Fragment>
</Box>
}
{scannedOneWire.length > 0 &&
<Box>
<Box justifyContent="space-between" display="flex">
<Typography sx={{fontWeight: 650}}>
Pin Port Sensors
</Typography>
</Box>
{scannedOneWire.map((scan,i) => {
let map = FindAvailablePositions([], device.settings.product).availability.get(quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY)
let portAddress = scan.settings?.oneWireData?.port
let portLabel = ""
map?.forEach(entry => {
let pin = entry as ConfigurablePin
if (pin.address && pin.address === portAddress) {
portLabel = pin.label
}
})
return(
<Box key={i} paddingX={2}>
<Box display="flex" justifyContent="space-between" alignItems={"center"}>
<Typography>Port: {portLabel}</Typography>
<Button variant="contained" color="primary" onClick={()=>{
removeScan(scan.key, pond.ObjectType.OBJECT_TYPE_DETECT_ONEWIRE)
}}>Clear Port Scan</Button>
</Box>
<ScannedOneWirePort scan={scan} componentSelectionCallback={componentSelection}/>
</Box>
)
})}
</Box>
}
<Box display="flex" flexDirection="row-reverse" marginTop={2}>
<Button disabled={components.length === 0} variant="contained" color="primary" onClick={()=>{setOpenDialog(true)}}>Add Components</Button>

View file

@ -0,0 +1,150 @@
import { Box, Checkbox, Grid2, MenuItem, TextField, Tooltip, Typography } from "@mui/material"
import { useMobile } from "hooks"
import { getFriendlyName, getSubtypes, Subtype } from "pbHelpers/ComponentType"
import { quack } from "protobuf-ts/quack"
import React, { useEffect, useState } from "react"
interface Props {
compData: quack.OneWireComponentData
componentSelect: (selected: boolean, componentData: quack.OneWireComponentData) => void
}
export default function PortComponent(props: Props){
const {compData, componentSelect} = props
// const [selectedSubtype, setSelectedSubtype] = useState<number>(0)
const [subtypeVal, setSubtypeVal] = useState("no subtype")//note that this is the friendly name and not the key or the value because it needs to be different from the other alias options
const [subtypeMap, setSubtypeMap] = useState<Map<string, number>>(new Map())
const [subtypeOptions, setSubtypeOptions] = useState<Subtype[]>([])
const [added, setAdded] = useState(false)
const isMobile = useMobile()
useEffect(()=>{
// setSelectedSubtype(compData.subtype)
let validSubtypes: Subtype[] = []
let subMap: Map<string, number> = new Map()
let subVal: string = "no subtype"
getSubtypes(compData.type).forEach(option => {
if(compData.subtype === option.key || compData.subtype === 0){
validSubtypes.push(option)
if(subVal === "no subtype"){
subVal = option.friendlyName
}
}
subMap.set(option.friendlyName, option.key)
})
setSubtypeOptions(validSubtypes)
setSubtypeMap(subMap)
setSubtypeVal(subVal)
},[compData])
const subtypeSelector = () => {
return (
<TextField
value={subtypeVal}
disabled={added}
fullWidth
onChange={(event) => {
setSubtypeVal(event.target.value)
// setSelectedSubtype(subtypeMap.get(event.target.value) ?? 0)
compData.subtype = subtypeMap.get(event.target.value) ?? 0
}}
select>
<MenuItem key={-1} value={"no subtype"}>Select the Component Subtype</MenuItem>
{subtypeOptions.map((s, i) => (
<MenuItem key={i} value={s.friendlyName}>{s.friendlyName}</MenuItem>
))}
</TextField>
)
}
const selectCompCheckbox = () => {
return (
<Tooltip
title="Add to list to be added to the device"
children={
<Checkbox
value={added}
onChange={(_, checked) => {
setAdded(checked)
componentSelect(checked, compData)
}}
/>
}
/>
)
}
const desktopDisplay = () => {
return (
<Box padding={2}>
<Grid2 container spacing={2} direction="row" wrap="nowrap" alignContent="center" alignItems="center" justifyContent="space-between">
<Grid2 size={3}>
<Typography>
{getFriendlyName(compData.type)}
</Typography>
</Grid2>
<Grid2 size={3}>
{subtypeOptions.length < 2
?
<Typography>
{subtypeVal}
</Typography>
:
subtypeSelector()}
</Grid2>
<Grid2 size={2}>
<Typography>
Cable ID: {compData.cableId}
</Typography>
</Grid2>
<Grid2 size={2}>
<Typography>
Nodes: {compData.nodeCount}
</Typography>
</Grid2>
<Grid2 size={1}>
{selectCompCheckbox()}
</Grid2>
</Grid2>
</Box>
)
}
const mobileDisplay = () => {
return(
<Box padding={2}>
<Grid2 container spacing={2} direction="row" wrap="nowrap" alignContent="center" alignItems="center" justifyContent="space-between">
<Grid2 size={5}>
<Typography>
{getFriendlyName(compData.type)}
</Typography>
<Typography>
Cable ID: {compData.cableId}
</Typography>
<Typography>
Nodes: {compData.nodeCount}
</Typography>
</Grid2>
<Grid2 size={5}>
{subtypeOptions.length < 2
?
<Typography>
{subtypeVal}
</Typography>
:
subtypeSelector()}
</Grid2>
<Grid2 size={2}>
{selectCompCheckbox()}
</Grid2>
</Grid2>
</Box>
)
}
return (
<React.Fragment>
{isMobile ? mobileDisplay() : desktopDisplay()}
</React.Fragment>
)
}

View file

@ -0,0 +1,57 @@
import { Box, MenuItem, TextField, Typography } from "@mui/material"
import { extension, getFriendlyName, getSubtypes, Subtype } from "pbHelpers/ComponentType"
import { pond } from "protobuf-ts/pond"
import { quack } from "protobuf-ts/quack"
import React, { useEffect, useState } from "react"
import PortComponent from "./PortComponent"
import { Component } from "models"
interface Props {
scan: pond.DetectOneWire
componentSelectionCallback: (newComponents: Component[], checked: boolean) => void
}
export default function ScannedOneWirePort(props: Props){
const {scan, componentSelectionCallback} = props
const [portComponents, setPortComponents] = useState<quack.OneWireComponentData[]>([])
useEffect(()=>{
setPortComponents(scan.settings?.oneWireData?.components ?? [])
},[scan])
const componentSelected = (checked: boolean, componentData: quack.OneWireComponentData) => {
let newComponents: Component[] = []
//use the component data to create a new Component
//build a component with given information and defaults for the rest
let primaryComponent = Component.create()
primaryComponent.settings.type = componentData.type
primaryComponent.settings.subtype = componentData.subtype
primaryComponent.settings.address = scan.settings?.oneWireData?.port ?? 0
primaryComponent.settings.addressType = quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY
primaryComponent.settings.name = getFriendlyName(componentData.type, componentData.subtype)
//need to check if the component is chainable, meaning the address type will reflect the cable id
let ext = extension(componentData.type, componentData.subtype)
if(ext.isChainable){
primaryComponent.settings.addressType = componentData.cableId + 8
}
newComponents.push(primaryComponent)
componentSelectionCallback(newComponents, checked)
}
return (
<React.Fragment>
{portComponents.length > 0 ? portComponents.map((compData, i) => {
return (
<PortComponent key={i} compData={compData} componentSelect={(checked, component) => {
componentSelected(checked, component)
}}/>
)
}):
<Box>
<Typography>No Components Found</Typography>
</Box>}
</React.Fragment>
)
}

View file

@ -3125,6 +3125,14 @@
"rpm": 1750,
"diameter": 0,
"horsepower": 30
},
{
"id": 445,
"name": "CALDWELL GGL-81011",
"kind": "centrifugal(low speed)",
"rpm": 0,
"diameter": 0,
"horsepower": 10
}
]
}

View file

@ -1,6 +1,7 @@
import BinyardDisplay from "bin/BinyardDisplay";
import DisplayDrawer from "common/DisplayDrawer";
import MapMarkerSettings from "maps/MapMarkerSettings";
import { Bin } from "models";
//import Bins from "pages/Bins";
import { pond } from "protobuf-ts/pond";
import { useBinYardAPI, useGlobalState, useSnackbar } from "providers";
@ -14,10 +15,11 @@ interface Props {
removeMarker: (key: string) => void;
updateMarker: (key: string, objectSettings: pond.BinYardSettings) => void;
moveMap: (long: number, lat: number) => void;
binClicked: (bin: Bin) => void
}
export default function BinYardDrawer(props: Props) {
const { open, onClose, selectedYard, yards, removeMarker, updateMarker, moveMap } = props;
const { open, onClose, selectedYard, yards, removeMarker, updateMarker, moveMap, binClicked } = props;
const [{as}] = useGlobalState();
const [yard, setYard] = useState<pond.BinYardSettings>(pond.BinYardSettings.create());
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
@ -85,7 +87,12 @@ export default function BinYardDrawer(props: Props) {
const drawerBody = () => {
//return <Bins insert yardFilter={yard.key} />;
return <BinyardDisplay insert yardKey={yard.key} />;
return (
<BinyardDisplay
insert
yardKey={yard.key}
binClicked={binClicked}/>
)
};
/**

View file

@ -1659,6 +1659,18 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
<React.Fragment>
<BinYardDrawer
open={yardDrawer}
binClicked={(bin) => {
//move the map to the correct bin location, assuming the bin was plotted on the map
let location = bin.location()
if(location !== null && location !== undefined){
moveMap(location.latitude, location.longitude, zoomLevels.close, isMobile)
}
//close the yarddrawer
setYardDrawer(false)
//open the bin drawer using the correct bin
setObjectKey(bin.key())
setBinDrawer(true)
}}
onClose={() => {
if (new Date().valueOf() > endTime) {
setYardDrawer(false);

View file

@ -64,6 +64,21 @@ const featureVersions: Map<string, FeatureVersionByPlatform> = new Map([
v2CellBlue: "2.1.7",
v2EthBlue: "2.1.7"
}
],[
"detectOneWire",
{
photon: "N/A",
electron: "N/A",
v2Wifi: "N/A",
v2Cell: "N/A",
v2WifiS3: "N/A",
v2CellS3: "N/A",
v2CellBlack: "2.1.10",
v2CellGreen: "N/A",
v2WifiBlue: "2.1.10",
v2CellBlue: "2.1.10",
v2EthBlue: "2.1.10"
}
]
]);
export class Device {
@ -166,6 +181,59 @@ export class Device {
return max
}
private versionComparison(deviceVersion: string, requiredVersion: string): boolean {
const parse = (v: string) => {
const [core, pre] = v.split("-");
const parts = core.split(".").map(n => parseInt(n, 10));
let preLabel = "";
let preNum = 0;
if (pre) {
const match = pre.match(/^([a-zA-Z]+)(\d*)$/);
if (match) {
preLabel = match[1].toLowerCase();
preNum = match[2] ? parseInt(match[2], 10) : 0;
}
}
return { parts, preLabel, preNum };
};
const rank = (label: string): number => {
switch (label) {
case "alpha": return 1;
case "beta": return 2;
case "rc": return 3;
default: return 0; // release
}
};
const a = parse(deviceVersion);
const b = parse(requiredVersion);
// Compare major/minor/patch
const len = Math.max(a.parts.length, b.parts.length);
for (let i = 0; i < len; i++) {
const av = a.parts[i] ?? 0;
const bv = b.parts[i] ?? 0;
if (av > bv) return true;
if (av < bv) return false;
}
// Handle prerelease vs release
if (!a.preLabel && b.preLabel) return true; // release > prerelease
if (a.preLabel && !b.preLabel) return false;
// Both prereleases → compare rank
if (a.preLabel !== b.preLabel) {
return rank(a.preLabel) >= rank(b.preLabel);
}
// Same prerelease label → compare numeric suffix
return a.preNum >= b.preNum;
}
public featureSupported(feature: string): boolean {
let versions = featureVersions.get(feature);
if (!versions) {
@ -173,27 +241,27 @@ export class Device {
}
switch (this.settings.platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON:
return this.status.firmwareVersion >= versions.photon;
return this.versionComparison(this.status.firmwareVersion, versions.photon);
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
return this.status.firmwareVersion >= versions.electron;
return this.versionComparison(this.status.firmwareVersion, versions.electron);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR:
return this.status.firmwareVersion >= versions.v2Cell;
return this.versionComparison(this.status.firmwareVersion, versions.v2Cell);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI:
return this.status.firmwareVersion >= versions.v2Wifi;
return this.versionComparison(this.status.firmwareVersion, versions.v2Wifi);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3:
return this.status.firmwareVersion >= versions.v2WifiS3;
return this.versionComparison(this.status.firmwareVersion, versions.v2WifiS3);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_S3:
return this.status.firmwareVersion >= versions.v2CellS3;
return this.versionComparison(this.status.firmwareVersion, versions.v2CellS3);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK:
return this.status.firmwareVersion >= versions.v2CellBlack;
return this.versionComparison(this.status.firmwareVersion, versions.v2CellBlack);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN:
return this.status.firmwareVersion >= versions.v2CellGreen;
return this.versionComparison(this.status.firmwareVersion, versions.v2CellGreen);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
return this.status.firmwareVersion >= versions.v2WifiBlue;
return this.versionComparison(this.status.firmwareVersion, versions.v2WifiBlue);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
return this.status.firmwareVersion >= versions.v2CellBlue;
return this.versionComparison(this.status.firmwareVersion, versions.v2CellBlue);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
return this.status.firmwareVersion >= versions.v2EthBlue;
return this.versionComparison(this.status.firmwareVersion, versions.v2EthBlue);
default:
return false;
}

View file

@ -1,6 +1,7 @@
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
import { cloneDeep } from "lodash";
import { Result } from "pbHelpers/Enums";
export class Interaction {
public settings: pond.InteractionSettings = pond.InteractionSettings.create();
@ -53,6 +54,18 @@ export class Interaction {
return subtype;
}
//these two are not mutually exclusive, it is possible to be an alert and a control
public isAlert(): boolean {
if(this.settings.result?.type === Result.report) return true
if(this.settings.notifications?.reports && this.settings.notifications.notify) return true
return false
}
public isControl(): boolean {
if(this.settings.result?.type !== Result.report) return true //as of right now all interactions that are not report are some sort of control
return false
}
public static upToSubtype(nodeNumber: number): number {
let subtype = 0;
switch (nodeNumber) {

View file

@ -0,0 +1,89 @@
import { Add, ExpandMore } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Grid2, IconButton, List, ListItem, ListSubheader, Typography } from "@mui/material";
import { Component } from "models";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond";
import React, { useState } from "react";
import NewObjectInteraction from "./NewObjectInteraction";
import { Option } from "common/SearchSelect";
export interface Alert {
conditions: pond.InteractionCondition[];
sourceComponents: Component[];
}
interface Props {
alerts: Alert[]
permissions: pond.Permission[]
addNew?: () => void
}
export default function Alerts(props: Props){
const {alerts, addNew, permissions} = props
// const [openNewAlert, setOpenNewAlert] = useState(false)
const readableCondition = (condition: pond.InteractionCondition) => {
let describer = describeMeasurement(condition.measurementType);
let type = describer.label();
let comparison =
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
? "Exactly"
: condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
? "Above"
: "Below";
let value = describer.toDisplay(condition.value);
return (
<Box>
<Typography>{type + " " + comparison + " " + value + describer.GetUnit()}</Typography>
</Box>
);
};
const alertAccordion = (alert: Alert) => {
return (
<Accordion style={{ width: "100%" }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Grid2 container direction="column">
{alert.conditions.map((condition, i) => (
<Grid2 key={i}>
{readableCondition(condition)}
</Grid2>
))}
</Grid2>
</AccordionSummary>
<AccordionDetails>
<List>
<ListSubheader>Reporting Components</ListSubheader>
{alert.sourceComponents.map((comp, i) => (
<ListItem key={i}>{comp.name()}</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
);
};
return (
<React.Fragment>
<List style={{ margin: 5 }}>
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<Typography>
Alerts
</Typography>
{addNew &&
<Button
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
onClick={()=>{addNew()}}
variant="contained"
color="primary">
<Add /> Alert
</Button>
}
</ListSubheader>
{alerts.map((alert, i) => (
<ListItem key={i}>{alertAccordion(alert)}</ListItem>
))}
</List>
</React.Fragment>
);
}

View file

@ -0,0 +1,152 @@
import { Add, ExpandMore } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Divider, IconButton, List, ListItem, ListSubheader, Menu, MenuItem, Typography } from "@mui/material";
import { Component, Device, Interaction } from "models";
import { interactionResultText } from "pbHelpers/Interaction";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
export interface Control {
conditions: pond.InteractionCondition[];
sourceComponents: Component[];
result: pond.InteractionResult | null;
sinkComponent: Component
device: Device
}
interface Props {
devices: Device[]
controls: Control[]
permissions: pond.Permission[]
addNew?: (device: Device) => void
}
export default function Controls(props: Props){
const { devices, controls, addNew, permissions } = props
const [deviceControls, setDeviceControls] = useState<Map<number, Control[]>>(new Map())
const [openDeviceMenu, setOpenDeviceMenu] = useState(false)
const [anchor, setAnchor] = useState<null | HTMLElement>(null)
useEffect(()=>{
let map: Map<number, Control[]> = new Map()
controls.forEach(control => {
let mappedControls = map.get(control.device.id())
if(mappedControls){
mappedControls.push(control)
}else{
map.set(control.device.id(), [control])
}
})
setDeviceControls(map)
},[controls])
const readableConditions = (control: Control) => {
let conditions: string = ""
let firstSource = control.sourceComponents[0]
control.conditions.forEach((condition, index) => {
let describer = describeMeasurement(condition.measurementType, firstSource.type(), firstSource.subType());
let type = describer.label();
let comparison =
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
? "Exactly"
: condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
? "Above"
: "Below";
let value = describer.toDisplay(condition.value);
conditions = conditions + (index !== 0 ? " and " : "") + type + " " + comparison + " " + value + describer.GetUnit()
})
return conditions
};
const controlAccordion = (control: Control) => {
let tempInteraction = Interaction.create()
tempInteraction.settings.result = control.result
let resultText = interactionResultText(tempInteraction, control.sinkComponent)
return (
<Accordion sx={{width: "100%"}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box>
<Typography>
{resultText}:
</Typography>
<Typography>
{readableConditions(control)}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<List>
<ListSubheader>Controlling Components</ListSubheader>
{control.sourceComponents.map((comp, i) => (
<ListItem key={i}>{comp.name()}</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
)
}
const deviceMenu = () => {
return (
<Menu
open={openDeviceMenu}
anchorEl={anchor}
onClose={() => {
setAnchor(null)
setOpenDeviceMenu(false)
}}>
{devices.map(dev => (
<MenuItem
key={dev.id()}
onClick={() => {
setAnchor(null)
setOpenDeviceMenu(false)
if (addNew) addNew(dev)
}}>
{dev.name()}
</MenuItem>
))}
</Menu>
)
}
return (
<React.Fragment>
{deviceMenu()}
<List style={{ margin: 5 }}>
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<Typography>
Controls
</Typography>
{addNew &&
<Button
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
variant="contained"
onClick={(e)=>{
setOpenDeviceMenu(true)
setAnchor(e.currentTarget)
}}
color="primary">
<Add />Control
</Button>
}
</ListSubheader>
{devices.map(device => (
<React.Fragment key={device.id()}>
<Typography sx={{paddingY: 1, paddingX: 2}}>{device.name()}</Typography>
<Divider />
{deviceControls.get(device.id()) ? deviceControls.get(device.id())?.map((control, i) => (
<ListItem key={i}>{controlAccordion(control)}</ListItem>
))
:
<Typography sx={{paddingY: 1, paddingX: 2}}>No Control Interactions For Device</Typography>
}
</React.Fragment>
))}
</List>
</React.Fragment>
)
}

View file

@ -0,0 +1,111 @@
import { Button, List, ListItem, ListSubheader, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useNotificationAPI } from "providers";
import React, { useEffect, useRef, useState } from "react";
import { getThemeType } from "theme/themeType";
interface Props {
objectKey: string
objectType: pond.ObjectType
}
const useStyles = makeStyles(() => {
return ({
dark: {
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
padding: 5
},
light: {
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
padding: 5
},
})
})
export default function Notifications(props: Props){
const {objectKey, objectType} = props
const classes = useStyles();
const [recentNotifications, setRecentNotifications] = useState<pond.Notification[]>([]);
const [notificationsLoading, setNotificationsLoading] = useState(false)
const loadingRef = useRef(false)
const [{as}] = useGlobalState();
const notificationAPI = useNotificationAPI();
const [totalNotifications, setTotalNotifications] = useState(0)
//get the notifications for the components on an object
useEffect(() => {
if (loadingRef.current) return
loadingRef.current = true
notificationAPI
.listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as)
.then(resp => {
setRecentNotifications(resp.data.notifications);
setTotalNotifications(resp.data.total);
})
.catch(_err => {})
.finally(() => {
loadingRef.current = true;
});
}, [objectKey, objectType, notificationAPI]);
const loadMoreNotifications = () => {
let current = recentNotifications;
setNotificationsLoading(true);
notificationAPI
.listObjectNotifications(
objectKey,
objectType,
10,
current.length,
undefined,
undefined,
undefined,
as
)
.then(resp => {
if (resp.data.notifications.length > 0) {
let c = current.concat(resp.data.notifications);
setRecentNotifications([...c]);
}
})
.catch(_err => {})
.finally(() => {
setNotificationsLoading(false);
});
};
return (
<List style={{ margin: 5 }}>
<ListSubheader>
<Typography sx={{paddingY: 2}}>
Notifications
</Typography>
</ListSubheader>
{recentNotifications && (
<React.Fragment>
{recentNotifications.map((notification, i) => (
<ListItem key={i} className={i % 2 === 0 ? classes.dark : classes.light}>
<Typography style={{ fontWeight: 650 }}>
{moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")}
</Typography>{" "}
:{" "}
<Typography>
{notification.settings?.title + " - " + notification.settings?.subtitle}
</Typography>
</ListItem>
))}
{recentNotifications.length < totalNotifications && (
<ListItem key={"button"}>
<Button onClick={loadMoreNotifications} disabled={notificationsLoading}>
Get More
</Button>
</ListItem>
)}
</React.Fragment>
)}
</List>
);
}

View file

@ -0,0 +1,229 @@
import { Box, Button, Card, Typography } from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import Alerts, { Alert } from "./Alerts";
import Controls, { Control } from "./Controls";
import { Component, Device, Interaction } from "models";
import { quack } from "protobuf-ts/quack";
import { Option } from "common/SearchSelect";
import { useInteractionsAPI } from "hooks";
import { useGlobalState } from "providers";
import { extension, getFriendlyName } from "pbHelpers/ComponentType";
import { sameComponentID } from "pbHelpers/Component";
import Notifications from "./Notifications";
import { pond } from "protobuf-ts/pond";
import React from "react";
import NewObjectInteraction from "./NewObjectInteraction";
interface Props {
linkedComponents: Map<string, Component>; //the component key to the component object
componentDevices: Map<string, number>;
objectKey: string
objectType: pond.ObjectType
devices: Device[];
permissions: pond.Permission[]
}
export default function ObjectInteractions(props: Props) {
const { linkedComponents, componentDevices, devices, objectKey, objectType, permissions } = props
const [{as}] = useGlobalState()
//list of alerts, each alert contains a list of interactions with matching conditions
const [alerts, setAlerts] = useState<Alert[]>([]);
//list of interactions relating to a controller
const [controls, setControls] = useState<Control[]>([]);
const interactionsAPI = useInteractionsAPI();
const [openNewInteraction, setOpenNewInteraction] = useState(false)
const [allSinks, setAllSinks] = useState<Component[]>([]);
// const [typeOptions, setTypeOptions] = useState<Option[]>([]);
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>(undefined)
const matchConditions = (interaction: Interaction, set: Alert | Control) => {
//if the number of conditions are not the same they are different
if (interaction.settings.conditions.length !== set.conditions.length) {
return false;
}
//continue with the comparison
let matching = true;
interaction.settings.conditions.forEach((condition, i) => {
if (
condition.comparison !== set.conditions[i].comparison ||
condition.measurementType !== set.conditions[i].measurementType ||
condition.value !== set.conditions[i].value
) {
matching = false;
}
});
return matching;
};
const matchResult = (interaction: Interaction, set: Control) => {
let interactionResult = interaction.settings.result
let setResult = set.result
let matching = true
// if anything in the result for the interaction is different from the set make matching false
if(interactionResult?.type !== setResult?.type ||
interactionResult?.mode !== setResult?.mode ||
interactionResult?.value !== setResult?.value ||
interactionResult?.dutyCycle !== setResult?.dutyCycle
){
matching = false
}
return matching
}
const findDevice = (deviceID: number) => {
for (let device of devices) {
if (device.id() === deviceID) return device
}
}
const findSinkComponent = (sinks: Component[], interactionSink: quack.ComponentID, sourceDevice: number) => {
for (let component of sinks) {
if (sameComponentID(component.location(), interactionSink) && sourceDevice === componentDevices.get(component.key())){
return component
}
}
}
const load = useCallback(()=>{
const newInteractions: Interaction[] = [];
const interactionSourceMap = new Map<string, string>();
const sinkOptions: Component[] = [];
const alerts: Alert[] = [];
const controls: Control[] = [];
const run = async () => {
// Run all component API fetches in parallel
await Promise.all(
[...linkedComponents].map(async ([key, comp]) => {
const device = componentDevices.get(key);
if (device) {
const resp = await interactionsAPI.listInteractionsByComponent(
device,
comp.location(),
undefined,
as
);
resp.forEach(interaction => interactionSourceMap.set(interaction.key(), key));
newInteractions.push(...resp);
}
// Collect controller components
if (extension(comp.type()).isController) {
sinkOptions.push(comp);
}
})
);
};
run().then(() => {
newInteractions.forEach(interaction => {
//alert and control are not mutally exclusive, it is possible to be both if the control is sending notifications
if (interaction.isAlert()) {
// Find matching alert
let similarAlertIndex = alerts.findIndex(alert =>
matchConditions(interaction, alert)
);
const compKey = interactionSourceMap.get(interaction.key());
const component = compKey ? linkedComponents.get(compKey) : undefined;
if (component) {
if (similarAlertIndex === -1) {
alerts.push({
conditions: interaction.settings.conditions,
sourceComponents: [component],
});
} else {
alerts[similarAlertIndex].sourceComponents.push(component);
}
}
}
if (interaction.isControl()) {
if (!interaction.settings.sink) return //if there is no sink it cant be a control so move on to the next
const compKey = interactionSourceMap.get(interaction.key());
if (!compKey) return //we have to have a valid component key
const component = linkedComponents.get(compKey);
const deviceID = componentDevices.get(compKey);
if (!deviceID) return //we have to know which device it is for
const controlDevice = findDevice(deviceID)
if (!controlDevice) return //failed to get the device from the array
const sinkComponent = findSinkComponent(sinkOptions, interaction.settings.sink, deviceID)
if (!sinkComponent) return //failed to get the correct sink component
let similarControlIndex = controls.findIndex(control =>
matchConditions(interaction, control) &&
matchResult(interaction, control) &&
sameComponentID(interaction.settings.sink, control.sinkComponent.location()) && //if the sink for the interaction is the same address as the existing control
deviceID === control.device.id() //if the device id for source component is the same as the device id in the control
);
if (component && interaction.settings.result) {
if (similarControlIndex === -1) {
controls.push({
conditions: interaction.settings.conditions,
result: interaction.settings.result,
sourceComponents: [component],
sinkComponent: sinkComponent,
device: controlDevice
});
} else {
controls[similarControlIndex].sourceComponents.push(component);
}
}
}
});
setAllSinks(sinkOptions);
setAlerts(alerts);
setControls(controls);
}).catch(err => {
console.error("Interaction fetch error:", err)
})
},[linkedComponents, interactionsAPI, componentDevices, as])
useEffect(() => {
load()
}, [load]);
return (
<React.Fragment>
<NewObjectInteraction
open={openNewInteraction}
onClose={(refresh) => {
setOpenNewInteraction(false)
if(refresh){
load()
}
}}
linkedComponents={linkedComponents}
componentDevices={componentDevices}
device={selectedDevice}/>
<Card raised style={{ margin: 5 }}>
<Box padding={1}>
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Interactions</Typography>
<Controls
controls={controls}
permissions={permissions}
devices={devices}
addNew={(device) => {
setSelectedDevice(device)
setOpenNewInteraction(true)
}}/>
<Alerts
alerts={alerts}
permissions={permissions}
addNew={() => {
setSelectedDevice(undefined)
setOpenNewInteraction(true)
}}/>
<Notifications objectKey={objectKey} objectType={objectType}/>
</Box>
</Card>
</React.Fragment>
)
}

View file

@ -50,11 +50,9 @@ import { GrainCable } from "models/GrainCable";
// import { Controller } from "models/Controller";
import { Pressure } from "models/Pressure";
import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material";
import { getThemeType } from "theme";
import BinGraphs from "bin/graphs/BinGraphs";
import BinTour from "bin/BinTour";
import { getBinModel } from "common/DataImports/BinCables/BinCableImporter";
import ObjectAlerts from "objects/ObjectAlerts";
import DevicePresetController from "device/presets/devicePresetController";
import { DevicePreset } from "models/DevicePreset";
import BinVisualizerV2 from "bin/BinVisualizerV2";
@ -69,6 +67,7 @@ import TaskViewer from "tasks/TaskViewer";
import ButtonGroup from "common/ButtonGroup";
import BinTransactions from "bin/BinTransactions";
import { CO2 } from "models/CO2";
import ObjectInteractions from "objects/objectInteractions/ObjectInteractions";
interface TabPanelProps {
children?: React.ReactNode;
@ -196,6 +195,7 @@ export default function Bin(props: Props) {
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]);
const [heaters, setHeaters] = useState<Controller[]>([]);
const [fans, setFans] = useState<Controller[]>([]);
const [activeDetails, setActiveDetails] = useState([0])
const [detail, setDetail] = useState<
"inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions"
>("inventory");
@ -806,7 +806,7 @@ export default function Bin(props: Props) {
{overview()}
{preferences && binComponents(preferences)}
</Grid>
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
<Grid id="tour-conditions" size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING ||
bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && (
@ -825,82 +825,66 @@ export default function Bin(props: Props) {
<BinStorageConditions bin={bin} headspaceCO2={headspaceCO2} cables={grainCables} />
</Box>
</Grid>
<Grid id="tour-graphs" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}>
<Box paddingBottom={2} display="flex">
<ButtonGroup
<Grid id="tour-details" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}>
<Box id="tour-graph-tabs" paddingBottom={2} display="flex">
<ButtonGroup
toggle
toggledButtons={activeDetails}
buttons={[
{
title: "Inventory",
function: () => setDetail("inventory")
function: () => {
setDetail("inventory")
setActiveDetails([0])
}
},
{
title: "Sensors",
function: () => setDetail("sensors")
function: () => {
setDetail("sensors")
setActiveDetails([1])
}
},
{
title: "Analysis",
function: () => setDetail("analytics")
function: () => {
setDetail("analytics")
setActiveDetails([2])
}
},
{
title: "Alerts",
function: () => setDetail("alerts")
function: () => {
setDetail("alerts")
setActiveDetails([3])
}
},
{
title: "Presets",
function: () => setDetail("presets")
function: () => {
setDetail("presets")
setActiveDetails([4])
}
},
{
title: "Transactions",
function: () => setDetail("transactions")
function: () => {
setDetail("transactions")
setActiveDetails([5])
}
}
]}
/>
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={detail}
exclusive
size="small"
aria-label="detail">
<StyledToggle
value={"inventory"}
aria-label="Inventory Details"
onClick={() => setDetail("inventory")}>
Inventory
</StyledToggle>
<StyledToggle
onClick={() => setDetail("sensors")}
value={"sensors"}
aria-label="Bin Sensor Graphs">
Sensors
</StyledToggle>
<StyledToggle
onClick={() => setDetail("analytics")}
value={"analytics"}
aria-label="Bin Analysis Graphs">
Analysis
</StyledToggle>
<StyledToggle
onClick={() => setDetail("alerts")}
value={"alerts"}
aria-label="Device Alerts">
Alerts
</StyledToggle>
<StyledToggle
onClick={() => setDetail("presets")}
value={"presets"}
aria-label="Bin Mode Presets">
Presets
</StyledToggle>
</StyledToggleButtonGroup> */}
</Box>
{detail === "alerts" && (
<Box>
<ObjectAlerts
<ObjectInteractions
linkedComponents={components}
componentDevices={componentDevices}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
devices={devices}
objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
permissions={permissions}
/>
</Box>
)}
@ -916,23 +900,27 @@ export default function Bin(props: Props) {
/>
</Box>
)}
{(detail === "inventory" || detail === "sensors" || detail === "analytics") && (
<BinGraphs
bin={bin}
binLoading={binLoading}
plenums={plenums}
cables={grainCables}
pressures={pressures}
fans={fans}
componentDevices={componentDevices}
display={detail}
compMap={components}
compositionNameMap={compositionNameMap}
/>
)}
{detail === "transactions" &&
<Box>
<BinTransactions bin={bin} permissions={permissions} refresh={refresh}/>
</Box>
}
{(detail === "inventory" || detail === "sensors" || detail === "analytics") && (
<Box>
<BinGraphs
bin={bin}
binLoading={binLoading}
plenums={plenums}
cables={grainCables}
pressures={pressures}
fans={fans}
componentDevices={componentDevices}
display={detail}
compMap={components}
compositionNameMap={compositionNameMap}
/>
</Box>
)}
</Grid>
</Grid>
</Card>
@ -1033,11 +1021,13 @@ export default function Bin(props: Props) {
</TabPanelMine>
<TabPanelMine value={mobileTab} index={4}>
<Box>
<ObjectAlerts
<ObjectInteractions
linkedComponents={components}
componentDevices={componentDevices}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
devices={devices}
objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
permissions={permissions}
/>
</Box>
</TabPanelMine>
@ -1175,7 +1165,11 @@ export default function Bin(props: Props) {
</Box>
{objectTeams()}
{deviceMenu()}
<BinTour />
<BinTour
setDetailTabState={(detail, buttonIndex) => {
setDetail(detail)
setActiveDetails([buttonIndex])
}}/>
</PageContainer>
);
}

View file

@ -1,4 +1,4 @@
import { Button, Card, Divider, List, ListItem, Typography } from "@mui/material";
import { Box, Button, Card, Divider, List, ListItem, Typography } from "@mui/material";
import Grid from '@mui/material/Grid2';
import { Component, Device, Interaction, User } from "models";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
@ -169,8 +169,8 @@ export default function DevicePage() {
const loadPortScan = () => {
deviceAPI.listFoundComponents(parseInt(deviceID))
.then(resp => {
if (resp.data.foundComponents?.i2c){
setScannedAddresses(resp.data.foundComponents ?? undefined)
if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){
setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined)
}
})
.catch(err => {
@ -411,7 +411,16 @@ export default function DevicePage() {
/>
</Grid>
{/* add grid card here for I2C detected components */}
{(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
<Grid size={{ xs: 12 }}>
<DeviceScannedComponents
scannedComponents={scannedAddresses}
device={device}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
refreshCallback={loadDevice}/>
</Grid>
}
{diagnosticComponents.map(comp => (
<Grid size={{ xs: 12 }} key={comp.key()}>
<ComponentDiagnostics
@ -432,8 +441,8 @@ export default function DevicePage() {
refreshCallback={loadDevice}
/>
</Grid>
{scannedAddresses &&
<Grid size={{ xs: 6, sm: 6, lg: 4, xl: 4 }}>
{(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
<Grid size={{ xs: 6, sm: 6, lg: 8, xl: 8 }}>
<DeviceScannedComponents
scannedComponents={scannedAddresses}
device={device}

View file

@ -1,13 +1,15 @@
import { useDeviceAPI, useMobile, useUserAPI } from "hooks";
import PageContainer from "./PageContainer";
import { useEffect, useState } from "react";
import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Typography } from "@mui/material";
import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Grid2, MenuItem, TextField, Typography } from "@mui/material";
import { useBinAPI, useGlobalState, useTeamAPI } from "providers";
import { CheckCircleOutline } from "@mui/icons-material";
import { green } from "@mui/material/colors";
import Tour, { TourStep } from "common/Tour";
import Emoji from "react-emoji-render";
import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel";
import { pond } from "protobuf-ts/pond";
import { User } from "models";
// import { color } from "framer-motion";
// interface Props {
@ -21,7 +23,7 @@ export default function SignupCallback () {
const deviceAPI = useDeviceAPI();
const binAPI = useBinAPI();
const isMobile = useMobile()
const [{ user }] = useGlobalState();
const [globalState, dispatch] = useGlobalState();
const whiteLabel = getWhitelabel()
const [loading, setLoading] = useState(false)
@ -35,6 +37,7 @@ export default function SignupCallback () {
const [doneBins, setDoneBins] = useState(false)
const [doneTeams, setDoneTeams] = useState(false)
const [doneDevices, setDoneDevices] = useState(false)
const [user, setUser] = useState<User>(globalState.user);
useEffect(() => {
setLoading(true)
@ -55,6 +58,124 @@ export default function SignupCallback () {
return loading === false && doneBins && doneTeams && doneDevices
}
const changePressureUnit = (value: any) => {
let updatedUser = User.clone(user)
let pressureUnit: pond.PressureUnit;
switch (value) {
case 1:
pressureUnit = pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS;
break;
case 2:
pressureUnit = pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER;
break;
default:
pressureUnit = pond.PressureUnit.PRESSURE_UNIT_UNKNOWN;
break;
}
updatedUser.settings.pressureUnit = pressureUnit;
setUser(updatedUser)
};
const changeTemperatureUnit = (value: any) => {
let updatedUser = User.clone(user);
let temperatureUnit: pond.TemperatureUnit;
switch (value) {
case 1:
temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS;
break;
case 2:
temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT;
break;
default:
temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_UNKNOWN;
break;
}
updatedUser.settings.temperatureUnit = temperatureUnit;
setUser(updatedUser);
};
const unitPreferences = () => {
const { pressureUnit, temperatureUnit, distanceUnit, grainUnit } = user.settings;
return (
<Grid2 container>
<Grid2 size={{ xs: 12 }}>
<TextField
select
fullWidth
id="pressureUnit"
name="pressureUnit"
label="Pressure Unit"
value={pressureUnit ? pressureUnit : pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER}
onChange={event => changePressureUnit(event.target.value)}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS}>
Kilopascals (kPa)
</MenuItem>
<MenuItem value={pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER}>
Inches of Water (iwg)
</MenuItem>
</TextField>
</Grid2>
<Grid2 size={{ xs: 12 }}>
<TextField
select
fullWidth
id="temperatureUnit"
name="temperatureUnit"
label="Temperature Unit"
value={
temperatureUnit ? temperatureUnit : pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
}
onChange={event => changeTemperatureUnit(event.target.value)}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS}>Celsius (°C)</MenuItem>
<MenuItem value={pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT}>
Fahrenheit (°F)
</MenuItem>
</TextField>
</Grid2>
<Grid2 size={{ xs: 12 }}>
<TextField
select
fullWidth
id="distanceUnit"
name="distanceUnit"
label="Distance Unit"
value={distanceUnit ? distanceUnit : pond.DistanceUnit.DISTANCE_UNIT_METERS}
// onChange={event => changeDistanceUnit(event.target.value)}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_METERS}>Meters (m)</MenuItem>
<MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_FEET}>Feet (ft)</MenuItem>
</TextField>
</Grid2>
{IsAdaptiveAgriculture() && (
<Grid2 size={{ xs: 12 }}>
<TextField
select
fullWidth
id="grainUnit"
name="grainUnit"
label="Grain Unit"
value={grainUnit ? grainUnit : pond.GrainUnit.GRAIN_UNIT_BUSHELS}
// onChange={event => changeGrainUnit(event.target.value)}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_BUSHELS}>Bushels (bu)</MenuItem>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_WEIGHT}>Tonnes (mT)</MenuItem>
</TextField>
</Grid2>
)}
</Grid2>
);
};
const content = () => {
return (
<DialogContent sx={{ margin: "auto", textAlign: "center" }}>
@ -104,12 +225,17 @@ export default function SignupCallback () {
content: (
<>
<Typography variant="body2" >
In the user menu, you can make changes to your profile, and adjust unit preferences.
In the user menu, you can make changes to your profile, such as adjusting unit preferences.
</Typography>
<br/>
<Typography variant="body2" >
If you are part of a team, you can select it here as well.
</Typography>
<br />
<Typography variant="body2" >
Set your unit preferences here now, they can be changed later from this menu:
</Typography>
{unitPreferences()}
</>
),
target: "#tour-user-menu",
@ -150,35 +276,104 @@ export default function SignupCallback () {
target: "#tour-dashboard",
placement: "right"
})
if (IsAdaptiveAgriculture()) steps.push({
title: "Bins",
steps.push({
title: "Tasks",
content: (
<>
<Typography variant="body2" >
You can view your bins and their inventory here.
You can view and create tasks here and assign a start and end time for them.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's bins will be displayed here instead.
If you are viewing as a team you will see the teams tasks, you can even assign tasks to team members.
</Typography>
</>
),
target: "#tour-bins",
placement: "right",
disableBeacon: true,
target: "#tour-tasks",
placement: "right"
})
//tasks step
if (IsAdaptiveAgriculture()) {
steps.push({
title: "Visual Farm",
content: (
<>
<Typography variant="body2" >
You can view your Visual Farm here, it will allow tou to draw fields and plot bins an a map.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's fields and bins will be displayed here instead.
</Typography>
</>
),
target: "#tour-visual-farm",
placement: "right",
disableBeacon: true,
})
steps.push({
title: "Fields",
content: (
<>
<Typography variant="body2" >
You can view your fields you have drawn on the map here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's fields will be displayed here instead.
</Typography>
</>
),
target: "#tour-field-list",
placement: "right",
disableBeacon: true,
})
steps.push({
title: "Bins",
content: (
<>
<Typography variant="body2" >
You can view your bins and their inventory here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's bins will be displayed here instead.
</Typography>
</>
),
target: "#tour-bins",
placement: "right",
disableBeacon: true,
})
steps.push({
title: "Contracts",
content: (
<>
<Typography variant="body2" >
You can view and track any contracts you have created here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's contracts will be displayed here instead.
</Typography>
</>
),
target: "#tour-contracts",
placement: "right",
disableBeacon: true,
})
}
return steps;
};
const endTour = () => {
setIsTourRunning(false);
// if (user) {
// user.status.finishedBinIntro = moment().toJSON();
// userAPI
// .updateUser(userID, user.protobuf())
// .then(() => dispatch({ key: "user", value: user }))
// .catch((err: any) => error(err));
// }
if (user) {
userAPI
.updateUser(user.id(), user.protobuf())
.then(() => dispatch({ key: "user", value: user }))
.catch((err: any) => console.error(err));
}
};
const closeDialog = () => {

View file

@ -168,6 +168,7 @@ export interface ComponentTypeExtension {
//if the map does not exist then just use the array from the device availability,
//if the map does exist filter the availability after claims to find matching address between them and use that
subtypeI2CMap?: Map<number, number[]>
isChainable?: boolean
}
export interface Summary {

View file

@ -68,6 +68,7 @@ export function CapacitorCable(subtype: number = 0): ComponentTypeExtension {
isSource: true,
isArray: true,
isCalibratable: false,
isChainable: true,
addressTypes: addressTypes,
interactionResultTypes: [],
states: [],

View file

@ -176,6 +176,7 @@ export function GrainCable(subtype: number = 0): ComponentTypeExtension {
isSource: true,
isArray: true,
isCalibratable: false,
isChainable: true,
addressTypes: [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, quack.AddressType.ADDRESS_TYPE_I2C],
interactionResultTypes: [],
states: [],

View file

@ -64,6 +64,7 @@ export function PressureCable(subtype: number = 0): ComponentTypeExtension {
isArray: true,
hasFan: true,
isCalibratable: false,
isChainable: true,
addressTypes: [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY],
interactionResultTypes: [],
states: [],

View file

@ -23,6 +23,7 @@ export interface IDeviceAPIContext {
) => Promise<AxiosResponse<pond.GetDevicePageDataResponse>>;
getMulti: (ids: number[] | string[], otherTeam?: string) => Promise<AxiosResponse<pond.GetMultiDeviceResponse>>;
detectI2C: (id: number, otherTeam?: string) => Promise<AxiosResponse<pond.DetectI2CResponse>>;
detectOneWire: (id: number, port: number, otherTeam?: string) => Promise<AxiosResponse<pond.DetectOneWireResponse>>
listFoundComponents: (id: number, otherTeam?: string) => Promise<AxiosResponse<pond.ListFoundComponentsResponse>>
getGeoJson: (id: number | string, demo?: boolean, otherTeam?: string) => Promise<any>;
getMultiGeoJson: (ids: number[] | string[], otherTeam?: string) => Promise<any>;
@ -148,7 +149,8 @@ export interface IDeviceAPIContext {
keys?: string[],
types?: string[]
) => Promise<any>;
removeFoundComponents: (id: number, key: string, otherTeam?: string) => Promise<AxiosResponse<pond.RemoveFoundComponentsResponse>>;
removeAllFoundComponents: (id: number,otherTeam?: string) => Promise<AxiosResponse<pond.RemoveAllFoundComponentsResponse>>;
removeFoundComponents: (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => Promise<AxiosResponse<pond.RemoveFoundComponentsResponse>>;
}
export const DeviceAPIContext = createContext<IDeviceAPIContext>({} as IDeviceAPIContext);
@ -962,6 +964,19 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
})
}
const detectOneWire = (id: number, port: number, otherTeam?: string) => {
let url = "/devices/" + id + "/detectOneWire?port=" + port;
const view = otherTeam ? otherTeam : as
if(view) url = url + "?as=" + view
return new Promise<AxiosResponse<pond.DetectOneWireResponse>>((resolve, reject) => {
put<pond.DetectOneWireResponse>(pondURL(url)).then(resp => {
return resolve(resp)
}).catch(err => {
return reject(err)
})
})
}
const listFoundComponents = (id: number, otherTeam?: string) => {
let url = "/devices/" + id + "/listScannedComponents";
const view = otherTeam ? otherTeam : as
@ -975,10 +990,10 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
})
}
const removeFoundComponents = (id: number, key: string, otherTeam?: string) => {
let url = "/devices/" + id + "/removeFoundComponents/" + key;
const removeFoundComponents = (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => {
let url = "/devices/" + id + "/removeFoundComponents/" + key + "?scanType=" + type;
const view = otherTeam ? otherTeam : as
if(view) url = url + "?as=" + view
if(view) url = url + "&as=" + view
return new Promise<AxiosResponse<pond.RemoveFoundComponentsResponse>>((resolve, reject) => {
del<pond.RemoveFoundComponentsResponse>(pondURL(url)).then(resp => {
return resolve(resp)
@ -988,6 +1003,19 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
})
}
const removeAllFoundComponents = (id: number, otherTeam?: string) => {
let url = "/devices/" + id + "/removeAllFoundComponents";
const view = otherTeam ? otherTeam : as
if(view) url = url + "?as=" + view
return new Promise<AxiosResponse<pond.RemoveAllFoundComponentsResponse>>((resolve, reject) => {
del<pond.RemoveAllFoundComponentsResponse>(pondURL(url)).then(resp => {
return resolve(resp)
}).catch(err => {
return reject(err)
})
})
}
return (
<DeviceAPIContext.Provider
value={{
@ -1035,8 +1063,10 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
updateComponentPreferences,
listDeviceComponentPreferences,
detectI2C,
detectOneWire,
listFoundComponents,
removeFoundComponents
removeFoundComponents,
removeAllFoundComponents
}}>
{children}
</DeviceAPIContext.Provider>