Getting started¶
Pick a driver¶
| Driver | For | Python | Rust | C# | C++ | C |
|---|---|---|---|---|---|---|
| Logix | ControlLogix, CompactLogix, GuardLogix, Micro800 | LogixDriver |
logix::driver::LogixDriver |
LogixDriver |
ciplink::Logix |
ciplink_logix_new |
| SLC | SLC 500, MicroLogix | SLCDriver |
slc::driver::SlcDriver |
SlcDriver |
ciplink::Slc |
ciplink_slc_new |
| CIP | any EtherNet/IP device: generic messages, identity, browsing | CIPDriver |
cip::CipDriver |
CipDriver |
ciplink::Cip |
ciplink_cip_new |
Each driver holds one connection to one device. Create more drivers for more connections.
Connect¶
Opening a driver connects and registers a session. A Logix driver also reads the controller info and uploads the tag list (see Tag list and controller info to make that faster or lazy). Close the driver when you're done; every binding also closes it when the driver is released.
from ciplink import LogixDriver
# a with block opens the driver and closes it at the end
with LogixDriver("192.0.2.10") as plc:
print(plc.info["product_name"], plc.info["name"])
# or open and close it yourself, e.g. for a long-lived connection
plc = LogixDriver("192.0.2.10")
plc.open()
try:
print(plc.read("Counter"))
finally:
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 *name = NULL;
if (ciplink_get_plc_name(plc, &name) == CIPLINK_STATUS_OK) {
printf("%s\n", name); /* a JSON string, e.g. "SimPLC" */
ciplink_string_free(name);
}
}
ciplink_driver_free(plc); /* closes the connection; NULL is ignored */
Requests on one driver are sent one at a time, so one driver can be shared between threads. For requests in parallel, use one driver per thread. The drivers don't send keep-alives: a controller may close a connection that is idle for about a minute, so a long-lived connection should make a request at least that often. If a request fails with a connection error (timeout, reset), the driver drops the connection; open it again to reconnect.
Paths¶
The path names the device, the same way Logix shows a route.
| Form | Example | Use for |
|---|---|---|
| IP address | 192.0.2.10 |
devices without a backplane (drives, Micro800), or a controller in slot 0 |
| IP address and slot | 192.0.2.10/2 |
a controller in another slot of the backplane |
| CIP route | 192.0.2.10/bp/2/enet/192.0.2.20/bp/0 |
a device reached through other modules |
- In a route,
backplaneorbpis the backplane port andenetthe Ethernet port. Segments can be separated by/,\or,. - A non-standard TCP port follows the IP address:
192.0.2.10:44819. - The Logix and SLC drivers turn the first two forms into a route to the controller (adding
bp/0when no slot is given). The CIP driver uses the path as it is, so192.0.2.10addresses the Ethernet module itself.
Options¶
| Option | Default | What it does |
|---|---|---|
socket_timeout |
5 s | timeout of connecting, sends and receives; none blocks forever |
connection_size |
4000 (500 for SLC) | largest connected message; above 511 bytes uses a Large Forward Open, with a fallback to 500 if the module refuses it |
init_tags (Logix) |
true | upload the tag list on open |
init_program_tags (Logix) |
true | include program-scoped tags in that upload |
tag_namespace_filter (Logix) |
"" |
keep only tags whose name (without Program:X.) starts with this |
lazy_tags (Logix) |
false | skip the upload and look tags up on first use |
tag_cache_file (Logix) |
none | keep the tag list in a file, so a reconnect to an unchanged controller uploads nothing |
use ciplink::cip::CipOptions;
use ciplink::logix::driver::{LogixDriver, LogixOptions};
use std::time::Duration;
let plc = LogixDriver::new("192.0.2.10", LogixOptions {
lazy_tags: true,
tag_cache_file: Some("plc.cache".into()),
cip: CipOptions { socket_timeout: Some(Duration::from_secs(2)), connection_size: 500 },
..LogixOptions::default()
})?;
The connection size and timeout can also be changed on a driver: set the connection size before opening (or close first), and a new timeout applies to an open connection at once.
Check the results¶
Reads and writes return one result per tag, in order. A result has the tag name, the value, the data type and an
error. A failed tag has an error message and an error kind (not_found, bad_value, access and so
on) instead of a value, and the other tags of the call are not affected. Only connection failures and bad arguments
raise an exception (Python, C#, C++), return Err (Rust) or return a status code (C).
from ciplink import ErrorKind
for tag in plc.read("Counter", "NoSuchTag"):
if tag: # a value and no error
print(tag.tag, tag.value, tag.type)
elif tag.error_kind == ErrorKind.NOT_FOUND:
print(f"{tag.tag} is not in the controller")
else:
print(f"{tag.tag} failed: {tag.error}")
if all(plc.write(("Counter", 0), ("Tank.Setpoint", 75.5))):
print("both written")
use ciplink::ErrorKind;
for tag in plc.read(&["Counter", "NoSuchTag"])? {
match (&tag.value, &tag.error) {
(Some(value), None) => println!("{} = {value:?} ({:?})", tag.tag, tag.data_type),
(_, Some(error)) if error.kind == ErrorKind::NotFound => println!("{} is not in the controller", tag.tag),
(_, error) => println!("{} failed: {error:?}", tag.tag),
}
}
for (const ciplink::Tag& tag : plc.read({"Counter", "NoSuchTag"})) {
if (tag)
std::cout << tag.tag << " = " << ciplink::to_json(tag.value) << " (" << *tag.type << ")\n";
else if (tag.error->kind == "not_found")
std::cout << tag.tag << " is not in the controller\n";
else
std::cout << tag.tag << " failed: " << tag.error->message << "\n";
}
/* results are JSON: [{"tag": str, "value": ..., "type": str|null,
"error": null | {"message": str, "kind": str, "status": int|null, "ext": int|null}}, ...] */
const char *tags[] = {"Counter", "NoSuchTag"};
char *json = NULL;
if (ciplink_read(plc, tags, 2, &json) == CIPLINK_STATUS_OK) {
printf("%s\n", json);
ciplink_string_free(json);
} else {
char *err = ciplink_last_error(plc);
fprintf(stderr, "read failed: %s\n", err);
ciplink_string_free(err);
}
See Values and results for how each PLC type appears in each language.