In this material, we’ll explore how to create a module using a set of special terminal utilities provided by Industrial Builder. The build kit is automatically added to the target folder when creating external modules, before compiling the program components. Of course, this kit is available in the application's project repository. Let’s try creating a module for accessing a force-torque sensor.

The Device

For a robot to sense the objects it interacts with, it needs sensory devices. A force-torque sensor is one such device: it enables the robot not only to pat-pat-pat but also to feel its own touches. The force-torque sensor returns data about the forces and torques acting upon it.

So, we’ve got ourselves a sensor. How do we use it? First, identify the manufacturer and find the software. Luckily, we already know – it's ATI. We visit the manufacturer's website and look for the software. Which is absolutely essential, without it, any device is just a mounting or useless decoration.
Nice, there’s not only software but also source code. The best option is a simple script to read data. And it exists – it's Net F/T C Sample.

Script

The project contains source code in the form of a single file (netft.c) and is meant to be compiled using the GNU C Compiler. We rewrite the script from C to Swift, using the Foundation framework. AI is quite suitable for this purpose. We used Google Gemini 2.0 Flash with the following prompt:

Now, rewrite this code in the main.swift file of a terminal application created using ...

The resulting script looks like this:

import Foundation
// Response structure (similar to C-structure RESPONSE)
struct Response
{
var rdt_sequence: UInt32
var ft_sequence: UInt32
var status: UInt32
var ftData: [Int32]
}
let port: UInt16 = 49152
let command: UInt16 = 2
let numSamples: UInt32 = 1
let axes = ["Fx", "Fy", "Fz", "Tx", "Ty", "Tz"]
// Check that the IP address is provided as an argument
guard CommandLine.arguments.count >= 2 else
{
fputs("Usage: \(CommandLine.arguments[0]) IPADDRESS\n", stderr)
exit(1)
}
let ip = CommandLine.arguments[1]
// Create a socket
let socketFD = socket(AF_INET, SOCK_DGRAM, 0)
guard socketFD >= 0 else
{
perror("socket")
exit(2)
}
// Create a request
var request = [UInt8](repeating: 0, count: 8)
withUnsafeBytes(of: UInt16(0x1234).bigEndian)
{
request.replaceSubrange(0..<2, with: $0)
}
withUnsafeBytes(of: command.bigEndian)
{
request.replaceSubrange(2..<4, with: $0)
}
withUnsafeBytes(of: numSamples.bigEndian)
{
request.replaceSubrange(4..<8, with: $0)
}
// Set address
var addr = sockaddr_in()
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = port.bigEndian
inet_pton(AF_INET, ip, &addr.sin_addr)
// Connect to the address
let result = withUnsafePointer(to: &addr)
{
$0.withMemoryRebound(to: sockaddr.self, capacity: 1)
{
connect(socketFD, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
}
}
guard result == 0 else
{
perror("connect")
close(socketFD)
exit(3)
}
// Send the request
let sent = request.withUnsafeBytes
{
send(socketFD, $0.baseAddress, $0.count, 0)
}
guard sent == request.count else
{
perror("send")
close(socketFD)
exit(4)
}
// Receive the response
var responseBuffer = [UInt8](repeating: 0, count: 36)
let received = responseBuffer.withUnsafeMutableBytes
{
recv(socketFD, $0.baseAddress, $0.count, 0)
}
guard received == 36 else
{
perror("recv")
close(socketFD)
exit(5)
}
close(socketFD)
// Parse the response
func extractUInt32(from data: [UInt8], offset: Int) -> UInt32
{
return data.withUnsafeBytes {
UInt32(bigEndian: $0.load(fromByteOffset: offset, as: UInt32.self))
}
}
func extractInt32(from data: [UInt8], offset: Int) -> Int32
{
return data.withUnsafeBytes
{
Int32(bigEndian: $0.load(fromByteOffset: offset, as: Int32.self))
}
}
var response = Response(
rdt_sequence: extractUInt32(from: responseBuffer, offset: 0),
ft_sequence: extractUInt32(from: responseBuffer, offset: 4),
status: extractUInt32(from: responseBuffer, offset: 8),
ftData: (0..<6).map
{ i in
extractInt32(from: responseBuffer, offset: 12 + i * 4)
}
)
// Print the result
print(String(format: "Status: 0x%08x", response.status))
for (i, val) in response.ftData.enumerated()
{
print("\(axes[i]): \(val)")
}
view raw netft.swift hosted with ❤ by GitHub

Save it under a name like netft.swift in the same folder as the Modules Building Kit scripts. Open the folder in Terminal. You can also launch scripts by double-clicking and entering parameters afterward.

Compilation

To convert the script into a ready-to-use application, all we need is LCompile:
bash
./LCompile.command netft.swift

As a result, we’ll get a Unix executable and a terminal application project that can be opened in Xcode.

If you run the script with the -c flag:
bash
./LCompile.command netft.swift -c

Then after compiling the executable, the project package and source listing will be deleted. This form of LCompile is used for batch compilation of many software components — for instance, as part of an industrial module. However, you’ll most likely want to keep the project and source file for editing purposes.

Note: If you just run any script from the Modules Building Kit, it will prompt you to enter a file/folder name depending on its function (except for MPCompile, which works with all industrial module packages in the folder from which it’s launched).

Development

The usual development flow for a software component involves either creating an application project via IndustrialAppPackageMake or converting an existing single listing into an application project using LtPConvert.

Then, the resulting project is edited and compiled using PBuild, after which the compiled file is placed in the same folder as the project package.

To convert an existing netft.swift listing into an application project:
bash
./LtPConvert.command netft.swift

Or, to create a new application project with the IndustrialKit framework imported:
bash
./IndustrialAppPackageMake.command netft

Now we can browse its contents and modify it:

When using LtPConvert, the selected listing is placed in the Sources folder and renamed to main. Since this is a standard application project, you can add additional source files, resources, and connect any other libraries.

You can check the build correctness in Xcode, but for testing terminal applications, it’s best to compile via PBuild and run from the terminal.

To compile the project:
bash
./PBuild.command netft_Project

As a result, the executable will appear in the project folder, from where it can be launched:
bash
./netft

Make a part

To connect to the force-torque sensor, we connect to the local industrial network via Ethernet. Configure the settings through the corresponding service (in the TCP/IP), named Ethernet (if it’s a built-in port), or according to the name of the controller if using an adapter:

Option Value
Configure IPv4 Manually
IP Address 169.254.5.10
Subnet Mask 255.255.255.0


Connect the Ethernet cable and try launching netft. Specify the IP address of the force-torque sensor in the local industrial network (in our case, it is 169.254.5.178):
bash
./netft 169.254.5.178

And we get the following output:
bash
Status: 0x80020000
Fx: -64551326
Fy: -43820232
Fz: -6076648
Tx: -7989061
Ty: 833312
Tz: -2394311

Let’s take a look at how that appears: