Tutorial KinematicController
Tutorial |
Topic |
---|
Kinematic controller from Python |
Level |
Basic skills of Python are helpful |
Time to complete |
(I don't know yet) |
Authors |
FBXL5 |
FreeCAD version |
0.20. and later |
Example files |
None |
See also |
None |
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.
Macro sections
Basic structure of a simple Python macro
#! python
# -*- coding: utf-8 -*-
# (c) 2022 Your name LGPL
def main():
pass
if __name__ == '__main__':
# This will be true only if the file is "executed"
# but not if imported as module
main()
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.
def findTheDrivingConstraints(document_name):
# search through the Objects and find the driving constraint
driver_list = []
for each in document_name.Objects:
if each.Label[-6:] == 'Driver' :
driving_constraint = each.Name
driver_list.append(driving_constraint)
return driver_list
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.
def main():
kin_doc = App.ActiveDocument # Kinematic Document
drivers = findTheDrivingConstraints(kin_doc)
if len(drivers) < 1:
print("No driver found!")
elif len(drivers) > 1:
print("Not supported yet!")
else:
print(drivers)
The macro so far...
#! python
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL
def findTheDrivingConstraints(document_name):
# search through the Objects and find the driving constraint
driver_list = []
for each in document_name.Objects:
if each.Label[-6:] == 'Driver' :
driving_constraint = each.Name
driver_list.append(driving_constraint)
return driver_list
def main():
kin_doc = App.ActiveDocument # Kinematic Document
drivers = findTheDrivingConstraints(kin_doc)
if len(drivers) < 1:
print("No driver found!")
elif len(drivers) > 1:
print("Not supported yet!")
else:
print(drivers)
if __name__ == '__main__':
# This will be true only if the file is "executed"
# but not if imported as module
main()
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:
from PySide2.QtWidgets import (QDialog)
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.
class ControlPanel(QDialog):
"""
docstring for ControlPanel.
"""
def __init__(self, Document, actuator_list):
super(ControlPanel, self).__init__()
self.initUI(Document, actuator_list)
def initUI(self, Document, actuator_list):
# Setting up class parameters
# the window has 640 x 480 pixels and is centered by default
# now make the window visible
self.show()
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.
form = ControlPanel(kin_doc, drivers)
form.exec_()
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...
#! python
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL
# imports and constants
from PySide2.QtWidgets import (QDialog)
class ControlPanel(QDialog):
"""
docstring for ControlPanel.
"""
def __init__(self, Document, actuator_list):
super(ControlPanel, self).__init__()
self.initUI(Document, actuator_list)
def initUI(self, Document, actuator_list):
# Setting up class parameters
# the window has 640 x 480 pixels and is centered by default
# now make the window visible
self.show()
def findTheDrivingConstraints(document_name):
# search through the Objects and find the driving constraint
driver_list = []
for each in document_name.Objects:
if each.Label[-6:] == 'Driver' :
driving_constraint = each.Name
driver_list.append(driving_constraint)
return driver_list
def main():
kin_doc = App.ActiveDocument # Kinematic Document
drivers = findTheDrivingConstraints(kin_doc)
if len(drivers) < 1:
print("No driver found!")
elif len(drivers) > 1:
print("Not supported yet!")
else:
form = ControlPanel(kin_doc, drivers)
form.exec_()
if __name__ == '__main__':
# This will be true only if the file is "executed"
# but not if imported as module
main()
Setting parameters
Now it is time to fill the initUI() section:
...
def initUI(self, Document, actuator_list):
# Setting up class parameters
self.document = Document
self.actuators = actuator_list
self.actuator = self.document.getObject(self.actuators[0])
self.driver_type = self.getDriverType(self.actuator)
# the window has 640 x 480 pixels and is centered by default
# now make the window visible
self.show()
...
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:
...
def getDriverType(self, constraint):
ANGLE_CONSTRAINTS = [
"Angle",
"PlaneCoincident",
"AxialAlignment",
"PlaneAlignment"
]
DISTANCE_CONSTRAINTS = [
"PointDistance",
"PointsDistance"
]
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
return "Angle"
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
return "Distance"
else:
return "Length"
...
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.
...
# the window has 640 x 480 pixels and is centered by default
#- set window dimensions
self.setMaximumWidth(400)
self.setMaximumHeight(200)
self.setMinimumWidth(400)
self.setMinimumHeight(200)
self.setWindowTitle(self.actuator.Label + ": " + self.driver_type)
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
# now make the window visible
...
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. I set to TRUE, a picture is taken with each step of the motion.
...
self.steps_value = 10
self.sequence = False
if self.driver_type == "Angle" :
self.current_value = self.actuator.Angle
self.start_value = (self.current_value - 15)
self.end_value = (self.current_value + 15)
self.unit_suffix = (" °")
elif self.driver_type == "Distance" :
self.current_value = float(str(self.actuator.Distance)[:-3])
self.start_value = 0.001 # Distance must not be <= 0
self.end_value = (self.current_value + 10)
self.unit_suffix = (" mm")
else:
self.current_value = float(str(self.actuator.Offset)[:-3])
self.start_value = (self.current_value - 10)
self.end_value = (self.current_value + 10)
self.unit_suffix = (" mm")
...
to be continued...