In this material, we will explore the creation of a digital twin of a robotic technological complex (RTC) using an industrial framework. We will create a simple application for the robot to pick up a shaft using a gripper on its end point.

You can download the initial version we will be working with here. And the final version – here.

Preparation

To begin, let's create the UI of the application. Add a scene view and two buttons on top of it – for starting and resetting the RTC. The workspace_scene should be displayed in this view. Add a prepare function that will be executed during scene initialization.

To prevent the objects placed in the scene from falling down, let's add a floor using the add_floor function, placing the call to it in prepare.

Next, let's create an instance of Workspace – the RTC space.

Workspace Content View
Preview
import SwiftUI
import SceneKit
import IndustrialKit
let workspace_scene = SCNScene()
var workspace = Workspace()
struct ContentView: View
{
var body: some View
{
ObjectSceneView(scene: workspace_scene, on_init: { scene_view in prepare() })
.frame(width: 512, height: 512)
.background
{
Rectangle()
.fill(.thinMaterial)
.shadow(radius: 4)
}
.overlay(alignment: .bottomTrailing)
{
HStack
{
Button(action: reset)
{
Text("Reset")
}
.buttonStyle(.bordered)
Button(action: perform)
{
Label("Perform", systemImage: "play")
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
}
func prepare()
{
// Add floor
add_floor()
}

func add_floor()
{
// Create floor node
let floor = SCNNode(geometry: SCNPlane(width: 1024, height: 1024))
// Add material
if let material = floor.geometry?.firstMaterial
{
material.diffuse.contents = UIColor.gray
material.lightingModel = .constant
material.reflective.intensity = 0
material.isDoubleSided = true
}
// Set floor parameters
floor.eulerAngles.x = -.pi / 2
floor.position.y = -0.05
floor.physicsBody = .static()
floor.opacity = 0.4
// Add to workspace
workspace_scene.rootNode.addChildNode(floor)
}

Adding Components

Our robotic complex implies the presence of a robot, a tool, and a part, one instance of the corresponding models each. The modules required for their creation are already built into the package. Therefore, to create a production object, it will be sufficient to initialize it with a name and module.

In the Application Playground, access to resources is organized in a rather specific way, so the module resources are placed in a separate resource folder, and their address strings in the modules contain only the scene file name itself.

Add Robot

Let's start with the manipulator. Set the position of its local coordinate system, offsetting it forward and upward and rotating it downward. Next, specify that the robot is placed in the RTC workspace. Be sure to initialize the update – in our case, it is only needed to update the manipulator links via inverse kinematics. Finally, add our robot to the workspace.

All the described creation functionality is placed in the prepare_robot function, which is called in the prepare function.

func prepare_robot()
{
// Create robot by module
let arm = Robot(name: "6DOF", module: _6DOF_Module)
// Set robot position
arm.location = [0, 0, 0]
arm.origin_location = [200, 0, 100]
arm.origin_rotation = [90, 90, 0]
arm.is_placed = true
// Perform robot model update
arm.perform_update()
// Add robot in workspace
workspace.add_robot(arm)
}

Note that continuous cyclic updating is also used (for both robots and tools) to obtain statistical data and synchronize with the real device via a connector.


Add Tool

Next, let's create a gripper from the module of the same name. Since the gripper should be attached to the robot, specify that it is attached, as well as the name of the robot on whose end point to attach it. We place all the functionality in the prepare_tool function.

func prepare_tool()
{
// Create tool by module
let gripper = Tool(name: "Gripper", module: Gripper_Module)
// Set tool location
gripper.is_placed = true
gripper.is_attached = true
gripper.attached_to = "6DOF"
// Add tool to workspace
workspace.add_tool(gripper)
}

Of course, the tool can also stand separately. If the tool is not attached, it is placed in accordance with the specified position parameters in space, which are not taken into account when attaching it to the robot. Also, only one tool can be attached to one robot.


Add Part

It remains to add the shaft. Initialize it from the module, set the position in space – it stands in front of the robot. Also specify the physical body parameters of the part. In our case, it is dynamic.

func prepare_part()
{
// Create part by module
let shaft = Part(name: "Shaft", module: Shaft_Module)
// Set part location
shaft.location = [200, 0, 30]
shaft.physics_type = .ph_dynamic
shaft.is_placed = true
// Add part to workspace
workspace.add_part(shaft)
}

Deployment

To deploy the virtual model of the RTC in the scene, place a call to the connect_scene function of our workspace in the prepare function. Pass workspace_scene as the input argument.

If everything is done correctly, we will see our RTC in the scene view.

Deployment View
Preview
import SwiftUI
import SceneKit
import IndustrialKit
let workspace_scene = SCNScene()
var workspace = Workspace()
struct ContentView: View
{
var body: some View
{
ObjectSceneView(scene: workspace_scene, on_init: { scene_view in prepare() })
.frame(width: 512, height: 512)
.background
{
Rectangle()
.fill(.thinMaterial)
.shadow(radius: 4)
}
.overlay(alignment: .bottomTrailing)
{
HStack
{
Button(action: reset)
{
Text("Reset")
}
.buttonStyle(.bordered)
Button(action: perform)
{
Label("Perform", systemImage: "play")
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
}
func prepare()
{
// Add floor
add_floor()
// Prepare components
prepare_robot()
prepare_tool()
prepare_part()
// Deploying
workspace.connect_scene(workspace_scene)
}

Next, add a function to reset the scene – reset. In it, we sequentially delete all the nested nodes of workspace_scene, re-add the floor, and then deploy the digital twin of the RTC. When resetting, there is no need to recreate the workspace.

func reset()
{
workspace_scene.rootNode.remove_all_child_nodes()
add_floor()
workspace.connect_scene(workspace_scene)
}

Handling

Now that everything is ready, let's move on to controlling our complex. In this material, we will not use IMA and programs, it will be sufficient for us to sequentially call single operations.

Just as the robot selects positions for execution, the workspace selects devices for control. Let's select the robot and tool by their names in the RTC. After that, let's start the gripper operation and manipulator displacement operations on the selected devices.

We put all the functionality in the perform function.

func perform()
{
workspace.select_robot(name: "6DOF")
workspace.select_tool(name: "Gripper")
workspace.selected_tool.perform(code: 1)
{
workspace.selected_robot.move_to(point: PositionPoint(x: 150, y: 0, z: 0))
{
workspace.selected_tool.perform(code: 0)
{
workspace.selected_robot.move_to(point: PositionPoint(x: 0, y: 0, z: 0))
{
print("Finished")
}
}
}
}
}

It remains to assign calls to the reset and perform functions to the corresponding buttons of our main view. The final version of the application will look like this.


Conclusion

So, we have successfully mastered the construction of a robotic complex using IndustrialKit. We learned about the principle of filling the workspace with components and organizing them into a single production system.

Almost any robotic production is a complex of interconnected controlled devices, and the possibility of their mutual integration is a priority task in industrial automation.