frontend/src/device/DeviceWizard.tsx
2025-11-03 16:41:26 -06:00

321 lines
11 KiB
TypeScript

import {
Avatar,
Box,
Card,
CardContent,
CardHeader,
List,
ListItem,
ListItemIcon,
ListItemText,
ListSubheader,
Menu,
MenuItem,
Theme,
Tooltip,
Typography
} from "@mui/material";
import ComponentSettings from "component/ComponentSettings";
import { Component, Device } from "models";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import {
DeviceAvailabilityMap,
FindAvailablePositions,
OffsetAvailabilityMap
} from "pbHelpers/DeviceAvailability";
import { GetDeviceProductIcon, GetDeviceProductLabel } from "products/DeviceProduct";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useComponentAPI, useDeviceAPI, useGlobalState, useSnackbar } from "providers";
import React, { useState } from "react";
import DeviceSVG, { PortInformation } from "./deviceSVG";
import { makeStyles } from "@mui/styles";
interface Props {
device: Device;
//permissions: pond.Permission[];
components: Component[];
refreshCallback: () => void;
scanInProgress?: boolean
}
const useStyles = makeStyles((theme: Theme) => {
return ({
card: {
// position: "relative",
// display: "flex",
// height: "100%",
// flexDirection: "column",
// overflow: "visible"
minHeight: "200px"
},
cardHeader: {
padding: theme.spacing(1),
paddingLeft: theme.spacing(2)
},
cardContent: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
padding: theme.spacing(1),
paddingTop: 0
},
avatarIcon: {
width: theme.spacing(3),
height: theme.spacing(3)
}
})
});
export default function DeviceWizard(props: Props) {
const { device, components, refreshCallback, scanInProgress } = props;
const [{as}] = useGlobalState();
const [componentDialogOpen, setComponentDialogOpen] = useState(false);
const [availableOffsets, setAvailableOffsets] = useState<OffsetAvailabilityMap>(new Map());
const [availablePositions, setAvailablePositions] = useState<DeviceAvailabilityMap>(new Map());
const compAPI = useComponentAPI();
const [menuAnchor, setMenuAnchor] = useState<null | Element>(null);
const [portComponents, setPortComponents] = useState<Component[]>([]);
const [componentToUpdate, setComponentToUpdate] = useState<Component | undefined>();
const { error, success } = useSnackbar();
const [ComponentSettingsMode, setComponentSettingsMode] = useState<"add" | "remove" | "update">(
"add"
);
const classes = useStyles();
const [selectedPort, setSelectedPort] = useState<PortInformation>();
const deviceAPI = useDeviceAPI()
const { openSnack } = useSnackbar()
const [buttonClicked, setButtonClicked] = useState(false);
//const [addressType, setAddressType] = useState<quack.AddressType>(quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY);
const wizardMenu = () => {
return (
<Menu
id="groupMenu"
anchorEl={menuAnchor}
open={Boolean(menuAnchor)}
onClose={() => setMenuAnchor(null)}
keepMounted
disableAutoFocusItem>
<MenuItem
key={"add"}
onClick={() => {
setComponentSettingsMode("add");
setComponentToUpdate(undefined);
setComponentDialogOpen(true);
}}>
Add Component
</MenuItem>
{/* <MenuItem key={"list_error"} onClick={() => {
}}>
List Errors
</MenuItem> */}
{selectedPort && selectedPort.autoDetectable && (
<Tooltip title="Used to Detect Grain Cables">
<MenuItem
key={"auto"}
onClick={() => {
setAutoDetect();
}}>
Auto Detect
</MenuItem>
</Tooltip>
)}
{selectedPort && selectedPort.addressType === quack.AddressType.ADDRESS_TYPE_I2C && device.featureSupported("detectI2C") && (
<Tooltip title="Used to tell the Device to scan its I2C port for any plugged in sensors">
<MenuItem
key={"scanI2C"}
disabled={scanInProgress || buttonClicked}
onClick={() => {
setButtonClicked(true)
deviceAPI.detectI2C(device.id())
.then(resp => {
openSnack("Device will scan port on next check-in")
}).catch(err => {
openSnack("Command failed to send to device")
})
}}>
Scan Port
</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>
{portComponents.map(comp => (
<ListItem
key={comp.key()}
onClick={() => {
setComponentToUpdate(comp);
setComponentSettingsMode("update");
setComponentDialogOpen(true);
}}>
<ListItemIcon>
<Avatar
className={classes.avatarIcon}
alt={comp.subTypeName() + "Icon"}
src={GetComponentIcon(comp.type(), comp.subType())}
/>
</ListItemIcon>
<ListItemText>{comp.name()}</ListItemText>
</ListItem>
))}
</List>
)}
</Menu>
);
};
/**
* sets an autodetect component on the port that was selected
* TODO: when the auto detect gets expanded out of being a grain cable component this will have to be re-factored to use the new component type since it is hard coded
*/
const setAutoDetect = () => {
if (selectedPort) {
let componentSettings = pond.ComponentSettings.create();
componentSettings.type = quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE; // use the grain cable component
componentSettings.subtype = quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_DIAG; //use the port diagnostic subtype
componentSettings.addressType = quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY;
componentSettings.address = selectedPort.address; // the address of the selected port
componentSettings.measurementPeriodMs = 600000; //set the measurement period to 10 mins
componentSettings.reportPeriodMs = 600000;
componentSettings.name = "Port " + selectedPort.label + " Auto Detect";
compAPI
.add(device.id(), componentSettings, as)
.then(resp => {
success("Added Auto Detect Component to Port");
})
.catch(err => {
error("Failed to Add Auto Detect Component to Port");
})
.finally(() => {
refreshCallback();
});
}
};
/**
* function that restricts the pin availabilty in the availability map to the pin of the port that was selected
* @param port port that was selected
* @param availability availability map of the device
* @returns modified availability map
*/
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 }]);
//since we now have a component type that can be pins or i2c need to remove i2c options if they selected a pin port
currentAvailability.set(quack.AddressType.ADDRESS_TYPE_I2C, new Map());
break;
case quack.AddressType.ADDRESS_TYPE_I2C:
//since we now have a component type that can be pins or i2c need to remove pin options if they selected the i2c port
currentAvailability.set(quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, []);
break;
}
return currentAvailability;
};
const findPortComponents = (components: Component[], port: PortInformation) => {
let pc: Component[] = [];
switch (port.addressType) {
case quack.AddressType.ADDRESS_TYPE_I2C:
components.forEach(comp => {
if (comp.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C) {
pc.push(comp);
}
});
break;
case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
components.forEach(comp => {
if (comp.settings.address === port.address) {
pc.push(comp);
}
});
}
return pc;
};
const setupCard = () => {
const deviceIcon = GetDeviceProductIcon(device)
return (
<Card className={classes.card} raised>
<CardHeader
avatar={
<Avatar
variant="square"
src={deviceIcon}
className={classes.avatarIcon}
alt={"devIcon"}
/>
}
title={GetDeviceProductLabel(device.settings.product) + " - ID:" + device.id()}
className={classes.cardHeader}
/>
<CardContent style={{ paddingTop: 0 }}>
<Typography variant="caption">
Select a port on the device below to modify components
</Typography>
<Box paddingLeft={"15%"} paddingRight={"15%"} paddingTop={"20px"}>
<DeviceSVG
device={device}
openMenu={(e, port) => {
let available = FindAvailablePositions(components, device.settings.product);
setAvailableOffsets(available.offsetAvailability);
setAvailablePositions(adjustAvailablePositions(port, available.availability));
//setAddressType(port.addressType);
setSelectedPort(port);
setPortComponents(findPortComponents(components, port));
setMenuAnchor(e.currentTarget);
}}
/>
</Box>
</CardContent>
</Card>
);
};
return (
<React.Fragment>
{setupCard()}
<ComponentSettings
mode={ComponentSettingsMode}
component={componentToUpdate}
device={device}
isDialogOpen={componentDialogOpen}
closeDialogCallback={() => setComponentDialogOpen(false)}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
refreshCallback={refreshCallback}
canEdit
addressTypeRestriction={selectedPort && selectedPort.addressType}
/>
{wizardMenu()}
</React.Fragment>
);
}