We have been investigating JSON for describing manufacturing transactions and put together a small program to generate some using the STEP Python interface. We felt this was a nice example, so we are sharing it below.
The JSON output for the simple facing workingstep is shown below. It summarizes the position within the process, tool, operation, feature, and some other basic parameters.
2023-02-09 - Updated with some toolpath information
{name:thready_facing_op_revolved,tools: {1: {id:1,type:ENDMILL,overall_assembly_length: {value: 1.0,unit:MM},effective_cutting_diameter: {value: 50.6,unit:MM},maximum_depth_of_cut: {value: 1.5,unit:MM},hand_of_cut: null,coolant_through_tool: null,number_of_effective_teeth: 1.0,edge_radius: {value: 0.03,unit:MM},tool_cutting_edge_angle: {value: 0.0,unit:DEG} } },project: {id:thready_facing_op_revolved,main_workplan: {id:Workplan,type:WORKPLAN,enabled: true,as_is_geometry: null,to_be_geometry:e2408c22-a893-11ed-a9b5-18dbf245276c,removal_geometry: null,twin_start:2023-02-07T16:32:31-05:00,twin_end:2023-02-07T16:32:32-05:00,elements: [ {id:minimal geometry facing,type:WORKPLAN,enabled: true,as_is_geometry: null,to_be_geometry: null,removal_geometry: null,twin_start:2023-02-07T16:32:31-05:00,twin_end:2023-02-07T16:32:32-05:00,elements: [ {id:WS 1 face roughing and finishing,type:MACHINING_WORKINGSTEP,enabled: true,as_is_geometry: null,to_be_geometry: null,removal_geometry: null,twin_start:2023-02-07T16:32:31-05:00,twin_end:2023-02-07T16:32:32-05:00,operation: {id: ,type:PLANE_FINISH_MILLING,tool: {$ref:#/tools/1},toolpath: [ {id:clear WS 1 TP 1,type:CUTTER_LOCATION_TRAJECTORY,basiccurve: {name: ,type:polyline,points: [ [ 0.0, -53.36, 115.0 ], [ 0.0, -53.36, 110.0 ] ] } }, {id:entry WS 1 TP 2,type:CUTTER_LOCATION_TRAJECTORY,basiccurve: {name: ,type:polyline,points: [ [ 0.0, -53.36, 110.0 ], [ 0.0, -53.36, 104.0 ] ] } }, {id:WS 1 TP 3,type:CUTTER_LOCATION_TRAJECTORY,basiccurve: {name: ,type:polyline,points: [ [ 0.0, -53.36, 104.0 ], [ 0.0, 53.36, 104.0 ] ] } }, {id:WS 1 TP 4,type:CUTTER_LOCATION_TRAJECTORY,basiccurve: {name: ,type:polyline,points: [ [ 0.0, 53.36, 104.0 ], [ 0.0, 53.36, 115.0 ] ] } } ],axial_cutting_depth: {value: 1.0,unit:MM},overcut_length: null },feature: {id:disk feature,type:REVOLVED_FLAT,workpiece:6b35d480-930c-451e-93c6-232565a10e05,explicit_representation: [69ebf3b7-4982-42f1-80c6-51dde4459c0d],placement: [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 104.0, 1.0 ],profile_length: {value: 46.0,unit:MM} } } ],setup: null } ],setup: null } } }
The program to make the JSON is shown below. Just using the built
in python json.dumps
on a STEP object gives an "Object is
not JSON serializable" error. Beating on the jsons package gets a bit
further but ultimately runs into a circular reference error.
In any case, just serializing AIM and ARM attributes in a
mechanical manner would not be useful to the receiver. We define a
JSONEncoder
subclass to handle the different ARM
constructs and fine tune the JSON into something useful for a
particular purpose.
The JSONEncoder class uses the JSONFMT
table of
functions keyed on the ARM type to find a function that returns a
plain dictionary for a given STEP construct, which is used for the
json. Customization is done with a special function for each ARM
type.
# MAKE JSON FROM STEP OBJECTS # import sys import json import uuid from json import JSONEncoder from pathlib import Path from steptools import step from steptools.step import AptAPI as apt import stepdatetime # JSON utility functions ----------------------------------------------- def json_boolean(strval, tval, fval, dflt=None) -> bool: if not strval: return dflt strval = strval.casefold() if strval == tval.casefold(): return True if strval == fval.casefold(): return False return None def exec_enabled(o) -> bool: # convert strings and defaults to simple T/F if o is None: return False return json_boolean(o['enabled'], 'enabled', 'disabled', True) def json_uuid(o) -> str: if o is None: return None # get existing or set to new uuid U = apt.set_uuid(o, str(uuid.uuid1())) return U def json_faces(lst) -> list: if len(lst) == 0: return None VAL = list() for o in lst: VAL.append(json_uuid(o)) return VAL def json_measure(o) -> dict: if o is None: return None if step.isinstance(o, 'measure_with_unit'): V = o.value_component[0] U = step.Unit.fromstep(o.unit_component).name return { 'value': V, 'unit': U } return None def json_tool_ref(o) -> dict: if o is None: return None return { '$ref': '#/tools/' + o['its_id'], } # EXECUTABLES -------------------------------------------------- # ARM atts on executables # 'as_is_geometry': None, # 'enabled': None, # 'fixture_geometry': None, # 'its_id': 'Workingstep 1', # 'its_security_classification': <step.ArmCollection size 0>, # 'machine_used': None, # 'process_properties': <step.ArmCollection size 0>, # 'removal_geometry': None, # 'to_be_geometry': None, # 'twin_end': None, # 'twin_exception': None, # 'twin_plan': None, # 'twin_source': None, # 'twin_start': None, # 'twin_worktime': None} # AIM atts on executables # 'name': 'Workingstep 1', # 'consequence': '', # 'description': 'machining', # 'purpose': '', def json_exec(o) -> dict: D = { 'id': o['its_id'], # 'uuid': json_uuid(o), 'type': step.arm_type(o), 'enabled': exec_enabled(o), 'as_is_geometry': json_uuid(o['as_is_geometry']), 'to_be_geometry': json_uuid(o['to_be_geometry']), 'removal_geometry': json_uuid(o['removal_geometry']), } D['twin_start'] = stepdatetime.asisoformat(o.twin_start) D['twin_end'] = stepdatetime.asisoformat(o.twin_end) return D def json_enabled_elements(lst) -> list: VAL = list() for o in lst: if exec_enabled(o): VAL.append(o) return VAL # OPERATIONS -------------------------------------------------- def json_operation(o) -> dict: # 'approach': None, # 'cam_properties': <step.ArmCollection size 0>, # 'consequence': '', # 'description': '', # 'its_id': 'start', # 'its_machine_functions': <step.Object ARM MILLING_MACHINE_FUNCTIONS #39952 machining_functions>, # 'its_machining_strategy': None, # 'its_op_security_classification': <step.ArmCollection size 0>, # 'its_technology': <step.Object ARM MILLING_TECHNOLOGY #39931 machining_technology>, # 'its_tool': <step.Object ARM TWIST_DRILL #39905 machining_tool>, # 'its_tool_direction': None, # 'its_toolpath': <step.ArmCollection size 6>, # 'name': 'start', # 'overcut_length': None, # 'process_properties': <step.ArmCollection size 0>, # 'purpose': '', # 'retract': None, # 'retract_plane': None, # 'start_point': None return { 'id': o['its_id'], 'type': step.arm_type(o), # not on probing # 'machine_functions': o['its_machine_functions'], # 'technology': o['its_technology'], 'tool': json_tool_ref(o['its_tool']), 'toolpath': o['its_toolpath'], } def json_drilling(o) -> dict: D = json_operation(o) D['cutting_depth'] = json_measure(o.cutting_depth) #D['previous_diameter'] = json_measure(o.previous_diameter) #D['dwell_time_bottom'] = json_measure(o.dwell_time_bottom) #D['feed_on_retract'] = json_measure(o.feed_on_retract) return D def json_plane_milling(o) -> dict: D = json_operation(o) D['axial_cutting_depth'] = json_measure(o.axial_cutting_depth) D['overcut_length'] = json_measure(o.overcut_length) return D # CUTTING TOOLS -------------------------------------------------- def json_tool(o) -> dict: return { 'id': o['its_id'], 'type': step.arm_type(o), } def json_milling_machine_cutting_tool(o) -> dict: D = json_tool(o) # cutting components D['overall_assembly_length'] = json_measure(o.overall_assembly_length) D['effective_cutting_diameter'] = json_measure(o.effective_cutting_diameter) D['maximum_depth_of_cut'] = json_measure(o.maximum_depth_of_cut) D['hand_of_cut'] = o.hand_of_cut D['coolant_through_tool'] = json_boolean( o.hand_of_cut, 'supported', 'not supported' ) return D def json_milling_tool(o) -> dict: D = json_milling_machine_cutting_tool(o) D['number_of_effective_teeth'] = o.number_of_effective_teeth D['edge_radius'] = json_measure(o.edge_radius) return D def json_endmill(o) -> dict: D = json_milling_tool(o) D['tool_cutting_edge_angle'] = json_measure(o.tool_cutting_edge_angle) return D def json_drilling_tool(o) -> dict: D = json_milling_machine_cutting_tool(o) D['point_angle'] = json_measure(o.point_angle) return D def json_countersink(o) -> dict: D = json_drilling_tool(o) D['minimum_cutting_diameter'] = json_measure(o.minimum_cutting_diameter) D['maximum_usable_length'] = json_measure(o.maximum_usable_length) return D def json_user_defined_milling_tool(o) -> dict: D = json_milling_machine_cutting_tool(o) D['identifier'] = o.identifier D['corner_radius'] = json_measure(o.corner_radius) D['corner_radius_center_horizontal'] = json_measure(o.corner_radius_center_horizontal) D['corner_radius_center_vertical'] = json_measure(o.corner_radius_center_vertical) D['taper_angle'] = json_measure(o.taper_angle) D['tip_outer_angle'] = json_measure(o.tip_outer_angle) return D # FEATURES -------------------------------------------------- def json_feature(o) -> dict: D = { 'id': o['its_id'], 'type': step.arm_type(o), 'workpiece': json_uuid(o['its_workpiece']), 'explicit_representation': json_faces(o['explicit_representation']), } try: D['placement'] = step.Xform.fromstep(o.feature_placement) except: pass return D def json_revolved_flat(o) -> dict: D = json_feature(o) # flat edge shape has linear profile length PROF = o['flat_edge_shape'] D['profile_length'] = json_measure(PROF.profile_length) return D # TOOLPATHS -------------------------------------------------- def json_toolpath(o) -> dict: D = { 'id': o['its_id'], 'type': step.arm_type(o), 'basiccurve': json_curve(o['basiccurve']), } return D def json_curve(o) -> dict: if step.isinstance(o, 'polyline'): D = { 'name': o['name'], 'type': step.type(o), 'points': [], } pts = D['points'] for obj in o.points: pts.append(step.Vec.fromstep(obj)) return D return o # ------------------------------------------------------------ # FORMAT TABLE # # This is a simple dictionary that goes from the ARM type of an object # to a function that returns a dictionary for the json. To deal with # the inheritance, I have some common functions for execs, operations, # etc. that can be combined with | with a dictionary for the specifics # of a particular ARM type. # # JSONFMT = { # 'description': None, # 'formation': <step.Object #11 product_definition_formation>, # 'frame_of_reference': <step.Object #17 product_definition_context>, # 'id': '', # 'its_id': 'thready_fixed', # 'its_manufacturer': None, # 'its_manufacturer_organization': None, # 'its_owner': <step.Object ARM PERSON_AND_ADDRESS #1190 person>, # 'its_owner_organization': None, # 'its_release': <step.Object #443 date_and_time>, # 'its_security_classification': <step.ArmCollection size 0>, # 'its_status': <step.Object ARM APPROVAL #412 approval>, # 'its_workpieces': <step.ArmCollection size 1>, # 'main_workplan': <step.Object ARM WORKPLAN #1469 machining_workplan>} 'PROJECT': (lambda o: { 'id': o['its_id'], 'main_workplan': o['main_workplan'], }), # 'its_channel': None, # 'its_elements': <step.ArmCollection size 1>, # 'its_minimum_machine_params': None, # 'its_setup': <step.Object ARM SETUP #409 product_def inition_formation>, # 'planning_operation': None, # 'toolpath_orientation': <step.Object #1473 axis2_placement_3d>, 'WORKPLAN': (lambda o: json_exec(o) | { 'elements': json_enabled_elements(o['its_elements']), #'toolpath_orientation': step.Xform.fromstep(o['toolpath_orientation']), 'setup': o['its_setup'], }), 'SELECTIVE': (lambda o: json_exec(o) | { 'elements': json_enabled_elements(o['its_elements']), }), 'PARALLEL': (lambda o: json_exec(o) | { 'branches': json_enabled_elements(o['branches']), }), 'NON_SEQUENTIAL': (lambda o: json_exec(o) | { 'elements': json_enabled_elements(o['its_elements']), }), # 'final_features': <step.ArmCollection size 0>, # 'its_feature': <step.Object ARM FEATURE_TEMPLATE #1485 datum_feature_and_instanced_feature_and_thread>, # 'its_operation': <step.Object ARM THREADING_ROUGH #1381 threading_turning_operation>, # 'its_secplane': <step.Object #379 plane>, # 'its_secplane_rep': <step.Object #378 representation>, # 'toolpath_orientation': None, 'MACHINING_WORKINGSTEP': (lambda o: json_exec(o) | { 'operation': o['its_operation'], 'feature': o['its_feature'], #'toolpath_orientation': step.Xform.fromstep(o['toolpath_orientation']), }), # Operations 'BORING': json_operation, 'BOTTOM_AND_SIDE_FINISH_MILLING': json_operation, 'BOTTOM_AND_SIDE_ROUGH_MILLING': json_operation, 'CENTER_MILLING': json_operation, 'CONTOURING_FINISH': json_operation, 'CONTOURING_ROUGH': json_operation, 'CUTTING_IN': json_operation, 'DRILLING': json_drilling, 'FACING_FINISH': json_operation, 'FACING_ROUGH': json_operation, 'FREEFORM_FINISH_MILLING': json_operation, 'FREEFORM_OPERATION': json_operation, 'FREEFORM_ROUGH_MILLING': json_operation, 'GROOVING_FINISH': json_operation, 'GROOVING_ROUGH': json_operation, 'KNURLING': json_operation, 'MULTISTEP_DRILLING': json_drilling, 'PLANE_FINISH_MILLING': json_plane_milling, 'PLANE_ROUGH_MILLING': json_plane_milling, 'REAMING': json_operation, 'SIDE_FINISH_MILLING': json_operation, 'SIDE_ROUGH_MILLING': json_operation, 'TAPPING': json_operation, 'THREADING_FINISH': json_operation, 'THREADING_ROUGH': json_operation, # Probing Operations 'WORKPIECE_COMPLETE_PROBING': json_operation, 'WORKPIECE_PROBING': json_operation, # NC Functions 'DISPLAY_MESSAGE': json_exec, 'EXTENDED_NC_FUNCTION': json_exec, 'INDEX_TABLE': json_exec, 'LOAD_TOOL': json_exec, 'NON_SEQUENTIAL': json_exec, 'OPTIONAL_STOP': json_exec, 'PROGRAM_STOP': json_exec, 'RETURN_HOME': json_exec, 'UNLOAD_TOOL': json_exec, # Features 'CATALOGUE_THREAD': json_feature, 'CHAMFER': json_feature, 'CIRCULAR_BOSS': json_feature, 'CIRCULAR_CLOSED_SHAPE_PROFILE': json_feature, 'CIRCULAR_PATTERN': json_feature, 'CLOSED_POCKET': json_feature, 'COMPLEX_BOSS': json_feature, 'COMPOUND_FEATURE': json_feature, 'COUNTERBORE_HOLE': json_feature, 'COUNTERBORE_HOLE_TEMPLATE': json_feature, 'COUNTERSUNK_HOLE': json_feature, 'COUNTERSUNK_HOLE_TEMPLATE': json_feature, 'DEF INED_MARKING': json_feature, 'DEF INED_THREAD': json_feature, 'DIAGONAL_KNURL': json_feature, 'DIAMOND_KNURL': json_feature, 'EDGE_ROUND': json_feature, 'FEATURE_TEMPLATE': json_feature, 'GENERAL_FEATURE': json_feature, 'GENERAL_OUTSIDE_PROFILE': json_feature, 'GENERAL_PATTERN': json_feature, 'GENERAL_REVOLUTION': json_feature, 'GENERAL_SHAPE_PROFILE': json_feature, 'GROOVE': json_feature, 'OPEN_POCKET': json_feature, 'OUTER_DIAMETER': json_feature, 'OUTER_DIAMETER_TO_SHOULDER': json_feature, 'PARTIAL_CIRCULAR_SHAPE_PROFILE': json_feature, 'PLACED_FEATURE': json_feature, 'PLANAR_FACE': json_feature, 'RECTANGULAR_BOSS': json_feature, 'RECTANGULAR_CLOSED_SHAPE_PROFILE': json_feature, 'RECTANGULAR_OPEN_SHAPE_PROFILE': json_feature, 'RECTANGULAR_PATTERN': json_feature, 'REVOLVED_FLAT': json_revolved_flat, 'ROUND_HOLE': json_feature, 'ROUND_HOLE_TEMPLATE': json_feature, 'ROUNDED_END': json_feature, 'SLOT': json_feature, 'SPHERICAL_CAP': json_feature, 'STEP': json_feature, 'STRAIGHT_KNURL': json_feature, 'TOOL_KNURL': json_feature, 'TOOLPATH_FEATURE': json_feature, # Cutting Tools 'BALLNOSE_ENDMILL': json_endmill, 'BULLNOSE_ENDMILL': json_endmill, 'COMBINED_DRILL_AND_REAMER': json_tool, 'COMBINED_DRILL_AND_TAP': json_tool, 'COUNTERBORE': json_drilling_tool, 'COUNTERSINK': json_countersink, 'DOVETAIL_MILL': json_milling_tool, 'DRILL': json_drilling_tool, 'ENDMILL': json_endmill, 'FACEMILL': json_milling_tool, 'GENERAL_TURNING_TOOL': json_tool, 'GROOVING_TOOL': json_tool, 'KNURLING_TOOL': json_tool, 'PROFILED_END_MILL': json_endmill, 'REAMING_CUTTING_TOOL': json_tool, 'ROTATING_BORING_CUTTING_TOOL': json_tool, 'SHOULDERMILL': json_milling_tool, 'SIDE_MILL': json_milling_tool, 'SPADE_DRILL': json_drilling_tool, 'SPOTDRILL': json_drilling_tool, 'STEP_DRILL': json_drilling_tool, # has more 'T_SLOT_MILL': json_milling_tool, 'TAPERED_REAMER': json_tool, 'TAPPING_CUTTING_TOOL': json_tool, 'THREAD_MILL': json_milling_tool, 'TOUCH_PROBE': json_tool, 'TURNING_MACHINE_CUTTING_TOOL': json_tool, 'TURNING_THREADING_TOOL': json_tool, 'TWIST_DRILL': json_drilling_tool, 'TWIST_DRILL_BASE': json_drilling_tool, 'USER_DEFINED_MILLING_TOOL': json_user_def ined_milling_tool, 'USER_DEFINED_TURNING_TOOL': json_tool, # Toolpaths 'CUTTER_LOCATION_TRAJECTORY': json_toolpath, } # extra keyword args in dumps are passed to the ctor. can use that to # implement an omit_null keyword. class STEPEncoder(JSONEncoder): def default(self, o): if isinstance(o, step.ArmCollection): return list(o) if not isinstance(o, step.Object): return o if step.isinstance(o, 'RoseAggregate'): return list(o) # arm type usually returns a single name but might return a # list of types instead TLIST = step.arm_type(o) if not isinstance(TLIST,list): TLIST = [ TLIST ] # return first json cvt function that we find for AT in TLIST: FN = JSONFMT.get(AT) if callable(FN): return FN(o) return None # TOP LEVEL OBJECT -------------------------------------------------- def make_json_root(d) -> dict: D = { 'name': d.name(), 'tools': {}, 'project': apt.get_current_project(), } for obj in step.DesignCursor(d, "machining_tool"): if not obj.name: continue D['tools'][obj.name] = obj return D def main() -> int: """Export project from a file""" step.verbose(False) args = iter(sys.argv) # Parse command line arguments PROG = next(args) SRC = None DST = None while True: arg = next(args, None) if arg is None: break if arg ==-o: DST = next(args, None) if DST is None: print(option: -o <file>, file = sys.stderr ) sys.exit(1) continue SRC = Path(arg) break if SRC is None: print(no input file specified, file = sys.stderr ) sys.exit(1) if DST is None: DST = SRC.with_suffix('.json') # read data, must recognize arm objects before using adaptive D = apt.open_project(SRC) OBJ = make_json_root(D) y = json.dumps(OBJ, indent=4, cls=STEPEncoder) print (y) # may need to save design if it has changed because of new uuids return 0 if __name__ == '__main__': sys.exit(main())