Friday, September 19, 2008

Batch Control Language (BCL)

QUST has two macro/scripting languages, SCL and BCL. BCL is short for Batch Control Language, and it is basically there to let you automate almost everything you can do through the QUEST User Interface. This automation can be done through straight-up BCL scripts, which are plain-text files containing a series of BCL formatted commands. An example of such a command is:

CREATE SOURCE CLASS 'Source1'

which will create a new source class named Source1. This will also create one element in the Source1 class. You can locate this element using the follwing command:

LOCATE ELEMENT 'Source1_1' AT 100 , 100 , 0

This kind of scripting can be tedious and error-prone. The good thing is, almost all BCL commands are supported for use through SCL programming. This means you can write an SCL script to read a list of element names and locations, and execute BCL calls to create and locate those elements.

To use BCL within an SCL script, you use the BCL( command_text : String ) : Integer function. All you do is provide a string argument that contains your BCL command, and SCL will have QUEST execute it. The BCL function returns an integer containing the error code, so you can know whether or not the command executed properly. A list of error code numbers and their corresponding descriptions can be found in the bclerr.inc file in your QUESTlib\Include folder.

It's also possible to have QUEST execute a BCL script as soon as the QUEST.exe program starts. Details for setting up QUEST to run this way from Excel can be found here.

So, assuming you've set up QUEST to run a BCL script generated in Excel, you may now be wondering how to generate BCL scripts in Excel. I have set up a number of Excel VBA User Defined Functions (UDFs) that can be used to generate a subset of all the BCL commands. I'm currently working on building a version of this that I can release for everyone to use, but until then why not start building your own BCL UDFs?

upper & lower case

Sometimes in writing QUEST logics, especially those that deal with user input or reading from text files, I've had to compare two pieces of text for equality where the case of the text wasn't an issue.  The thing with SCL is that text comparisons are case sensitive, which meant I needed a way to convert a text string to either all caps or all lower case characters.  I never found anything for this in QUEST's SCL docs, so I made my own, and here they are.  The routines work by reading each character in a string individually, and seeing if it's already in the desired case.  If not, we just add or subtract 32 to/from its ASCII value to convert between upper and lower case characters in ASCII text.


routine is_lower( the_string : String ) : Integer
Const
   first_lower 97
   last_lower 122
   upper_to_lower 32
Var
   the_char : String  
   result : Integer
Begin
   the_char = leftstr( the_string , 1 )
   if( asc( the_char ) <= last_lower AND asc( the_char ) >= first_lower ) then
      result = true
   else
      result = false
   endif
   return result
End

routine is_upper( the_string : String ) : Integer
Const
   first_upper 65
   last_upper 90
   upper_to_lower 32
Var
   the_char : String  
   result : Integer
Begin
   the_char = leftstr( the_string , 1 )
   if( asc( the_char ) <= last_upper AND asc( the_char ) >= first_upper ) then
      result = true
   else
      result = false
   endif
   return result
End          

routine upper( the_string : String ) : String  
--Return upper case version of the_string
Const
   first_lower 97
   last_lower 122
   upper_to_lower 32
Var
   i : Integer
   result , check_char : String  
Begin
result = ''
for i = 1 to len( the_string ) do
   check_char = substr( the_string , i , 1 )
   if( is_lower( check_char ) ) then
      check_char = chr( asc( check_char ) - 32 )
   endif
   result = result + check_char
endfor
return result
End

routine lower( the_string : String ) : String  
--Return upper case version of the_string
Const
   first_lower 97
   last_lower 122
   upper_to_lower 32
Var
   i : Integer
   result , check_char : String  
Begin
result = ''
for i = 1 to len( the_string ) do
   check_char = substr( the_string , i , 1 )
   if( is_upper( check_char ) ) then
      check_char = chr( asc( check_char ) + 32 )
   endif
   result = result + check_char
endfor
return result
End

Inquire Element Location

The QUEST BCL Language has a command called "Inquire Element Location", which just takes the specified element's location and prints it to the BCL output window.  It may not seem at first to be of much use there, but we can make use of a special variable declaration in SCL to access this output text.

