ciplink¶
Read and write Allen-Bradley PLC tags over EtherNet/IP from Python, C, C++, C# or Rust.
ciplink is a fork of pycomm3 rebuilt on a shared Rust core. Every language calls the same driver, so tag reads, packet packing and error handling behave the same everywhere. In Python it keeps the familiar pycomm3 API and uses several times less CPU per call.
flowchart LR
PY[Python] -->|PyO3| CORE
CPP[C / C++] -->|C ABI| CORE
CS[C# / .NET] -->|P/Invoke| CORE
RS[Rust] --> CORE
CORE[[ciplink core<br/>Rust]] -->|EtherNet/IP| PLC[(Logix · Micro800<br/>SLC · MicroLogix)]
Install¶
Python 3.9 or later. The wheels contain the compiled core, so no Rust toolchain is needed.
.NET 8 or later. The package contains the native library for Windows x64, Linux x64 and macOS.
Download ciplink-ffi-<version>-<platform> from the
releases page: it holds ciplink.h and the libraries.
Add the header-only ciplink.hpp
(C++17) next to it.
Download ciplink-ffi-<version>-<platform> from the
releases page: include/ciplink.h, plus a shared and a
static library in lib/.
Quick start¶
The path is the controller's IP address, IP/slot for a ControlLogix outside slot 0, or a full route such as
192.0.2.10/bp/2/enet/192.0.2.20/bp/0. Every read and write returns one result per tag. A failure is reported on
that tag's result rather than thrown, so one bad tag never fails a whole batch.
use ciplink::logix::driver::{LogixDriver, LogixOptions};
use ciplink::Value;
let plc = LogixDriver::new("192.0.2.10", LogixOptions::default())?;
plc.open()?;
for tag in plc.read(&["Counter", "Tank.Level"])? {
println!("{} = {:?} {:?}", tag.tag, tag.value, tag.error);
}
plc.write(&[("Counter", Value::DInt(0)), ("Tank.Setpoint", Value::Real(75.5))])?;
plc.close()?;
#include <stdio.h>
#include "ciplink.h"
CiplinkDriver *plc = NULL;
if (ciplink_logix_new("192.0.2.10", NULL, &plc) != CIPLINK_STATUS_OK || ciplink_open(plc) != CIPLINK_STATUS_OK) {
char *err = ciplink_last_error(NULL);
fprintf(stderr, "%s\n", err);
ciplink_string_free(err);
ciplink_driver_free(plc);
return 1;
}
const char *tags[] = {"Counter", "Tank.Level"};
char *json = NULL;
if (ciplink_read(plc, tags, 2, &json) == CIPLINK_STATUS_OK) {
printf("%s\n", json); /* [{"tag":"Counter","value":42,"type":"DINT","error":null}, ...] */
ciplink_string_free(json);
}
if (ciplink_write(plc, "[[\"Counter\", 0], [\"Tank.Setpoint\", 75.5]]", &json) == CIPLINK_STATUS_OK)
ciplink_string_free(json);
ciplink_driver_free(plc); /* closes the connection */
What it does¶
| Controllers | ControlLogix, CompactLogix, GuardLogix and Micro800 with the Logix driver; SLC 500 and MicroLogix with the SLC driver; generic CIP messaging to any EtherNet/IP device with the CIP driver |
| Tag types | atomic types, arrays, strings, UDTs (whole or by member), AOIs, bits of integers, bit strings (BYTE, WORD, DWORD, LWORD) and BOOL arrays |
| Throughput | many tags packed into each packet, with large values fragmented and chunked automatically |
| Tag discovery | the tag list and UDT definitions are uploaded on connect, or loaded on first use with lazy_tags; a tag cache file makes restarts near-instant |
| Online-edit safe | tags are addressed by instance ID and checked against the controller's tag-table version, so an online edit never silently returns the wrong tag |
| Extras | tag descriptions, PLC clock, device discovery and RSWho-style browsing, controller-side trends, a tag server for web apps, an opt-in adapter role, and a simulated controller for tests |
The C, C++ and C# examples in this wiki are fragments: put them inside main (C, C++) or a top-level program
(C#). Rust examples run inside a function that returns ciplink::Result<()> or
Result<(), Box<dyn std::error::Error>>.
Where to get help¶
- Questions and bug reports: GitHub issues
- Source: github.com/RowanRama/ciplink
- Packages: PyPI, crates.io, NuGet, GitHub releases