Crates.io | twincatads-rs |
lib.rs | twincatads-rs |
version | 1.0.1 |
created_at | 2022-03-31 18:26:47.648074+00 |
updated_at | 2025-09-09 14:55:18.390406+00 |
description | Rust wrapper for TwinCAT ADS library provided with Beckhoff TwinCAT. |
homepage | https://automateddesign.com |
repository | https://adc360.visualstudio.com/adc-public/_git/twincatads-rs |
max_upload_size | |
id | 559783 |
size | 277,957 |
A rust wrapper for the TwinCAT ADS library provided by Beckhoff. Includes a convenient client class that makes connecting to a target trivial. Uploads symbol and data type information from the target to easily read and write structures and arrays. With 0.3.2, we switched to Tokio channels for safe, asynchronous communication and easy integration into other projects.
See src/bin/commtest.rs for a complete usage example. Note that commtest is always changing to test in different applications.
When trying to connect via ADS on a system where TwinCAT XAR or XAE is already installed and running, the connections will fail. In those cases, you need to use the TcAdsDll to connect. This crate uses Bindgen to create a wrapper. Both 32- and 64-bit builds should now be supported.
When building this project on Windows, you may encounter errors from the twincatads-rs crate related to bindgen. This is typically due to an incorrect C++ toolchain being found by the Rust build process.
Your cargo build fails with an error message that includes lines like:
error: unknown type name '__int64'
error: '__declspec' attributes are not enabled; use '-fdeclspec' or '-fms-extensions'
warning: '__stdcall' calling convention is not supported for this target
These errors occur because bindgen (the tool Rust uses to parse C/C++ headers) has found an incompatible compiler toolchain (like GNU/MinGW) instead of the required MSVC-compatible toolchain (LLVM/Clang). The TwinCAT ADS header files use Microsoft-specific syntax that GNU-style compilers do not understand by default.
Set the environment variable to point directly to the bin folder of your LLVM installation.
$env:LIBCLANG_PATH="C:\Program Files\LLVM\bin"
This will only be maintained as long as the terminal session is open.
Note that this is the only solution if your computer has already set an environment variable for LIBCLANG_PATH and we need cargo to override it. The next session tries to set the proper environment variable by configuring cargo, but Cargo will not override environment variables that already exist. If your system already has already set an environment variable for LIBCLANG_PATH, this command-line solution is the proper choice.
This solution is more elegant and the preferred option, but will only work if your computer has NOT already set an environment variable for LIBCLANG_PATH. Cargo will not override environment variables that already exist. If your system already has already set an environment variable for LIBCLANG_PATH, use the command-line solution above.
Follow these steps to ensure the build process uses the correct tools.
If you don't have it already, you must install LLVM, which provides the Clang compiler tools that bindgen needs.
Download the latest pre-built Windows binary from the official LLVM website.
Run the installer. Crucially, ensure you check the box "Add LLVM to the system PATH" during installation.
The most reliable way to ensure Cargo finds the correct Clang installation is to configure the project to point directly to it.
Create a file named config.toml inside a .cargo directory in the root of this project.
Project Structure:
your-project/
├── .cargo/
│ └── config.toml <-- Create this file
├── src/
│ └── main.rs
└── Cargo.toml
Add the following content to .cargo/config.toml:
# .cargo/config.toml
[env]
# Force bindgen/clang-sys to use the libclang from the official LLVM installation.
# This avoids issues with other toolchains (MinGW, etc.) that might be on the PATH.
# Note the required double backslashes for the TOML string.
LIBCLANG_PATH = "C:\\Program Files\\LLVM\\bin"
After creating the configuration file, run a clean build from your terminal:
cargo clean
cargo build
Your project should now compile successfully. This configuration is checked into version control, so any developer who clones the repository and installed LLVM in the matching location will automatically use the correct path, avoiding this issue in the future.
Function blocks and structures containing reference variables can't be properly parsed. Individual items within those FBs and Structs can be parsed, but not the item as a whole.
Reading a complete structure is a design goal for the twincatads-rs. However, deserializing a complete function block is not currently a feature in use and isn't currently targetted.
Reading a string that is not declared as T_MaxString using read_symbol_value is not currently supported. Use read_symbol_string_value instead.
Writing strings with write_symbol_value is similarly limited. write_symbol_value needs a type:
if let Err(err) = client.write_symbol_value::<MaxString>(
"GM.sTarget",
MaxString::from_string("I'm not even supposed to be here today.")
) {
println!("An error occurred writing <MaxString> for: {}", err);
}
It's cleaner to simply use write_symbol_string_value:
if let Err(err) = client.write_symbol_string_value(
"GM.sTarget",
"There is no spoon."
) {
println!("An error occurred writing the tag: {}", err);
}
Fixed strings are possible:
fixed_str: [u8; 32],
read_symbol_value
and write_symbol_value
need a known type with only fixed-width members, so nothing dynamic like String
or Vec
. If
a structure is predefined, it can be used with read_symbol_value
and write_symbol_value
.
Example:
use zerocopy::{AsBytes, FromBytes};
#[derive(FromBytes, AsBytes, Default, Debug)]
#[repr(C)]
struct MyStruct {
field1: u32,
field2: u64,
}
...
let st = MyStruct {
field1 : 123,
field2: 456
};
if let Err(err) = client.write_symbol_value::<MyStruct> ("GVL.myStructVariable", &st) {
println!("You blew it!");
}
However, dynamic programs that don't know symbols or types until run-time don't always have the luxury of pre-defined, static types.
Instead, those values are stored in mechutil::VariantValue instances. Structure symbols registered for on data change notification
will automatically be parsed into a VariantValue::Object. To write a structure stored in a VariantValue::Object, use write_symbol_variant_value
.
The VariantValue::Object must have the exact same structure as the PLC. Structures in the target don't always byte-align the same as they do in the local client; the adsclient handles adjusting the alignment of the data.
twincatads-rs = "0.3"
The bindings to Bindgen requires LLVM.
With earlier versions of Bindgen, we needed to have LLVM installed specifically. You can download the latest and greatest of LLVM here: https://releases.llvm.org/download.html
Find LLVM-XX.XX.XX-win64.exe on the releases page. Before building, make sure to have set the LIBCLANG_PATH.
Example:
SET LIBCLANG_PATH=c:\Program Files\LLVM\bin
However, we have not needed this with more current versions of Bindgen, although Bindgen's documention still lists the requirement. Our advice is to install as needed.
To build matching the default target of your machine (usually 64-bit):
cargo build
To specifically build a 64-bit version:
cargo build --target x86_64-pc-windows-msvc
To build a 32-bit version:
cargo build --target i686-pc-windows-msvc
For a more complete example, see src/bin/commtest.rs.
Using the recommended client library, including channels for notifications.
let (mut client, rx) = AdsClient::new();
// Supply AMS Address. If not set, localhost is used.
client.set_address("192.168.127.1.1.1");
// Supply ADS port. If not set, the default of 851 is used.
// You should generally use the default.
client.set_port(851);
// Make the connection to the ADS router
client.initialize();
// Write a value to a symbol in the PLC
if let Err(err) = client.write_symbol_string_value(
"GM.sTarget",
"There is no spoon."
) {
println!("An error occurred writing the tag: {}", err);
}
match client.read_symbol_value::<MaxString>("GM.sEcho") {
Ok(val) => info!("STRING VALUE: {:?}", val.to_string()),
Err(err) => error!("I failed to read the string tag: {}", err)
}
match client.read_symbol_value::<MaxString>("GK.sReadTest") {
Ok(val) => info!("STRING VALUE: {:?}", val.to_string()),
Err(err) => error!("I failed to read the string tag: {}", err)
}
match client.read_symbol_string_value("GM.sEcho") {
Ok(val) => info!("STRING VALUE: {:?}", val.to_string()),
Err(err) => error!("I failed to read the string tag: {}", err)
}
const NOTIFY_TAG :&str = "GM.fReal";
if let Err(err) = client.register_symbol(NOTIFY_TAG) {
error!("Failed to register symbol: {}", err);
}
else {
let mut line = String::new();
// On the parent context (e.g., main thread), listen for notifications
thread::spawn(move || {
//
// When you call .iter() on a Receiver, you get an iterator that blocks waiting for new messages,
// and it continues until the channel is closed.
//
for notification in rx.iter() {
println!("Notification received: {} : {}", notification.name, notification.value);
// Handle the notification...
}
});
std::io::stdin().read_line(&mut line).expect("Failed to read line");
// if let Err(err) = client.unregister_symbol(NOTIFY_TAG) {
// error!("Failed to unregister symbol: {} ", err);
// }
}
// Make sure the ADS client is closed.
client.finalize();
Simple example using the Beckhoff API directly.
use std::{time};
use twincatads_rs::*;
pub fn hello_ads() {
println!("Hello, ADS!");
unsafe {
let val = AdsGetDllVersion();
println!("The ADS Dll version is {}", val);
let client_port = AdsPortOpen();
println!("The ADS client port is {}", client_port);
let mut local_addr = AmsAddr::new();
AdsGetLocalAddress(&mut local_addr);
println!("local addr is {}.{}.{}.{}.{}.{}",
local_addr.netId.b[0],
local_addr.netId.b[1],
local_addr.netId.b[2],
local_addr.netId.b[3],
local_addr.netId.b[4],
local_addr.netId.b[5]
);
std::thread::sleep(time::Duration::from_millis(1000));
AdsPortClose();
println!("Goodbye...");
}
}
MIT
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Myself and my company are not related to Beckhoff.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.