Surface Finish: Tool Paths and Robot Motion
SurfaceFinishService (client.surface_finish) builds a tool path on an
object and turns it into robot motion, for sanding, polishing, painting,
dispensing and similar tasks. It works in stages. Data from every stage is
kept and can be read, and redoing a stage clears the results of later
stages.
Note
SurfaceFinishService requires the surface_finish license
(Activate Licenses).
Stage 1 Stage 2 Stage 3 Export
GenerateNominalPath ──▶ RefinePath ──────────▶ CreateRobotMotion ──────▶ GenerateRCATrajectoryFile
nominal_cartesian refined_cartesian basic_solution /
advanced_solution
The result is a surface_finish.Path config (type CONFIG_TYPE_SURFACE_PATH)
parented to the object. Read it with SurfaceFinishService.Get or
ConfigService.Get.
Stage 1: nominal path
GenerateNominalPath(GenerateNominalPathRequest) takes a header (the
path name, with parent set to the object the path lies on) and one path
generator. It returns path_names: one request can create several paths,
for example a raster split into separate passes.
Request field |
Source |
Description |
|---|---|---|
|
any |
|
|
mesh |
|
|
mesh |
|
|
CAD |
|
|
CAD |
|
|
CAD |
|
Mesh sketch path
path_req = GenerateNominalPathRequest(
header=ConfigHeader(
name="path_long_sketch",
parent="mesh_1"
),
mesh_sketch_path=MeshSketchPath(
points=[
Vector3(x=-0.05, y=0.0, z=0.00),
Vector3(x=-0.45, y=0.0, z=0.00)],
blending_radius=0.0,
fixed_spacing=0.01,
align_with_initial_direction=True
)
)
path_response = client.surface_finish.stub.GenerateNominalPath(path_req)
if (path_response.status.code == 0) and len(path_response.path_names) > 0:
print("Successfully generated sketch path for the mesh.")
else:
print(f"Surface finish service call failed: {path_response.status.message}")
return
Mesh raster path
path_req = GenerateNominalPathRequest(
header=ConfigHeader(
name="path",
parent="mesh_1"
),
mesh_raster_path=MeshRasterPath(
fixed_spacing=0.02,
blending_radius=0.0,
border_offset=0.0,
raster_line_spacing=0.04,
raster_path_joining_distance_threshold=0.08,
principal_rotation_angle=0.0,
align_with_initial_direction=True
)
)
path_response = client.surface_finish.stub.GenerateNominalPath(path_req)
if (path_response.status.code == 0) and len(path_response.path_names) > 0:
print("Successfully generated raster path for the mesh.")
else:
print(f"Surface finish service call failed: {path_response.status.message}")
return
Key MeshRasterPath parameters (all lengths in m, angles in rad):
raster_line_spacingDistance between raster lines.
fixed_spacingDistance between waypoints along a line.
blending_radiusCorner blending radius.
border_offsetInward offset from the region border.
raster_path_joining_distance_thresholdLines closer than this are joined into one continuous path.
principal_rotation_angleorspecified_directionRaster direction: an angle about the mesh’s principal direction, or a world-frame vector projected onto the mesh.
minimum_toolpath_lengthShorter paths after splitting are dropped.
align_with_initial_direction/align_with_previous/align_with_directionHow each waypoint’s X axis is oriented.
CAD-based generators
EdgePath, FaceRasterPath and FaceSketchPath follow the CAD edges or
faces listed in edges or faces. Each entry is the OCCT entry string
(TDF_Label) of a TopoDS_Edge or TopoDS_Face in the STEP file. In
C++, flexiv::pdk::surface_finish::ToOcctEntryString(shape, shape_tool)
(<flexiv/pdk/surface_finish/occt_entry_string.hpp>) computes it. The helper
exists only in builds with FLEXIV_PDK_ENABLE_OCCT_INTEGRATION=ON, and the
binary release ships its header only when the library contains it.
The object must have a Visual whose mesh.filename is the STEP file. If an
object has several visual files, set cad_filename (or mesh_filename) to
choose one.
Waypoint spacing for CAD paths (and for MeshSketchPath) is either
fixed_spacing (m) or variable_spacing, a VariableSpacing (min_spacing, max_spacing, position_tolerance,
orientation_tolerance). FaceRasterPath can be bounded by an
EdgeBorder (keep a distance from given edges) or a PolygonBorder.
Stage 2: refinement
Optional. RefinePath(RefinePathRequest(path_name, refinement)) modifies
the nominal waypoints. Stage 3 uses refined_cartesian if it is present,
otherwise nominal_cartesian. PathRefinement operations are applied in this order:
reversed_order: reverse the waypoint order;align_with_previous: rotate each waypoint about its local Z to align with the previous one;rotation_offset: rotate each waypoint in its own frame;position_offset: translate each waypoint in its own frame.
Warning
Set rotation_offset to the identity (w=1) if you do not want
a rotation. An unset quaternion is all zeros.
Stage 3: robot motion
CreateRobotMotion(CreateRobotMotionRequest(path_name, solver_preferences))
computes joint trajectories.
Field |
Description |
|---|---|
|
Required. The system that executes the path. |
|
Required. The TCP (tool) or ECP (external control point) that follows
the path, e.g. |
|
|
|
Desired elbow swivel angle and external axis positions. |
|
How external axes may move (none, preset, preferred, not used). |
|
Ignore rotation about the tool axis (for example, polishing wheels). |
|
Start configuration. If empty, the current robot state is used. |
|
ECP only: the robot holds the part and moves it past a fixed tool. |
print("Running basic solve for the generated long path...")
try:
for path_name in path_long_sketch_names:
solve_req = CreateRobotMotionRequest(
path_name=path_name,
solver_preferences=SolverPreferences(
system_name="system_1",
tcp_ecp_name="system_1.arm.flange",
solver_type=SolverPreferences.SOLVER_TYPE_BASIC
)
)
solve_response = client.surface_finish.stub.CreateRobotMotion(solve_req)
if (solve_response.status.code == 0):
print(f"Successfully created robot motion for path {path_name}.")
else:
print(f"Failed to create robot motion for path {path_name}: {solve_response.status.message}")
except grpc.RpcError as e:
print(f"Failed to solve path config: {e}")
return
Process parameters and export
Each CartesianWaypoint carries velocity, acceleration and a
wrench (force/torque to apply in contact). Edit them on the path and save
it back with ConfigService.Save(SaveConfigRequest(surface_path=...)). Then
GenerateRCATrajectoryFile writes a robot trajectory (.traj) file on the
server, at <workcell>/linear_path/<path_name>/<path_name>.traj, where
<workcell> is the active workcell’s folder in the server’s workcell
directory (flexiv_pdk_data/workcell/ for the containerized server, see
Set Up the PDK Server).
# velocity and acceleration for the robot motion
print("Setting Cartesian velocity, acceleration, and force for the generated path...")
try:
for path_name in path_long_sketch_names:
get_req = GetRequest(path_name=path_name)
get_response = client.surface_finish.stub.Get(get_req)
if (get_response.status.code == 0):
path_config = get_response.path
for waypoint in path_config.nominal_cartesian:
waypoint.velocity = 0.1
waypoint.acceleration = 0.2
waypoint.wrench.force.z = 5
for waypoint in path_config.refined_cartesian:
waypoint.velocity = 0.1
waypoint.acceleration = 0.2
waypoint.wrench.force.z = 5
save_req = SaveConfigRequest(surface_path=path_config)
client.config.stub.Save(save_req)
print(f"Successfully set Cartesian velocity, acceleration, and force for path {path_name}.")
else:
print(f"Failed to get path config for {path_name}: {get_response.status.message}")
except grpc.RpcError as e:
print(f"Failed to get path config: {e}")
return
# 7. Generate the RCA trajectory for the robot motion.
# The generated trajectory .traj file will be saved in
# the PDK server as {workcell}/linear_path/{path_name}/{path_name}.traj.
print("Generating RCA trajectory for the robot motion...")
try:
for path_name in path_long_sketch_names:
generate_rca_traj_req = GenerateRCATrajectoryFileRequest(
path_name=path_name,
)
generate_rca_traj_response = client.surface_finish.stub.GenerateRCATrajectoryFile(generate_rca_traj_req)
if (generate_rca_traj_response.status.code == 0):
print(f"Successfully created RCA trajectory for path {path_name}.")
else:
print(f"Failed to create RCA trajectory for path {path_name}: {generate_rca_traj_response.status.message}")
except grpc.RpcError as e:
print(f"Failed to generate RCA trajectory: {e}")
return
Complete example
The example prints grpc.RpcError instead of raising it and does not check
every status.code. Production code should check both; see
Error Handling.
"""
Example: Create a toolpath sketch from a plane mesh and generate a RCA trajectory for a robot to follow the path.
This script demonstrates how to:
1. Create a new workcell
2. Add a plane mesh object to the scene
3. Add a robot system to the scene
4. Generate a sketch path for a mesh using the surface finish service
5. Solve for the robot motion along the generated path
6. Set Cartesian velocity, acceleration, and force for the robot motion
7. Generate the RCA trajectory for the robot motion
Note: To run this example, copy the plane mesh file "plane_1m_10x10.obj" in the assets directory to "flexiv_pdk_data/user_data/meshes/" on the server machine. That folder is beside your flexiv_pdk folder.
"""
import os
import grpc
import time
from flexiv.pdk import Client
from flexiv.pdk.workcell.workcell_service_pb2 import CreateRequest, NameRequest
from flexiv.pdk.config_service_pb2 import SaveConfigRequest, TypeNameConfigRequest, CONFIG_TYPE_SURFACE_PATH
from flexiv.pdk.scene.object_pb2 import Object
from flexiv.pdk.system.system_pb2 import System
from flexiv.pdk.system.components.manipulator_pb2 import Manipulator
from flexiv.pdk.config_header_pb2 import ConfigHeader
from flexiv.pdk.spatial.geometry_pb2 import SE3Pose, Vector3, Quaternion
from flexiv.pdk.spatial.shape_pb2 import Visual, Mesh, Shape
from flexiv.pdk.surface_finish.service_pb2 import GenerateNominalPathRequest, CreateRobotMotionRequest, GetRequest, GenerateRCATrajectoryFileRequest
from flexiv.pdk.surface_finish.solver_preferences_pb2 import SolverPreferences
from flexiv.pdk.surface_finish.generator.mesh_sketch_path_pb2 import MeshSketchPath
# Helper to construct header
def make_header(name, parent="world", px=0.0, py=0.0, pz=0.0, rw=1.0, rx=0.0, ry=0.0, rz=0.0):
return ConfigHeader(
name=name,
parent=parent,
pose_in_parent=SE3Pose(
position=Vector3(x=px, y=py, z=pz),
rotation=Quaternion(x=rx, y=ry, z=rz, w=rw)
)
)
def make_mesh_visual(origin, mesh_path, scale=1.0):
return Visual(
origin=SE3Pose(
position=Vector3(x=origin[0], y=origin[1], z=origin[2]),
rotation=Quaternion(x=0.0, y=0.0, z=0.0, w=1.0)
),
shape=Shape(
mesh=Mesh(
filename=mesh_path,
scale=Vector3(x=scale, y=scale, z=scale)
)
)
)
def main():
target = os.environ.get("PDK_SERVER_ADDR", "localhost:50052")
print(f"Connecting to the PDK server at {target}...")
# We use insecure channel, wrapped in our Client
with Client(target) as client:
# 1. Create a workcell
workcell_name = "example_workcell"
# Remove any existing workcell
print(f"Removing any existing workcell with name: {workcell_name}")
try:
client.workcell.stub.Remove(NameRequest(name=workcell_name))
except grpc.RpcError as e:
print(f"Remove workcell failed or not present: {e}")
print(f"Creating workcell: {workcell_name}")
req = CreateRequest(name=workcell_name, description="Example workcell for linear path trajectory generation")
try:
client.workcell.stub.Create(req)
client.workcell.stub.Load(NameRequest(name=workcell_name))
except grpc.RpcError as e:
print(f"Workcell operation failed or environment not running a server yet: {e}")
# 2. Add plane mesh object to the scene
print("Adding plane mesh object to the scene...")
obj1 = Object(header=make_header("mesh_1", px=0.8, py=0.0, pz=0.0), visual=[make_mesh_visual((0.0, 0.0, 0.0), "/home/flexiv/user_data/meshes/plane_1m_10x10.obj")])
try:
client.config.stub.Save(SaveConfigRequest(object=obj1))
except grpc.RpcError:
pass
# 3. Add a robot system to the scene
print("Saving robot system to the scene...")
sys1 = System(
header=make_header("system_1", px=0.05, py=0.0, pz=0.0),
control_box_serial_number="CX01-01-T1-000001",
manipulator=Manipulator(
name="arm",
arm_serial_number="A02L-00-M3-000001"
),
)
try:
client.config.stub.Save(SaveConfigRequest(system=sys1))
except grpc.RpcError:
pass
# 4. Run the surface finish service to generate a sketch path for the mesh. The specified points
# are in the mesh's local frame, and the generated path will be saved as a new config in the scene.
print("Generating sketch path for the mesh...")
try:
path_req = GenerateNominalPathRequest(
header=ConfigHeader(
name="path_long_sketch",
parent="mesh_1"
),
mesh_sketch_path=MeshSketchPath(
points=[
Vector3(x=-0.05, y=0.0, z=0.00),
Vector3(x=-0.45, y=0.0, z=0.00)],
blending_radius=0.0,
fixed_spacing=0.01,
align_with_initial_direction=True
)
)
path_response = client.surface_finish.stub.GenerateNominalPath(path_req)
if path_response.status.code != 0:
print(f"Surface finish service call failed: {path_response.status.message}")
return
print("Successfully generated sketch path for the mesh.")
except grpc.RpcError as e:
print(f"Surface finish service call failed: {e}")
return
path_long_sketch_names = path_response.path_names
# 5. Run Basic solve to compute the robot joint positions for the generated path
print("Running basic solve for the generated long path...")
try:
for path_name in path_long_sketch_names:
solve_req = CreateRobotMotionRequest(
path_name=path_name,
solver_preferences=SolverPreferences(
system_name="system_1",
tcp_ecp_name="system_1.arm.flange",
solver_type=SolverPreferences.SOLVER_TYPE_BASIC
)
)
solve_response = client.surface_finish.stub.CreateRobotMotion(solve_req)
if (solve_response.status.code == 0):
print(f"Successfully created robot motion for path {path_name}.")
else:
print(f"Failed to create robot motion for path {path_name}: {solve_response.status.message}")
except grpc.RpcError as e:
print(f"Failed to solve path config: {e}")
return
# 6. Get the generated path config and set the Cartesian
# velocity and acceleration for the robot motion
print("Setting Cartesian velocity, acceleration, and force for the generated path...")
try:
for path_name in path_long_sketch_names:
get_req = GetRequest(path_name=path_name)
get_response = client.surface_finish.stub.Get(get_req)
if (get_response.status.code == 0):
path_config = get_response.path
for waypoint in path_config.nominal_cartesian:
waypoint.velocity = 0.1
waypoint.acceleration = 0.2
waypoint.wrench.force.z = 5
for waypoint in path_config.refined_cartesian:
waypoint.velocity = 0.1
waypoint.acceleration = 0.2
waypoint.wrench.force.z = 5
save_req = SaveConfigRequest(surface_path=path_config)
client.config.stub.Save(save_req)
print(f"Successfully set Cartesian velocity, acceleration, and force for path {path_name}.")
else:
print(f"Failed to get path config for {path_name}: {get_response.status.message}")
except grpc.RpcError as e:
print(f"Failed to get path config: {e}")
return
# 7. Generate the RCA trajectory for the robot motion.
# The generated trajectory .traj file will be saved in
# the PDK server as {workcell}/linear_path/{path_name}/{path_name}.traj.
print("Generating RCA trajectory for the robot motion...")
try:
for path_name in path_long_sketch_names:
generate_rca_traj_req = GenerateRCATrajectoryFileRequest(
path_name=path_name,
)
generate_rca_traj_response = client.surface_finish.stub.GenerateRCATrajectoryFile(generate_rca_traj_req)
if (generate_rca_traj_response.status.code == 0):
print(f"Successfully created RCA trajectory for path {path_name}.")
else:
print(f"Failed to create RCA trajectory for path {path_name}: {generate_rca_traj_response.status.message}")
except grpc.RpcError as e:
print(f"Failed to generate RCA trajectory: {e}")
return
if __name__ == "__main__":
main()