Coding the Macro

This chapter covers the actual coding of the macro, based on the pseudocode created in the previous chapter. The macro code will be developed in sections, to make it easier to understand and to test. Each additional feature will be integrated into the macro code written for the previous feature.

This macro will make use of a number of built-in macro commands and functions. However, the focus of this tutorial is on the actual syntactical coding of a macro, not on describing each and every macro command. From the Learning SmartCAM table of contents, refer to the Automation/Customization - MCL Reference section, the child documents Reference: Macro Commands and Reference: Macro Functions topics contain descriptions of the macro commands.

The macro will be named: C:\MACROS\PATHLEN.MCL

Initial Checks

There are two conditions that the macro needs to validate before jumping into the main loop and scanning the database. In pseudocode these are:

// Check that a process model is loaded. If not error out
IF last element number is 0, no file is loaded
   Display error: Must have an open PM model with geometry
   Exit macro
ENDIF
			
// Make sure the output file path is valid and usable. Otherwise no point in continuing
IF outputfile can not be opened for Writing
   Display error: Output path invalid or read-only. Try another path
   Exit macro
ENDIF

From the textual description (the pseudocode) it is easy to see there are two checks that need to be made at the start of the file.

When comparing the actual MCL code to the pseudocode, notice there is not a direct one to one mapping between the two. The pseudocode is a high-level description of the functionality, not a step-by-step human language version of the macro.

Some variables will need to be removed, at the start of the macro. This way you can be sure that any existing variables with the same name do not interfere with the proper execution of the macro. They can be removed individually, such as the following:

VAR_REMOVE[VN="ErrMsg"]
VAR_REMOVE[VN="FileOpen"]

Or, perhaps a better way, is to add a prefix to the variable name. Then use the VAR_REMOVE_LIST[LST=""] macro command to remove them all with one command. We will use the underscore (_) character as the prefix in these examples. You are able to use the * wildcard, which matches for all strings, as part of the argument. So, VAR_REMOVE_LIST[LST="_*"] removes all variables that begin with the underscore character.

Here is the generated MCL code:

// PATHLEN.MCL
// Calculates toolpath lengths, for each used STEP, in current PM file.
// Outputs lengths to a ASCII text report file

// User Variables
STRING:#OUTFILE="C:\\TEMP\\PATHLEN.TXT"   // path\fname.ext of output file

// System Variables
VAR_REMOVE_LIST[LST="_*"]

STRING:#_ErrMsg		   // contains error message text
INTEGER:#_FileOpen = 0	// is the output file open? Default is FALSE (0). TRUE is 1

// Check that a process model is loaded. If not error out
IF (TOTEL() = 0)
   // No geometry - an empty file. Error out
   #_ErrMsg = "Macro requires an open Process Model file."
   GOTO(ERR)
ENDIF

