Digital Thread
Dave Loffredo
2017-10-31

Case Study: Add CAM process to the Digital Thread

Our Digital Thread Gateway regularly brings in CAM process from Catia, NX, and Mastercam, but decades of unique special-purpose planning software is used deep within organizations around the world.

Let's make a simple interface for CAM process. Many legacy systems use APT concepts, so we use that as our starting place. We will use C#, but we could also use Javascript and Node.js, or Visual Basic. First we create a new digital thread model, and fill in some basic information like units. We also create a few API helper objects. [Download the whole program]

// Create simple digital thread process.  This C# project
// calls the STEP-NC API using the .NET interface. 
//
AptStepMaker apt = new AptStepMaker();	// API objects
Process pro = new Process();
Feature fea = new Feature();
Tolerance tol = new Tolerance();

// start the program
apt.PartNo("Makino part");
fea.OpenNewWorkpiece("Makino part");

// Define a tool
apt.Millimeters();
apt.CamModeOn();
apt.DefineTool(0.25, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0);

Next, we will describe an operation. We can describe some tool motions, and then associate some higher level information with the operation, like a name and a feature. The tool paths are handled by a function that we will discuss later.

// Read toolpath data, we will discuss this later
ParseClfile("Plane_1.cldata", apt, pro);

// Define feature and operation
long ws_id = apt.GetCurrentWorkingstep();
pro.PlaneFinishMilling(ws_id, 0, 3);
fea.PlanarFace(ws_id, "first plane", 0, 40, 25);
apt.SetName(ws_id, "First planar facing operation");

Associativity is the heart of the digital thread, so we next improve the process by merging in a CAD model for our finished product and creating a simple block model for our initial source material.

// Read workpiece geometry as STEP
apt.Workpiece("hole_model.stp");

// Make geoemtry for Rawpiece
long wp_id = apt.GetCurrentWorkplan ();
apt.MakeRawBox (wp_id, -50, -25, 0, 50, 25, 31);

We improve the process again by describing the tool geometry in greater detail. Here we generate a shape, but we could merge in a CAD model for the tool instead. We also merge in a CAD model for part fixture and save the whole rich digital thread process.

// Make geometry for cutter
long tl_id = apt.GetCurrentTool ();
pro.SetToolLength(tl_id, 100);
apt.SetToolIdentifier(apt.GetToolNumber(tl_id), "Makino FFCAM tool");
apt.GenerateToolGeometry(tl_id);
pro.SetToolOverallAssemblyLength(tl_id, 80);

apt.Fixture("Example_vise.stp");
long fixture_id = apt.GetCurrentFixture();
apt.PutWorkpiecePlacement(fixture_id, 0, -25, 41.5, 0, 1, 0, 1, 0, 0);

apt.SaveAsModules("sample_thread_process");

There are many places to expand this and link more information. Tolerances, and links to shape details are new opportunities opened by the thread. Studies have shown that manufacturing creates ten files for every one file created by design, each one of which contains extra value that can improve the digital thread.

Creating Detailed Tool Moves

Individual tool moves are often described using text files inspired by the 1960's APT language. An example of these sort of commands is show below:

PPRINT/BEGIN-SECTION,DEFINE
UNITS/MM
SET/MODE,ABSOL
PPRINT/BEGIN-SECTION,PROCESS-START-PATH
PPRINT/MOTION,FROM,TYPE,DEFAULT
FROM/0.0000000,0.0000000,50.0000000,0.0000000,0.0000000,1.0000000
SELECT/TOOL,45
LOAD/TOOL,45
RAPID
GOTO/0.0000000,0.0000000,50.0000000
SPINDL/RPM,1310,CLW
RAPID
GOTO/5.6943646,-2.1512949,50.0000000
FEDRAT/PERMIN,141
GOTO/5.6943646,-2.1512949,28.0000000
CIRCLE/1.5623138,0.6640481,28.0,-0.0,-0.0,-1.0,5.0,0.002,0.5,10.0,0.0

Whatever the source, we can harvest and load the movement information into the digital thread. The function below is a simple parser for APT-style commands that you can adapt for your own situation.

We begin by opening the file and reading it line by line. A switch statement handles each of the keywords and calls the appropriate action. We discuss individual actions below. You can also examine them in the complete program.

