finished the card to detect i2c components and select ones to add

This commit is contained in:
csawatzky 2025-06-27 14:19:55 -06:00
parent eebb2407f4
commit c8105ef624
4 changed files with 232 additions and 72 deletions

View file

@ -238,6 +238,7 @@ export default function ComponentSettings(props: Props) {
componentType: quack.ComponentType componentType: quack.ComponentType
): number[] => { ): number[] => {
let positions = availablePositions.get(addressType); let positions = availablePositions.get(addressType);
console.log(positions)
if (!positions) return []; if (!positions) return [];
switch (addressType) { switch (addressType) {
case quack.AddressType.ADDRESS_TYPE_I2C: case quack.AddressType.ADDRESS_TYPE_I2C:
@ -412,6 +413,7 @@ export default function ComponentSettings(props: Props) {
for (let i = 0; i < addressTypes.length; i++) { for (let i = 0; i < addressTypes.length; i++) {
let addressType = addressTypes[i]; let addressType = addressTypes[i];
let availablePositions = getAvailablePositions(addressType, type); let availablePositions = getAvailablePositions(addressType, type);
console.log(availablePositions)
availableMenuItemPositions.push( availableMenuItemPositions.push(
<MenuItem key={addressType} divider disabled> <MenuItem key={addressType} divider disabled>

View file

@ -1,4 +1,4 @@
import { Box, Card, DialogContent, FormControlLabel, Radio, RadioGroup, Step, StepLabel, Stepper } from "@mui/material"; import { Box, Button, Card, DialogActions, DialogContent, DialogTitle, FormControlLabel, Grid2, Radio, RadioGroup, Step, StepLabel, Stepper, Typography } from "@mui/material";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { getSubtypes, Subtype } from "pbHelpers/ComponentType"; import { getSubtypes, Subtype } from "pbHelpers/ComponentType";
import { pond, quack } from "protobuf-ts/pond"; import { pond, quack } from "protobuf-ts/pond";
@ -7,10 +7,16 @@ 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 ResponsiveDialog from "common/ResponsiveDialog";
import { useComponentAPI, useSnackbar } from "hooks";
interface Props { interface Props {
scannedComponents: pond.ComponentAddressMap scannedComponents: pond.ComponentAddressMap
device: Device device: Device
availablePositions: DeviceAvailabilityMap
availableOffsets: OffsetAvailabilityMap
refreshCallback: () => void
} }
interface CompStep { interface CompStep {
@ -19,12 +25,15 @@ interface CompStep {
} }
export default function DeviceScannedComponents(props: Props){ export default function DeviceScannedComponents(props: Props){
const {scannedComponents, device} = props const {scannedComponents, device, availablePositions, availableOffsets, refreshCallback} = props
const compAPI = useComponentAPI();
const [scannedI2C, setScannedI2C] = useState<pond.DetectI2C>() const [scannedI2C, setScannedI2C] = useState<pond.DetectI2C>()
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 [currentStep, setCurrentStep] = useState(0) const [currentStep, setCurrentStep] = useState(0)
const [settingsValid, setSettingsValid] = useState(false); const [settingsValid, setSettingsValid] = useState(false);
const [openDialog, setOpenDialog] = useState(false);
useEffect(()=>{ useEffect(()=>{
if (scannedComponents.i2c) setScannedI2C(scannedComponents.i2c) if (scannedComponents.i2c) setScannedI2C(scannedComponents.i2c)
@ -38,6 +47,7 @@ export default function DeviceScannedComponents(props: Props){
completed: false completed: false
}) })
}) })
setSteps(steps)
},[components]) },[components])
const stepper = () => { const stepper = () => {
@ -57,37 +67,136 @@ export default function DeviceScannedComponents(props: Props){
); );
}; };
const addComponentContent = () => { const addComponents = () => {
let c: pond.MultiComponentSettings = pond.MultiComponentSettings.create();
components.forEach(component => {
c.components.push(component.settings);
});
compAPI
.addMultiComponents(device.id(), c)
.then(resp => {
success("Components added to Device");
})
.catch(err => {
error("One or more component failed to add");
})
.finally(() => {
refreshCallback();
});
};
const addComponentDialog = () => {
return ( return (
<DialogContent> <ResponsiveDialog open={openDialog} onClose={()=>{setOpenDialog(false)}}>
{stepper()} <DialogTitle>Adding Components</DialogTitle>
<ComponentForm <DialogContent>
component={components[currentStep]} {stepper()}
canEdit <ComponentForm
device={device} component={components[currentStep]}
componentChanged={(component, isValid) => { canEdit
components[currentStep].settings = component.settings; device={device}
setSettingsValid(isValid); componentChanged={(component, isValid) => {
}} components[currentStep].settings = component.settings;
/> setSettingsValid(isValid);
</DialogContent> }}
/>
</DialogContent>
<DialogActions>
<Grid2 container direction="row" justifyContent="space-between">
<Grid2>
<Button
variant="contained"
color="primary"
onClick={() => {
close();
}}>
Cancel
</Button>
</Grid2>
<Grid2>
{currentStep !== 0 && (
<Button
sx={{marginX: 1}}
variant="contained"
color="primary"
onClick={() => {
setCurrentStep(currentStep - 1);
}}>
Back
</Button>
)}
{currentStep !== steps.length - 1 && (
<React.Fragment>
<Button
sx={{marginX: 1}}
variant="contained"
color="primary"
onClick={() => {
setCurrentStep(currentStep + 1);
}}>
Next
</Button>
</React.Fragment>
)}
<Button
sx={{marginX: 1}}
variant="contained"
color="primary"
onClick={() => {
//setConfirmationDialogOpen(true)
setOpenDialog(false)
addComponents();
}}>
Confirm
</Button>
</Grid2>
</Grid2>
</DialogActions>
</ResponsiveDialog>
); );
}; };
const componentSelection = (newComponent: Component, checked: boolean) => {
let cloneList = cloneDeep(components)
if(checked){
//add the component to the list
cloneList.push(newComponent)
}else{
//remove any components that match the component location: (type)-(addressType)-(address)
components.forEach((comp, i) => {
//check the components location against the one in the list
if(comp.locationString() === newComponent.locationString()){
cloneList.splice(i, 1)
}
})
}
setComponents(cloneList)
}
return ( return (
<Card> <React.Fragment>
{scannedI2C && <Card raised sx={{padding: 1}}>
<React.Fragment> {scannedI2C !== undefined &&
<Box>I2C Components Found</Box> <React.Fragment>
{scannedI2C?.settings?.foundAddresses.map((addr, index) => { <Box>
return ( <Typography sx={{fontWeight: 650}}>
<ScannedI2C key={index} decimalAddress={addr} deviceProduct={device.settings.product} updateComponentList={()=>{}} /> I2C Components Found
) </Typography>
})} </Box>
</React.Fragment> {scannedI2C?.settings?.foundAddresses.map((addr, index) => {
} return (
</Card> <ScannedI2C key={index} decimalAddress={addr} deviceProduct={device.settings.product} componentSelectionCallback={componentSelection} availablePositions={availablePositions.get(quack.AddressType.ADDRESS_TYPE_I2C) ?? []}/>
)
})}
</React.Fragment>
}
<Box display="flex" flexDirection="row-reverse" marginTop={2}>
<Button disabled={components.length === 0} variant="contained" color="primary" onClick={()=>{setOpenDialog(true)}}>Add Components</Button>
</Box>
</Card>
{addComponentDialog()}
</React.Fragment>
) )
} }

View file

@ -1,5 +1,7 @@
import { Box, Button, FormControlLabel, MenuItem, Radio, RadioGroup, TextField } from "@mui/material"; import { Box, Button, Checkbox, FormControlLabel, Grid2, MenuItem, Radio, RadioGroup, TextField, Tooltip } from "@mui/material";
import { Component } from "models";
import { getAddressTypes, getFriendlyName, getSubtypes, Subtype } from "pbHelpers/ComponentType"; import { getAddressTypes, getFriendlyName, getSubtypes, Subtype } from "pbHelpers/ComponentType";
import { ComponentAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability";
import { GetProductAvailability } from "products/DeviceProduct"; import { GetProductAvailability } from "products/DeviceProduct";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack"; import { quack } from "protobuf-ts/quack";
@ -8,29 +10,29 @@ import { useEffect, useState } from "react";
interface Props { interface Props {
decimalAddress: number decimalAddress: number
deviceProduct: pond.DeviceProduct deviceProduct: pond.DeviceProduct
updateComponentList: () => void componentSelectionCallback: (component: Component, checked: boolean) => void
availablePositions: DevicePositions
} }
export default function ScannedI2C(props: Props){ export default function ScannedI2C(props: Props){
const {decimalAddress, deviceProduct} = props const {decimalAddress, deviceProduct, componentSelectionCallback, availablePositions} = props
const [types, setTypes] = useState<quack.ComponentType[]>([]) const [types, setTypes] = useState<quack.ComponentType[]>([])
const [subtypeOptions, setSubtypeOptions] = useState<Array<Subtype>>([]) const [subtypeOptions, setSubtypeOptions] = useState<Array<Subtype>>([])
const [selectedType, setSelectedType] = useState<quack.ComponentType>(quack.ComponentType.COMPONENT_TYPE_INVALID) const [selectedType, setSelectedType] = useState<quack.ComponentType>(quack.ComponentType.COMPONENT_TYPE_INVALID)
const [selectedSubtype, setSelectedSubtype] = useState(0) const [selectedSubtype, setSelectedSubtype] = useState(0)
const [addressInUse, setAddressInUse] = useState(false)
useEffect(()=>{ useEffect(()=>{
//convert the address to hex
let hexAddress = parseInt(decimalAddress.toString(16))
hexAddress = 0x2a //hardcoding for testing
let types: quack.ComponentType[] = [] let types: quack.ComponentType[] = []
//find what component types use that address //find what component types use that address, this is different than the available positions passed in
//this is meant to get all possibilities according to product type and the given address, availablePositions is the restriction of what is available
let productAvailability = GetProductAvailability(deviceProduct) let productAvailability = GetProductAvailability(deviceProduct)
if(productAvailability){ if(productAvailability){
let i2cMap = productAvailability.get(quack.AddressType.ADDRESS_TYPE_I2C) let i2cMap = productAvailability.get(quack.AddressType.ADDRESS_TYPE_I2C)
if(i2cMap){ if(i2cMap){
i2cMap.forEach((val, key) => { i2cMap.forEach((val, key) => {
let addresses = val as number[] ?? [] let addresses = val as number[] ?? []
if(addresses.length > 0 && addresses.includes(hexAddress)){ if(addresses.length > 0 && addresses.includes(decimalAddress)){
types.push(key) types.push(key)
} }
}) })
@ -40,6 +42,7 @@ export default function ScannedI2C(props: Props){
if(types.length > 0) { if(types.length > 0) {
setSelectedType(types[0]) setSelectedType(types[0])
buildSubtypeOptions(types[0]) buildSubtypeOptions(types[0])
checkAddressAvailable(types[0])
} }
setTypes(types) setTypes(types)
},[decimalAddress, deviceProduct]) },[decimalAddress, deviceProduct])
@ -61,47 +64,86 @@ export default function ScannedI2C(props: Props){
setSubtypeOptions(validSub) setSubtypeOptions(validSub)
} }
const checkAddressAvailable = (compType: quack.ComponentType) => {
let i2cMap = availablePositions as ComponentAvailabilityMap
let compAddresses = i2cMap.get(compType) ?? []
if (!compAddresses.includes(decimalAddress)){
setAddressInUse(true)
}else{
setAddressInUse(false)
}
}
const componentSelected = (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => {
//build a component with given information and defaults for the rest
let component = Component.create()
component.settings.type = selectedType
component.settings.subtype = selectedSubtype
component.settings.addressType = quack.AddressType.ADDRESS_TYPE_I2C
component.settings.address = decimalAddress
component.settings.name = getFriendlyName(selectedType, selectedSubtype)
//return the component and the checked status to the parent
componentSelectionCallback(component, checked)
}
return ( return (
<Box> <Box>
Sensor 1 Sensor 1
<RadioGroup <Grid2 container direction="row" alignItems="center" justifyContent="space-between">
value={selectedType} <Grid2 size={3}>
onChange={(_, value) => { <RadioGroup
console.log(value) value={selectedType}
let v = parseInt(value) onChange={(_, value) => {
if (!isNaN(v)){ let v = parseInt(value)
buildSubtypeOptions(v) if (!isNaN(v)){
} buildSubtypeOptions(v)
}}> checkAddressAvailable(v)
{types.map((type, i) => { }
return ( }}>
<FormControlLabel {types.map((type, i) => {
key={i} return (
value={type} <FormControlLabel
control={<Radio />} key={i}
label={getFriendlyName(type)} value={type}
control={<Radio />}
label={getFriendlyName(type)}
/>
)
})}
</RadioGroup>
</Grid2>
<Grid2 size={8}>
{subtypeOptions.length > 0 &&
<TextField
value={selectedSubtype}
fullWidth
onChange={(event)=>{
let val = +event.target.value
setSelectedSubtype(val)
}}
select>
{subtypeOptions.map(s => (
<MenuItem value={s.key}>{s.friendlyName}</MenuItem>
))}
</TextField>
}
</Grid2>
<Grid2 size={1}>
<Tooltip
title="Add to list to be added to the device"
children={
<Checkbox
onChange={componentSelected}
disabled={addressInUse}
/> />
) }
})} />
</RadioGroup>
{subtypeOptions.length > 0 && </Grid2>
<TextField </Grid2>
value={selectedSubtype}
fullWidth
onChange={(event)=>{
let val = +event.target.value
console.log(event.target.value)
setSelectedSubtype(val)
}}
select>
{subtypeOptions.map(s => (
<MenuItem value={s.key}>{s.friendlyName}</MenuItem>
))}
</TextField>
}
<Button>Add Components</Button>
</Box> </Box>
) )
} }

View file

@ -167,7 +167,9 @@ export default function DevicePage() {
const loadPortScan = () => { const loadPortScan = () => {
deviceAPI.listFoundComponents(device.id()) deviceAPI.listFoundComponents(device.id())
.then(resp => { .then(resp => {
setScannedAddresses(resp.data.foundComponents ?? undefined) if (resp.data.foundComponents?.i2c){
setScannedAddresses(resp.data.foundComponents ?? undefined)
}
}) })
.catch(err => { .catch(err => {
console.log(err) console.log(err)
@ -427,7 +429,12 @@ export default function DevicePage() {
</Grid> </Grid>
{scannedAddresses && {scannedAddresses &&
<Grid size={{ xs: 6, sm: 6, lg: 4, xl: 4 }}> <Grid size={{ xs: 6, sm: 6, lg: 4, xl: 4 }}>
<DeviceScannedComponents scannedComponents={scannedAddresses} device={device}/> <DeviceScannedComponents
scannedComponents={scannedAddresses}
device={device}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
refreshCallback={loadDevice}/>
</Grid> </Grid>
} }
{diagnosticComponents.length > 0 && {diagnosticComponents.length > 0 &&