Skip to content
RowanRamaPublic

Repository files navigation

ciplink

Read and write Allen-Bradley PLC tags over EtherNet/IP from Python, C, C++, C# or Rust.

CI PyPI crates.io NuGet Python License: MIT Docs

Documentation · Install · Tag server · Quick start · Simulator · Build from source


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-style API and uses several times less CPU per call.

The documentation has a guide to every feature with examples in Python, Rust, C#, C++ and C side by side.

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)]
Loading

Features

Controllers ControlLogix, CompactLogix, GuardLogix and Micro800 with LogixDriver; SLC 500 and MicroLogix with SLCDriver; generic CIP messaging to any EtherNet/IP device with CIPDriver
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 binary tag cache makes restarts near-instant
Online-edit safe tags are addressed by instance ID and checked against the controller's tag-table version on every read and write, so an online edit never silently returns the wrong tag
Extras tag descriptions, PLC clock, device discovery, RSWho-style device browsing, module identity, and a simulated controller for testing
Trends the controller samples tags at a fixed period (down to 1 ms) and the driver uploads them in batches, like a Studio 5000 trend

Install

Language Command
🐍 Python 3.9+ pip install ciplink
🦀 Rust cargo add ciplink
🟣 C# / .NET 8+ dotnet add package Ciplink
⚙️ C / C++ ciplink.h, ciplink.hpp and prebuilt libraries on the releases page

The Python wheels and the NuGet package already contain the compiled core, so you don't need a Rust toolchain.

Tag server

ciplink-server owns the PLC connections of a site and serves tag values to web apps over a WebSocket, with a React client (@ciplink/react): one connection and one poll loop per PLC, however many screens are open, with write permissions, limits, momentary buttons and an audit log set in a TOML config.

ciplink-server run --config tagserver.toml          # pip install ciplink, cargo install ciplink-server, or the releases page
ciplink-server run --config tagserver.toml --mock   # simulated values, no PLC needed

It also runs inside a program: ciplink.server.TagServer (Python), ciplink_server::ServerHandle (Rust), ciplink::Server (C++), Ciplink.TagServer (C#) and ciplink_server_start (C). See docs/server.md.

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 result, with a message and an error kind (not found, bad value, access denied and so on), rather than raised as an exception.

Python
from ciplink import LogixDriver

with LogixDriver("192.0.2.10") as plc:
    print(plc.read("Counter"))           # Tag(tag='Counter', value=42, type='DINT', error=None)
    level, recipe = plc.read("Tank.Level", "Recipe{10}")
    plc.write(("Counter", 0), ("Tank.Setpoint", 75.5))
Rust
use ciplink::Value;
use ciplink::logix::driver::{LogixDriver, LogixOptions};

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))]);
C#
using Ciplink;

using var plc = new LogixDriver("192.0.2.10");
plc.Open();
Tag counter = plc.Read("Counter");
Console.WriteLine(counter.IsOk ? $"{counter.Name} = {counter.Value}" : counter.Error);
plc.Write("Counter", 0);
C++
#include "ciplink.hpp"

ciplink::Logix plc("192.0.2.10");
plc.open();
for (const ciplink::Tag& t : plc.read({"Counter", "Tank.Level"}))
    std::cout << t.tag << " = " << ciplink::to_json(t.value) << "\n";
plc.write_tag("Counter", 0);

Trends

For data faster than polling gives, a trend has the controller sample tags every period_ms into its own trend buffer (the trend object Studio 5000 uses), on a connection of its own. Each sample carries the controller's timestamp. A pen can be any atomic tag, member or array element of up to 32 bits (BOOL, SINT, INT, DINT, USINT, UINT, UDINT, REAL) or a bit (Tag.3). 64-bit types (LINT, LREAL) can't be trended. The controller deletes the trend when it's closed or when its connection drops. Every binding has trends: plc.trend(...) in Python, Rust and C++, plc.Trend(...) in C#, ciplink_trend_start in C.

import time
from ciplink import LogixDriver

with LogixDriver("192.0.2.10") as plc:
    with plc.trend("Motor.Speed", "Tank.Level", "Valve.Open", period_ms=1) as trend:
        while trend.running:
            time.sleep(0.1)
            for sample in trend.read():          # TrendSample(timestamp_us, elapsed_us, values)
                print(sample.timestamp, sample.values)

capacity (default 4096) bounds the samples waiting for read(); the oldest are dropped when it is full and counted in dropped_samples. duration (seconds) stops the trend by itself.

Browse devices

Like RSWho in FactoryTalk Linx: get_ports() lists a device's ports, browse_port() finds the module in each slot, the node at each address or, on an EtherNet/IP port, every device on the port's subnet. Every node found has a route to browse further ("" is the device at the IP, "bp/3" the module in slot 3, "bp/2/enet/192.168.1.10" a device behind the module in slot 2). python -m examples.device_browser is a small tree browser built on them. Every binding has browsing: get_ports/browse_port/scan_port in Rust, C++ and Python (iter_port streams), GetPorts/BrowsePort/ ScanPort in C#, ciplink_get_ports/ciplink_browse_port/ciplink_scan_port in C.

from ciplink import CIPDriver

with CIPDriver("192.0.2.10") as drv:
    for port in drv.get_ports():                      # e.g. Backplane, EtherNet/IP
        for node in drv.browse_port("", port):        # every slot, or every address of the subnet
            print(node["route"], node["identity"] and node["identity"]["product_name"])

Adapter (opt-in)

An Adapter (in every binding) turns an application into an EtherNet/IP I/O device: a Logix controller finds it in RSWho and owns it in its I/O tree, exchanging input and output assemblies every RPI, and ciplink's browsing shows it with a ciplink badge and what it runs. Nothing listens unless the application starts an Adapter. It needs the EtherNet/IP ports (TCP/UDP 44818, UDP 2222) of its IP address to itself, so it cannot run beside FactoryTalk Linx or RSLinx on the same address. See docs/adapter.md.

Test without a PLC

ciplink-sim is a simulated Logix or SLC controller that speaks real EtherNet/IP. Load it with tags and UDTs from a JSON file or an L5X export, then point any driver at 127.0.0.1.

cargo run -p ciplink-sim -- --config tools/sim/example_controller.json --port 44818

Build from source

You need Rust (stable). The Python package also needs Python 3.9+ with uv, and the C# binding needs the .NET 8+ SDK.

cargo test --workspace                # core, simulator, C ABI and the Python ↔ Rust parity harness
uvx maturin build --release           # Python wheel, written to target/wheels/
bash bindings/cpp/build_and_test.sh   # C++ binding
dotnet test bindings/dotnet           # C# binding

See CONTRIBUTING.md for how the parity fixtures are generated and how releases are published.

Project layout
Path Contents
crates/ciplink the Rust driver core
crates/ciplink-py, ciplink/ the Python extension module and package
crates/ciplink-ffi the C ABI and ciplink.h
bindings/cpp, bindings/dotnet the C++ and C# bindings
crates/ciplink-sim the simulated controller

License

MIT. Built on pycomm3 by Ian Ottoway and pycomm by Agostino Ruscito.

Releases

Packages

Contributors

Languages