#! /usr/bin/env bash

# check that the openframe gpio connections based on io_in and io_out connections

# Uses lvs_config to optionally extract a spice netlist
# Requires a netlist $WORK_ROOT/ext/$DESIGN_NAME.gds.spice
# Converts this netlist to $WORK_ROOT/ext/$DESIGN_NAME.cdl.gz

#   Copyright 2023 D. Mitch Bailey  cvc at shuharisystem dot com

#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

# Parameters are read from $WORK_ROOT/cvcrc.oeb which is copied from the default in $LVS_ROOT if non-existent.

# Return codes
# 1: LVS_ROOT not set
# 2: Missing file
# 3: Unrecognized block name
# 4: CVC execution error
# 5: OEB related error
# 6: OEB related warning

# Use cases
# run_openframe_check [--noextract] [lvs_config_file [top_layout [layout_file]]]

if [[ $1 == "--noextract" ]]; then
	export EXTRACT_LAYOUT=no
	shift
else
	export EXTRACT_LAYOUT=yes
fi

usage="usage: run_openframe_check [--noextract] [lvs_config_file [top_layout [layout_file]]]]"
if [[ $# -gt 3 ]]; then
	echo $usage
	exit 1
fi

# Check for LVS_ROOT
if [[ -z "$LVS_ROOT" ]]; then
	echo "LVS_ROOT not set."
	exit 1
fi

CONFIG_FILE=$1
DESIGN_NAME=${2:+"-d $2"}

if [[ $# -ne 0 ]]; then # if config file not specified, skip and use current environment
	source <($LVS_ROOT/set_lvs_env.py -c $CONFIG_FILE $DESIGN_NAME)
fi
if [[ ! -v EXTRACT_FLATGLOB ]]; then
	echo "ERROR: LVS environment problem."
	exit 1
fi
export TOP_LAYOUT=${2:-$TOP_LAYOUT}
export LAYOUT_FILE=${3:-$LAYOUT_FILE}

export DESIGN_NAME=$TOP_LAYOUT

echo "DESIGN NAME: $DESIGN_NAME"

echo "WORK_ROOT   : ${WORK_ROOT:=$(pwd)/$DESIGN_NAME}"
echo "LOG_ROOT    : ${LOG_ROOT:=$(pwd)/$DESIGN_NAME}"
echo "SIGNOFF_ROOT: ${SIGNOFF_ROOT:=$(pwd)/$DESIGN_NAME}"
export LOG_ROOT SIGNOFF_ROOT WORK_ROOT

mkdir -p $LOG_ROOT $SIGNOFF_ROOT $WORK_ROOT
export RESULTS_DIR=$WORK_ROOT/ext

rm -f $WORK_ROOT/cvc.oeb* $LOG_ROOT/cvc.oeb* $SIGNOFF_ROOT/cvc.oeb*

export RESULTS_DIR=$WORK_ROOT/ext
if [[ $EXTRACT_LAYOUT == yes ]]; then
	env RUN_DIR=$RESULTS_DIR CIFIN_STYLE= EXTRACT_STYLE= LOG_FILE=ext.log $LVS_ROOT/run_extract
	ext_status=$?
	gzip -c -f $RESULTS_DIR/$DESIGN_NAME.gds.spice > $SIGNOFF_ROOT/$DESIGN_NAME.gds.spice.gz
fi

echo " "
echo "Running CVC for gpio check..."

# Create cdl file from extracted spice file if it doesn't exist
if [[ ! -f $WORK_ROOT/$DESIGN_NAME.cdl.gz || $SIGNOFF_ROOT/$DESIGN_NAME.gds.spice.gz -nt $WORK_ROOT/$DESIGN_NAME.cdl.gz ]]; then
	if [[ ! -f $SIGNOFF_ROOT/$DESIGN_NAME.gds.spice.gz ]]; then
		echo "Could not create cdl file from $SIGNOFF_ROOT/$DESIGN_NAME.gds.spice.gz"
		exit 2
	else
		echo "Creating $WORK_ROOT/$DESIGN_NAME.cdl.gz"
		gunzip -c $SIGNOFF_ROOT/$DESIGN_NAME.gds.spice.gz |
			$LVS_ROOT/tech/$PDK/spi2cdl - |
			gzip -c >$WORK_ROOT/$DESIGN_NAME.cdl.gz
	fi
fi

# Copy default power file to work area if it doesn't exist
#shopt -u nocasematch
#BASE_NAME=${DESIGN_NAME//[A-Z0-9][A-Z0-9]_}  # remove leading 2 byte prefices
if [[ ! -f $WORK_ROOT/cvc.power.$DESIGN_NAME ]]; then
	if [[ -f $LVS_ROOT/tech/$PDK/cvc.power.$DESIGN_NAME ]]; then
		cp $LVS_ROOT/tech/$PDK/cvc.power.$DESIGN_NAME $WORK_ROOT/cvc.power.$DESIGN_NAME
	else
		echo "
ERROR: Could not find $WORK_ROOT/cvc.power.$DESIGN_NAME"
		exit 2
	fi
fi

if [[ ! -f $WORK_ROOT/cvcrc.oeb ]]; then
	if [[ ! -f $WORK_ROOT/cvcrc ]]; then
		if [[ -f $LVS_ROOT/tech/$PDK/cvcrc ]]; then
			cp $LVS_ROOT/tech/$PDK/cvcrc $WORK_ROOT/cvcrc
		else
			echo "
ERROR: Could not create $WORK_ROOT/cvcrc.oeb"
			exit 2
		fi
	fi
	sed 's/cvc.log/cvc.oeb.log/' $WORK_ROOT/cvcrc >$WORK_ROOT/cvcrc.oeb
fi

if [[ "$DESIGN_NAME" == *openframe_project_wrapper ]]; then
	echo "c 6" >$WORK_ROOT/cvc.oeb.script
	for ((i = 0; i <= 43; i++)); do
		cat >>$WORK_ROOT/cvc.oeb.script <<-cvcin
			gn analog_io[$i]
			gn analog_noesd_io[$i]
			gn gpio_analog_en[$i]
			gn gpio_analog_pol[$i]
			gn gpio_analog_sel[$i]
			gn gpio_dm0[$i]
			gn gpio_dm1[$i]
			gn gpio_dm2[$i]
			gn gpio_holdover[$i]
			gn gpio_ib_mode_sel[$i]
			gn gpio_in[$i]
			gn gpio_in_h[$i]
			gn gpio_inp_dis[$i]
			gn gpio_oeb[$i]
			gn gpio_out[$i]
			gn gpio_slow_sel[$i]
			gn gpio_vtrip_sel[$i]
		cvcin
	done
	echo "q" >>$WORK_ROOT/cvc.oeb.script
else
	echo "ERROR: unrecognized top block >$DESIGN_NAME<"
	exit 3
fi

start_time=$SECONDS
cvc_rv -i $WORK_ROOT/cvcrc.oeb <$WORK_ROOT/cvc.oeb.script

cvc_status=$?

runtime=$((SECONDS - start_time))
hours=$((runtime / 3600))
minutes=$(((runtime % 3600) / 60))
seconds=$(((runtime % 3600) % 60))
printf "Runtime: %d:%02d:%02d (hh:mm:ss)\n" $hours $minutes $seconds >>$WORK_ROOT/cvc.oeb.log

if [[ $WORK_ROOT != $LOG_ROOT ]]; then
	cp $WORK_ROOT/cvc.oeb.log $LOG_ROOT/.
fi

awk '
	{ sub(/\r$/, ""); } # Remove windows newlines
	/^> gn / {
		net = gensub(/.*\[([0-9]*)\]/, "\\1", 1);
	}
	/^> gn gpio_inp_dis/ {
		getline; getline;
		GetLevel(inp_dis_connections, inp_dis_min, inp_dis_sim, inp_dis_max, net);
		#print "inp_dis", net, inp_dis_sim[net];
		next;
	}
	/^> gn gpio_in/ {
		getline; getline;
		in_connections[net] += $3 + $5 + $7;
	}
	/^> gn analog_io/ || /^> gn analog_noesd_io/ {
		getline; getline;
		analog_connections[net] += $3 + $5 + $7;
	}
	/^> gn gpio_out/ {
		getline; getline;
		GetLevel(out_connections, out_min, out_sim, out_max, net);
		if ( out_min[net] != "" && out_min[net] == out_sim[net] && out_sim[net] == out_max[net] ) {
			fixed_out[net] = ReplaceLoopback(out_sim[net]);
		}
	}
	/^> gn gpio_oeb/ {
		getline; getline;
		GetLevel(oeb_connections, oeb_min, oeb_sim, oeb_max, net);
		if ( oeb_min[net] != "" && oeb_min[net] == oeb_sim[net] && oeb_sim[net] == oeb_max[net] ) {
			fixed_oeb[net] = ReplaceLoopback(oeb_sim[net]);
		}
	}
	/^> gn gpio_analog_en/ {
		getline; getline;
		GetLevel(analog_en_connections, analog_en_min, analog_en_sim, analog_en_max, net);
	}
	/^> gn gpio_analog_pol/ {
		getline; getline;
		GetLevel(analog_pol_connections, analog_pol_min, analog_pol_sim, analog_pol_max, net);
	}
	/^> gn gpio_analog_sel/ {
		getline; getline;
		GetLevel(analog_sel_connections, analog_sel_min, analog_sel_sim, analog_sel_max, net);
	}
	/^> gn gpio_dm0/ {
		getline; getline;
		GetLevel(dm0_connections, dm0_min, dm0_sim, dm0_max, net);
	}
	/^> gn gpio_dm1/ {
		getline; getline;
		GetLevel(dm1_connections, dm1_min, dm1_sim, dm1_max, net);
	}
	/^> gn gpio_dm2/ {
		getline; getline;
		GetLevel(dm2_connections, dm2_min, dm2_sim, dm2_max, net);
	}
	/^> gn gpio_holdover/ {
		getline; getline;
		GetLevel(holdover_connections, holdover_min, holdover_sim, holdover_max, net);
	}
	/^> gn gpio_ib_mode_sel/ {
		getline; getline;
		GetLevel(ib_mode_connections, ib_mode_min, ib_mode_sim, ib_mode_max, net);
	}
	/^> gn gpio_slow_sel/ {
		getline; getline;
		GetLevel(slow_connections, slow_min, slow_sim, slow_max, net);
	}
	/^> gn gpio_vtrip_sel/ {
		getline; getline;
		GetLevel(vtrip_connections, vtrip_min, vtrip_sim, vtrip_max, net);
	}
	function GetLevel(connections, min, sim, max, net) {
		# 2 blank lines mark the end of a segment
		connections[net] = $3 + $5 + $7;
		while ( NF > 0 ) {
			if ( /^Min path/ ) {
				while ( NF > 0 ) {
					gsub(/_uq\*/, "");
					min[net] = $1;
					getline;
				}
			} else if ( /^Sim path/ ) {
				while ( NF > 0 ) {
					gsub(/_uq\*/, "");
					sim[net] = $1;
					getline;
				}
			} else if ( /^Max path/ ) {
				while ( NF > 0 ) {
					gsub(/_uq\*/, "");
					max[net] = $1;
					getline;
				}
			} else {
				while ( NF > 0 ) {
					getline;
				}
			}
			getline;
		}
	}
	function ReplaceLoopback(net) {
		if ( net ~ /gpio_loopback_zero/ ) return "vssd1";
		if ( net ~ /gpio_loopback_one/ ) return "vccd1";
		return net;
	}
	function GetBit(bit, connections) {
		if ( bit ~ /gpio_loopback_zero/ || bit ~ /vssd*/ ) {
			return "0";
		} else if ( bit ~ /gpio_loopback_one/ || bit ~ /vccd*/ ) {
			return "1";
		} else if ( connections == 0 ) {
			return "x";
		} else {
			return "?";
		}
	}
	function IsLow(signal) {
		return ( signal ~ /vss/ || signal ~ /VGND/ || ( signal != "" && signal == 0 ) );
	}
	function IsHigh(signal) {
		return ( signal ~ /vccd/ || signal ~ /vdd/ || signal ~ /VPWR/ || signal == 1 );
	}
	END {
		GPIO_TYPE["0", "0", "0"] = "USER_STD_ANALOG";
		GPIO_TYPE["0", "0", "1"] = "USER_STD_INPUT_NOPULL";
		GPIO_TYPE["1", "1", "0"] = "USER_STD_OUTPUT";
		GPIO_TYPE["1", "1", "1"] = "USER_STD_INPUT_PULL";
		#print "oeb connections:", length(oeb_connections);
		print " gpio |   in   |   out  | analog |  oeb min/sim/max  | Configuration (dm[2:0] vtrip slow analog[pol,sel,en] ib input_dis holdover)";
		     #"  xx  | xxxxxx | xxxxxx | xxxxxx | vxxx*/vxxx*/vxxx* |
		delete messages;
		for ( i in oeb_connections ) {
			out_min[i] = ReplaceLoopback(out_min[i]);
			out_sim[i] = ReplaceLoopback(out_sim[i]);
			out_max[i] = ReplaceLoopback(out_max[i]);
			oeb_min[i] = ReplaceLoopback(oeb_min[i]);
			oeb_sim[i] = ReplaceLoopback(oeb_sim[i]);
			oeb_max[i] = ReplaceLoopback(oeb_max[i]);
			dm2 = GetBit(dm2_sim[i], dm2_connections[i]);
			dm1 = GetBit(dm1_sim[i], dm1_connections[i]);
			dm0 = GetBit(dm0_sim[i], dm0_connections[i]);
			vtrip = GetBit(vtrip_sim[i], vtrip_connections[i]);
			slow = GetBit(slow_sim[i], slow_connections[i]);
			analog_pol = GetBit(analog_pol_sim[i], analog_pol_connections[i]);
			analog_sel = GetBit(analog_sel_sim[i], analog_sel_connections[i]);
			analog_en = GetBit(analog_en_sim[i], analog_en_connections[i]);
			ib_mode = GetBit(ib_mode_sim[i], ib_mode_connections[i]);
			input_dis = GetBit(inp_dis_sim[i], inp_dis_connections[i]);
			holdover = GetBit(holdover_sim[i], holdover_connections[i]);
			configuration = sprintf("%s%s%s%s%s%s%s%s%s%s%s",
				dm2, dm1, dm0, vtrip, slow, analog_pol, analog_sel, analog_en, ib_mode, input_dis, holdover);
			printf "  %2d  | %6s | %6s | %6s | %5s/%5s/%5s | %s", \
				i, (in_connections[i] == 0) ? "" : sprintf("%6d", in_connections[i]), \
				(i in fixed_out) ? fixed_out[i] : (out_connections[i] == 0) ? "" : sprintf("%6d", out_connections[i]), \
				(analog_connections[i] == 0) ? "" : sprintf("%6d", analog_connections[i]), \
				oeb_min[i], oeb_sim[i], oeb_max[i], configuration;
			gpio_config = GPIO_TYPE[dm2, dm1, dm0];
			if ( gpio_config == "USER_STD_INPUT_PULL" ) {
				if ( IsHigh(out_sim[i]) ) {
					gpio_config = "USER_STD_INPUT_PULLUP";
				} else {
					gpio_config = "USER_STD_INPUT_PULLDOWN";
				}
			}
			if ( gpio_config == "USER_STD_OUTPUT" && IsLow(input_dis) ) {
				gpio_config = "USER_STD_BIDIRECTIONAL";
			}
			printf " -> %s ", gpio_config ? gpio_config : "UNKNOWN";
			error_count = length(messages);
			switch(gpio_config) {
			case /USER_STD_INPUT_NOPULL/: {
				if ( in_connections[i] == 0 ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: missing user input connection", i);
				}
				if ( out_connections[i] == 0 && ! (i in fixed_out) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: output connection required", i);
				} else if ( out_connections[i] > 0 && ! (i in fixed_out) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: output connection to input gpio ignored", i);
				}
				if ( oeb_connections[i] == 0 && ! (i in fixed_oeb) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: oeb connection required", i);
				} else if ( oeb_connections[i] > 0 && ! (i in fixed_oeb) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: oeb connection to input gpio ignored", i);
				}
				if ( analog_connections[i] > 0 ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: analog connection to user input gpio", i);
				}
				break;
			}
			case /USER_STD_INPUT_PULLDOWN/: {
				if ( in_connections[i] == 0 ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: missing user input connection", i);
				}
				if ( ! IsLow(out_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: pulldown mode requires low output", i);
				}
				if ( ! IsLow(oeb_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: pulldown mode requires low oeb", i);
				}
				if ( analog_connections[i] ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: analog connection to user input gpio", i);
				}
				break;
			}
			case /USER_STD_INPUT_PULLUP/: {
				if ( in_connections[i] == 0 ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: missing user input connection", i);
				}
				if ( ! IsHigh(out_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: pullup mode requires high output", i);
				}
				if ( ! IsLow(oeb_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: pullup mode requires low oeb", i);
				}
				if ( analog_connections[i] ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: analog connection to user input gpio", i);
				}
				break;
			}
			case /USER_STD_OUTPUT/: {
				if ( in_connections[i] > 0 ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: input connection to output gpio - undriven", i);
				}
				if ( out_connections[i] == 0 && ! (i in fixed_out) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: missing output connection for output gpio", i);
				} else if ( i in fixed_out ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: output fixed at %s", i, fixed_out[i]);
				}
				if ( oeb_connections[i] == 0 && ! (i in fixed_oeb) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: missing oeb for output gpio", i);
				} else if ( IsHigh(oeb_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: oeb is always high - output disabled", i);
				} else if ( ! IsLow(oeb_min[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: oeb is never low - output never enabled", i);
				}
				if ( analog_connections[i] ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: analog connection to output gpio", i);
				}
				break;
			}
			case /USER_STD_BIDIRECTIONAL/: {
				if ( in_connections[i] == 0 && ! IsLow(oeb_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: missing input connection to bidirectional gpio", i);
				}
				if ( out_connections[i] == 0 && ! (i in fixed_out) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: missing output connection for bidirectional gpio", i);
				} else if ( i in fixed_out && ! IsHigh(oeb_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: output fixed at %s", i, fixed_out[i]);
				}
				if ( oeb_connections[i] == 0 && ! (i in fixed_oeb) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: missing oeb for bidirection gpio", i);
				} else if ( IsHigh(oeb_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: bidirectional oeb is always high - always input mode", i);
				} else if ( IsLow(oeb_sim[i]) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: bidirectional oeb is always low - always output mode", i);
				} else {
					if ( ! IsLow(oeb_min[i]) ) {
						messages[length(messages)+1] = sprintf("GPIO %2d: Warning: oeb is never low - bidirectional output never enabled", i);
					}
					if ( ! IsHigh(oeb_max[i]) ) {
						messages[length(messages)+1] = sprintf("GPIO %2d: Warning: oeb is never high - bidirectional input never enabled", i);
					}
				}
				if ( analog_connections[i] ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: analog connection to bidirectional gpio", i);
				}
				break;
			}
			case /USER_STD_ANALOG/: {
				if ( in_connections[i] > 0 ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: input connection to analog gpio - undriven", i);
				}
				if ( out_connections[i] == 0 && ! (i in fixed_out) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: output connection required", i);
				} else if ( out_connections[i] > 0 && ! (i in fixed_out) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: output connection to analog gpio ignored", i);
				}
				if ( oeb_connections[i] == 0 && ! (i in fixed_oeb) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: oeb connection required", i);
				} else if ( oeb_connections[i] > 0 && ! (i in fixed_oeb) ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: oeb connection to analog gpio ignored", i);
				}
				if ( analog_connections[i] == 0 ) {
					messages[length(messages)+1] = sprintf("GPIO %2d: Warning: missing analog connection to analog gpio", i);
				}
				break;
			}
			default: {
				messages[length(messages)+1] = sprintf("GPIO %2d: Warning: unrecognized dm configuration [%s%s%s]- connectivity confirmation required", i, dm2, dm1, dm0);
				if ( analog_connections[i] > 0 ) {
					if ( in_connections[i] > 0 || (out_connections[i] > 0 && ! (i in fixed_out)) ) {  # both digital and analog
						messages[length(messages)+1] = sprintf("GPIO %2d: Warning: both analog and digital connections", i);
					} else if ( ! ( oeb_sim[i] ~ /vccd/ || oeb_sim[i] ~ /vdd/ ) ) {  # analog signals should disable output
						messages[length(messages)+1] = sprintf("GPIO %2d: Warning: oeb expected high for analog", i);
					}
				} else if ( in_connections[i] > 0 && out_connections[i] == 0 ) {
					if ( ! ( oeb_sim[i] ~ /vccd/ || oeb_sim[i] ~ /vdd/ ) ) {  # input only signals should never enable output
						messages[length(messages)+1] = sprintf("GPIO %2d: Warning: oeb expected high for input only", i);
					}
				} else if ( out_connections[i] > 0 && ! (i in fixed_out) ) {
					if ( oeb_min[i] !~ /vss/ ) {  # output signals must be enable-able
						messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: oeb must have possible low for output", i);
					} else if ( in_connections[i] > 0 && oeb_sim[i] ~ /vss/ ) {  # input is always driven by output
						messages[length(messages)+1] = sprintf("GPIO %2d: Warning: output always drives input. sky130: OK for pull up/down.", i);
					}
				}
			}
			}
			if ( configuration ~ /x/ ) {
				messages[length(messages)+1] = sprintf("GPIO %2d: ERROR: all gpio configuration signals must be connected", i);
			} else if ( ! IsLow(vtrip) || ! IsLow(slow) || ! IsLow(analog_pol) || ! IsLow(analog_sel) || ! IsLow(analog_en) || ! IsLow(ib_mode) || ! IsLow(holdover) ) {
				messages[length(messages)+1] = sprintf("GPIO %2d: Warning: unrecognized configuration - expected vtrip, slow, analog*, ib, hold to be low", i);
			}
			new_errors = length(messages) - error_count;
			if ( new_errors > 0 ) {
				printf("%d warnings/errors", new_errors);
			}
			printf "\n";
		}
		if ( length(messages) == 0 ) {
			printf("\nNo warnings or errors detected\n");
		} else {
			printf("\n*** Detected the following warnings and/or errors: ***\n");
		}
		for ( line = 1; line <= length(messages); line++) {
			print messages[line];
		}
	}' $LOG_ROOT/cvc.oeb.log |
	tee $WORK_ROOT/cvc.oeb.report

if [[ $WORK_ROOT != $SIGNOFF_ROOT ]]; then
	cp $WORK_ROOT/cvc.oeb.report $SIGNOFF_ROOT/cvc.oeb.report
fi

if [[ $(wc -l < $SIGNOFF_ROOT/cvc.oeb.report) -lt 10 ]]; then
	# CVC finished abnormally
	exit 5

elif [[ $cvc_status -eq 0 && $(grep -c '^CVC: End:' $LOG_ROOT/cvc.oeb.log) -ne 1 ]]; then
	# CVC finished abnormally
	exit 5

elif [[ $cvc_status -eq 0 && $(grep -c ERROR $SIGNOFF_ROOT/cvc.oeb.report) -gt 0 ]]; then
	# CVC finished normally, but detected errors
	exit 6

elif [[ $cvc_status -eq 0 && $(grep -c Warning $SIGNOFF_ROOT/cvc.oeb.report) -gt 0 ]]; then
	# CVC finished normally, but detected warings
	exit 4

else
	exit $cvc_status
fi
