updated the api to use a new variable for whether to load all nodes, this will still use the show errors boolean for the component page

fixed issue in the filter function in the grain cable model that caused it to return empty when a top node was not set
updated the bin svg to have the disabled nodes greyed out
This commit is contained in:
csawatzky 2025-07-31 13:20:31 -06:00
parent 35e8996c29
commit 23223aa1a8
10 changed files with 134 additions and 109 deletions

2
package-lock.json generated
View file

@ -10911,7 +10911,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#dc29e71ec9598556c1b7cdff4dc02f74ecbac20c", "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#1b8dd6d6cdcef9c1cf5184db97ef9e06b3df0bed",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -87,54 +87,48 @@ export default function BinCard(props: Props) {
//loop through the cables in the bin status to prep for display //loop through the cables in the bin status to prep for display
bin.status.grainCables.forEach(cable => { bin.status.grainCables.forEach(cable => {
let c: GrainCable = new GrainCable(); let c: GrainCable = new GrainCable();
//flip the cables so that for display node 1 is at the bottom //flip the cables so that for display node 1 is at the bottom,
let cableTemps = cloneDeep(cable.celcius); let cableTemps = cloneDeep(cable.celcius);
let cableHums = cloneDeep(cable.relativeHumidity); let cableHums = cloneDeep(cable.relativeHumidity);
let cableEMC = cloneDeep(cable.moisture); let cableEMC = cloneDeep(cable.moisture);
c.temperatures = cableTemps.reverse(); c.temperatures = cableTemps.reverse();
c.humidities = cableHums.reverse(); c.humidities = cableHums.reverse();
c.grainMoistures = cableEMC.reverse(); c.grainMoistures = cableEMC.reverse();
c.topNode = cable.topNode; c.topNode = cable.topNode;
c.excludedNodes = cable.excludedNodes;
newGrainCables.push(c); newGrainCables.push(c);
//determine the averages for temp/RH/EMC for the bin using all of the cables //filter out the excluded nodes and any nodes above the top node
let temps = cloneDeep(cable.celcius); let filteredNodes = c.filteredNodes(true)
let hums = cloneDeep(cable.relativeHumidity);
let moistures = cloneDeep(cable.moisture);
const spliceEnd = cable.topNode > 0 ? cable.topNode : temps.length; allTemps.push(...filteredNodes.temps);
let grainTemps = temps.splice(0, spliceEnd); allHums.push(...filteredNodes.humids);
let grainHums = hums.splice(0, spliceEnd); allMoistures.push(...filteredNodes.moistures);
let grainEMCs = moistures.splice(0, spliceEnd);
allTemps.push(...grainTemps);
allHums.push(...grainHums);
allMoistures.push(...grainEMCs);
//set the hottest and coldest nodes //set the hottest and coldest nodes
if (coldestNode === undefined || Math.min(...grainTemps) < coldestNode.temp) { if (coldestNode === undefined || Math.min(...filteredNodes.temps) < coldestNode.temp) {
let lowTempIndex = let lowTempIndex =
grainTemps.indexOf(Math.min(...grainTemps)) === -1 filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1
? 0 ? 0
: grainTemps.indexOf(Math.min(...grainTemps)); : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps));
coldestNode = { coldestNode = {
temp: grainTemps[lowTempIndex], temp: filteredNodes.temps[lowTempIndex],
humid: grainHums[lowTempIndex], humid: filteredNodes.humids[lowTempIndex],
emc: grainEMCs[lowTempIndex] emc: filteredNodes.moistures[lowTempIndex]
}; };
} }
if (hottestNode === undefined || Math.max(...grainTemps) > hottestNode.temp) { if (hottestNode === undefined || Math.max(...filteredNodes.temps) > hottestNode.temp) {
let highTempIndex = let highTempIndex =
grainTemps.indexOf(Math.max(...grainTemps)) === -1 filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps)) === -1
? 0 ? 0
: grainTemps.indexOf(Math.max(...grainTemps)); : filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps));
hottestNode = { hottestNode = {
temp: grainTemps[highTempIndex], temp: filteredNodes.temps[highTempIndex],
humid: grainHums[highTempIndex], humid: filteredNodes.humids[highTempIndex],
emc: grainEMCs[highTempIndex] emc: filteredNodes.moistures[highTempIndex]
}; };
} }

