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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)") | |
} |
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: