Tutorial KinematicController

Introduction
This tutorial describes how to generate a simple kinematic controller to use with assemblies created with the Assembly3 workbench out of some lines of Python code.

Any text editor can be used to code. My choice is Atom, but FreeCAD's built-in editor works well, too.

The following code examples can be copied and pasted into an empty text file and then saved under a name of your choice as a *.py or *.FCMacro file.

Basic structure of a simple Python macro
It consist of a main routine (function) and a switch checking if the macro is used as a container for classes, methods etc. or if it is run on its own. Only the second option will start the main routine.

The routine is empty yet and awaits content.

Find driving constraints
The driving constraints are objects within a FreeCAD document. They need to be marked so that they can be found.

For this controller the suffix Driver has to be attached to the label of a driving constraint. It may be separated by a "." or "-" for clarity, but this tool will only search for the last 6 characters of the label.

A routine receiving a document name ad returns a list of driving constraints (the names in this case) will do the job.

The main routine loads the name of the active document into the variable kin_doc and then calls the function findTheDrivingConstraints and hands over the content of kin_doc. The returned list is loaded into drivers which is then checked to contain exactly one item. If that is the case it is finally printed to the report view to check the result.

The macro so far...

Back to the top

Control panel
The control panel is built from Qt widgets, one main window containing several input/output widgets.

Each widget has to be imported before it can be used, but they can be imported as a single set. And the import line is placed close to the top of the script.

Main window
For the main window an import line looks like this:

The main window called ControlPanel is a class object instantiated from the QDialog widget.

It has two init methods. __init__ initialises the new class object, handles incoming arguments, and starts initUI which manages all widgets within the main window.

To launch the control panel an instance of the new class called form will be created with the document name and the list of driving constraints transferred to this instance. Finally the exec_ method of the class opens the dialog window.

Both lines replace the print command in the else branch of the main section.

Running the macro will display a clean empty dialog window waiting for widgets:



And the macro so far...

Back to the top

Setting parameters
Now it is time to fill the initUI section:

The actuator to be used is the first item in the actuators list. (The list contains one single item now, but if the controller can handle more than one driving constraint in the future, it will hold more items.)

Method getDriverType
For later use we need the driver type (Angle, Distance, Length) and so a method getDriverType has to be defined:

This method checks if the type of the given constraint can be found in one of the lists to return which kind of dimension has to be controlled.

It is assumed that in the kinematic document the driver is marked correctly and working if edited manually. In this case there is no need to filter out geometric constraints such as Colinear or PointsCoincidence (but here would be the place to do so...)

Window properties
The window size is defined by its minimum and maximum dimensions. Same values mean a fixed size.

The titel shows the driver name and whether its an angle, a distance, or a length. Finally the window is told to stay on top of all windows.

Back to the top

Setting more parameters
Next step is to extract the current value of the driver and set default start and end values depending on the driver type.

A distance cannot be negative and exactly zero puzzles the solver and so the start value is set to 0.001. Angles accept negative values and get symmetric values. (If lengths accept negative values has to be proven finally...)

The unit suffix must be kept for returning the value to the constraint property in the end. Distances and lengths need values with units.

Dealing with units and displaying values as strings in several widgets requires to convert numbers into strings and back again quite often.

To complete the parameters we set a default number of steps that should be computed when the motion is automated and the sequence toggle is set to TRUE, a picture is taken with each step of the motion.

Back to the top

Labels
Now three labels are added to display the start, end, and current value.

First the class QLabel must be imported i.e. the import list has to be extended like this:

Back in the initUI section we insert:

The placement is done with the inherited setGeometry method. In this case the description of a rectangle is used (X position, Y position, width, height).

The first and third lines could be combined, but it is not recommended for clarity reasons: self.label_end = QLabel((str(round(self.end_value, 1)) + self.unit_suffix), self)

Running the macro with a kinematic assembly document would create a dialog window like this:



And the macro so far...

Back to the top

Slider
To change the current value to any number between start and end value a slider widget would fit.

First the class QSlider must be imported i.e. the import list has to be extended like this:

Back in the initUI section and right after the labels section we insert:

The slider button is placed with the setValue method. Its value has to be calculated from the current value and a step ratio. The ratio has to be calculated whenever a start or end value is changed and so we insert another method after the getDriverType method.

To work with a ratio instead of altering the slider's min and max values has the advantage of a finer resolution for small values.

And after this one comes another method defining what to do when the slider position or the slider value changes. The onActuatorSlider method is called by the connect method which also hands over the slider value as an argument.

It recalculates the current value from the slider position, rewrites the text of the label self.label_current and changes the constraint property according to the driver type.

Running the command asm3CmdQuickSolve starts the solver to rearrange the assembly parts with the altered value.

The dialog window with the slider should look like this and is ready to control a motion:



We can start a dialog window for any opened document, they won't interfere with each other.

Back to the top

Text entry fields
To set the start and end value we use a line edit widget.

First the class QLineEdit must be imported i.e. the import list has to be extended like this:

Back in the initUI section and between the labels and the slider sections we insert:

The entry fields display the default start and end values. They are not complete until we add the methods to deal with altered entries. This will be done by the methods self.onEntryStart and self.onEntry that are inserted between the self.stepRatio and the self.onActuatorSlider section.

Both convert the received string value to a floating point number and change self.start_value/self.end_value and the corresponding label accordingly. After that the slider value is updated.

The dialog window with text entry fields should look like this and is ready to change the range of a motion:



And the macro so far...

Back to the top

Motion
To move the assembly parts automatically we need two buttons to trigger the motions, one towards the start position and one towards the end position. These two and a close button are using a QPushButton widget.

Small assemblies compute a bit too fast and show jumps instead of a smooth motion. To slow it down we use the sleep method of the time module which has to be imported first.

Another import and another widget:

Back in the initUI section we insert the buttons after the slider section:

The methods dealing with pressed buttons are self.onForward, self.onBackward, and self.onClose. They are inserted after the onActuatorSlider section.

The method self.onClose invokes the inherited method self.close which just closes the dialog window and so ends the macro.

Both self.onForward and self.onBackward count the steps that are left to go to reach the wanted position and calculate the length of a step according to the number of steps. For now we go with the default number of 10 steps.

Each round on the while loop increases/decreases the current value and updates the slider values which triggers onActuatorSlider in the background (see Slider section). After a pause to let the computer provide another updated 3D view counting down the steps left to go finishes the loop.

With no steps left the slider is set to the first/last slider position, just in case if a rounding error had occurred.

The dialog window with buttons should look like this and can now move the assembly by 10 steps towards the wanted start/end position:



And the macro so far...

Back to the top

to be continued...