static void ParseClfile(string file_name, AptStepMaker apt, Process pro)
{
    StringBuilder sb = new StringBuilder();
    if (!File.Exists(file_name)) return;
    using (StreamReader sr = new StreamReader(file_name, Encoding.UTF8))
    {
	while (sr.Peek() >= 0)
	{
	    string stBuffer = sr.ReadLine();

	    string[] parts = stBuffer.Split('/');
	    switch (parts[0])
	    {
		case "FROM":   /* define setup */        break;
		case "GOTO":   /* linear move */ 	 break;
		case "CIRCLE": /* arc move */  		 break;
		case "RAPID":  /* set feed to rapid */ 	 break;
		case "FEDRAT": /* set feed to value */   break;
		case "SPINDL": /* set spindle speed */ 	 break;
		case "SELECT": /* tool changing */  break;
		case "LOAD":   /* tool changing */  break;
		case "UNITS":  /* specify units */ break;
		case "SET":    /* modal states */ break;
		case "PPRINT": /* user-defined information */ break;

		default:
		    Console.WriteLine(stBuffer);
		    break;
	    }
	}
    }
}

The FROM statement defines a workpiece setup. It has additional parameters for the setup origin and the Z axis direction. We call the WorkplanSetup() function to define this coordinate system transform, and use (1,0,0) as the X axis direction.

case "FROM":
    string[] fnumbers = parts[1].Split(',');
    double fx = double.Parse(fnumbers[0]);
    double fy = double.Parse(fnumbers[1]);
    double fz = double.Parse(fnumbers[2]);
    double fi = double.Parse(fnumbers[3]);
    double fj = double.Parse(fnumbers[4]);
    double fk = double.Parse(fnumbers[5]);
    if (fk != 1)
    {
	long plan_id = apt.GetCurrentWorkplan();
	apt.WorkplanSetup(plan_id, fx, fy, fz, fi, fj, fk, 1, 0, 0);
    }
    break;

The GOTO statement is the work-horse of APT, and describes a linear move to a new coordinate. There is also a multi-axis version of the command that includes an ijk tool axis vector.

case "GOTO":
    string[] numbers = parts[1].Split(',');
    double x = double.Parse(numbers[0]);
    double y = double.Parse(numbers[1]);
    double z = double.Parse(numbers[2]);
    apt.GoToXYZ("", x, y, z);
    break;

The CIRCLE statement describes an arc move, with a center point, axis, radius and more. It is normally followed by a GOTO with the endpoint of the arc move.

case "CIRCLE":
    string[] cnumbers = parts[1].Split(',');
    double cx = double.Parse(cnumbers[0]);
    double cy = double.Parse(cnumbers[1]);
    double cz = double.Parse(cnumbers[2]);
    double ci = double.Parse(cnumbers[3]);
    double cj = double.Parse(cnumbers[4]);
    double ck = double.Parse(cnumbers[5]);
    double radius = double.Parse(cnumbers[6]);
    double tol = double.Parse(cnumbers[7]);
    double tol2 = double.Parse(cnumbers[8]);
    double tool_diameter = double.Parse(cnumbers[9]);
    double tool_radius = double.Parse(cnumbers[10]);

    // set tool radius and diameter from circle data
    long tool_id = apt.GetCurrentTool();
    pro.SetToolCornerRadius(tool_id, tool_radius);
    pro.SetToolDiameter(tool_id, tool_diameter);

    string next_line = sr.ReadLine();
    string[] next_parts = next_line.Split('/');
    if (next_parts[0] != "GOTO")
    {
	Console.WriteLine("Error Parsing GOTO following circle:" + next_line);
    }
    else
    {
	string[] enumbers = next_parts[1].Split(',');
	double ex = double.Parse(enumbers[0]);
	double ey = double.Parse(enumbers[1]);
	double ez = double.Parse(enumbers[2]);
	if (ck == 1)
	    apt.ArcXYPlane("", ex, ey, ez, cx, cy, cz, radius, false);
	else if (ck == -1)
	    apt.ArcXYPlane("", ex, ey, ez, cx, cy, cz, radius, true);
	else
	    Console.WriteLine("Error Parsing axis for circle:" + stBuffer);
    }
    break;

The RAPID and FEDRAT statements change the commanded feed rate of the machine. The FEDRAT also has unit information associated with it. The STEP-NC API that we are using was designed to be convenient for use with APT, so it remembers state information, like this feedrate when creating new moves.