To illustrate using the BCL output from SCL here's an example SCL macro which will print the location of every element in the current model to a tab-delimited text file, and launch that file for viewing in Excel.  I have another macro which I may share that will let you read this format back into QUEST, modifying element locations as read from the text file.

Handling 6 Degree of Freedom data in SCL

QUEST elements are located in 3D space in the QUEST world, and can be rotated about 3 axes, giving a total of six pieces of information required to completely locate some element in 3D space in QUEST.  In order to give our SCL code some degree of modularity and elegance, we need an easy way of passing all the location information.

We can settle for just passing all six parameters around every time we want to work with QUEST locations.  Over time this can get old, especially if you work a lot with locating elements in QUEST.  Also it helps to have a good methodology in place for dealing with locations, like having a routine for pulling the location of an element, or a procedure for locating an element with little hassle.

The solution I use is to have an SCL Structure for a 3D location.  From the QUEST SCL docs:

A structure is a group of variables which can be manipulated collectively or individually. A structure is analogous to a record which consists of many fields. 

What this means to me is that I can have as much information stored in a structure as I need, and move that information around by passing just a single variable.

The name I chose for my 3D location structure was "spot", and here's how I've defined a spot in SCL:

structure spot
x : Real
y : Real 
z : Real
yaw : Real
pitch : Real
roll : Real
elem_there : Element
endstructure

Basically the spot is just keeping track of 3D location and orientation, storing these values as type real.  I also have a handle to an element, which you would set when inquiring about en element's location.  My thinking there was that some day I might need a way to populate locations in a QUEST model without overlapping items, so I added the elem_there field.

One thing to note about using structures, is that you can work with a structure variable, or a pointer to a structure variable.  Those of you with experience in C probably know the distinctions and caveats to each technique, but I usually use a pointer to a structure, just because it's a little less confusing thinking about whether or not the structure I'm using is what I think it is.

Moving forward, we now need a way to inquire the location of an element in QUEST and return a pointer to a spot structure, after populating a spot.  The routine itself makes use of a bcl command called "INQUIRE ELEMENT LOCATION", which simply places the location of the element in a comma-delimited text string on the BCL command output window.

Accessing the text in the BCL command output window is pretty simple, just declare a variable at the top of your scl file in a declaration section called "BCL_VAR".  The string to declare for the BCL command output window is "bcl_msg : String".  The only other BCL_VAR is "bcl_status : Real" which will tell you the result of a BCL command.  More on that another time.

So your BCL_VAR declaration should look like this:

BCL_VAR
   bcl_msg : String

Now for the inquire_loaction routine:
routine inquire_location( the_element : Element ) : @spot
Const
   the_delim ','
Var
   bcl_err : Integer
   the_loc : String
   result : @spot
Begin
   result = new()
   if( the_element <> NULL ) then
      bcl_err = BCL( "INQUIRE ELEMENT '" + the_element->name + "' LOCATION" )
      the_loc = bcl_msg
      parse_str( the_loc , the_delim , result->x , result->y , result->z , result->yaw , result->pitch , result->roll )
   endif
   return result
End

A few things to note:
  • result is declared as being of type @spot, which is a pointer to a spot structure
  • We make use of the new() routine to allocate memory for a spot structure.  We have to remember to free this portion of memory later using the free() procedure.
  • We make use of the parse_str routine for parsing the string into chunks for us.  This is a built in routine in SCL.
  • We use the -> symbol as a member pointer rather than a dot because we're working with a pointer to a structure.  If we were using a straight structure, we'd us a dot.
The routine basically just executes the "INQUIRE ELEMENT LOCATION" BCL command, extracts the output text from the BCL output window, and parses it to a new pointer to a spot structure, and returns the pointer to the calling function.

To tie it all together, see the write_elem_locs.scl file to see how we can build a tab-delimited text file of all our element names and locations, and launch the file in Excel.

You can execute the macro by saving it to an SCLMACROS folder in a QUEST library, or you can compile the file and execute the write_all_locations function through BCL.