# Please use the
# http://google-styleguide.googlecode.com/svn/trunk/pyguide.html
# as documentation format for this class
# disable some pylint checks.
# pylint: disable=broad-except, too-many-arguments
# pylint: disable=too-many-lines, too-many-public-methods
""" This module provides the Python class that allows XML-RPC communication
with 3D Mechanics Editor.
Example:
>>> import controllab
>>> tdmech = controllab.TDMech()
"""
from __future__ import absolute_import, print_function
import collections
import math
import os
from .tool import ExeTool
from .wrapper import Wrapper, WrapperError
"""from .wrapper import dict2value_with_properties
from .support import key_value_to_dict"""
"""
# Try to import numpy (needed for linearize_model()
try:
import numpy as np
except ImportError:
pass
CodeTarget = collections.namedtuple(
'CodeTarget', ['name', 'submodelselection'])
ModelInfo = collections.namedtuple('ModelInfo', ['name', 'identifier'])
ImplementationInfo = collections.namedtuple(
'Implementation', [
'name', 'implementation', 'implementations'])
"""
[docs]class TDMechWrapper(Wrapper):
"""3D Mechanics Editor scripting interface for Python.
This is a Python wrapper for 3D Mechanics's XML-RPC interface.
Attributes:
errorlevel: The level of errors that should be raised.
"""
DEFAULT_PORT = 5680
def _get_default_exetool(self, version=None, **kwargs):
""" Get the default ExeTool for TDMechWrapper. Optionally a different
version of the default tool can be started.
Args:
version (optional) : Will be passed on to ExeTool.__init__
Default: '5.0'
Returns:
ExeTool : An ExeTool representing 3D Mechanics Editor
"""
return ExeTool('TDMECH', version or ExeTool.DEFAULT_XXSIM_VERSION)
def _set_xrc_server(self, xrc): # pylint: disable=arguments-differ
""" Correctly sets the self.server attribute.
Args:
xrc (xmlrpclib.ServerProxy) : The serverproxy
Example:
>>> import xmlrpc.client as xmlrpclib
>>> _set_xrc_server(self,
... xmlrpclib.Server_proxy('http://localhost:5680'))
"""
self.server = xrc.tdmech
[docs] def connect(self, uri='http://localhost:{}'.format(DEFAULT_PORT),
version=ExeTool.DEFAULT_XXSIM_VERSION, **kwargs):
"""Create a connection with 3D Mechanics Editor using XML-RPC
Args:
uri (str): The URI to connect to.
Defaults to 'http://localhost:5680' for connecting to 3D Mechanics Editor.
Returns:
bool: True if successful, False otherwise.
Example:
>>> connect('http://localhost:5680')
True
"""
return super(TDMechWrapper, self).connect(uri, version, **kwargs)
[docs] def get_version(self):
"""Get the version number of the 3D Mechanics instance to which a connection was made.
Returns:
dict: A dictionary with the following fields:
* ``major``: The major number of the version number.
* ``minor``: The minor number of the version number.
* ``patch``: The patch number of the version number.
* ``build``: The build number of the version number.
False: On failure.
Example:
>>> tdmech.get_version()
{'major': 5,
'minor': 0,
'patch': 0,
'build': 10000}
"""
try:
# Obtain the version information via XML-RPC call.
version_struct = self.server.getVersion()
# Obtain the version string.
version_number_string = version_struct['version']
# Split the version number string.
version_numbers_array = version_number_string.split(".")
# Return a dictionary with all version data.
return {'major':int(version_numbers_array[0]),
'minor':int(version_numbers_array[1]),
'patch':int(version_numbers_array[2]),
'build':int(version_numbers_array[3])}
except Exception as error:
self.errorhandler(error)
return False
[docs] def open_model(self, model):
"""Opens a model file from its path.
Args:
model (str): Name of the file or model to load.
Returns:
int: The identifier of the model (1 or higher).
It can be used to select the active model
Returns False on error.
The model is activated and initialized after opening.
Example:
>>> model_id = tdmech.open_model('c:\\\\temp\\\\myproject.3dm')
"""
try:
# Check whether the provided model exists
if os.path.isabs(model) and not os.path.isfile(model):
raise FileNotFoundError('File not found: {}'.format(model))
# Open the model
reply = self.server.openModel({'name': model})
# the actual XML-RPC call
if reply is not None:
return reply['identifier']
except Exception as error:
self.errorhandler(error)
return False
[docs] def close_model(self, close_window=False):
""" Closes active 3D Mechanics model without saving changes
Args:
close_window (bool, optional): indicate if the window should be
closed if other models are also still open
Returns:
bool: True when closed properly, False otherwise
Example:
>>> result = tdmech.close_model()
"""
try:
# the actual XML-RPC call
return self.server.closeModel({'closewindow': close_window})
except Exception as error:
self.errorhandler(error)
return False
[docs] def save_model(self, filename=None):
""" Saves the changes made to active 3D Mechanics model
Args:
filename (str, optional): The filename that should be used to save the
active model. If omitted, 3D Mechanics Editor will save the model under its
current name if it has any.
Returns:
bool: True on success, False otherwise
Example:
>>> result = tdmech.save_model('c:\\\\temp\\\\mymodel.3dm')
"""
try:
if filename is None:
filename = ''
# the actual XML-RPC call
return self.server.saveModel({'name': filename})
except Exception as error:
self.errorhandler(error)
return False
[docs] def generate_code(self):
""" Generates code for the active model. It uses the current setting in the model
this will change, where we can pass arguments, like filename, option to generate scenerey
generate icon, and what kind of icon
Returns:
filename (str): The 20-sim submodel filename the code has been generated in
submodelname (str): The submodel name used in the scenery export, see scenery argument
scenery (str): The 20-sim 3D scenery filename. This can be imported in a 3D animation plot
Example:
>>> result = tdmech.generate_code()
"""
try:
# the actual XML-RPC call
return self.server.generateCode()
except Exception as error:
self.errorhandler(error)
return False
[docs] def generate_submodel(self, filename, currentConfigAsInitial=False, generateIcon=1, iconFilename=None):
""" Generates submodel code for the active model.
Args:
filename (str): The 20-sim submodel filename for which the code should be generated
currentConfigAsInitial (bool, optional): should the current configuration be used as initial value
generateIcon (int, optional): 0=no icon, 1=automatic (raytrace), 2=from file
iconFilename (str, optional): bitmap file to be used as filename. Only used if generateIcon == 2
Returns:
bool: True when code has been generated properly, False otherwise
Example:
>>> result = tdmech.generate_submodel('mysubmodel.emx')
"""
try:
if iconFilename is None:
iconFilename = ''
# the actual XML-RPC call
return self.server.generateSubmodel(
{'filename': filename,
'currentConfigAsInitial': currentConfigAsInitial,
'generateIcon': generateIcon,
'iconFilename': iconFilename})
except Exception as error:
self.errorhandler(error)
return False
[docs] def generate_scenery(self, filename, submodelname, generateJoints=False):
""" Generates submodel code for the active model.
Args:
filename (str): The scenery filename to be generated
submodelname (str): the hierarchical name of the submodel generated by 3D mechanics
generateJoints (boolean, optional): Indicates if generate joint representation should be generated
Returns:
bool: True when scenery has been generated properly, False otherwise
Example:
>>> result = tdmech.generate_scenery('myscenery.scn', 'mysubmodelin20sim')
"""
try:
# the actual XML-RPC call
return self.server.generateScenery(
{'filename': filename,
'submodelname': submodelname,
'generateJoints': generateJoints})
except Exception as error:
self.errorhandler(error)
return False
[docs] def get_design_parameters(self, parameters=None):
"""Returns a list with the specified design parameters
Args: list of parameters to return. If empty return the full list
Returns:
list *or* None: list with a design parameters dict
(when retrieving more than one parameter)
False is returned on a failure
Examples:
To retrieve a list of design parameters you can use:
>>> params = tdmech.get_design_parameters(['Body1.Ixx','Body2.PosX'])
The output is a list of parameter dicts with the following fields:
* ``name``: the name of the retrieved parameter(s)
* ``size``: the size of the parameter(s), for now this is always 1
* ``values``: the values of the retrieved parameter(s)
* ``unit``: the unit of the retrieved parameters(s)
* ``global``: the global boolean of the retrieved parameter(s)
To retrieve a single of parameter you can use
>>> tdmech.get_design_parameters('Body1.Ixx')
[{'name': 'Body1.Ixx',
'size': [1],
'values': [1.0]}.
'unit': 'kg',
'global: True]
"""
if not parameters:
parameters = []
if isinstance(parameters, list):
# Make sure that the list contains a dict
try:
if len(parameters) > 0:
new_parameters_list = []
for parameter in parameters:
if isinstance(parameter, dict) and 'name' in parameter.keys():
# Correct layout
new_parameters_list.append(parameter)
elif isinstance(parameter, str):
# List of str
new_parameters_list.append({'name': parameter})
parameters = new_parameters_list
except:
pass
elif isinstance(parameters, str):
# Apparently only a name given. Create a proper array of structs
parameters = [{'name': parameters}]
try:
return self.server.getDesignParameters(parameters)
except Exception as error:
self.errorhandler(error)
return False
[docs] def set_design_parameters(self, parameters, values = None):
"""Set a list of design parameters
Args: list of parameters to set.
Returns:
True if the function succeeds
Examples:
To set a list of design parameters you can use:
>>> tdmech.set_design_parameters([{'name': 'Body1.Ixx', 'size': [1], 'value': [1.0], 'unit': 'kg', 'global': True}, {'name': 'Body1.Iyy', 'size': [1], 'value': [2.0], 'unit': 'kg', 'global': True}])
To update an existing of parameter you can use
>>> tdmech.set_design_parameters('Body1.Ixx', 1.0)
True
"""
try:
if not parameters:
return False
# Argument checks
_parameters = []
if isinstance(parameters, list):
if values is not None and isinstance(values, list):
# Parameter name array + value array
# This way of calling requires existing design parameters
if len(parameters) != len(values):
self.errorhandler(Exception('Size mismatch between parameters and values array'))
return False
else:
# Build the list of structs
for index in range(0, len(parameters)):
if isinstance(values[index], list):
value_arr = values[index]
for i in range(len(value_arr)):
value_arr[i] = float(value_arr[i])
else:
value = float(values[index])
value_arr = [value]
_parameters.append({'name': parameters[index], 'value': value_arr})
else:
# Expected: list of dict
if len(parameters) == 0:
return False
for index in range(0, len(parameters)):
if isinstance(parameters[index], dict):
_parameters.append(parameters[index])
else:
self.errorhandler(Exception('Type mismatch for parameter list element {0}. Expected type: dict'.format(index)))
return False
elif isinstance(parameters, str) and values is not None:
# Update one existing design parameter
if isinstance(values, list):
value_arr = values
for i in range(len(value_arr)):
value_arr[i] = float(value_arr[i])
else:
value = float (values)
value_arr = [value]
_parameters.append({'name': parameters, 'value': value_arr})
else:
self.errorhandler(Exception("Unexpected argument type '{0}' for the parameters argument".format(type(parameters))))
return False
return self.server.setDesignParameters(_parameters)
except Exception as error:
self.errorhandler(error)
return False
[docs] def set_STL_file(self, body_name: str, filename: str, properties: dict = None):
""" Set/change the STL representation of a body
Args:
body_name (str): The name of an existing body in the 3D model
filename (str): The name of the STL file to set for this body
properties (dict, optional): A dictionary with additional settings
related to the STL representation (reserved for future use)
Returns:
True when the function succeeds, False otherwise
"""
try:
_properties = []
# TODO: Parse the properties dictionary and create an array with key-value
# dicts for each item
return self.server.setSTLFile({'bodyname': body_name, 'filename': filename})
except Exception as error:
self.errorhandler(error)
return False