case "RAPID":
    apt.Rapid();
    break;

case "FEDRAT":
    string[] fparms = parts[1].Split(',');
    if (fparms[0] == "PERMIN")
    {
	double feed = double.Parse(fparms[1]);
	apt.FeedrateUnit("mmpm");
	apt.Feedrate(feed);
    }
    else
    {
	Console.WriteLine("Unknown feed unit:" + fparms[0]);
    }
    break;

The SPINDL statement describes the spindle speed using mnemonics like CCW, CLW, and OFF. Within the thread, we describe the speed using the mathematical convention (right-hand rule) of positive for counter clockwise, zero for off, and negative for clockwise.

case "SPINDL":
    string[] sparms = parts[1].Split(',');
    if (sparms[0] == "RPM")
    {
	double speed = double.Parse(sparms[1]);
	bool ccw = true;
	if (sparms.Length > 2)
	{
	    if (sparms[2] == "CCW")
		ccw = true;
	    else if (sparms[2] == "CLW")
		ccw = false;
	    else
		Console.WriteLine("Unknown spindle direction:" + sparms[2]);
	}
	apt.SpindleSpeedUnit("rpm");
	if (ccw == true)
	    apt.SpindleSpeed(speed);
	else
	    apt.SpindleSpeed(-speed);
    }
    else if (sparms[0] == "OFF")
	apt.SpindleSpeed(0);
    else
    {
	Console.WriteLine("Unknown spindle unit:" + sparms[0]);
    }
    break;

There are many dialects of APT that differ in their handling of tools. The SELECT or SELCTL statements may assign a number to a recently declared tool, and the LOAD or LOADTL statements often indicate that a tool should load into the spindle. This is something that most people will customize for their own local usage.

case "SELECT":
    string[] slparms = parts[1].Split(',');
    if (slparms[0] == "TOOL")
    {
	long tool_num = long.Parse(slparms[1]);
	apt.SELCTLTool(tool_num);
    }
    else
    {
	Console.WriteLine("Unknown SELECT:" + slparms[0]);
    }
    break;

case "LOAD":
    string[] lparms = parts[1].Split(',');
    if (lparms[0] == "TOOL")
    {
	long tool_num = long.Parse(lparms[1]);
	apt.LoadTool(tool_num);
    }
    else
    {
	Console.WriteLine("Unknown LOAD:" + lparms[0]);
    }
    break;

Thee UNITS and SET statements declare modal information like the length units and whether coordinates are given as absolute or relative values.

case "UNITS":
    string[] uparms = parts[1].Split(',');
    if (uparms[0] == "MM")
	apt.Millimeters();
    else if (uparms[1] == "INCH")
	apt.Inches();
    else
    {
	Console.WriteLine("Unknown UNITS:" + uparms[0]);
    }
    break;

case "SET":
    string[] tparms = parts[1].Split(',');
    if (tparms[0] == "MODE")
    {
	if (tparms[1] != "ABSOL")
	    Console.WriteLine("Cannot SET/MODE:" + tparms[1]);
    }
    else
    {
	Console.WriteLine("Unknown SET:" + tparms[0]);
    }
    break;

Finally, the PPRINT directive is often used with free-form data to provide extra information from the local system. We can populate the digital thread with it in the most appropriate place.

case "PPRINT":
    string[] pparms = parts[1].Split(',');
    if (pparms[0] == "MOTION" && pparms[1] == "CUT" && pparms[2] == "TYPE")
    {
	if (pparms[3] == "FACE-CUT")
	{
	    long ws_id = apt.GetCurrentWorkingstep();
	    apt.WorkingstepAddPropertyDescriptiveMeasure(ws_id, "MySystem", "FACE-CUT");
	}
	else
	    Console.WriteLine("Unknown MOTION TYPE:" + pparms[3]);
    }
    else
    {
	Console.WriteLine(stBuffer);
    }
    break;

This is a sampling of the type of information that you can add to the digital thread. The data is associative so that you can understand the tolerances that are being machined, the part faces impacted, as well as any other high-level cam parameters.

Finally, the open character of the data means that you can add new capabilities over time with feedback from the machine, probed flexible setup, tool management, speed and feed monitoring, and other digital thread advances.