Go library
go-ethernetip implements EtherNet/IP and CIP in Go, in both roles: a client and a scanner (the originator) and a server (the target, or adapter). It has no dependencies outside the Go standard library and no cgo. The authoritative reference for each symbol is the godoc on pkg.go.dev; API.md in the repository is the map. This page follows v0.3.1.
Install
Section titled “Install”go get github.com/otfabric/go-ethernetipGo 1.23 or later. The API is v0: it may change in minor versions until v1.0.0, with every change in RELEASE.md.
| Package | What it is |
|---|---|
ethernetip |
Shared types: sentinel and typed errors, Logger, Metrics, State |
client |
Originator of explicit messages; discovery |
scanner |
Originator of Class 0 and Class 1 I/O connections |
server |
Target (adapter): objects, sessions, connections |
logix |
Rockwell Logix tag services, client and emulation (vendor specific) |
eds |
Electronic Data Sheets: parse, validate, format, build, generate Go |
cip, encap, implicit |
The codecs: CIP data types, paths and messages; encapsulation; I/O datagrams. Pure, so useful on their own to analysers and test tools |
A client
Section titled “A client”Every network call takes a context.Context. A request waits until the earlier of the context’s deadline and the client’s WithTimeout (5 s by default).
c, err := client.Dial(ctx, "192.168.1.10") // port 44818if err != nil { log.Fatal(err)}defer c.Close()
id, err := c.Identity(ctx) // Get_Attributes_All of @1/1, decodeddata, err := c.GetAttributeSingle(ctx, cip.ClassAssembly, 100, 3)
// Several requests in one Multiple Service Packet.resps, err := c.MultipleService(ctx, []cip.Request{ {Service: cip.ServiceGetAttributeSingle, Path: cip.AttributePath(cip.ClassIdentity, 1, 1)}, {Service: cip.ServiceGetAttributeSingle, Path: cip.AttributePath(cip.ClassIdentity, 1, 7)},})| Method | Service |
|---|---|
GetAttributeSingle, SetAttributeSingle |
0x0E, 0x10 |
GetAttributesAll, SetAttributesAll |
0x01, 0x02 |
GetAttributeList |
0x03 |
Reset |
0x05 |
Identity |
0x01 on @1/1, decoded |
MultipleService |
0x0A on @2/1 |
Do(ctx, cip.Request) |
anything else: an object-specific service code and its data |
Paths are built with cip.AttributePath(class, instance, attribute) or parsed: cip.ParsePath("@1/1/7") or the hex octets of a padded EPATH, cip.ParsePath("20 01 24 01 30 07").
Routing and Class 3
Section titled “Routing and Class 3”A controller behind its communication module is reached with a route. client.WithRoute(route) sends every unconnected request through the module’s Unconnected_Send and prefixes the path of every Forward_Open:
route, _ := cip.ParseRoute("1/0") // port 1 (backplane), slot 0; "1/0,2/192.168.2.20" goes onc, err := client.Dial(ctx, "192.168.1.10", client.WithRoute(route))OpenConnection opens a Class 3 connection to the Message Router (Large: true for Large_Forward_Open above 511 octets). The returned *client.Conn has the same helpers; its requests go in SendUnitData with a sequence count. The target closes it after Timeout without a request (60 s by default), and Close sends Forward_Close.
cn, err := c.OpenConnection(ctx, client.ConnectionOptions{})name, err := cn.GetAttributeSingle(ctx, cip.ClassIdentity, 1, 7)_ = cn.Close(ctx)client.WithMaxPending(n) lets n unconnected requests from different goroutines be outstanding on one session, matched by sender context.
Discovery
Section titled “Discovery”client.Discover(ctx, client.DiscoverOptions{Address: "192.168.1.255"}) sends exactly one ListIdentity datagram, to a broadcast address or one device, and collects the replies until Wait. MaxDelay asks devices to spread their answers. client.ListIdentity(ctx, addr) asks one device over UDP; Client.ListIdentity, ListServices and ListInterfaces ask over the session.
A scanner: cyclic I/O
Section titled “A scanner: cyclic I/O”scanner.New(c) binds UDP port 2222 and opens I/O connections to the client’s target:
s, err := scanner.New(c)io, err := s.Open(ctx, scanner.IOConfig{ ConfigInstance: 151, OutputInstance: 150, InputInstance: 100, OutputSize: 32, InputSize: 32, RPI: 10 * time.Millisecond, Reconnect: &scanner.Reconnect{}, // reopen after a timeout})_ = io.SetOutput(out) // sent from the next interval onin, run, at := io.Input() // the last input, the run flag, when it arrived_ = io.Close(ctx) // Forward_CloseSizes are application data sizes: the scanner adds the sequence count and the run/idle header to the Forward_Open connection size. The default formats are what targets commonly expect, O->T with the 32-bit run/idle header and T->O modeless; OutputFormat and InputFormat change them.
| Connection | OutputInstance |
Notes |
|---|---|---|
| Exclusive owner | the output assembly | One per output assembly; the target refuses a second (0x0106) |
| Input only | the input-only heartbeat point, OutputSize: 0 |
Many may share the inputs |
| Listen only | the listen-only heartbeat point, Multicast: true |
Needs a multicast connection to the same inputs (0x0119) |
Multicast: true asks for the inputs by multicast, and the scanner joins the group the target returns. An owner and its listen-only connections can run on one scanner endpoint. ConfigData sends configuration data in the Forward_Open, ClassZero opens a Class 0 connection without a sequence count, Key adds an electronic key to the path, and OnInput and OnState report each input and each change of state. Without Reconnect a timed-out connection ends: Done closes and Err matches ErrTimeout. With it, the scanner sends Forward_Open again with backoff, keeping the output, the input and the callbacks.
An adapter
Section titled “An adapter”server.New serves the Identity, Message Router, Assembly, Connection Manager, Port, TCP/IP Interface and Ethernet Link objects. Assemblies receive I/O connections:
srv, err := server.New(server.WithIdentity(cip.Identity{ VendorID: 0xFFFF, DeviceType: cip.DeviceTypeCommunicationsAdapter, ProductCode: 1, Revision: cip.Revision{Major: 1, Minor: 0}, SerialNumber: 42, ProductName: "My adapter",}))in, _ := srv.AddAssembly(100, server.AssemblyInput, 32)out, _ := srv.AddAssembly(150, server.AssemblyOutput, 32)_, _ = srv.AddAssembly(151, server.AssemblyConfig, 10)out.OnWrite(func(data []byte, run bool) { /* drive the outputs */ })_ = in.SetData(inputs) // produced to every connection from the next RPIlog.Fatal(srv.ListenAndServe(":44818")) // TCP and UDP 44818, I/O on UDP 2222The server enforces the ownership rules of the Connection Manager and answers every refusal with its extended status. Multicast productions are shared by every connection to the same inputs, and SetData triggers change-of-state connections. Objects of your own implement server.Object, or are built from server.NewObject(class, revision) with attributes and services. Options bound what a peer can make the server hold: WithMaxSessions (64), WithMaxConnections (128), WithMaxPacket (4096 octets) and the inactivity timeout (120 s). WithSlot answers routed requests for one backplane slot, and WithDiscoveryDelay spreads broadcast ListIdentity replies.
Logix tags
Section titled “Logix tags”Package logix speaks the Rockwell Automation tag services, which only Logix controllers and compatible devices implement:
route, _ := cip.ParseRoute("1/0")c, _ := client.Dial(ctx, "192.168.1.10", client.WithRoute(route))lc := logix.NewClient(c)
v, err := lc.Read(ctx, "Program:Main.Speeds[3]", 1)f, _ := v.Float(0)dv, _ := logix.Ints(cip.TypeDINT, 42)err = lc.Write(ctx, "Counter", dv)err = lc.WriteBit(ctx, "Flags.3", 4, true) // Read-Modify-Writetags, err := lc.ListTags(ctx, "")Read and Write switch to the fragmented services for large values, ReadMany packs several reads into Multiple Service Packets, and Template and DecodeStruct read structure definitions and decode user-defined types. Over a Class 3 connection with Large: true and logix.WithMaxData(3900), transfers are as large as programming tools use. BOOL arrays are addressed by DWORD, as in Logix.
logix.NewTagServer() attached to a server.Server emulates a controller’s tag database (symbolic and instance addressing, fragmentation, bit writes, the tag list, templates, the program name), which pycomm3 and cpppo accept. It is a test tool: no programs, tasks, modes or alarms.
EDS files
Section titled “EDS files”f, err := eds.Parse(file)for _, issue := range eds.Validate(f) { fmt.Println(issue) // errors and warnings with line numbers}src, err := eds.GenerateGo(f, eds.GenerateOptions{Package: "drive"})Format writes canonical text, Build writes an EDS for a device you describe, and GenerateGo turns one into a Go package: identity constants and the electronic key, a descriptor and range check per parameter, enumerations, assembly structs with Encode and Decode, and a scanner.IOConfig per I/O connection. Validation checks structure and consistency; it is not ODVA’s EZ-EDS check. Vendor EDS files are usually not redistributable, so keep them out of source control and generate code locally.
client.WithTLS and server.WithTLS take a standard *tls.Config and use port 2221: EtherNet/IP over TLS, the transport of CIP Security, for explicit messaging. Set MinVersion to TLS 1.2 or later, and for mutual authentication ClientAuth: tls.RequireAndVerifyClientCert on the server. I/O has no protection: DTLS is not implemented, and neither are the CIP Security objects.
Errors
Section titled “Errors”Errors match the sentinels of the root package with errors.Is, or carry the peer’s answer:
| Error | Meaning |
|---|---|
ErrTimeout |
No answer before the deadline, or an I/O connection’s data stopped |
ErrConnectFailed, ErrConnectionLost, ErrNotConnected |
The session could not be opened, ended during a request, or is gone |
ErrProtocol |
The peer violated the protocol |
ErrUnsupported |
The peer does not support the service or attribute (0x08, 0x14), or the platform lacks a feature |
ErrInvalidOption, ErrClosed, ErrSession, ErrBusy |
Bad arguments, a call after Close, an encapsulation refusal, a local limit |
*ethernetip.CIPError |
A CIP response with a general status other than success: service, path, status and extended status, with the Connection Manager’s reason named |
*ethernetip.EncapError |
An encapsulation status other than success |
ethernetip: Forward_Open @6/1: connection failure (0x01), extended 0x0127 (invalid O->T size) 0x0026Methods that return a response return it together with the error, so the data of partial transfers is not lost. A cancelled context returns context.Canceled. ERRORS.md lists every error and what the server answers.
Logging and metrics
Section titled “Logging and metrics”Logging is silent by default. WithLogger takes an ethernetip.Logger; NewSlogLogger and NewSlogFieldLogger wrap a log/slog handler, with constant messages and fields (component, remote, session, connection). WithMetrics takes callbacks for connections, packets and decode errors, and per request, per handled service and per I/O datagram sent, received or dropped with its reason. Client.State() and IO.State() report disconnected, connecting, connected or timed out, and IO.Stats() counts datagrams.
Example programs
Section titled “Example programs”The examples directory has runnable programs: quickstart, io, logix and eds run both ends in one process; adapter, scanner and discover work with real devices.
go run ./examples/quickstartLimitations
Section titled “Limitations”- CIP Security: TLS for explicit messaging only. No DTLS for I/O, no CIP Security, EtherNet/IP Security or Certificate Management objects.
- No bridging: the server answers routes to its own slot only.
- Not implemented: Class 2 connections, redundant owners, CIP Safety, CIP Motion, Device Level Ring, QoS, the Parameter and File objects, and the diagnostic services of the Connection Manager.
- I/O: one scanner per host on UDP 2222, unless targets honour a Sockaddr Info item; originator and target must reach each other without NAT. The server filters O->T datagrams by source address only.
- The Logix emulation is a test tool, without programs, tasks, modes or alarms.
- Not ODVA conformance tested, and no conformance claim. Products need their own ODVA vendor ID; the default 0xFFFF marks an unregistered originator.
The complete list is in docs/LIMITATIONS.md.