// Make sure output file path is valid and usable.
// Open file in WRITE mode. If problems, error out
F_OPEN[FN=#OUTFILE, TY="W"]   // opens file for writing

// Was there an error opening the file?
IF (F_ERROR() <> 0)
   // There was an error. Display text and exit
   #_ErrMsg = "Error Opening File: " + #OUTFILE + "\n" + F_ERRSTR()
   GOTO(ERR)
ELSE
   #_FileOpen = 1
ENDIF


// Error handling
@ERR
// Display the error text and then exit the macro
PAUSE[TX=#_ErrMsg, SR=3]   // display 3 row message box
GOTO(DONE)

// Exit the macro
@DONE
// Is the Output file still open? If so, close it before exiting
IF (#_FileOpen = 1)
   F_CLOSE[FN=#OUTFILE]
ENDIF

A few things to take note of in this code. First, the macro starts with a header (in comments) that explains what the macro is supposed to do.

The #OUTFILE variable contains the fully qualified path and filename for the output report file. To change the path or name, modify this variable. Use a variable for these tasks so that later the location and name can be changed by modifying a single variable. Not modifying a number of inline references - this will be more clear when the code for writing the report is generated.

The #_ErrMsg variable contains the error message text. A choice was made regarding error messages. In this macro, all errors are handled in one location - the @ERR section. This is a preference; it would have been valid to display the error message text in the same area as the error itself. However, that would have entailed having a number of different PAUSE[] statements scattered throughout the macro. A choice was made to consolidate into one location, so that there is only one PAUSE[] and all the error text content is passed using a variable.

The #_FileOpen variable keeps track of whether or not the Output file is opened for writing. There is only once case where this is important, the second test to see whether the file could be opened for writing. If this test failed, it would jump to the @ERR section and then the @DONE section. These are shared so there needed to be a way of tracking whether or not the Output file was closed.

In the real-world, the programmer would likely just close the output file in the same IF statement block where the test was run. This would negate the need for #_FileOpen. An exception was made in this case to give the reader an example of how flag variables are used.

#_FileOpen is a flag variable. It exists solely to give some sort of status information - in this instance, whether or not a file is opened.

VAR_REMOVE_LIST[] is used to remove variables from the SmartCAM variable list. This is done do make sure all variables defined and used in the macro are of the correct data type and not previously defined by other macros or other instances of this macro being run.

Main Loop

The "Main" loop is where the SmartCAM database is traversed and each element is checked and/or tallied. As indicated in the pseudocode, the main loop will contain two inner loops; one to check whether the current Step number has already been tallied, and a second to march through the Process Model element data and total up the lengths of elements with a given Step ID.

The pseudocode for this main loop is as follows:

// The main routine.
LOOP through entire database, starting at element 1
   Is current element a layer?
	  IF Yes:
		 Get Step number and description
		 
		 // Don't check the same Step number more than once
		 Is Step number already checked?
		 IF No,
			Add Step and Description to list of checked steps
			
			// From the current Element position, check each remaining element and see if it is the
			// same step number as current
			LOOP from current element to End of Elements
			   Check each element
			   IF Layer
				  skip
			   ENDIF
			   IF Step
				  Get Step number
				  IF Step matches current Step
					 Get length and add to running total
				  ENDIF
			   ENDIF
			END LOOP
		 ELSE
			// Step is already checked, skip it and move to the next element
		 ENDIF
   ELSE If No
	  Nothing to do, move to next element
   ENDIF
END LOOP

Inside the main loop, as covered, there are two inner loops. The first is to see whether or not a given Step ID has already been checked. The second marches through the database looking for elements with matching Step numbers and then totals the lengths.

Step Already Checked

This macro will use two arrays to keep track of data. The first is an INTEGER array that stores the Step Numbers; it is #_StepIDArray[]. When an element is checked, if it is a Step, the routine reads each element in this array to see if it exists. If it does not exist, it is considered a new Step and the system drops into the second loop - the loop that checks for other elements with the same Step number. If it is in the array, that means this Step number has already been checked and no further effort is needed.

The loop used to check and see if a Step has already been tallied is:

	  // Have we already checked this step?
	  #_TmpCtr = 0
	  WHILE (#_TmpCtr <= #_CurArrayPtr)
		 IF (#_ElmtStep = #_StepIDArray[#_TmpCtr])
			// It matches. This Step has been checked, set StepScan flag and jump out of loop
			#_StepScan = 0   // do not scan for this step
			   GOTO(ExitChkLoop)
			ELSE
			   #_TmpCtr = #_TmpCtr + 1
			ENDIF
		 ENDW

		 // Loop ran thru entire array. No matches, this is a new Step
		 #_StepIDArray[#_CurArrayPtr] = #_ElmtStep   // save the step number
		 #_StepScan = 1   // scan for other elements of this Step
		 #_CurrStep = #_ElmtStep   // This is the step ID to check for

// Label used as destination to jump out of WHILE Loop
// When checking to see if Step already totalled up
@ExitChkLoop
   ENDIF

#_CurArrayPtr is a counter that keeps track of the number of elements in the Step ID array. So the loop starts as 0 and checks to the end of the array. If there is a matching Step number in the array, then a GOTO is used to jump out of the test.

If no match is found, that means this is a new Step Number. So the #_StepIDArray[] is updated with the new Step Number. A flag variable #_StepScan is set to 1 which means the rest of the Process Model needs to be scanned for elements with matching Step numbers.

Elements with same Step Number

The second inner loop scans the Process Model database looking for other elements with the same Step number. When found, the length is tallied and stored in a second array: #_StpLenArray[].

The database is only scanned when the flag variable #_StepScan is set to 1. And #_StepScan is only set to one when there is a new Step to check for.

The following is the MCL code to handle this scan:

   // If StepScan is TRUE (1) then scan for other elements with same STEP as
   // #_ElmtStep. Get length and add to array
   IF (#_StepScan = 1)
	  // Loop thru remaining PM data
	  #_TmpCtr = #_CurrCntr
	  WHILE (#_TmpCtr <= TOTEL())
		 #_ElmtStep = STEP(#_TmpCtr)   // returns Step # or -1 if Layer

		 IF (#_ElmtStep = #_CurrStep)
			// They match. Get length and add to running total
			#_StpLenArray[#_CurArrayPtr] = #_StpLenArray[#_CurArrayPtr] + LENE(#_TmpCtr)
		 ENDIF

		 // Increment counter
		 #_TmpCtr = #_TmpCtr + 1
	  ENDW

	  // Given STEP has been totaled. Increment counters
	  #_CurArrayPtr = #_CurArrayPtr + 1
	  #_StepScan = 0   // turn this off until needed again	
   ENDIF

Notice the inner loop is not starting at element 1 - the first element in the PM database. Since each element is checked as it is found, in the main loop, the inner loop only needs to scan from the current position in the database to the end. Elements prior to the current location have already been checked.

Output

The last section of the macro is the output, in this instance a report. The report displays the Step Number and the total length of geometry for that step. The Steps are output in sequence order, meaning the first step found in the database is the first step output.

At the end of the report the combined total cut length is listed.

//   Database scanned and ready to report
#_CurrCntr = 0

WHILE(#_CurrCntr < #_CurArrayPtr)
   // F_WRITE does not currently work with ARRAYS. Store data in temporary variables then output
   #_IOUTPUT = #_StepIDArray[#_CurrCntr]
   #_DOUTPUT = #_StpLenArray[#_CurrCntr]
   F_WRITE[FN=#OUTFILE, FMT="Step: %I : Length = %D3.4~n", VR="IOUTPUT, DOUTPUT"]

   // Calculate combined total
   #_LenTotal = #_LenTotal + #_StpLenArray[#_CurrCntr]
   #_CurrCntr = #_CurrCntr + 1
ENDW

// Output total
F_WRITE[FN=#OUTFILE, FMT="Total Length: %D3.4~n", VR="LenTotal"]

// Done
GOTO(Done)

Output consists simply of writing a formatted output string to the open text file.

The routine simply starts at position 0, in the two arrays, and increments to the last elements in the arrays. At the same time, a counter is updated that totals up all the different cut lengths into a total for all the steps in the file.

Finally, a GOTO is used to jump down to the end of the macro to exit. This is needed to avoid running the @ERR code.

The Finished Macro

The following is the finished macro. This macro code shows all the variables needed and the related VAR_REMOVE[] commands used to make sure each variable is declared anew. The main loop is shown, which simply incrementally steps down through the process model element data, one element at a time. Then allows the two inner loops to handle the bulk of the work.

// PATHLEN.MCL
// Calculates toolpath lengths, for each used STEP, in the current PM file.
// Output lengths to a ASCII text report file 

// User variables
STRING:#OUTFILE="C:\\TEMP\\PATHLEN.TXT"	// path/fname of output file

//	System variables
VAR_REMOVE_LIST[LST="_*"]

STRING:#_ErrMsg			 // contains error message text
INTEGER:#_FileOpen = 0	  // is the output file open? Default is False (0). TRUE is 1
INTEGER:#_CurrCntr=1		// current element in main loop
INTEGER:#_CurrStep		  // Current Step id
INTEGER:#_ElmtStep		  // Step of just read element
INTEGER:#_CurArrayPtr=0	// Point to element in array to update
INTEGER:#_TmpCtr=0		  // Temporary counter
INTEGER:#_StepScan=0		// Should macro scan for matching elements?
INTEGER:#_IOUTPUT=0		 // Integer temp variable for holding numbers for F_WRITE
DECIMAL:#_LenTotal=0.0	  // Total length of all steps combined
DECIMAL:#_DOUTPUT=0.0	   // Decimal temp variable for holding numbers for F_WRITE

INTEGER:#_StepIDArray[] = ALLOCATE(500, 0)
DECIMAL:#_StpLenArray[] = ALLOCATE(500, 0.0)

// Initial Checks

// First, check and see if there is an open Process Model and that it has geometry
IF (TOTEL() = 0)
   // No geometry - so its an empty file. Error out.
   #_ErrMsg = "Macro requires an open Process Model file."
   GOTO(ERR)
ENDIF

// Next, open the output file. If problems, error out.
F_OPEN[FN=#OUTFILE, TY="W"]	// opens file for writing

// Was there a problem opening the file?
IF (F_ERROR() <> 0)
   // There was an error. Build the display message and exit
   #_ErrMsg = "Error Opening File: " + #OUTFILE + "\n" + F_ERRSTR()
   GOTO(ERR)
ELSE
   // File opened. Set FileOpen flag to true
   #_FileOpen = 1
ENDIF

// Main Loop
WHILE(#_CurrCntr <= TOTEL())
   // Is current element a STEP? If so, get number
   #_ElmtStep = STEP(#_CurrCntr) // returns Step # or -1 if layer
   IF (#_ElmtStep > 0)
	  // The element is a Step. See if Step is already totalled up. If so, skip
	  // if not, scan the remaining database for other matching STEP elements
	
	  // Have we already checked this step?
	  #_TmpCtr = 0
	  WHILE (#_TmpCtr <= #_CurArrayPtr)
		 IF (#_ElmtStep = #_StepIDArray[#_TmpCtr])
			// It matches. This Step has been checked, set StepScan flag and jump out of loop
			#_StepScan = 0   // do not scan for this step
			GOTO(ExitChkLoop)
		 ELSE
			#_TmpCtr = #_TmpCtr + 1
		 ENDIF
	  ENDW

	  // Loop ran thru entire array. No matches, this is a new Step
	  #_StepIDArray[#_CurArrayPtr] = #_ElmtStep   // save the step number
	  #_StepScan = 1   // scan for other elements of this Step
	  #_CurrStep = #_ElmtStep   // This is the step ID to check for

// Label used as destination to jump out of WHILE Loop
// When checking to see if Step already totalled up
@ExitChkLoop
   ENDIF

   // If StepScan is TRUE (1) then scan for other elements with same STEP as
   // #_ElmtStep. Get length and add to array
   IF (#_StepScan = 1)
	  // Loop thru remaining PM data
	  #_TmpCtr = #_CurrCntr
	  WHILE (#_TmpCtr <= TOTEL())
		 #_ElmtStep = STEP(#_TmpCtr)   // returns Step # or -1 if Layer

		 IF (#_ElmtStep = #_CurrStep)
			// They match. Get length and add to running total
			#_StpLenArray[#_CurArrayPtr] = #_StpLenArray[#_CurArrayPtr] + LENE(#_TmpCtr)
		 ENDIF

		 // Increment counter
		 #_TmpCtr = #_TmpCtr + 1
	  ENDW

	  // Given STEP has been totaled. Increment counters
	  #_CurArrayPtr = #_CurArrayPtr + 1
	  #_StepScan = 0   // turn this off until needed again	
   ENDIF

   // Done with this element, increment to the next
   #_CurrCntr = #_CurrCntr + 1
ENDW

// Database scanned and ready to report
#_CurrCntr = 0

WHILE(#_CurrCntr < #_CurArrayPtr)
   // F_WRITE does not currently work with ARRAYS. Store data in temporary variables then output
   #_IOUTPUT = #_StepIDArray[#_CurrCntr]
   #_DOUTPUT = #_StpLenArray[#_CurrCntr]
   F_WRITE[FN=#OUTFILE, FMT="Step: %I : Length = %D3.4~n", VR="IOUTPUT, DOUTPUT"]
	
   // Calculate combined total
   #_LenTotal = #_LenTotal + #_StpLenArray[#_CurrCntr]
   #_CurrCntr = #_CurrCntr + 1
ENDW

// Output total
F_WRITE[FN=#OUTFILE, FMT="Total Length: %D3.4~n", VR="LenTotal"]

// Done
GOTO(Done)

@ERR
// If jump here, there is an error. Display it and exit
PAUSE[TX=#_ErrMsg, SR=3]
GOTO(DONE)

@DONE
// Is the File still open?
IF (#_FileOpen = 1)
   F_CLOSE[FN=#OUTFILE]
ENDIF
PAUSE[TX="The macro is complete.", SR=1]

Enhancements

The macro described above can be considered just a framework. There are a number of enhancements and improvements that can be made to it. Taking an existing framework and using it as the basis for further development is called Code Reuse and it is a common and powerful programming technique.

The following are some ideas about how this macro can be extended. The actual extensions will be left as an exercise for the reader.

The example macro handles looping, branching, arrays, variable declarations, file input/output, status variables, and counters. With these simple techniques, the only real limitation on the types of utility macros that can be created is imagination.

Related Topics

Testing the Macro

Designing the Macro

From The Beginning Overview

SmartCAM Automation Overview