Merge branch 'staging_environment'

This commit is contained in:
Carter 2026-01-06 11:24:46 -06:00
commit 7e4899f7a7
29 changed files with 2093 additions and 660 deletions

4
package-lock.json generated
View file

@ -42,7 +42,7 @@
"mui-tel-input": "^7.0.0", "mui-tel-input": "^7.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10", "openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"query-string": "^9.2.1", "query-string": "^9.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
@ -10953,7 +10953,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c08965a296f2cd0799472fd8b163186cf6885d4c", "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#3f647071f211c4552c2acc23a8ecabb6904a4156",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -54,7 +54,7 @@
"mui-tel-input": "^7.0.0", "mui-tel-input": "^7.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10", "openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"query-string": "^9.2.1", "query-string": "^9.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",

View file

@ -1,6 +1,6 @@
// import { useAuth0 } from "@auth0/auth0-react"; // import { useAuth0 } from "@auth0/auth0-react";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import Tour from "common/Tour"; import Tour, { TourStep } from "common/Tour";
import { random } from "lodash"; import { random } from "lodash";
import moment from "moment"; import moment from "moment";
import { useGlobalState, useSnackbar, useUserAPI } from "providers"; import { useGlobalState, useSnackbar, useUserAPI } from "providers";
@ -8,8 +8,13 @@ import React, { useEffect, useState } from "react";
import { Step } from "react-joyride"; import { Step } from "react-joyride";
// import Emoji from "react-emoji-render"; // 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 { userID } = useAuth0();
const {setDetailTabState} = props
const [{ user }, dispatch] = useGlobalState(); const [{ user }, dispatch] = useGlobalState();
const userID = user.id() const userID = user.id()
const { error } = useSnackbar(); const { error } = useSnackbar();
@ -35,8 +40,8 @@ export default function BinTour() {
} }
}; };
const getTourSteps = (): Step[] => { const getTourSteps = (): TourStep[] => {
let steps: Step[] = [ let steps: TourStep[] = [
{ {
title: ( title: (
<React.Fragment> <React.Fragment>
@ -103,24 +108,12 @@ export default function BinTour() {
disableBeacon: false disableBeacon: false
}, },
{ {
title: "Graphs", title: "View Other Data",
content: ( content: (
<React.Fragment> <React.Fragment>
<Typography variant="body2" paragraph> <Typography variant="body2" paragraph>
Bin related analytics are displayed here. Use these tabs to view other Bin Details such as inventory levels over time,
</Typography> alerts and interactions for connected components, and data for attached sensors
</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.
</Typography> </Typography>
</React.Fragment> </React.Fragment>
), ),
@ -128,6 +121,122 @@ export default function BinTour() {
placement: "bottom", placement: "bottom",
disableBeacon: false 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", title: "Change Mode",
content: ( content: (
@ -138,12 +247,15 @@ export default function BinTour() {
<ul> <ul>
<li>Storage mode: default</li> <li>Storage mode: default</li>
<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=" ☀️" /> */} {/* <Emoji text=" ☀️" /> */}
</li> </li>
<li> <li>
Cooldown mode: use fans to hold bin temperature lower Hydrating mode: use humid air to hydrate grain (only visible when the target moisture is higher than the starting moisture in the bin settings)
{/* <Emoji text=" ❆❅" /> */}
</li> </li>
</ul> </ul>
</React.Fragment> </React.Fragment>

View file

@ -1727,32 +1727,34 @@ export default function BinVisualizer(props: Props) {
{modeChangeInProgress && {modeChangeInProgress &&
<CircularProgress color="primary" size={25}/> <CircularProgress color="primary" size={25}/>
} }
<ButtonGroup <Box id="tour-bin-mode">
disableAll={modeChangeInProgress} <ButtonGroup
buttons={[ disableAll={modeChangeInProgress}
{ buttons={[
title: "Storage", {
function: () => { title: "Storage",
setModeStorage() 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
}
} }
}, ]}
{ toggledButtons={determineToggle(mode)}
title: "Cooldown", toggle
function: () => { />
setModeCooldown() </Box>
}
},
{
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
/>
</Box> </Box>
); );
}; };

View file

@ -23,6 +23,12 @@ interface Props {
//startingTranslate: number; //startingTranslate: number;
valDisplay?: "high" | "low" | "average"; valDisplay?: "high" | "low" | "average";
insert?: boolean 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) => { const useStyles = makeStyles((_theme) => {
@ -40,7 +46,7 @@ const useStyles = makeStyles((_theme) => {
}); });
export default function BinsList(props: Props) { 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 classes = useStyles();
// const history = useHistory(); // const history = useHistory();
const navigate = useNavigate() const navigate = useNavigate()
@ -53,6 +59,14 @@ export default function BinsList(props: Props) {
navigate(path, { state: {bin: bins[i]} }); navigate(path, { state: {bin: bins[i]} });
}; };
const cardClicked = (index: number) => {
if(cardClickFunction) {
cardClickFunction(bins[index])
}else{
goToBin(index)
}
}
const leftArrow = () => { const leftArrow = () => {
const visibility = useContext(VisibilityContext); const visibility = useContext(VisibilityContext);
//const isFirstVisible = visibility.useIsVisible('first', false) //const isFirstVisible = visibility.useIsVisible('first', false)
@ -95,7 +109,7 @@ export default function BinsList(props: Props) {
key={i} key={i}
className={classes.gridListTile} className={classes.gridListTile}
onClick={() => { onClick={() => {
!duplicate && goToBin(i) !duplicate && cardClicked(i)
}}> }}>
<BinCardV2 <BinCardV2
bin={b} bin={b}
@ -129,7 +143,7 @@ export default function BinsList(props: Props) {
key={i} key={i}
className={classes.gridListTile} className={classes.gridListTile}
onClick={() => { onClick={() => {
!duplicate && goToBin(i) !duplicate && cardClicked(i)
}}> }}>
<BinCardV2 <BinCardV2
bin={b} bin={b}

View file

@ -42,6 +42,11 @@ import BinsList from "./BinsList";
interface Props { interface Props {
yardKey: string; yardKey: string;
insert?: boolean; 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) => ({ const useStyles = makeStyles((theme: Theme) => ({
@ -82,7 +87,7 @@ const useStyles = makeStyles((theme: Theme) => ({
})); }));
export default function BinyardDisplay(props: Props) { export default function BinyardDisplay(props: Props) {
const { yardKey, insert } = props; const { yardKey, insert, binClicked } = props;
const classes = useStyles(); const classes = useStyles();
const binAPI = useBinAPI(); const binAPI = useBinAPI();
const theme = useTheme(); const theme = useTheme();
@ -321,6 +326,7 @@ export default function BinyardDisplay(props: Props) {
bins={grainBins} bins={grainBins}
duplicateBin={duplicateBin} duplicateBin={duplicateBin}
title={"Grain Bins"} title={"Grain Bins"}
cardClickFunction={binClicked}
/> />
</Grid> </Grid>
</React.Fragment> </React.Fragment>
@ -338,6 +344,7 @@ export default function BinyardDisplay(props: Props) {
bins={fertBins} bins={fertBins}
duplicateBin={duplicateBin} duplicateBin={duplicateBin}
title={"Fertilizer Bins"} title={"Fertilizer Bins"}
cardClickFunction={binClicked}
/> />
</Grid> </Grid>
</React.Fragment> </React.Fragment>
@ -355,6 +362,7 @@ export default function BinyardDisplay(props: Props) {
bins={emptyBins} bins={emptyBins}
duplicateBin={duplicateBin} duplicateBin={duplicateBin}
title={"Empty Bins"} title={"Empty Bins"}
cardClickFunction={binClicked}
// loadMore={newTranslation => { // loadMore={newTranslation => {
// //only triggered in the scroll view so this will never trigger in grid view // //only triggered in the scroll view so this will never trigger in grid view
// if (yardBins.length < binTotal) { // 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 * 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 disableAll?: boolean
} }
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {

View file

@ -141,6 +141,28 @@ export default function DeviceWizard(props: Props) {
</MenuItem> </MenuItem>
</Tooltip> </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 && ( {portComponents.length > 0 && (
<List> <List>
<ListSubheader>Current Components</ListSubheader> <ListSubheader>Current Components</ListSubheader>
@ -204,7 +226,6 @@ export default function DeviceWizard(props: Props) {
*/ */
const adjustAvailablePositions = (port: PortInformation, availability: DeviceAvailabilityMap) => { const adjustAvailablePositions = (port: PortInformation, availability: DeviceAvailabilityMap) => {
let currentAvailability = availability; let currentAvailability = availability;
console.log(port)
switch (port.addressType) { switch (port.addressType) {
case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY: case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
currentAvailability.set(port.addressType, [{ address: port.address, label: port.label }]); 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 ScannedI2C from "./ScannedI2C";
import ComponentForm from "component/ComponentForm"; import ComponentForm from "component/ComponentForm";
import { Component, Device } from "models"; import { Component, Device } from "models";
import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability"; import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { useComponentAPI, useDeviceAPI, useSnackbar } from "hooks"; import { useComponentAPI, useDeviceAPI, useSnackbar } from "hooks";
import { ConfigurablePin } from "pbHelpers/AddressTypes";
import ScannedOneWirePort from "./OneWire/ScannedOneWirePort";
interface Props { interface Props {
scannedComponents: pond.ComponentAddressMap scannedComponents: pond.ComponentAddressMap
@ -31,17 +33,19 @@ export default function DeviceScannedComponents(props: Props){
const compAPI = useComponentAPI(); const compAPI = useComponentAPI();
const deviceAPI = useDeviceAPI() const deviceAPI = useDeviceAPI()
const [scannedI2C, setScannedI2C] = useState<pond.DetectI2C>() //the unmodified scan of addresses 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 [components, setComponents] = useState<Component[]>([])
const [steps, setSteps] = useState<CompStep[]>([]); const [steps, setSteps] = useState<CompStep[]>([]);
const { error, success } = useSnackbar(); const { error, success } = useSnackbar();
const [currentStep, setCurrentStep] = useState(0) const [currentStep, setCurrentStep] = useState(0)
const [settingsValid, setSettingsValid] = useState(false); const [settingsValid, setSettingsValid] = useState(false);
const [openDialog, setOpenDialog] = useState(false); const [openDialog, setOpenDialog] = useState(false);
const [clearOpen, setClearOpen] = useState(false);
useEffect(()=>{ useEffect(()=>{
//console.log(scannedComponents)
if (scannedComponents.i2c) setScannedI2C(scannedComponents.i2c) if (scannedComponents.i2c) setScannedI2C(scannedComponents.i2c)
if (scannedComponents.oneWire) setScannedOneWire(scannedComponents.oneWire)
//makes the array empty for testing //makes the array empty for testing
// if (scannedComponents.i2c) { // if (scannedComponents.i2c) {
// let clone = cloneDeep(scannedComponents.i2c) // let clone = cloneDeep(scannedComponents.i2c)
@ -76,7 +80,7 @@ export default function DeviceScannedComponents(props: Props){
} }
}) })
} }
setValidComponentAddresses(valid) setValidI2CComponentAddresses(valid)
},[scannedI2C]) },[scannedI2C])
const stepper = () => { const stepper = () => {
@ -200,11 +204,12 @@ export default function DeviceScannedComponents(props: Props){
}) })
}) })
} }
console.log(cloneList)
setComponents(cloneList) setComponents(cloneList)
} }
const removeScan = (key: string) => { const removeScan = (key: string, type: pond.ObjectType) => {
deviceAPI.removeFoundComponents(device.settings.deviceId, key) deviceAPI.removeFoundComponents(device.settings.deviceId, key, type)
.then(resp => { .then(resp => {
console.log("Cleared this scan") console.log("Cleared this scan")
}).catch(err => { }).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 ( return (
<React.Fragment> <React.Fragment>
{clearWarning()}
<Card raised sx={{padding: 1}}> <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 && {scannedI2C !== undefined &&
<React.Fragment> <Box marginBottom={5}>
<Box justifyContent="space-between" display="flex"> <Box justifyContent="space-between" display="flex">
<Typography sx={{fontWeight: 650}}> <Typography sx={{fontWeight: 650}}>
I2C Sensors I2C Sensors
</Typography> </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> </Box>
{validCompAddresses.length > 0 ? validCompAddresses.map((addr, index) => { {validI2CCompAddresses.length > 0 ? validI2CCompAddresses.map((addr, index) => {
return ( 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[]>()}/> <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> </Typography>
</Box> </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}> <Box display="flex" flexDirection="row-reverse" marginTop={2}>
<Button disabled={components.length === 0} variant="contained" color="primary" onClick={()=>{setOpenDialog(true)}}>Add Components</Button> <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, "rpm": 1750,
"diameter": 0, "diameter": 0,
"horsepower": 30 "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 BinyardDisplay from "bin/BinyardDisplay";
import DisplayDrawer from "common/DisplayDrawer"; import DisplayDrawer from "common/DisplayDrawer";
import MapMarkerSettings from "maps/MapMarkerSettings"; import MapMarkerSettings from "maps/MapMarkerSettings";
import { Bin } from "models";
//import Bins from "pages/Bins"; //import Bins from "pages/Bins";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useBinYardAPI, useGlobalState, useSnackbar } from "providers"; import { useBinYardAPI, useGlobalState, useSnackbar } from "providers";
@ -14,10 +15,11 @@ interface Props {
removeMarker: (key: string) => void; removeMarker: (key: string) => void;
updateMarker: (key: string, objectSettings: pond.BinYardSettings) => void; updateMarker: (key: string, objectSettings: pond.BinYardSettings) => void;
moveMap: (long: number, lat: number) => void; moveMap: (long: number, lat: number) => void;
binClicked: (bin: Bin) => void
} }
export default function BinYardDrawer(props: Props) { 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 [{as}] = useGlobalState();
const [yard, setYard] = useState<pond.BinYardSettings>(pond.BinYardSettings.create()); const [yard, setYard] = useState<pond.BinYardSettings>(pond.BinYardSettings.create());
const [openMarkerSettings, setOpenMarkerSettings] = useState(false); const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
@ -85,7 +87,12 @@ export default function BinYardDrawer(props: Props) {
const drawerBody = () => { const drawerBody = () => {
//return <Bins insert yardFilter={yard.key} />; //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> <React.Fragment>
<BinYardDrawer <BinYardDrawer
open={yardDrawer} 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={() => { onClose={() => {
if (new Date().valueOf() > endTime) { if (new Date().valueOf() > endTime) {
setYardDrawer(false); setYardDrawer(false);

View file

@ -64,6 +64,21 @@ const featureVersions: Map<string, FeatureVersionByPlatform> = new Map([
v2CellBlue: "2.1.7", v2CellBlue: "2.1.7",
v2EthBlue: "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 { export class Device {
@ -166,6 +181,59 @@ export class Device {
return max 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 { public featureSupported(feature: string): boolean {
let versions = featureVersions.get(feature); let versions = featureVersions.get(feature);
if (!versions) { if (!versions) {
@ -173,27 +241,27 @@ export class Device {
} }
switch (this.settings.platform) { switch (this.settings.platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
return this.status.firmwareVersion >= versions.v2EthBlue; return this.versionComparison(this.status.firmwareVersion, versions.v2EthBlue);
default: default:
return false; return false;
} }

View file

@ -1,6 +1,7 @@
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { or } from "utils/types"; import { or } from "utils/types";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { Result } from "pbHelpers/Enums";
export class Interaction { export class Interaction {
public settings: pond.InteractionSettings = pond.InteractionSettings.create(); public settings: pond.InteractionSettings = pond.InteractionSettings.create();
@ -53,6 +54,18 @@ export class Interaction {
return subtype; 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 { public static upToSubtype(nodeNumber: number): number {
let subtype = 0; let subtype = 0;
switch (nodeNumber) { 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 { Controller } from "models/Controller";
import { Pressure } from "models/Pressure"; import { Pressure } from "models/Pressure";
import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material"; import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material";
import { getThemeType } from "theme";
import BinGraphs from "bin/graphs/BinGraphs"; import BinGraphs from "bin/graphs/BinGraphs";
import BinTour from "bin/BinTour"; import BinTour from "bin/BinTour";
import { getBinModel } from "common/DataImports/BinCables/BinCableImporter"; import { getBinModel } from "common/DataImports/BinCables/BinCableImporter";
import ObjectAlerts from "objects/ObjectAlerts";
import DevicePresetController from "device/presets/devicePresetController"; import DevicePresetController from "device/presets/devicePresetController";
import { DevicePreset } from "models/DevicePreset"; import { DevicePreset } from "models/DevicePreset";
import BinVisualizerV2 from "bin/BinVisualizerV2"; import BinVisualizerV2 from "bin/BinVisualizerV2";
@ -69,6 +67,7 @@ import TaskViewer from "tasks/TaskViewer";
import ButtonGroup from "common/ButtonGroup"; import ButtonGroup from "common/ButtonGroup";
import BinTransactions from "bin/BinTransactions"; import BinTransactions from "bin/BinTransactions";
import { CO2 } from "models/CO2"; import { CO2 } from "models/CO2";
import ObjectInteractions from "objects/objectInteractions/ObjectInteractions";
interface TabPanelProps { interface TabPanelProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -196,6 +195,7 @@ export default function Bin(props: Props) {
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]); const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]);
const [heaters, setHeaters] = useState<Controller[]>([]); const [heaters, setHeaters] = useState<Controller[]>([]);
const [fans, setFans] = useState<Controller[]>([]); const [fans, setFans] = useState<Controller[]>([]);
const [activeDetails, setActiveDetails] = useState([0])
const [detail, setDetail] = useState< const [detail, setDetail] = useState<
"inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions" "inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions"
>("inventory"); >("inventory");
@ -806,7 +806,7 @@ export default function Bin(props: Props) {
{overview()} {overview()}
{preferences && binComponents(preferences)} {preferences && binComponents(preferences)}
</Grid> </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_DRYING ||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING || bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING ||
bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && ( 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} /> <BinStorageConditions bin={bin} headspaceCO2={headspaceCO2} cables={grainCables} />
</Box> </Box>
</Grid> </Grid>
<Grid id="tour-graphs" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}> <Grid id="tour-details" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}>
<Box paddingBottom={2} display="flex"> <Box id="tour-graph-tabs" paddingBottom={2} display="flex">
<ButtonGroup <ButtonGroup
toggle toggle
toggledButtons={activeDetails}
buttons={[ buttons={[
{ {
title: "Inventory", title: "Inventory",
function: () => setDetail("inventory") function: () => {
setDetail("inventory")
setActiveDetails([0])
}
}, },
{ {
title: "Sensors", title: "Sensors",
function: () => setDetail("sensors") function: () => {
setDetail("sensors")
setActiveDetails([1])
}
}, },
{ {
title: "Analysis", title: "Analysis",
function: () => setDetail("analytics") function: () => {
setDetail("analytics")
setActiveDetails([2])
}
}, },
{ {
title: "Alerts", title: "Alerts",
function: () => setDetail("alerts") function: () => {
setDetail("alerts")
setActiveDetails([3])
}
}, },
{ {
title: "Presets", title: "Presets",
function: () => setDetail("presets") function: () => {
setDetail("presets")
setActiveDetails([4])
}
}, },
{ {
title: "Transactions", 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> </Box>
{detail === "alerts" && ( {detail === "alerts" && (
<Box> <Box>
<ObjectAlerts <ObjectInteractions
linkedComponents={components} linkedComponents={components}
componentDevices={componentDevices} componentDevices={componentDevices}
objectType={pond.ObjectType.OBJECT_TYPE_BIN} devices={devices}
objectKey={bin.key()} objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
permissions={permissions}
/> />
</Box> </Box>
)} )}
@ -916,23 +900,27 @@ export default function Bin(props: Props) {
/> />
</Box> </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" && {detail === "transactions" &&
<Box>
<BinTransactions bin={bin} permissions={permissions} refresh={refresh}/> <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>
</Grid> </Grid>
</Card> </Card>
@ -1033,11 +1021,13 @@ export default function Bin(props: Props) {
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={mobileTab} index={4}> <TabPanelMine value={mobileTab} index={4}>
<Box> <Box>
<ObjectAlerts <ObjectInteractions
linkedComponents={components} linkedComponents={components}
componentDevices={componentDevices} componentDevices={componentDevices}
objectType={pond.ObjectType.OBJECT_TYPE_BIN} devices={devices}
objectKey={bin.key()} objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
permissions={permissions}
/> />
</Box> </Box>
</TabPanelMine> </TabPanelMine>
@ -1175,7 +1165,11 @@ export default function Bin(props: Props) {
</Box> </Box>
{objectTeams()} {objectTeams()}
{deviceMenu()} {deviceMenu()}
<BinTour /> <BinTour
setDetailTabState={(detail, buttonIndex) => {
setDetail(detail)
setActiveDetails([buttonIndex])
}}/>
</PageContainer> </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 Grid from '@mui/material/Grid2';
import { Component, Device, Interaction, User } from "models"; import { Component, Device, Interaction, User } from "models";
import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import { getContextKeys, getContextTypes } from "pbHelpers/Context";
@ -169,8 +169,8 @@ export default function DevicePage() {
const loadPortScan = () => { const loadPortScan = () => {
deviceAPI.listFoundComponents(parseInt(deviceID)) deviceAPI.listFoundComponents(parseInt(deviceID))
.then(resp => { .then(resp => {
if (resp.data.foundComponents?.i2c){ if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){
setScannedAddresses(resp.data.foundComponents ?? undefined) setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined)
} }
}) })
.catch(err => { .catch(err => {
@ -411,7 +411,16 @@ export default function DevicePage() {
/> />
</Grid> </Grid>
{/* add grid card here for I2C detected components */} {/* 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 => ( {diagnosticComponents.map(comp => (
<Grid size={{ xs: 12 }} key={comp.key()}> <Grid size={{ xs: 12 }} key={comp.key()}>
<ComponentDiagnostics <ComponentDiagnostics
@ -432,8 +441,8 @@ export default function DevicePage() {
refreshCallback={loadDevice} refreshCallback={loadDevice}
/> />
</Grid> </Grid>
{scannedAddresses && {(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
<Grid size={{ xs: 6, sm: 6, lg: 4, xl: 4 }}> <Grid size={{ xs: 6, sm: 6, lg: 8, xl: 8 }}>
<DeviceScannedComponents <DeviceScannedComponents
scannedComponents={scannedAddresses} scannedComponents={scannedAddresses}
device={device} device={device}

View file

@ -1,13 +1,15 @@
import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import { useDeviceAPI, useMobile, useUserAPI } from "hooks";
import PageContainer from "./PageContainer"; import PageContainer from "./PageContainer";
import { useEffect, useState } from "react"; 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 { useBinAPI, useGlobalState, useTeamAPI } from "providers";
import { CheckCircleOutline } from "@mui/icons-material"; import { CheckCircleOutline } from "@mui/icons-material";
import { green } from "@mui/material/colors"; import { green } from "@mui/material/colors";
import Tour, { TourStep } from "common/Tour"; import Tour, { TourStep } from "common/Tour";
import Emoji from "react-emoji-render"; import Emoji from "react-emoji-render";
import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel"; import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel";
import { pond } from "protobuf-ts/pond";
import { User } from "models";
// import { color } from "framer-motion"; // import { color } from "framer-motion";
// interface Props { // interface Props {
@ -21,7 +23,7 @@ export default function SignupCallback () {
const deviceAPI = useDeviceAPI(); const deviceAPI = useDeviceAPI();
const binAPI = useBinAPI(); const binAPI = useBinAPI();
const isMobile = useMobile() const isMobile = useMobile()
const [{ user }] = useGlobalState(); const [globalState, dispatch] = useGlobalState();
const whiteLabel = getWhitelabel() const whiteLabel = getWhitelabel()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -35,6 +37,7 @@ export default function SignupCallback () {
const [doneBins, setDoneBins] = useState(false) const [doneBins, setDoneBins] = useState(false)
const [doneTeams, setDoneTeams] = useState(false) const [doneTeams, setDoneTeams] = useState(false)
const [doneDevices, setDoneDevices] = useState(false) const [doneDevices, setDoneDevices] = useState(false)
const [user, setUser] = useState<User>(globalState.user);
useEffect(() => { useEffect(() => {
setLoading(true) setLoading(true)
@ -55,6 +58,124 @@ export default function SignupCallback () {
return loading === false && doneBins && doneTeams && doneDevices 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 = () => { const content = () => {
return ( return (
<DialogContent sx={{ margin: "auto", textAlign: "center" }}> <DialogContent sx={{ margin: "auto", textAlign: "center" }}>
@ -104,12 +225,17 @@ export default function SignupCallback () {
content: ( content: (
<> <>
<Typography variant="body2" > <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> </Typography>
<br/> <br/>
<Typography variant="body2" > <Typography variant="body2" >
If you are part of a team, you can select it here as well. If you are part of a team, you can select it here as well.
</Typography> </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", target: "#tour-user-menu",
@ -150,35 +276,104 @@ export default function SignupCallback () {
target: "#tour-dashboard", target: "#tour-dashboard",
placement: "right" placement: "right"
}) })
if (IsAdaptiveAgriculture()) steps.push({ steps.push({
title: "Bins", title: "Tasks",
content: ( content: (
<> <>
<Typography variant="body2" > <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> </Typography>
<br/> <br/>
<Typography> <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> </Typography>
</> </>
), ),
target: "#tour-bins", target: "#tour-tasks",
placement: "right", placement: "right"
disableBeacon: true,
}) })
//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; return steps;
}; };
const endTour = () => { const endTour = () => {
setIsTourRunning(false); setIsTourRunning(false);
// if (user) { if (user) {
// user.status.finishedBinIntro = moment().toJSON(); userAPI
// userAPI .updateUser(user.id(), user.protobuf())
// .updateUser(userID, user.protobuf()) .then(() => dispatch({ key: "user", value: user }))
// .then(() => dispatch({ key: "user", value: user })) .catch((err: any) => console.error(err));
// .catch((err: any) => error(err)); }
// }
}; };
const closeDialog = () => { 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 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 //if the map does exist filter the availability after claims to find matching address between them and use that
subtypeI2CMap?: Map<number, number[]> subtypeI2CMap?: Map<number, number[]>
isChainable?: boolean
} }
export interface Summary { export interface Summary {

View file

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

View file

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

View file

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

View file

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