View file

@ -86,6 +86,9 @@ const useStyles = makeStyles((theme: Theme) => {
cableNode: { cableNode: {
fill: "#fff" fill: "#fff"
}, },
excludedNode: {
fill: "grey"
},
hotNode: { hotNode: {
fill: "#fff", fill: "#fff",
animation: "$hotNode 1.0s infinite" animation: "$hotNode 1.0s infinite"
@ -168,7 +171,8 @@ interface NodeData { //this could be either a box or a circle depending
height: string | number height: string | number
borderRad: string | number borderRad: string | number
nodeNumber: number nodeNumber: number
nodeTemp: number nodeTemp: number,
excluded: boolean,
} }
interface CircleData { interface CircleData {
@ -427,6 +431,8 @@ export default function BinSVGV2(props: Props) {
); );
}; };
//console.log(cables)
const cableGrainEstimate = ( const cableGrainEstimate = (
points: GrainNodePoint[], points: GrainNodePoint[],
cableSpacing: number, cableSpacing: number,
@ -609,9 +615,31 @@ export default function BinSVGV2(props: Props) {
//determine how high to draw the node on the cable //determine how high to draw the node on the cable
const nodeY = nodeSpacingY * (index + 1) + minNodeY; const nodeY = nodeSpacingY * (index + 1) + minNodeY;
//if we are using the auto top nodes only show the heatmap for nodes at or below the top node //if we are using the auto top nodes only show the heatmap for nodes at or below the top node and it is not an ecluded node
if (cable.topNode > 0) { if (!cable.excludedNodes.includes(nodeNumber - 1)){
if (nodeNumber <= cable.topNode) { if (cable.topNode > 0) {
if (nodeNumber <= cable.topNode) {
// nodeHeatMap.push(
// <ellipse
// key={index}
// cx={cablePos}
// cy={nodeY}
// rx="120"
// ry="60"
// fill={getGrainGradient(temp)}
// />
// );
nodeHeatMapData.push({
key: index,
xCenter: cablePos,
yCenter: nodeY,
xRadius: "120",
yRadius: "60",
fillColour: getGrainGradient(temp)
})
}
} else if (nodeY > filledToY) {
//otherwise use the fill level of the bin
// nodeHeatMap.push( // nodeHeatMap.push(
// <ellipse // <ellipse
// key={index} // key={index}
@ -631,26 +659,6 @@ export default function BinSVGV2(props: Props) {
fillColour: getGrainGradient(temp) fillColour: getGrainGradient(temp)
}) })
} }
} else if (nodeY > filledToY) {
//otherwise use the fill level of the bin
// nodeHeatMap.push(
// <ellipse
// key={index}
// cx={cablePos}
// cy={nodeY}
// rx="120"
// ry="60"
// fill={getGrainGradient(temp)}
// />
// );
nodeHeatMapData.push({
key: index,
xCenter: cablePos,
yCenter: nodeY,
xRadius: "120",
yRadius: "60",
fillColour: getGrainGradient(temp)
})
} }
const displayWidth = 90; const displayWidth = 90;
const displayHeight = 45; const displayHeight = 45;
@ -682,20 +690,9 @@ export default function BinSVGV2(props: Props) {
width: displayWidth, width: displayWidth,
borderRad: showTempHum ? cornerRad : "16", borderRad: showTempHum ? cornerRad : "16",
nodeNumber: nodeNumber, nodeNumber: nodeNumber,
nodeTemp: temp nodeTemp: temp,
excluded: cable.excludedNodes.includes(nodeNumber - 1)
}) })
// temps.push(
// <text
// key={"node" + index + "temp"}
// x={cablePos}
// y={nodeY + 10}
// stroke="blue"
// filter="drop-shadow( 4px 4px 10px rgba(0, 0, 0, .7))"
// className={classes.tempHum}>
// {displayTemp.toFixed(1)}°
// </text>
// );
tempData.push({ tempData.push({
key: "node" + index + "temp", key: "node" + index + "temp",
x: cablePos, x: cablePos,
@ -704,16 +701,6 @@ export default function BinSVGV2(props: Props) {
value: displayTemp.toFixed(1) +"°", value: displayTemp.toFixed(1) +"°",
filter: "drop-shadow( 4px 4px 10px rgba(0, 0, 0, .7))" filter: "drop-shadow( 4px 4px 10px rgba(0, 0, 0, .7))"
}) })
// hums.push(
// <text
// key={"node" + index + "hum"}
// x={cablePos}
// y={nodeY + 10}
// stroke="green"
// className={classes.tempHum}>
// {ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0).toFixed(1)}%
// </text>
// );
humData.push({ humData.push({
key: "node" + index + "hum", key: "node" + index + "hum",
x: cablePos, x: cablePos,
@ -722,20 +709,6 @@ export default function BinSVGV2(props: Props) {
value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0).toFixed(1)+"%", value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0).toFixed(1)+"%",
}) })
!showTempHum && !showTempHum &&
// nodeClickers.push(
// <circle
// key={"node" + index}
// onClick={() => {
// if (cable.key() !== "" && cableNodeClicked) {
// cableNodeClicked(cable, nodeNumber);
// }
// }}
// cx={cablePos}
// cy={nodeY}
// className={classes.clickableArea}
// r="50"
// />
// );
nodeClickData.push({ nodeClickData.push({
key: "node"+index, key: "node"+index,
centerX: cablePos, centerX: cablePos,
@ -803,13 +776,13 @@ export default function BinSVGV2(props: Props) {
width={e.width} width={e.width}
height={e.height} height={e.height}
rx={e.borderRad} rx={e.borderRad}
className={classes.cableNode}/> className={e.excluded ? classes.excludedNode : classes.cableNode}/>
) )
} else { } else {
return ( return (
<g key={e.key}> <g key={e.key}>
{e.nodeNumber === topNode && topNodeHat(cablePos, e.y)} {e.nodeNumber === topNode && !e.excluded && topNodeHat(cablePos, e.y)}
<circle cx={cablePos} cy={e.y} className={nodeClass(e.nodeTemp)} r="16"></circle> <circle cx={cablePos} cy={e.y} className={e.excluded ? classes.excludedNode : nodeClass(e.nodeTemp)} r="16"></circle>
</g> </g>
) )
} }

View file

@ -382,20 +382,25 @@ export default function BinVisualizer(props: Props) {
//only use the values below the top node //only use the values below the top node
//because splice mutates the original array make a clone in the cables so the svg still has all of them for display //because splice mutates the original array make a clone in the cables so the svg still has all of them for display
//also reverse it so that the node 1 (end of the cable) is in the first position //also reverse it so that the node 1 (end of the cable) is in the first position
let tempClone = cloneDeep(cable.temperatures).reverse(); let filteredNodes = cable.filteredNodes(true)
let humClone = cloneDeep(cable.humidities).reverse(); temps.push(...filteredNodes.temps)
let emcClone = cloneDeep(cable.grainMoistures).reverse(); humids.push(...filteredNodes.humids)
emcs.push(...filteredNodes.moistures)
// let tempClone = cloneDeep(cable.temperatures).reverse();
// let humClone = cloneDeep(cable.humidities).reverse();
// let emcClone = cloneDeep(cable.grainMoistures).reverse();
//add the cable data to the proper arrays //add the cable data to the proper arrays
if (cable.topNode > 0) { // if (cable.topNode > 0) {
temps.push(...tempClone.splice(0, cable.topNode)); // temps.push(...tempClone.splice(0, cable.topNode));
humids.push(...humClone.splice(0, cable.topNode)); // humids.push(...humClone.splice(0, cable.topNode));
emcs.push(...emcClone.splice(0, cable.topNode)); // emcs.push(...emcClone.splice(0, cable.topNode));
} else { // } else {
//if the cable has no fill set (top node) then use all of the nodes // //if the cable has no fill set (top node) then use all of the nodes
temps = tempClone; // temps = tempClone;
humids = humClone; // humids = humClone;
emcs = emcClone; // emcs = emcClone;
} // }
//add the trend data to the proper arrays if the top node is set //add the trend data to the proper arrays if the top node is set
let cableTrendData = binTrend.cableTrend[cable.key()]; let cableTrendData = binTrend.cableTrend[cable.key()];
tempTrends.push(cableTrendData.grainCelciusTrend); tempTrends.push(cableTrendData.grainCelciusTrend);
@ -432,6 +437,7 @@ export default function BinVisualizer(props: Props) {
}; };
} }
}); });
//set the average conditions //set the average conditions
avgConditions = { avgConditions = {
tempC: average(temps), tempC: average(temps),

View file

@ -127,6 +127,7 @@ export default function ComponentChart(props: Props) {
const [expandData, setExpandData] = useState(false); const [expandData, setExpandData] = useState(false);
const [showOriginal, setShowOriginal] = useState(true); const [showOriginal, setShowOriginal] = useState(true);
const [showAveraged, setShowAveraged] = useState(false); const [showAveraged, setShowAveraged] = useState(false);
const [showAllNodes, setShowAllNodes] = useState(false);
// useEffect(() => { // useEffect(() => {
// if (prevComponent !== props.component) { // if (prevComponent !== props.component) {

View file

@ -97,6 +97,7 @@ export default function ComponentDiagnostics(props: Props) {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
as as
) )
.then(resp => { .then(resp => {

View file

@ -110,6 +110,7 @@ class ExportDataSettings extends React.Component<Props, State> {
[device.id().toString()], [device.id().toString()],
["device"], ["device"],
undefined, undefined,
undefined,
as as
) )
.then(resp => { .then(resp => {

View file

@ -10,10 +10,12 @@ export class GrainCable {
public settings: pond.ComponentSettings = pond.ComponentSettings.create(); public settings: pond.ComponentSettings = pond.ComponentSettings.create();
public status: pond.ComponentStatus = pond.ComponentStatus.create(); public status: pond.ComponentStatus = pond.ComponentStatus.create();
public lastReading: string = ""; public lastReading: string = "";
//bear in mind these three arrays are reversed due to the bin svg needing the first node on the bottom of the image and the svg being drawn from the top
public temperatures: number[] = []; public temperatures: number[] = [];
public humidities: number[] = []; public humidities: number[] = [];
public grainMoistures: number[] = []; public grainMoistures: number[] = [];
public topNode: number = 0; public topNode: number = 0;
public excludedNodes: number[] = [];
public static create(comp: Component): GrainCable { public static create(comp: Component): GrainCable {
let my = new GrainCable(); let my = new GrainCable();
@ -58,6 +60,7 @@ export class GrainCable {
my.grainMoistures = emc; my.grainMoistures = emc;
my.lastReading = lastReading; my.lastReading = lastReading;
my.topNode = comp.settings.grainFilledTo; my.topNode = comp.settings.grainFilledTo;
my.excludedNodes = comp.settings.excludedNodes;
return my; return my;
} }
@ -239,4 +242,41 @@ export class GrainCable {
component.lastMeasurement = lastMeasurements; component.lastMeasurement = lastMeasurements;
return component; return component;
} }
/**
* Filters out the excluded nodes and anything above the top node of the cable, note that the nodes are reversed during creation if a grain cable ie: node 1 is the end of the array
*
* @param unReversed will tell the function to reverse the array back to its original state
* @returns array of active node values
*/
public filteredNodes(unReversed?: boolean): {temps: number[], humids: number[], moistures: number[]} {
//make clones of the node values and reverse them back to their original state for index purposes
let temps: number [] = cloneDeep(this.temperatures).reverse()
let humids: number [] = cloneDeep(this.humidities).reverse()
let moistures: number[] = cloneDeep(this.grainMoistures).reverse()
//filter out the excluded nodes
temps = this.filter(temps)
humids = this.filter(humids)
moistures = this.filter(moistures)
if(!unReversed){
temps.reverse()
humids.reverse()
moistures.reverse()
}
return {temps, humids, moistures}
}
private filter(values: number[]): number[] {
let filtered: number[] = []
values.forEach((val, index) => {
//if the node is not excluded AND is either under the top node OR top node is not set
if(!this.excludedNodes.includes(index) && (index <= this.topNode || this.topNode === 0)){
filtered.push(val)
}
})
return filtered
}
} }

View file

@ -168,7 +168,8 @@ export default function DeviceComponent() {
getContextKeys(), getContextKeys(),
getContextTypes(), getContextTypes(),
showErrors, showErrors,
as showErrors,
as,
) )
.then(resp => { .then(resp => {
if (resp.data.measurements) { if (resp.data.measurements) {
@ -431,6 +432,7 @@ export default function DeviceComponent() {
getContextKeys(), getContextKeys(),
getContextTypes(), getContextTypes(),
showErrors, showErrors,
showErrors,
as as
) )
.then(response => { .then(response => {
@ -627,6 +629,7 @@ export default function DeviceComponent() {
getContextKeys(), getContextKeys(),
getContextTypes(), getContextTypes(),
showErrors, showErrors,
showErrors,
as) as)
.then(resp => { .then(resp => {
let newRows = UnitMeasurement.convertMeasurements( let newRows = UnitMeasurement.convertMeasurements(

View file

@ -98,6 +98,7 @@ export interface IComponentAPIContext {
keys?: string[], keys?: string[],
types?: string[], types?: string[],
showErrors?: boolean, showErrors?: boolean,
allNodes?: boolean,
otherTeam?: string otherTeam?: string
) => Promise<AxiosResponse<pond.SampleUnitMeasurementsResponse>>; ) => Promise<AxiosResponse<pond.SampleUnitMeasurementsResponse>>;
// possibly a deprecated function as it is not used anywhere // possibly a deprecated function as it is not used anywhere
@ -120,6 +121,7 @@ export interface IComponentAPIContext {
keys?: string[], keys?: string[],
types?: string[], types?: string[],
showErrors?: boolean, showErrors?: boolean,
allNodes?: boolean,
otherTeam?: string otherTeam?: string
) => Promise<AxiosResponse<pond.ListUnitMeasurementsResponse>>; ) => Promise<AxiosResponse<pond.ListUnitMeasurementsResponse>>;
} }
@ -480,6 +482,7 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
keys?: string[], keys?: string[],
types?: string[], types?: string[],
showErrors: boolean = true, showErrors: boolean = true,
allNodes: boolean = false,
otherTeam?: string otherTeam?: string
) => { ) => {
const view = otherTeam ? otherTeam : as const view = otherTeam ? otherTeam : as
@ -495,7 +498,8 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
(view ? "&as=" + view : "") + (view ? "&as=" + view : "") +
(keys ? "&keys=" + keys : "&keys=" + [device]) + (keys ? "&keys=" + keys : "&keys=" + [device]) +
(types ? "&types=" + types : "&types=" + ["device"]) + (types ? "&types=" + types : "&types=" + ["device"]) +
(showErrors ? "&showErrors=true" : "&showErrors=false"), (showErrors ? "&showErrors=true" : "&showErrors=false")+
(allNodes ? "&allNodes=true" : "&allNodes=false"),
demo demo
); );
return new Promise<AxiosResponse<pond.SampleUnitMeasurementsResponse>>((resolve, reject) => { return new Promise<AxiosResponse<pond.SampleUnitMeasurementsResponse>>((resolve, reject) => {
@ -545,6 +549,7 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
keys?: string[], keys?: string[],
types?: string[], types?: string[],
showErrors?: boolean, showErrors?: boolean,
allNodes: boolean = false,
otherTeam?: string otherTeam?: string
) => { ) => {
const view = otherTeam ? otherTeam : as const view = otherTeam ? otherTeam : as
@ -565,7 +570,8 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
(view ? "&as=" + view : "") + (view ? "&as=" + view : "") +
(keys ? "&keys=" + keys : "&keys=" + [device]) + (keys ? "&keys=" + keys : "&keys=" + [device]) +
(types ? "&types=" + types : "&types=" + ["device"]) + (types ? "&types=" + types : "&types=" + ["device"]) +
(showErrors ? "&showErrors=true" : "&showErrors=false") (showErrors ? "&showErrors=true" : "&showErrors=false") +
(allNodes ? "&allNodes=true" : "&allNodes=false")
); );
return new Promise<AxiosResponse<pond.ListUnitMeasurementsResponse>>((resolve,reject) => { return new Promise<AxiosResponse<pond.ListUnitMeasurementsResponse>>((resolve,reject) => {
get<pond.ListUnitMeasurementsResponse>(url).then(resp => { get<pond.ListUnitMeasurementsResponse>(url).then(resp => {