Discovering and browsing devices¶
Discover devices on the network¶
discover broadcasts a ListIdentity request, like the RSLinx EtherNet/IP driver, on every network adapter that is
up, and returns the identity of every device that answers within a second or so. list_identity asks a single
device. Neither needs an open driver.
from ciplink import CIPDriver
for device in CIPDriver.discover():
print(device["ip_address"], device["product_name"], device["revision"])
print(CIPDriver.list_identity("192.0.2.10"))
# {'encap_protocol_version': 1, 'ip_address': '192.0.2.10', 'vendor': 'Rockwell Automation/Allen-Bradley',
# 'product_type': 'Communications Adapter', 'product_code': 166, 'revision': {'major': 11, 'minor': 2},
# 'status': b'0\x00', 'serial': '00e2c0de', 'product_name': '1756-EN2T/D', 'state': 3}
#include <iostream>
#include "ciplink.hpp"
ciplink::Value devices = ciplink::discover(); // keep it: a loop over discover().as_array() would dangle
for (const ciplink::Value& device : devices.as_array())
std::cout << device["ip_address"].as_string() << " " << device["product_name"].as_string() << "\n";
std::cout << ciplink::to_json(ciplink::list_identity("192.0.2.10")) << "\n";
Browse like RSWho¶
A driver can walk through a device the way RSWho expands a tree:
get_ports(route)lists the ports of the device atroute(its Port object): the backplane, Ethernet ports and so on, with the addresses on each.browse_port(route, port)asks every address on one port for its identity: each slot of a backplane, each node of a ControlNet or DeviceNet network, or every host of an EtherNet/IP port's subnet (at most the /24 around it), each through the module.get_identity(route)reads the identity at any route.
A route is relative to the device at the driver's IP address (the driver's own path isn't used): "" is that
device, "bp/3" (or "1/3") the module in slot 3 of its backplane, "bp/2/enet/192.0.2.30" a device behind the
module in slot 2. Every node found carries its route, to browse further.
An empty slot or a missing device costs a timeout at the bridge (about 4 s), so browse_port probes from several
connections of its own at once (workers, 16 by default). scan_port (Python: iter_port) hands over each node as
it answers instead of waiting for all of them.
from ciplink import CIPDriver
with CIPDriver("192.0.2.10") as drv:
for port in drv.get_ports():
print(port["number"], port["type_name"], port["ip"], len(port["nodes"] or []))
for node in drv.browse_port("", port):
if node["identity"]:
print(" ", node["route"], node["identity"]["product_name"])
# 1 Backplane None 10
# 1/0 1756-L83E/B
# 1/1 1756-EN2T/D
# 2 EtherNet/IP 192.0.2.10 254
# 2/192.0.2.30 1734-AENTR/C
use ciplink::cip::{CipDriver, CipOptions};
let drv = CipDriver::new("192.0.2.10", CipOptions::default())?;
drv.open()?;
for port in drv.ports_at("")? {
println!("{} {}", port.number, port.type_name());
for node in drv.browse_port("", port.number, None, 16)? {
if let Some(identity) = node.identity {
println!(" {} {}", node.route, identity.product_name);
}
}
}
using Ciplink;
using var drv = new CipDriver("192.0.2.10");
drv.Open();
foreach (var port in drv.GetPorts())
{
Console.WriteLine($"{port!["number"]} {port["type_name"]}");
foreach (var node in drv.BrowsePort("", (int)port["number"]!))
if (node!["identity"] is { } identity)
Console.WriteLine($" {node["route"]} {identity["product_name"]}");
}
#include <iostream>
#include "ciplink.hpp"
ciplink::Cip drv("192.0.2.10");
drv.open();
ciplink::Value ports = drv.get_ports();
for (const ciplink::Value& port : ports.as_array()) {
std::cout << port["number"].as_int() << " " << port["type_name"].as_string() << "\n";
drv.scan_port("", uint16_t(port["number"].as_int()), [](const ciplink::Value& node) {
if (!node["identity"].is_null())
std::cout << " " << node["route"].as_string() << " "
<< node["identity"]["product_name"].as_string() << "\n";
return true; // false stops the scan
});
}
static bool on_node(void *user, const char *node_json) {
(void)user;
printf("%s\n", node_json); /* {"route": ..., "address": ..., "identity": {...}|null, "error": ...} */
return true; /* false stops the scan */
}
/* in main: */
CiplinkDriver *drv = NULL;
ciplink_cip_new("192.0.2.10", NULL, &drv);
if (ciplink_open(drv) == CIPLINK_STATUS_OK) {
char *ports = NULL;
if (ciplink_get_ports(drv, "", &ports) == CIPLINK_STATUS_OK) {
printf("%s\n", ports);
ciplink_string_free(ports);
}
ciplink_scan_port(drv, "", 1, NULL, 16, on_node, NULL); /* port 1: the backplane */
}
ciplink_driver_free(drv);
get_module_info(slot) is a shortcut for the identity of a slot in the driver's own backplane.
ciplink devices¶
A ciplink adapter answers like any other EtherNet/IP device, and it also says what it runs. Every node
that browse_port or scan_port finds at an IP address carries an app field: null for other devices, else the
application's name, version and detail (status text), its assemblies and connections, the ciplink_version,
and a summary for a device browser, such as Station · station 2 · owned by 192.0.2.5, RPI 20 ms. Reading it costs
one request per device found, and none for the slots of a backplane, since an adapter is a PC on a network.
get_app(route) reads it for a single device: Python drv.get_app(""), Rust drv.app_at(""), C# drv.GetApp(),
C++ drv.get_app(), C ciplink_get_app(drv, "", &json). examples/device_browser.py shows ciplink devices with
their app line.
python -m examples.device_browser in the repository is a small RSWho-style tree browser built on these calls.