"""This module is a collection of classes used to represent cut commands.
THINK uses three cut-file types: CLS, CUT, and CBF. While each file type has its
own formatting rules, they can all be used to represent the same list of commands.
Equivalency issues often arise when a command has multiple representations,
possibly a different model for each file type. This module ensures that there is
a single model for each command, and thus should prevent equivlanecy issues.
There are two main types of cut commands: :class:`MoveCmd` and :class:`InfoCmd`.
Notes:
Default Units (unless specified otherwise):
- distance (mm)
- velocity (m/s)
- acceleration (m/s/s)
Cut-file Format Spec Documents:
- CBF :download:`300071, Rev03, Spec, Cutfile Binary Format (CBF) <specs/300071 Rev03 Spec, Cutfile Binary Format (CBF).pdf>`
- CUT :download:`300072, Rev07, Spec, Cutfile ASCII Format (CUT) <specs/300072 Rev07 Spec Cutfile ASCII Format (CUT).pdf>`
- CLS :download:`300319, Rev01, CLSFCUT Interface Specification <specs/300319 Rev01 CLSFCUT Interface Specification.pdf>`
.. moduleauthor:: C.J. Geering <cgeering@thinksurgical.com>
"""
# import numpy as np # For all numpy 3-vectors, indices [0,1,2] --> [x,y,z] in R^3
from .mathext import *
from .cutter import CUTTERS
__all__ = ['CutCmd', 'MoveCmd', 'InfoCmd', 'PointCmd', 'OrientCmd',
'Orient5XCmd', 'LineCmd', 'Line5XCmd', 'HeaderCmd', 'HeaderExtCmd',
'CheckpointCmd', 'CutterCmd', 'PhaseCmd', 'StartshapeCmd',
'EndshapeCmd', 'DecelOnCmd', 'DecelOffCmd', 'SpeedCmd', 'AccelCmd',
'CutterOnCmd', 'CutterOffCmd', 'FCparmsCmd', 'VersionCmd',
'CommentCmd', 'CheckSumCmd', 'InvalidCutCmdError']
# TODO: Add docstrings
# TODO: rewrite eq method to be general by iterating through class attributes
# and looking up type.
# TODO: Let tol be set in a config file.
# TODO: sort through class attributes and start making the proper fields private
# TODO: Comparing tol is different than decimal places to write to.
# TODO: Generalize _compile_cut_cmd. Particularly, get rid of ' '.join().
# TODO: specify units in docs (i.e. m/s, m, s)
[docs]class CutCmd(object):
"""General cut command.
Base-class for all cut commands -- all cut commands must extend this class.
Args:
arg_list (List, optional): A list of argument values. Defaults to None.
Attributes:
is_move_cmd (bool): True if this command is a MoveCmd.
is_info_cmd (bool): True if this command is an InfoCmd.
req_chkpt_before (bool): True if this command requries a checkpoint as the previous command.
req_chkpt_after (bool): True if this command requries a checkpoint as the next command.
req_stop_before (bool): True if all movement must be stopped before this command.
req_stop_after (bool): True if all movement must be stopped at the end of this command.
Notes:
- Default tolerance for all operations is 1e-4.
"""
is_move_cmd = False
is_info_cmd = False
req_chkpt_before = False
req_chkpt_after = False
req_stop_before = False
req_stop_after = False
_title = 'GEN_CMD'
_cut_cmd_name = NotImplemented
_cbf_cmd_code = NotImplemented
_cbf_num_arg_bytes = NotImplemented
_cls_cmd_name = NotImplemented
_cmp_tol = 1e-4
def __init__(self, arg_list=None):
if arg_list is not None and not isinstance(arg_list, list):
raise TypeError('arg_list must be a List or None')
arg_list = [] if arg_list is None else arg_list
self._arg_list = arg_list
def __repr__(self):
return self._title
def __eq__(self, other):
if isinstance(other, self.__class__):
return self._arg_list == other._arg_list
return NotImplemented
def __ne__(self, other):
result = self.__eq__(other)
if result is NotImplemented:
return result
return not result
def _compile_cut_value(self):
return NotImplemented
def _compile_cbf_args(self):
return NotImplemented
[docs] def get_cbf(self):
"""Compile and retrieve the CBF byte-string for this commmand.
The command is compiled according to the format specified in document
:download:`300071, Rev03, Spec, Cutfile Binary Format (CBF)
<specs/300071 Rev03 Spec, Cutfile Binary Format (CBF).pdf>`. The
returned bytearray will not include the command sequence number.
Returns:
bytearray: [cmd code, cmd code, # arg bytes, 0, checksum, args]
Note:
The returned bytearray will not include the command sequence number.
"""
# TODO: add version as an optional input parameter
arg_list = self._compile_cbf_args()
arg_bytes = b''.join(arg_list)
checksum = compute_checksum(arg_bytes)
cmd_bytes = struct.pack('BBBBH',
self._cbf_cmd_code,
self._cbf_cmd_code,
self._cbf_num_arg_bytes,
0,
checksum)
return cmd_bytes + arg_bytes
[docs] def get_cbf_cmd_code(self):
"""Get the CBF command code for this command.
The command codes are listed in document :download:`300071, Rev03, Spec,
Cutfile Binary Format (CBF) <specs/300071 Rev03 Spec, Cutfile Binary Format (CBF).pdf>`.
Returns:
int: CBF command code
"""
return self._cbf_cmd_code
[docs] def get_cut(self):
"""Compile and retrieve the CUT command for this command.
The command is compiled according to the format specified in document
:download:`300072, Rev07, Spec, Cutfile ASCII Format (CUT)
<specs/300072 Rev07 Spec Cutfile ASCII Format (CUT).pdf>`.
Returns:
string: the CUT command (command name + command value)
"""
# TODO: put ' '.join here
cmd_value = self._compile_cut_value()
if cmd_value == '' or cmd_value is None:
return self._cut_cmd_name
else:
return self._cut_cmd_name + ' ' + cmd_value
[docs] def get_cls(self):
"""Compile and retrieve the CLS command for this command.
The command is compiled according to the format specified in document
:download:`300319, Rev01, CLSFCUT Interface Specification
<specs/300319 Rev01 CLSFCUT Interface Specification.pdf>`.
Returns:
string: the CLS command (for most commands this is the CUT command
with "$$CUT" prepended to it)
"""
return '$$CUT %s' % self.get_cut()
[docs]class MoveCmd(CutCmd):
"""Parent class for all Movement commands.
Attributes:
is_move_cmd (bool): True
Note:
Default constructor will not assign any start/end points/directions.
"""
is_move_cmd = True
_title = 'GEN_MOVE_CMD'
# TODO: add fcparms as an attribute
def __init__(self, arg_list=None):
super().__init__(arg_list)
# This is to initialize all points to None.
# It wont actually set any points if MoveCmd is instantiated.
self.start_pt = None #: numpy 3-vec to store start point. Default is None.
self.end_pt = None #: numpy 3-vec to store end point. Default is None.
self.start_dir = None #: numpy 3-vec to store start direction. Default is None.
self.end_dir = None #: numpy 3-vec to store end direction. Default is None.
def __eq__(self, other):
if isinstance(other, self.__class__):
self_vecs = self._get_vecs()
other_vecs = other._get_vecs()
for i in range(len(self_vecs)):
if not cmp_vec(self_vecs[i], other_vecs[i], self._cmp_tol):
return False
return True
return NotImplemented
def __repr__(self):
repr_val = '%s:\n\t' % self._title
if self.start_pt is not None:
repr_val += 'start_pt: %s\n\t' % self.start_pt
if self.end_pt is not None:
repr_val += 'end_pt: %s\n\t' % self.end_pt
if self.start_dir is not None:
repr_val += 'start_dir: %s\n\t' % self.start_dir
if self.end_dir is not None:
repr_val += 'end_dir: %s\n\t' % self.end_dir
return repr_val
def _get_vecs(self):
# Order matters
return [vec for vec in [self.start_pt, self.end_pt, self.start_dir,
self.end_dir] if vec is not None]
def _compile_cut_value(self):
return ' '.join(list(map(np_vec_to_cut_vec, self._get_vecs())))
def _compile_cbf_args(self):
return [np_vec_to_bytes(vec) for vec in self._get_vecs()]
[docs] def get_cls(self):
"""Over-ride CutCmd.get_cls().
Returns:
error: NotImplemented
Note:
This function over-rides the get_cls() function for the CutCmd class.
"""
return NotImplemented
[docs]class PointCmd(MoveCmd):
_title = 'POINT'
_cut_cmd_name = 'point'
_cbf_cmd_code = 0x33
_cbf_num_arg_bytes = 12
def __init__(self, arg_list):
super().__init__(arg_list)
self.start_pt = arg_list[0]
def __repr__(self):
return '%s:\n\tgoal_point: %s' % (self._title, self.start_pt)
[docs]class OrientCmd(MoveCmd):
_title = 'ORIENT'
_cut_cmd_name = 'orient'
_cbf_cmd_code = 0x58
_cbf_num_arg_bytes = 24
def __init__(self, arg_list):
super().__init__(arg_list)
self.start_dir = arg_list[0]
def __repr__(self):
return '%s:\n\tgoal_dir: %s' % (self._title, self.start_dir)
# TODO: defined for Orient because of stupid reserved bytes...
def _compile_cbf_args(self):
return [np_vec_to_bytes(self.start_dir), get_reserved_bytes(12)]
[docs]class Orient5XCmd(MoveCmd):
_title = 'ORIENT5X'
_cut_cmd_name = 'orient5b'
_cbf_cmd_code = 0x59
_cbf_num_arg_bytes = 24
def __init__(self, arg_list):
super().__init__(arg_list)
self.start_dir = arg_list[0]
self.end_dir = arg_list[1]
[docs]class LineCmd(MoveCmd):
_title = 'LINE'
_cut_cmd_name = 'line'
_cbf_cmd_code = 0xB6
_cbf_num_arg_bytes = 24
def __init__(self, arg_list):
super().__init__(arg_list)
self.start_pt = arg_list[0]
self.end_pt = arg_list[1]
# TODO: Should movement commands have a length check on init args?
# TODO: Should movement commands have a type check on init args?
[docs]class Line5XCmd(MoveCmd):
_title = 'LINE5X'
_cut_cmd_name = 'line5b'
_cbf_cmd_code = 0xB7
_cbf_num_arg_bytes = 48
def __init__(self, arg_list):
super().__init__(arg_list)
self.start_pt = arg_list[0]
self.end_pt = arg_list[1]
self.start_dir = arg_list[2]
self.end_dir = arg_list[3]
[docs]class InfoCmd(CutCmd):
is_info_cmd = True
_title = 'GEN_INFO_CMD'
[docs]class CheckpointCmd(InfoCmd):
_title = 'CHECKPOINT'
_cut_cmd_name = 'checkpoint'
_cbf_cmd_code = 0x3C
_cbf_num_arg_bytes = 32
def __init__(self, arg_list):
super().__init__(arg_list)
self.name = arg_list[0]
self.recovery_pt = arg_list[1]
# TODO: Add check to make sure per_comp is positive? If so, add test as well.
self.per_comp = float(arg_list[2])
def __repr__(self):
return '%s:\n\tname: %s\n\trecovery_pt: %s\n\tpercent_comp: %s' \
% (self._title, self.name, self.recovery_pt, self.per_comp)
def __eq__(self, other):
# TODO: Should we be checking if name is equal?
if isinstance(other, self.__class__):
result = cmp_vec(self.recovery_pt, other.recovery_pt, self._cmp_tol) and \
cmp_flt(self.per_comp, other.per_comp, self._cmp_tol) and \
self.name == other.name
return result
return NotImplemented
def _compile_cut_value(self):
val_list = [self.name, np_vec_to_cut_vec(self.recovery_pt, decimals=4),
float_to_cut_string(self.per_comp, decimals=4)]
return ' '.join(val_list)
def _compile_cbf_args(self):
return [str_to_bytes(self.name, 16), np_vec_to_bytes(self.recovery_pt),
float_to_bytes(self.per_comp)]
[docs]class CutterCmd(InfoCmd):
_title = 'CUTTER'
_cut_cmd_name = 'cutter'
_cbf_cmd_code = 0xA3
_cbf_num_arg_bytes = 32
def __init__(self, arg_list):
super().__init__(arg_list)
pn = arg_list[0]
if pn not in CUTTERS:
raise RuntimeError('Cutter \'%s\' is not a valid Cutter.' % pn)
self.cutter = CUTTERS[pn]
def __repr__(self):
return '%s:\n\tname: %s' % (self._title, self.cutter.part_number)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.cutter == other.cutter
return NotImplemented
def _compile_cut_value(self):
# TODO: should decimals be set to 4?
val_list = [self.cutter.part_number,
float_to_cut_string(self.cutter.length, decimals=4),
float_to_cut_string(self.cutter.radius, decimals=4),
float_to_cut_string(self.cutter.height, decimals=4)]
return ' '.join(val_list)
def _compile_cbf_args(self):
return [str_to_bytes(self.cutter.part_number, 16),
float_to_bytes(self.cutter.length),
float_to_bytes(self.cutter.radius),
float_to_bytes(self.cutter.height),
get_reserved_bytes(4)]
[docs] def get_cls(self):
return 'LOAD/TOOL,%s' % self.cutter.part_number
[docs]class PhaseCmd(InfoCmd):
_title = 'PHASE'
_cut_cmd_name = 'phase'
_cbf_cmd_code = 0x21
_cbf_num_arg_bytes = 16
# TODO: should there be format checks on name on init?
def __init__(self, arg_list):
super().__init__(arg_list)
self.name = arg_list[0]
self.cutter = None
def __repr__(self):
return '%s:\n\tname: %s' % (self._title, self.name)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.name == other.name
return NotImplemented
[docs] def set_cutter(self, cutter_pn):
self.cutter = CUTTERS[cutter_pn]
def _compile_cut_value(self):
# TODO: should there be data type/format checking here?
return self.name
def _compile_cbf_args(self):
return [str_to_bytes(self.name, 16)]
[docs] def get_cls(self):
# TODO: This is a hack to make sure each "TOOL PATH" command has a cutter.
cutter_pn = '123456' if self.cutter is None else self.cutter.part_number
return 'TOOL PATH/%s,TOOL,%s' % (self.name, cutter_pn)
class ShapeCmd(InfoCmd):
_cbf_num_arg_bytes = 6
def __init__(self, arg_list):
super().__init__(arg_list)
self.name = arg_list[0]
self.num_moves = arg_list[1]
def __repr__(self):
return '%s:\n\tname: %s\n\tnum_moves: %s' % (self._title, self.name, self.num_moves)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.name == other.name and self.num_moves == other.num_moves \
and self._title == other._title
return NotImplemented
def _compile_cut_value(self):
return '%s %d' % (self.name, self.num_moves)
def _compile_cbf_args(self):
return [str_to_bytes(self.name, 5), u_int_to_bytes(self.num_moves, 1)]
[docs]class StartshapeCmd(ShapeCmd):
_title = 'START_SHAPE'
_cut_cmd_name = 'startshape'
_cbf_cmd_code = 0xC9
[docs]class EndshapeCmd(ShapeCmd):
_title = 'END_SHAPE'
_cut_cmd_name = 'endshape'
_cbf_cmd_code = 0x65
class DecelCmd(InfoCmd):
_cbf_num_arg_bytes = 0
def __eq__(self, other):
if isinstance(other, self.__class__):
return True
return NotImplemented
def _compile_cut_value(self):
return None
def _compile_cbf_args(self):
return []
[docs]class DecelOnCmd(DecelCmd):
_title = 'DECEL_ON'
_cut_cmd_name = 'decel_on'
_cbf_cmd_code = 0x4E
[docs]class DecelOffCmd(DecelCmd):
_title = 'DECEL_OFF'
_cut_cmd_name = 'decel_off'
_cbf_cmd_code = 0x2B
[docs]class SpeedCmd(InfoCmd):
_title = 'SPEED'
_cut_cmd_name = 'speed'
_cbf_cmd_code = 0x8B
_cbf_num_arg_bytes = 4
def __init__(self, arg_list):
super().__init__(arg_list)
# TODO: should arg_list[0] be cast to float?
self.speed = arg_list[0]
def __repr__(self):
return '%s:\n\tspeed: %s' % (self._title, self.speed)
def __eq__(self, other):
if isinstance(other, self.__class__):
return cmp_flt(self.speed, other.speed, self._cmp_tol)
return NotImplemented
def _compile_cut_value(self):
return float_to_cut_string(self.speed, decimals=4)
def _compile_cbf_args(self):
return [float_to_bytes(self.speed)]
[docs] def get_cls(self):
mps_to_mmps = 60000
return 'FEDRAT/MMPM,%s' % float_to_cut_string(mps_to_mmps*self.speed, decimals=4)
[docs]class AccelCmd(InfoCmd):
_title = 'ACCEL'
_cut_cmd_name = 'accel'
_cbf_cmd_code = 0x47
_cbf_num_arg_bytes = 8
def __init__(self, arg_list):
super().__init__(arg_list)
self.accel = arg_list[0]
self.decel = arg_list[1]
def __repr__(self):
return '%s:\n\taccel: %s\n\tdecel: %s' % (self._title, self.accel, self.decel)
def __eq__(self, other):
if isinstance(other, self.__class__):
return cmp_flt(self.accel, other.accel, self._cmp_tol) and \
cmp_flt(self.decel, other.decel, self._cmp_tol)
return NotImplemented
def _compile_cut_value(self):
return ' '.join([float_to_cut_string(self.accel, decimals=4),
float_to_cut_string(self.decel, decimals=4)])
def _compile_cbf_args(self):
return [float_to_bytes(self.accel), float_to_bytes(self.decel)]
class CutterControlCmd(InfoCmd):
_cbf_num_arg_bytes = 0
def __eq__(self, other):
if isinstance(other, self.__class__):
return True
return NotImplemented
def _compile_cut_value(self):
return None
def _compile_cbf_args(self):
return []
[docs]class CutterOnCmd(CutterControlCmd):
_title = 'CUTTER_ON'
_cut_cmd_name = 'cutter_on'
_cbf_cmd_code = 0xBB
[docs]class CutterOffCmd(CutterControlCmd):
_title = 'CUTTER_OFF'
_cut_cmd_name = 'cutter_on'
_cbf_cmd_code = 0x66
[docs]class FCparmsCmd(InfoCmd):
_title = 'FC_PARMS'
_cut_cmd_name = 'fcparms'
_cbf_cmd_code = 0xFC
_cbf_num_arg_bytes = 16
def __init__(self, arg_list):
super().__init__(arg_list)
self.nominal_speed = arg_list[0]
self.max_speed = arg_list[1]
self.min_speed = arg_list[2]
self.max_force = arg_list[3]
def __repr__(self):
return '%s:\n\tnominal_speed: %s\n\tmax_speed: %s\n\tmin_speed: %s\n\tmax_force: %s' \
% (self._title, self.nominal_speed, self.max_speed, self.min_speed, self.max_force)
def __eq__(self, other):
if isinstance(other, self.__class__):
result = cmp_flt(self.nominal_speed, other.nominal_speed, self._cmp_tol) and \
cmp_flt(self.max_speed, other.max_speed, self._cmp_tol) and \
cmp_flt(self.min_speed, other.min_speed, self._cmp_tol) and \
cmp_flt(self.max_force, other.max_force, self._cmp_tol)
return result
return NotImplemented
def _get_val_list(self):
# Order matters
return [self.nominal_speed, self.max_speed, self.min_speed, self.max_force]
def _compile_cut_value(self):
return ' '.join([float_to_cut_string(val, decimals=4) for val in self._get_val_list()])
def _compile_cbf_args(self):
return list(map(float_to_bytes, self._get_val_list()))
[docs]class VersionCmd(InfoCmd):
_title = 'VERSION'
_cbf_cmd_code = 0x55
_cbf_num_arg_bytes = 32
def __eq__(self, other):
if isinstance(other, self.__class__):
return True
return NotImplemented
[docs]class CheckSumCmd(InfoCmd):
_title = 'CHECK_SUM'
def __eq__(self, other):
if isinstance(other, self.__class__):
return True
return NotImplemented
# Cut Command Specific Exceptions
[docs]class InvalidCutCmdError(Exception):
def __init__(self, cmd):
msg = '%s is not a valid CUT command.' % cmd
super().__init__(msg)