Merge branch 'staging_environment' into field_dashboard

This commit is contained in:
csawatzky 2025-08-20 10:55:26 -06:00
commit 1028f0639b
38 changed files with 992 additions and 231 deletions

4
package-lock.json generated
View file

@ -42,7 +42,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"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#field_dashboard",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
@ -10911,7 +10911,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#0cdf4d5e3038ef13318da0841886a7c9f0926f8d",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#37e19bde4218b369e028606c009729112668ccc6",
"dependencies": {
"protobufjs": "^6.8.8"
}

View file

@ -50,7 +50,8 @@ function App() {
const skipCallbacks = [
"/johndeere",
"/cnhi"
"/cnhi",
"/libracart"
]
return (

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -24,7 +24,7 @@ import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
import ObjectTeams from "teams/ObjectTeams";
// import BinDuplication from "./BinDuplication";
import BinDuplication from "./BinDuplication";
import BinsIcon from "products/Bindapt/BinsIcon";
import HelpIcon from "@mui/icons-material/Help";
import BinSensors from "./BinSensors";
@ -269,14 +269,14 @@ export default function BinActions(props: Props) {
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback}
/>
{/* <BinDuplication
<BinDuplication
open={openState.duplication}
closeDialog={() => {
setOpenState({ ...openState, duplication: false });
}}
bin={bin}
refreshCallback={refreshCallback}
/> */}
/>
<RemoveSelfFromObject
scope={binScope(key)}
label={label}

View file

@ -506,9 +506,6 @@ export default function BinCard(props: Props) {
+
</IconButton>
)}
{/* <Box position="absolute" top={4} right={4}>
<BinModeDot mode={bin.settings.mode} />
</Box> */}
<Box padding={1}>
<Typography
align="left"

View file

@ -0,0 +1,72 @@
import {
Button,
DialogActions,
DialogContent,
DialogTitle,
TextField,
Typography
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Bin } from "models";
import { useBinAPI, useSnackbar } from "providers";
import { useEffect, useState } from "react";
interface Props {
open: boolean;
bin: Bin;
closeDialog: () => void;
refreshCallback: () => void;
}
export default function BinDuplication(props: Props) {
const { open, bin, closeDialog, refreshCallback } = props;
const [binDupName, setBinDupName] = useState("(copy of) " + bin.name());
const binAPI = useBinAPI();
const { openSnack } = useSnackbar();
useEffect(() => {
setBinDupName("(copy of) " + bin.name());
}, [bin]);
const duplicate = () => {
let settings = bin.settings;
settings.name = binDupName;
binAPI
.addBin(settings)
.then(resp => {
openSnack("Successfully duplicated bin");
})
.catch(err => {
openSnack("Failed to duplicate bin");
});
refreshCallback();
closeDialog();
};
return (
<ResponsiveDialog open={open} onClose={closeDialog}>
<DialogTitle>Create Duplicate Bin</DialogTitle>
<DialogContent>
<Typography>
This will create a new bin with the same settings as {bin.name()}.
</Typography>
<TextField
id={"duplicateName"}
fullWidth
margin="normal"
label="Name of Copy"
value={binDupName}
onChange={e => setBinDupName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} style={{ color: "red" }}>
Cancel
</Button>
<Button onClick={duplicate} color="primary">
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -58,7 +58,7 @@ import { Bin } from "models";
import moment from "moment";
import { GetBinShapeDescribers } from "pbHelpers/Bin";
import { pond } from "protobuf-ts/pond";
import { useBinAPI, useBinYardAPI, useGlobalState } from "providers";
import { useBinAPI, useBinYardAPI, useGlobalState, useLibraCartProxyAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
// import { useHistory } from "react-router";
import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils";
@ -176,7 +176,7 @@ export default function BinSettings(props: Props) {
const grainOptions = GrainOptions();
const grainUseOptions = GetGrainUseOptions();
const [inputCapacity, setInputCapacity] = useState<string>("");
const [{ as }] = useGlobalState();
const [{ user, as }] = useGlobalState();
const [grainDiff, setGrainDiff] = useState(0);
const [isCustomInventory, setIsCustomInventory] = useState<boolean>(false);
const [grainUpdate, setGrainUpdate] = useState(false);
@ -197,10 +197,13 @@ export default function BinSettings(props: Props) {
const [inventoryControl, setInventoryControl] = useState<pond.BinInventoryControl>(
pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN
);
const [storageType, setStorageType] = useState<pond.BinStorage>(
pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
);
//libracart stuff
const libracartAPI = useLibraCartProxyAPI()
const [lcDestination, setlcDestination] = useState<Option | null>()
const [lcDestinationOptions, setlcDestinationOptions] = useState<Option[]>([])
useEffect(() => {
if (open) {
@ -365,9 +368,33 @@ export default function BinSettings(props: Props) {
}
}, [binYardAPI, form.yardKey, userID, as, props.binYards]);
const loadLibraCartDestinations = useCallback(()=>{
if(inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART){
//load the destinations for the user/team they are viewing as
let options: Option[] = []
libracartAPI.listDestinations(0,0,undefined, as)
.then(resp=>{
//set the options for the search select
resp.data.destinations.forEach(d => {
let newOp: Option = {
label: d.name,
value: d.id
}
options.push(newOp)
//set the current option based on the key in the form
if (d.id === form.libracartDestinationKey){
setlcDestination(newOp)
}
})
setlcDestinationOptions(options)
}).catch(()=>{})
}
},[libracartAPI, inventoryControl, as, form.libracartDestinationKey])
useEffect(() => {
loadBinYards();
}, [loadBinYards]);
loadLibraCartDestinations();
}, [loadBinYards, loadLibraCartDestinations]);
useEffect(() => {
if (bin?.settings.specs?.bushelCapacity) {
@ -713,6 +740,8 @@ export default function BinSettings(props: Props) {
return "Auto (cable)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR:
return "Hybrid (lidar)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART:
return "Auto (LibraCart)"
default:
return "Manual"
}
@ -860,7 +889,7 @@ export default function BinSettings(props: Props) {
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC}
control={<Radio />}
label={"Auto"}
label={"Auto (Cable)"}
/>
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR}
@ -872,6 +901,13 @@ export default function BinSettings(props: Props) {
control={<Radio />}
label={"Hybrid (Lidar)"}
/>
{user.hasFeature("libra-cart") &&
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART}
control={<Radio />}
label={"Auto (LibraCart)"}
/>
}
</RadioGroup>
</AccordionDetails>
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC &&
@ -912,6 +948,25 @@ export default function BinSettings(props: Props) {
/>
</Box>
}
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART &&
<Box width="100%" padding={2}>
<SearchSelect
label="LibraCart Destination"
selected={lcDestination}
changeSelection={option => {
let newForm = form;
newForm.libracartDestinationKey = option?.value;
setForm(newForm);
setlcDestination(option ? option : null);
}}
disabled={!canEdit}
options={lcDestinationOptions}
/>
<Typography variant="caption">
The linked bins inventory will be adjusted When the LibraCart Destination data is synced every 6 hours
</Typography>
</Box>
}
</Accordion>
{empty ? (
<Box marginTop={3} display="flex" flexDirection="column" alignItems="center">

View file

@ -30,7 +30,6 @@ const useStyles = makeStyles((_theme) => {
position: "relative",
minHeight: "233px",
height: "auto !important",
width: "184px",
padding: 2
},
hidden: {
@ -113,7 +112,7 @@ export default function BinsList(props: Props) {
return (
<Grid container direction="row">
{bins.map((b, i) =>
isMobile ? (
(
<Grid
size={{
xs: 6,
@ -133,21 +132,6 @@ export default function BinsList(props: Props) {
valDisplay={valDisplay}
/>
</Grid>
) : (
<Box
key={i}
style={{ width: "184px" }}
className={classes.gridListTile}
onClick={() => {
!duplicate && goToBin(i)
}}>
<BinCardV2
bin={b}
duplicateBin={duplicateBin}
dupHovered={setDuplicate}
valDisplay={valDisplay}
/>
</Box>
)
)}
</Grid>

View file

@ -0,0 +1,67 @@
import { useTheme } from "@mui/material";
import MaterialChartTooltip from "charts/MaterialChartTooltip";
import moment from "moment";
import { Area, AreaChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
export interface LevelAreaData {
timestamp: number;
value: number;
}
interface Props {
data: LevelAreaData[]
fill: string
customHeight?: string | number
}
export default function BinLevelAreaGraph(props: Props) {
const { data, customHeight, fill } = props
const theme = useTheme();
const now = moment();
return (
<ResponsiveContainer width={"100%"} height={customHeight ?? 350}>
<AreaChart data={data}>
<XAxis
allowDataOverflow
dataKey="timestamp"
domain={["dataMin", "dataMax"]}
name="Time"
tickFormatter={timestamp => {
let t = moment(timestamp);
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
}}
scale="time"
type="number"
tick={{ fill: theme.palette.text.primary }}
stroke={theme.palette.divider}
interval="preserveStartEnd"
/>
<YAxis />
<Area
dataKey={"value"}
fill={fill}
stroke={fill}
strokeWidth={3}
type="monotone"
/>
<Tooltip
animationEasing="ease-out"
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
labelFormatter={timestamp => moment(timestamp).format("lll")}
content={(props: TooltipProps<any, any>) => {
return (
<MaterialChartTooltip
{...props}
valueFormatter={value => {
return value + " Bushels";
}}
/>
);
}}
/>
</AreaChart>
</ResponsiveContainer>
)
}

View file

@ -1,21 +1,18 @@
import {
Box,
Card,
CircularProgress,
FormControlLabel,
Grid,
Switch,
Typography
} from "@mui/material";
import { blue, grey, red, yellow, orange } from "@mui/material/colors";
import BarGraph, { BarData, RefArea } from "charts/BarGraph";
import BarGraph, { BarData } from "charts/BarGraph";
import { Bin } from "models";
import moment, { Moment } from "moment";
import { pond, quack } from "protobuf-ts/pond";
import { pond } from "protobuf-ts/pond";
import { useBinAPI, useGlobalState } from "providers";
import { useCallback, useEffect, useState } from "react";
import { Legend } from "recharts";
import { getGrainUnit } from "utils";
import BinLevelAreaGraph from "./BinLevelAreaGraph";
interface Props {
binLoading: boolean;
@ -27,12 +24,16 @@ interface Props {
customHeight?: number | string;
}
interface InventoryAt {
timestamp: number;
value: number;
}
export default function BinLevelOverTime(props: Props) {
const { bin, fertilizerBin, colour, startDate, endDate, customHeight } = props;
const binAPI = useBinAPI();
const [{as}] = useGlobalState();
const [data, setData] = useState<BarData[]>([]);
const [modeAreas, setModeAreas] = useState<RefArea[]>([]);
const [inventoryData, setInventoryData] = useState<InventoryAt[]>([]);
const [dataLoading, setDataLoading] = useState(false);
const [capacity, setCapacity] = useState<number | undefined>();
@ -76,33 +77,15 @@ export default function BinLevelOverTime(props: Props) {
.listHistoryBetween(bin.key(), 500, startDate.toISOString(), endDate.toISOString())
.then(resp => {
let data: BarData[] = [];
let modeAreas: RefArea[] = [];
let lastBushels = -1;
//values for ref areas
let x1 = 0;
let x2 = 0;
let y1 = -50;
let y2 = -200;
let currentMode: pond.BinMode;
let currentMode: pond.BinMode | undefined = undefined
let modeData: BarData[] = []
resp.data.history.forEach(hist => {
//build the data for the inventory bar graph
if (hist.settings?.inventory) {
let settings = pond.BinSettings.fromObject(hist.settings);
let bushels = hist.settings.inventory.grainBushels ?? 0;
if (bushels !== lastBushels || currentMode !== settings.mode) {
x1 = moment(hist.timestamp).valueOf();
x2 = moment(hist.timestamp).valueOf();
modeAreas.push({
x1: x1,
x2: x2,
y1: cap ? cap * -0.1 : y1,
y2: cap ? cap * -0.2 : y2,
fill: getFill(settings.mode)
});
currentMode = settings.mode;
let newData: BarData = {
if (bushels !== lastBushels) {
let newData: InventoryAt = {
timestamp: moment(hist.timestamp).valueOf(),
value: fertilizerBin
? Math.round(bushels * 35.239)
@ -114,10 +97,21 @@ export default function BinLevelOverTime(props: Props) {
lastBushels = bushels;
}
}
//build the data for the mode change bar graph
let histBin = Bin.create()
histBin.settings = hist.settings ?? pond.BinSettings.create()
if(hist.settings && currentMode !== hist.settings.mode){
currentMode = hist.settings.mode
modeData.push({
timestamp: moment(hist.timestamp).valueOf(),
value: 1,
fill: getFill(currentMode)
})
}
});
if (data.length === 0) {
let bushels = bin.settings.inventory?.grainBushels ?? 0;
let currentTime = moment().valueOf();
if (data.length === 0) {
let bushels = bin.bushels();
data.push({
value: fertilizerBin
? Math.round(bushels * 35.239)
@ -126,17 +120,16 @@ export default function BinLevelOverTime(props: Props) {
: bushels,
timestamp: currentTime
});
modeAreas.push({
x1: currentTime,
x2: currentTime,
y1: cap ? cap * -0.1 : y1,
y2: cap ? cap * -0.2 : y2,
fill: getFill(bin.settings.mode)
});
}
setData(data);
setModeAreas(modeAreas);
if(modeData.length === 0){
modeData.push({
timestamp: currentTime,
value: 1,
fill: getFill(bin.settings.mode)
})
}
setInventoryData(data);
setModeData(modeData)
})
.finally(() => {
setDataLoading(false);
@ -180,7 +173,7 @@ export default function BinLevelOverTime(props: Props) {
});
});
if (autoBarData.length === 0) {
let bushels = bin.settings.inventory?.grainBushels ?? 0;
let bushels = bin.bushels();
let currentTime = moment().valueOf();
autoBarData.push({
value: fertilizerBin
@ -191,7 +184,7 @@ export default function BinLevelOverTime(props: Props) {
timestamp: currentTime
});
}
setData(autoBarData);
setInventoryData(autoBarData);
})
.catch(err => {})
.finally(() => {
@ -220,6 +213,13 @@ export default function BinLevelOverTime(props: Props) {
})
}
})
if(modeData.length === 0){
modeData.push({
timestamp: moment().valueOf(),
value: 1,
fill: getFill(bin.settings.mode)
})
}
setModeData(modeData)
})
.catch(err => {
@ -238,7 +238,8 @@ export default function BinLevelOverTime(props: Props) {
let control = bin.inventoryControl()
//for automatic lidar and cables get the data from measurements as an object measurement
if(control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART
){
//load the measurement data for the bar graph
loadMeasurementData()
@ -270,23 +271,30 @@ export default function BinLevelOverTime(props: Props) {
};
const inventoryChart = () => {
if(inventoryData.length > 10){
return (
<BinLevelAreaGraph
data={inventoryData}
fill={colour}
/>
)
}else{
return (
<BarGraph
customHeight={customHeight}
data={data}
refAreas={modeAreas}
data={inventoryData}
barColour={colour}
yMax={capacity}
yMin={0}
graphLegend={modeAreas.length > 0 ? legend() : undefined}
labelColour="white"
labels
useGradient
/>
);
}
};
const autoBinModeChart = () => {
const binModeChart = () => {
return (
<BarGraph
data={modeData}
@ -305,9 +313,7 @@ export default function BinLevelOverTime(props: Props) {
Grain Levels Over Time
</Typography>
{dataLoading ? <CircularProgress /> : inventoryChart()}
{/* when using auto will need to make a new chart just for the bin modes since the auto inventory comes from another place */}
{(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC) && autoBinModeChart()}
{binModeChart()}
</Card>
);
}

View file

@ -28,6 +28,13 @@ export default function PeriodSelect(props: Props) {
}
}, [unit, prevUnit, onChange, value]);
useEffect(()=>{
let initialUnit: TimeUnit = bestUnit(initialMs);
let initialValue = milliToX(initialMs, initialUnit).toString();
setValue(initialValue)
setUnit(initialUnit)
},[initialMs])
const changeValue = (event: any) => {
let value = event.target.value;
setValue(value);

View file

@ -169,6 +169,7 @@ export default function ComponentSettings(props: Props) {
const [removeDialogOpen, setRemoveDialogOpen] = useState(false);
const [formComponent, setFormComponent] = useState<Component>(new Component());
const [cableID, setCableID] = useState(0);
const [expansionLine, setExpansionLine] = useState(0);
const [overlayIndex, setOverlayIndex] = useState(0);
const [overlayColourDialog, setOverlayCoulourDialog] = useState(false);
const [tabVal, setTabVal] = useState(0);
@ -223,6 +224,9 @@ export default function ComponentSettings(props: Props) {
const isFormValid = () => {
//let { component } = form;
let type = or(formComponent.settings.type, "");
if(supportsExpansion() && expansionLine === 0){
return false
}
return isComponentTypeValid(type) && isAddressValid(formComponent);
};
@ -306,6 +310,7 @@ export default function ComponentSettings(props: Props) {
const component = formComponent;
//component.settings.calibrationCoefficient = Number(form.coefficient);
if (cableID > 0) component.settings.addressType = cableID + 8;
component.settings.expansionLine = expansionLine
componentAPI
.add(device.id(), component.settings, as)
.then((_response: any) => {
@ -373,10 +378,10 @@ export default function ComponentSettings(props: Props) {
let ext = extension(type, subtype);
formComponent.settings.type = type;
formComponent.settings.subtype = subtype;
formComponent.settings.addressType = getAddressTypes(
formComponent.settings.addressType = or(addressTypeRestriction, getAddressTypes(
formComponent.settings.type,
formComponent.settings.subtype
)[0];
)[0]);
formComponent.settings.address = or(
getAvailablePositions(formComponent.settings.addressType, formComponent.settings.type)[0],
Component.create().settings.address
@ -399,11 +404,19 @@ export default function ComponentSettings(props: Props) {
};
const handlePositionChanged = (event: any) => {
setCableID(0)
setExpansionLine(0)
let f = cloneDeep(formComponent);
f.settings.address = event.target.value
.toString()
.split(":")
.pop();
//split the string
let split: string[] = event.target.value.toString().split(":")
// pop the addres off of the end of the array
f.settings.address = parseInt(split.pop() ?? "0")
// pop the address type out of the array next
f.settings.addressType = parseInt(split.pop() ?? "0")
// f.settings.address = event.target.value
// .toString()
// .split(":")
// .pop();
setFormComponent(f);
};
@ -490,6 +503,21 @@ export default function ComponentSettings(props: Props) {
);
};
const isCableComponent = () => {
return (formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ||
formComponent.settings.type ===
quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE ||
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE ||
(formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE &&
formComponent.subType() ===
quack.CapacitorCableSubtype.CAPACITOR_CABLE_SUBTYPE_FROG))
}
const supportsExpansion = () => {
return (formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE &&
formComponent.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C)
}
const addForm = () => {
return (
<DialogContent>
@ -543,14 +571,8 @@ export default function ComponentSettings(props: Props) {
formComponent.settings.subtype
)}
</TextField>
{(formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ||
formComponent.settings.type ===
quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE ||
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE ||
(formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE &&
formComponent.subType() ===
quack.CapacitorCableSubtype.CAPACITOR_CABLE_SUBTYPE_FROG)) &&
setComponentAddrType()}
{supportsExpansion() && setExpLine()}
{(isCableComponent() && !supportsExpansion())&& setComponentAddrType()}
</React.Fragment>
) : activeStep === 1 ? (
<ComponentForm
@ -582,6 +604,28 @@ export default function ComponentSettings(props: Props) {
</DialogContent>
);
};
const setExpLine = () => {
return (
<TextField
label="Expansion Line"
type="number"
margin="normal"
error={expansionLine < 1 || expansionLine > 12}
fullWidth
variant="outlined"
value={expansionLine}
helperText="Enter line the cable is connected to on your expander"
onChange={e => {
let number = parseInt(e.target.value);
if (number < 0 || isNaN(number)) number = 0;
if (number > 12) number = 12;
setExpansionLine(number);
}}
/>
);
};
const setComponentAddrType = () => {
return (
<TextField

View file

@ -198,16 +198,23 @@ export default function DeviceWizard(props: Props) {
/**
* function that restricts the pin availabilty in the availability map to the pin of the port that was selected
* does nothing for I2C ports as the restrictions for that are handled by the component type
* @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;
};

View file

@ -24,11 +24,14 @@ interface CompStep {
completed?: boolean;
}
let i2cBlacklistAddresses: number[] = [0x70]
export default function DeviceScannedComponents(props: Props){
const {scannedComponents, device, availablePositions, availableOffsets, refreshCallback} = props
const compAPI = useComponentAPI();
const deviceAPI = useDeviceAPI()
const [scannedI2C, setScannedI2C] = useState<pond.DetectI2C>()
const [scannedI2C, setScannedI2C] = useState<pond.DetectI2C>() //the unmodified scan of addresses
const [validCompAddresses, setValidComponentAddresses] = useState<quack.AddressData[]>([]) //the filtered array of address data
const [components, setComponents] = useState<Component[]>([])
const [steps, setSteps] = useState<CompStep[]>([]);
const { error, success } = useSnackbar();
@ -59,6 +62,19 @@ export default function DeviceScannedComponents(props: Props){
setSteps(steps)
},[components])
useEffect(()=>{
let valid: quack.AddressData[] = []
//filter the address data
scannedI2C?.settings?.foundAddresses.forEach(addrData => {
if(!i2cBlacklistAddresses.includes(addrData.address)){
if(!(addrData.address === 0x71 && addrData.expansionLine === undefined)){
valid.push(addrData)
}
}
})
setValidComponentAddresses(valid)
},[scannedI2C])
const stepper = () => {
return (
<Stepper nonLinear activeStep={currentStep} style={{ width: "100%" }}>
@ -205,9 +221,9 @@ export default function DeviceScannedComponents(props: Props){
</Typography>
<Button variant="contained" color="primary" onClick={()=>{removeScan(scannedI2C.key)}}>Clear Scan</Button>
</Box>
{scannedI2C.settings?.foundAddresses && scannedI2C.settings.foundAddresses.length > 0 ? scannedI2C?.settings?.foundAddresses.map((addr, index) => {
{validCompAddresses.length > 0 ? validCompAddresses.map((addr, index) => {
return (
<ScannedI2C key={index} sensorNum={index+1} decimalAddress={addr} deviceProduct={device.settings.product} componentSelectionCallback={componentSelection} availablePositions={availablePositions.get(quack.AddressType.ADDRESS_TYPE_I2C) ?? []}/>
<ScannedI2C key={index} sensorNum={index+1} addressData={addr} deviceProduct={device.settings.product} componentSelectionCallback={componentSelection} availablePositions={availablePositions.get(quack.AddressType.ADDRESS_TYPE_I2C) ?? []}/>
)
}) :
<Box sx={{

View file

@ -8,7 +8,7 @@ import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react";
interface Props {
decimalAddress: number
addressData: quack.AddressData
deviceProduct: pond.DeviceProduct
componentSelectionCallback: (component: Component[], checked: boolean) => void
availablePositions: DevicePositions
@ -16,7 +16,7 @@ interface Props {
}
export default function ScannedI2C(props: Props){
const {decimalAddress, deviceProduct, componentSelectionCallback, availablePositions, sensorNum} = props
const {addressData, deviceProduct, componentSelectionCallback, availablePositions, sensorNum} = props
const [types, setTypes] = useState<quack.ComponentType[]>([])
const [subtypeOptions, setSubtypeOptions] = useState<Array<Subtype>>([])
const [selectedPrimaryType, setSelectedPrimaryType] = useState<quack.ComponentType>(quack.ComponentType.COMPONENT_TYPE_INVALID)
@ -34,7 +34,7 @@ export default function ScannedI2C(props: Props){
if(i2cMap){
i2cMap.forEach((val, key) => {
let addresses = val as number[] ?? []
if(addresses.length > 0 && addresses.includes(decimalAddress)){
if(addresses.length > 0 && addresses.includes(addressData.address)){
types.push(key)
}
})
@ -47,7 +47,7 @@ export default function ScannedI2C(props: Props){
checkAddressAvailable(types[0])
}
setTypes(types)
},[decimalAddress, deviceProduct])
},[addressData, deviceProduct])
const buildSubtypeOptions = (compType: quack.ComponentType) => {
let subtypes = getSubtypes(compType)
@ -69,7 +69,7 @@ export default function ScannedI2C(props: Props){
const checkAddressAvailable = (compType: quack.ComponentType) => {
let i2cMap = availablePositions as ComponentAvailabilityMap
let compAddresses = i2cMap.get(compType) ?? []
if (!compAddresses.includes(decimalAddress)){
if (!compAddresses.includes(addressData.address)){
setAddressInUse(true)
}else{
setAddressInUse(false)
@ -86,7 +86,10 @@ export default function ScannedI2C(props: Props){
primaryComponent.settings.type = selectedPrimaryType
primaryComponent.settings.subtype = selectedPrimarySubtype
primaryComponent.settings.addressType = quack.AddressType.ADDRESS_TYPE_I2C
primaryComponent.settings.address = decimalAddress
primaryComponent.settings.address = addressData.address
primaryComponent.settings.expansionLine = addressData.expansionLine
primaryComponent.settings.muxLine = addressData.muxLine
primaryComponent.settings.name = getFriendlyName(selectedPrimaryType, selectedPrimarySubtype)
toAdd.push(primaryComponent)
@ -161,9 +164,6 @@ export default function ScannedI2C(props: Props){
</Grid2>
</Grid2>
{
}
</React.Fragment>
:
<Typography>Sensor Not Supported By Product</Typography>

View file

@ -0,0 +1,220 @@
import { Help } from "@mui/icons-material";
import {
Avatar,
Box,
Button,
DialogActions,
DialogContent,
DialogTitle,
Grid2 as Grid,
MenuItem,
TextField,
Tooltip,
Typography
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { teamScope, User } from "models";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useSnackbar, useUserAPI } from "providers";
import { useLibraCartProxyAPI } from "providers/pond/libracartProxyAPI";
import React, { useEffect, useState } from "react";
import TeamSearch from "teams/TeamSearch";
export default function LibraCartAccess() {
const [openDialog, setOpenDialog] = useState(false);
const [teamKey, setTeamKey] = useState("");
const [teamUsers, setTeamUsers] = useState<User[]>([]);
const [primaryUser, setPrimaryUser] = useState("");
const [libracartUsername, setLibraCartUserName] = useState("");
const [libracartCode, setLibraCartCode] = useState<string | null>("");
const [libracartAuth, setLibraCartAuth] = useState<string | null>("");
//const [activeStep, setActiveStep] = useState<number>(0);
const userAPI = useUserAPI();
const libracartAPI = useLibraCartProxyAPI();
//const [dataOps, setDataOps] = useState<pond.DataOption[]>([]);
const { openSnack } = useSnackbar();
const [{ as }] = useGlobalState();
const submitNewOrganization = () => {
if (libracartCode && libracartAuth) {
libracartAPI
.addAccount(teamKey, primaryUser, libracartCode, libracartAuth, libracartUsername)
.then(resp => {
openSnack("Added New Libra Cart Account Link");
})
.catch(err => {
openSnack("Failed to Add New Libra Cart Account Link");
});
setOpenDialog(false);
}
};
useEffect(() => {
if (teamKey !== "") {
userAPI.listObjectUsers(teamScope(teamKey)).then(resp => {
setTeamUsers(resp.data.users.map((u: pond.User) => User.any(u)));
});
}
}, [teamKey, userAPI]);
// useEffect(() => {
// let code = localStorage.getItem("state");
// if (code) {
// setLibraCartCode(code);
// setOpenDialog(true);
// }
// }, [searchParams, libracartCode]);
useEffect(() => {
let code = new URLSearchParams(window.location.search).get("state"); // this is the account_code
let auth = new URLSearchParams(window.location.search).get("authorization_token"); // this is the authorization used to verify where it came from
if (code && auth) {
setLibraCartCode(code);
setLibraCartAuth(auth);
setOpenDialog(true);
}
}, [window.location]);
const validate = () => {
let invalid = false;
if (libracartUsername === "" || teamKey === "" || primaryUser === "" || libracartCode === "") {
invalid = true;
}
return invalid;
};
const general = () => {
return (
<Box>
<TextField
margin="dense"
label="Libra Cart Email"
fullWidth
variant="outlined"
value={libracartUsername}
onChange={e => setLibraCartUserName(e.target.value)}
/>
<TeamSearch label="Team" setTeamCallback={setTeamKey} />
<TextField
disabled={teamKey === ""}
margin="dense"
label="Primary User"
select
fullWidth
variant="outlined"
value={primaryUser}
onChange={e => setPrimaryUser(e.target.value)}>
{teamUsers.map(u => (
<MenuItem key={u.id()} value={u.id()}>
<Grid container direction="row" alignItems="center">
<Grid>
<Avatar
alt={u.name()}
src={
u.settings.avatar && u.settings.avatar !== "" ? u.settings.avatar : undefined
}
/>
</Grid>
<Grid>
<Box marginLeft={2}>{u.name()}</Box>
</Grid>
</Grid>
</MenuItem>
))}
</TextField>
<TextField
margin="dense"
label="code"
disabled
fullWidth
variant="outlined"
value={libracartCode}
/>
<TextField
margin="dense"
label="authorization token"
disabled
fullWidth
variant="outlined"
value={libracartAuth}
/>
</Box>
);
};
const newOrgDialog = () => {
return (
<ResponsiveDialog
open={openDialog}
onClose={() => {
setOpenDialog(false);
}}>
<DialogTitle>Enter New Integration Link</DialogTitle>
<DialogContent>{general()}</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenDialog(false);
}}>
Close
</Button>
<Button
disabled={validate()}
onClick={submitNewOrganization}
variant="contained"
color="primary">
Submit
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
return (
<Box style={{ padding: 10 }}>
<Grid container direction="row" alignContent="center" alignItems="center" wrap="nowrap" width="100%" justifyContent="space-between">
<Grid container direction="row" alignContent="center" alignItems="center" wrap="nowrap">
<Grid>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
window.open(`https://staging.cloud.agrimatics.com/grain/integrations`, "_blank");
}}>
Link Libra Cart Account
</Button>
</Grid>
<Grid>
<Tooltip
title='The integration can be found by selecting my account in the corner,
selecting the "Manage Account" option and then selecting integrations in the left side menu'>
<Help />
</Tooltip>
</Grid>
</Grid>
<Grid>
<Tooltip title={"This will Sync the data for all LibraCart accounts linked to " + (as ? "this team" : "your account")}>
<Button
variant="contained"
color="primary"
onClick={e => {
libracartAPI.syncData().then(resp => {}).catch(err => {})
}}>
Sync
</Button>
</Tooltip>
</Grid>
</Grid>
<Box paddingTop={2}>
<Typography>
LibraCart data will sync every 6 hours, if the data is needed immediately you can select the Sync button. It may take up to 15 minutes for the data to appear in our platform.
</Typography>
</Box>
{newOrgDialog()}
</Box>
);
}

View file

@ -126,8 +126,10 @@ export class Bin {
}
public bushels(): number {
let control = this.settings.inventory?.inventoryControl;
let bushels = this.settings.inventory?.grainBushels || 0
if (this.settings.inventory?.inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR){
if (control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART){
bushels = this.status.grainBushels
}
return bushels
@ -137,7 +139,8 @@ export class Bin {
let fill = 0;
if (this.settings.inventory && this.settings.specs) {
let bushels = this.settings.inventory.grainBushels
if (this.settings.inventory.inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR){
if (this.settings.inventory.inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
this.settings.inventory.inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART){
bushels = this.status.grainBushels
}
fill = Math.round(

View file

@ -67,18 +67,25 @@ export class Component {
return quack.ComponentID.fromObject({
type: this.settings.type,
addressType: this.settings.addressType,
address: this.settings.address
address: this.settings.address,
expansionLine: this.settings.expansionLine,
muxLine: this.settings.muxLine
});
}
public locationString(): string {
return (
or(this.settings.type, 0).toString() +
let compositeLocation = or(this.settings.type, 0).toString() +
"-" +
or(this.settings.addressType, 0).toString() +
"-" +
or(this.settings.address, 0).toString()
);
if(this.settings.expansionLine){
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
}
if(this.settings.muxLine){
compositeLocation = compositeLocation + ":" + this.settings.muxLine
}
return compositeLocation;
}
public type(): quack.ComponentType {
@ -107,7 +114,7 @@ export class Component {
return 0;
}
public addressDescription = (deviceProduct?: pond.DeviceProduct) => {
public addressDescription(deviceProduct?: pond.DeviceProduct): string {
if (
this.settings.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY ||
(this.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 &&

View file

@ -42,11 +42,11 @@ const featureVersions: Map<string, FeatureVersionByPlatform> = new Map([
v2Cell: "N/A",
v2WifiS3: "N/A",
v2CellS3: "N/A",
v2CellBlack: "N/A",
v2CellBlack: "2.1.6",
v2CellGreen: "N/A",
v2WifiBlue: "N/A",
v2CellBlue: "N/A",
v2EthBlue: "N/A"
v2WifiBlue: "2.1.6",
v2CellBlue: "2.1.6",
v2EthBlue: "2.1.6"
}
]
]);

View file

@ -38,7 +38,7 @@ const Transactions = lazy(() => import("pages/Transactions"));
const BinCableEstimator = lazy(() => import("pages/BinCableEstimator"));
const APIDocs = lazy(() => import("pages/APIDocs"));
const Fields = lazy(()=> import("pages/Fields"));
const Marketplace = lazy(() => import("userFeatures/UserFeatures"))
const Marketplace = lazy(() => import("pages/Marketplace"))
const Logs = lazy(() => import("pages/Logs"))
const Firmware = lazy(() => import("pages/Firmware"));
const Nfc = lazy(() => import("pages/Nfc"));
@ -46,6 +46,7 @@ const Contracts = lazy(() => import("pages/Contracts"));
const Contract = lazy(() => import("pages/Contract"));
const JohnDeere = lazy(() => import("pages/JohnDeere"));
const CNHi = lazy(() => import("pages/CNHi"));
const LibraCart = lazy(() => import("pages/LibraCart"));
export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, "");
@ -340,6 +341,9 @@ export default function Router() {
{user.hasFeature("cnhi") &&
<Route path="cnhi" element={<CNHi />} />
}
{user.hasFeature("libra-cart") &&
<Route path="libracart" element={<LibraCart />} />
}
{/* Map routes */}
<Route path="visualFarm" element={<FieldMap />} />
<Route path="aviationMap" element={<AviationMap />} />

View file

@ -48,6 +48,7 @@ import DataDuckIcon from "products/Bindapt/DataDuckIcon";
import ContractsIcon from "products/CommonIcons/contractIcon";
import JohnDeereIcon from "products/CommonIcons/johnDeereIcon";
import CNHiIcon from "products/CommonIcons/cnhiIcon";
import LibraCartIcon from "products/CommonIcons/libracartIcon";
const drawerWidth = 230;
@ -436,7 +437,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
}
{(isAg || isStreamline) &&
{(isAg || isStreamline || user.hasFeature("admin")) &&
<Tooltip title="Marketplace" placement="right">
<ListItemButton
id="tour-marketplace"
@ -478,6 +479,20 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
}
{user.hasFeature("libra-cart") &&
<Tooltip title="LibraCart" placement="right">
<ListItemButton
id="tour-libraCart"
onClick={() => goTo("/libracart")}
classes={getClasses("/libracart")}
>
<ListItemIcon>
<LibraCartIcon />
</ListItemIcon>
{open && <ListItemText primary="LibraCart" />}
</ListItemButton>
</Tooltip>
}
</List>
)
}

View file

@ -1214,37 +1214,6 @@ export default function Bins(props: Props) {
}
]}
/>
{/* <StyledToggleButtonGroup
id="cardValueDisplay"
value={cardValDisplay}
exclusive
size="small"
aria-label="card detail">
<StyledToggle
value={"low"}
aria-label="low"
onClick={() => {
setCardValDisplay("low");
}}>
Low
</StyledToggle>
<StyledToggle
value={"average"}
aria-label="average"
onClick={() => {
setCardValDisplay("average");
}}>
Average
</StyledToggle>
<StyledToggle
value={"high"}
aria-label="high"
onClick={() => {
setCardValDisplay("high");
}}>
High
</StyledToggle>
</StyledToggleButtonGroup> */}
</Box>
</Grid>
<Grid >
@ -1270,41 +1239,6 @@ export default function Bins(props: Props) {
}
]}
/>
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={binView}
exclusive
size="small"
aria-label="detail">
<StyledToggle
value={"grid"}
aria-label="grid view"
onClick={() => {
setBinView("grid");
sessionStorage.setItem("binsView", "grid");
}}>
<ViewComfy />
</StyledToggle> */}
{/* hidden at dustins request so that grid view and list are the only two */}
{/* <StyledToggle
value={"scroll"}
aria-label="scroll view"
onClick={() => {
setBinView("scroll");
sessionStorage.setItem("binsView", "scroll");
}}>
<ViewColumn />
</StyledToggle> */}
{/* <StyledToggle
value={"list"}
aria-label="list view"
onClick={() => {
setBinView("list");
sessionStorage.setItem("binsView", "list");
}}>
<ViewList />
</StyledToggle>
</StyledToggleButtonGroup> */}
<MoreVert
className={classes.icon}
onClick={event => {

View file

@ -20,7 +20,7 @@ import PageContainer from "./PageContainer";
export default function CNHi() {
const [currentOrg, setCurrentOrg] = useState("");
const cnhiAPI = useCNHiProxyAPI();
const [organizations, setOrganizations] = useState<Map<string, pond.JDAccount>>(new Map());
const [organizations, setOrganizations] = useState<Map<string, pond.CNHiAccount>>(new Map());
const [fieldAccordion, setFieldAccordion] = useState(false);
const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]);
const [{ as }] = useGlobalState();

134
src/pages/LibraCart.tsx Normal file
View file

@ -0,0 +1,134 @@
import {
Box,
Button,
Checkbox,
FormControlLabel,
Grid2 as Grid,
MenuItem,
Select
} from "@mui/material";
import LibraCartAccess from "integrations/LibraCart/LibraCartAccess";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { useLibraCartProxyAPI } from "providers/pond/libracartProxyAPI";
import React, { useEffect, useState } from "react";
import PageContainer from "./PageContainer";
export default function LibraCart() {
const [currentOrg, setCurrentOrg] = useState("");
const libracartAPI = useLibraCartProxyAPI();
const [organizations, setOrganizations] = useState<Map<string, pond.LibraCartAccount>>(new Map());
const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]);
const [{ as }] = useGlobalState();
//load organizations for the user
useEffect(() => {
setCurrentOrg("");
libracartAPI
.listAccounts(0, 0, as)
.then(resp => {
let tempOrgs: Map<string, pond.LibraCartAccount> = new Map();
resp.data.accounts.forEach(org => {
let organization = pond.LibraCartAccount.fromObject(org);
tempOrgs.set(organization.key, organization);
});
setOrganizations(tempOrgs);
})
.catch(err => {});
}, [libracartAPI, as]);
useEffect(() => {
let organization = organizations.get(currentOrg);
if (organization) {
let currentOptions: pond.DataOption[] = organization.options ?? [];
setDataOptions(currentOptions);
}
}, [currentOrg, organizations]);
const updateOrgData = (checked: boolean, option: pond.DataOption) => {
let currentOps: pond.DataOption[] = dataOptions;
if (checked && !currentOps.includes(option)) {
currentOps.push(option);
} else if (!checked && currentOps.includes(option)) {
currentOps.splice(currentOps.indexOf(option), 1);
}
setDataOptions([...currentOps]);
};
const submit = () => {
libracartAPI.updateAccount(currentOrg, dataOptions, as ?? undefined).then(resp => {
//update the organization in the map to have the correct dataOptions
let org = organizations.get(currentOrg);
if (org) {
org.options = dataOptions;
}
});
};
const fieldOptions = () => {
return (
<React.Fragment>
<Grid container direction="row" spacing={2} alignItems="center" alignContent="center">
<Grid size={6}>
<FormControlLabel
label="Destinations"
control={
<Checkbox
checked={dataOptions.includes(pond.DataOption.DATA_OPTION_DESTINATIONS)}
onChange={(_, checked) => {
//setFields(!fields);
updateOrgData(checked, pond.DataOption.DATA_OPTION_DESTINATIONS);
}}
/>
}
/>
</Grid>
<Grid size={6}>
Import your destinations for the option to link it to a bin
</Grid>
</Grid>
</React.Fragment>
);
};
return (
<PageContainer>
<LibraCartAccess />
<Grid
container
direction="row"
justifyContent="space-between"
alignContent="center"
alignItems="center"
style={{ padding: 10 }}>
<Grid>
<Select
//style={{ maxWidth: 110 }}
title="LibraCart Account"
displayEmpty
disableUnderline={true}
value={currentOrg}
onChange={(event: any) => {
setCurrentOrg(event.target.value);
}}>
<MenuItem key={""} value={""}>
Select account to adjust accessable data
</MenuItem>
{Array.from(organizations.values()).map(org => {
return (
<MenuItem key={org.key} value={org.key}>
{org.username}
</MenuItem>
);
})}
</Select>
</Grid>
<Grid>
<Button onClick={submit} variant="contained" color="primary">
Update
</Button>
</Grid>
</Grid>
<Box style={{ padding: 10 }}>{fieldOptions()}</Box>
</PageContainer>
);
}

View file

@ -128,7 +128,8 @@ export default function Users() {
"developer",
"marketplace",
"installer",
"cnhi"
"cnhi",
"libracart"
].sort();
const [rows, setRows] = useState<User[]>([])

View file

@ -16,7 +16,9 @@ export const I2C: AddressTypeExtension = {
[quack.ComponentType.COMPONENT_TYPE_CAPACITANCE, 0x4f],
[quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE, 0x4f],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, 0x68],
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, 0x6b]
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, 0x6b],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, 0x70],
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, 0x6c]
]);
const offset = offsets.get(componentType);

View file

@ -16,6 +16,7 @@ export const GetBinShapeDescribers = (): BinShapeDescriber[] => {
];
};
// DEPRECATED -- the bon now knows its components from the relative object table
export const HasGrainCable = (bin?: pond.BinSettings): boolean => {
if (bin && bin.deviceComponents) {
bin.deviceComponents.forEach(dc => {

View file

@ -23,7 +23,9 @@ export function getComponentIDString(component?: Component): string {
? componentIDToString({
type: component.settings.type,
addressType: component.settings.addressType,
address: component.settings.address
address: component.settings.address,
expansionLine: component.settings.expansionLine,
muxLine: component.settings.muxLine
})
: "0-0-0";
}
@ -40,13 +42,18 @@ export function componentIDToString(componentID?: quack.IComponentID | null): st
if (!componentID) {
return "0-0-0";
}
return (
or(componentID.type, 0).toString() +
let compositeLocation = or(componentID.type, 0).toString() +
"-" +
or(componentID.addressType, 0).toString() +
"-" +
or(componentID.address, 0).toString()
);
if(componentID.expansionLine){
compositeLocation = compositeLocation + "-" + componentID.expansionLine
}
if(componentID.muxLine){
compositeLocation = compositeLocation + ":" + componentID.muxLine
}
return compositeLocation;
}
export function stringToComponentId(componentIDString: string): quack.ComponentID {

View file

@ -48,16 +48,16 @@ export function dragerGasDongle(subtype: number = 0): ComponentTypeExtension {
return {
type: quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE,
subtypes: [
{
key: quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_NONE,
value: "DRAGER_GAS_DONGLE_SUBTYPE_NONE",
friendlyName: "Drager Gas Chain"
},
{
key: quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_DEBUG_VOLTAGE,
value: "DRAGER_GAS_DONGLE_SUBTYPE_ADVANCED",
friendlyName: "Drager Gas Chain - Voltage Debug"
},
// {
// key: quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_NONE,
// value: "DRAGER_GAS_DONGLE_SUBTYPE_NONE",
// friendlyName: "Drager Gas Chain"
// },
// {
// key: quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_DEBUG_VOLTAGE,
// value: "DRAGER_GAS_DONGLE_SUBTYPE_ADVANCED",
// friendlyName: "Drager Gas Chain - Voltage Debug"
// },
// {
// key: quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_CO_CO2_NO2_O2,
// value: "DRAGER_GAS_DONGLE_SUBTYPE_ADVANCED",
@ -100,7 +100,7 @@ export function dragerGasDongle(subtype: number = 0): ComponentTypeExtension {
isSource: true,
isArray: true,
isCalibratable: false,
addressTypes: [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY],
addressTypes: [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, quack.AddressType.ADDRESS_TYPE_I2C],
interactionResultTypes: [],
states: [],
//this is apparently used by the interactions to determine what the possible options for measurement type are

View file

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

View file

@ -60,7 +60,9 @@ const DefaultAvailability: DeviceAvailabilityMap = new Map<quack.AddressType, De
[quack.ComponentType.COMPONENT_TYPE_CAPACITANCE, [0x50]],
[quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE, [0x50]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]]
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]], // the address cables will use when they are plugged into the expander with V2 device only
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]]
])
],
[quack.AddressType.ADDRESS_TYPE_DAC, [0, 1]],
@ -103,7 +105,8 @@ export class DeviceAvailability {
ClaimAddress(
addressType: quack.AddressType,
componentType: quack.ComponentType,
address: number
address: number,
expansionLine?: number
) {
let offset = addressType - 8;
if (offset > 0) addressType = addressType - (7 + offset);
@ -127,6 +130,9 @@ export class DeviceAvailability {
break;
case quack.AddressType.ADDRESS_TYPE_I2C:
case quack.AddressType.ADDRESS_TYPE_SPI:
if(expansionLine){
break;
}
let addressTypePositions = cloneDeep(
this.availability.get(addressType) as ComponentAvailabilityMap
);
@ -171,7 +177,8 @@ export function FindAvailablePositions(
available.ClaimAddress(
component.settings.addressType,
component.settings.type,
or(component.settings.address, 0)
or(component.settings.address, 0),
component.settings.expansionLine
);
}
});

View file

@ -246,12 +246,13 @@ export const BindaptV2MonitorAvailability: DeviceAvailabilityMap = new Map<
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
//[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
// [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], //component type deprecated?
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
@ -278,12 +279,13 @@ export const BindaptV2AutomateAvailability: DeviceAvailabilityMap = new Map<
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
//[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
// [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], //component type deprecated?
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],

View file

@ -0,0 +1,24 @@
// update logo images when they share official black and white
import LibraCartLogoWhite from "assets/marketplaceImages/LibraCartGrey.png";
import LibraCartLogoBlack from "assets/marketplaceImages/LibraCartGrey.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
interface Props {
type?: "light" | "dark";
}
export default function LibraCartIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? LibraCartLogoWhite : LibraCartLogoBlack;
}
return themeType === "light" ? LibraCartLogoBlack : LibraCartLogoWhite;
};
return <ImgIcon alt="libra-cart" src={src()} />;
}

View file

@ -52,7 +52,9 @@ export const MiVentV2Availability: DeviceAvailabilityMap = new Map<
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]]
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]],
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],

View file

@ -45,7 +45,8 @@ export {
useFileControllerAPI,
useContractAPI, //TODO: update api with resolve, reject
useJohnDeereProxyAPI, //TODO: update api with resolve, reject
useCNHiProxyAPI //TODO: update api with resolve, reject
useCNHiProxyAPI, //TODO: update api with resolve, reject
useLibraCartProxyAPI //TODO: update api with resolve, reject
} from "./pond/pond";
// export { SecurityContext, useSecurity } from "./security";
export { SnackbarContext, useSnackbar } from "./Snackbar";

View file

@ -0,0 +1,128 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import React, { createContext, PropsWithChildren, useContext } from "react";
//import { or } from "utils";
import { pondURL } from "./pond";
import { useGlobalState } from "providers/StateContainer";
export interface ILibraCartProxyAPIContext {
//add new organization
addAccount: (
teamKey: string,
userID: string,
libracartCode: string,
libracartAuth: string,
libracartUsername: string
) => Promise<AxiosResponse<pond.AddLibraCartAccountResponse>>;
//list organizations
listAccounts: (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListLibraCartAccountsResponse>>;
updateAccount: (
key: string,
options: pond.DataOption[],
as?: string
) => Promise<AxiosResponse<pond.UpdateLibraCartAccountResponse>>;
listDestinations: (
limit: number,
offset: number,
libracartKey?: string,
as?: string
) => Promise<AxiosResponse<pond.ListLibraCartDestinationsResponse>>;
syncData: () => Promise<any>
}
export const LibraCartProxyAPIContext = createContext<ILibraCartProxyAPIContext>(
{} as ILibraCartProxyAPIContext
);
interface Props {}
export default function LibraCartProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const [{as}] = useGlobalState();
const { post, get, put } = useHTTP();
const addAccount = (
teamKey: string,
userID: string,
libracartCode: string,
libracartAuth: string,
libracartUsername: string
) => {
return post<pond.AddLibraCartAccountResponse>(
pondURL(
"/libracartAccounts?team=" +
teamKey +
"&user=" +
userID +
"&code=" +
libracartCode +
"&auth=" +
libracartAuth +
"&libracartUsername=" +
libracartUsername
)
);
};
const listAccounts = (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => {
return get<pond.ListLibraCartAccountsResponse>(
pondURL(
"/libracartAccounts?limit=" +
limit +
"&offset=" +
offset +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
)
);
};
const updateAccount = (key: string, options: pond.DataOption[], as?: string) => {
return put<pond.UpdateLibraCartAccountResponse>(
pondURL(
"/libracartAccounts/" + key + "?options=" + options.toString() + (as ? "&as=" + as : "")
)
);
};
const listDestinations = (limit: number, offset: number, libracartKey?: string, as?: string) => {
return get<pond.ListLibraCartDestinationsResponse>(
pondURL(
"/libracartDestinations?limit" + limit + "&offset=" + offset + (libracartKey ? "&libracartKey=" + libracartKey : "") + (as ? "&as=" + as : "")
)
)
}
const syncData = () => {
return get(pondURL("/libracartImport" + (as ? "?as=" + as : "")))
}
return (
<LibraCartProxyAPIContext.Provider
value={{
addAccount,
listAccounts,
updateAccount,
listDestinations,
syncData
}}>
{children}
</LibraCartProxyAPIContext.Provider>
);
}
export const useLibraCartProxyAPI = () => useContext(LibraCartProxyAPIContext);

View file

@ -35,6 +35,7 @@ import ContractProvider, { useContractAPI } from "./contractAPI";
import UsageProvider, { useUsageAPI } from "./usageAPI";
import KeyManagerProvider, { useKeyManagerAPI } from "./keyManagerAPI";
import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI";
import LibraCartProvider, { useLibraCartProxyAPI } from "./libracartProxyAPI";
import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI";
import DevicePresetProvider, { useDevicePresetAPI } from "./devicePresetAPI";
// import NoteProvider from "providers/noteAPI";
@ -87,6 +88,7 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<ContractProvider>
<JohnDeereProvider>
<CNHiProvider>
<LibraCartProvider>
<UsageProvider>
<KeyManagerProvider>
<DevicePresetProvider>
@ -94,6 +96,7 @@ export default function PondProvider(props: PropsWithChildren<any>) {
</DevicePresetProvider>
</KeyManagerProvider>
</UsageProvider>
</LibraCartProvider>
</CNHiProvider>
</JohnDeereProvider>
</ContractProvider>
@ -167,5 +170,6 @@ export {
useKeyManagerAPI,
useJohnDeereProxyAPI,
useCNHiProxyAPI,
useLibraCartProxyAPI,
useDevicePresetAPI
};

View file

@ -5,6 +5,7 @@ import { useSnackbar, useUserAPI } from "hooks";
import React, { useEffect, useState } from "react";
import JohnDeereIcon from "products/CommonIcons/johnDeereIcon";
import CNHiIcon from "products/CommonIcons/cnhiIcon";
import LibraCartIcon from "products/CommonIcons/libracartIcon";
//import AgLogo from "assets/whitelabels/AdaptiveAgriculture/AGLogoSquare.png";
import {
IsAdaptiveAgriculture
@ -60,6 +61,14 @@ const agFeatureList: ProductDetails[] = [
"Integrate with the Case New Holland Industrial Center to bring your data into our platform"
// bulletPoints: ["bullet one", "bullet two"],
// questions: [],
},
{
featureName: "libra-cart",
featureLogo: <LibraCartIcon />,
featureCost: 0,
cardImage: FeatureImageTest,
featureTitle: "Libra Cart",
longDescription: "Integrate with Libra Cart to bring your data into our platform"
}
];
@ -80,7 +89,7 @@ export default function Marketplace() {
useEffect(() => {
let list: ProductDetails[] = [];
if (IsAdaptiveAgriculture()) {
if (IsAdaptiveAgriculture() || user.hasFeature("admin")) {
list = list.concat(agFeatureList);
}
// if(IsAdCon()){