Skip to content

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.

Terminal window
go get github.com/otfabric/go-ethernetip

Go 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

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 44818
if err != nil {
log.Fatal(err)
}
defer c.Close()
id, err := c.Identity(ctx) // Get_Attributes_All of @1/1, decoded
data, 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").

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 on
c, 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.

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.

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 on
in, run, at := io.Input() // the last input, the run flag, when it arrived
_ = io.Close(ctx) // Forward_Close

Sizes 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.

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 RPI
log.Fatal(srv.ListenAndServe(":44818")) // TCP and UDP 44818, I/O on UDP 2222

The 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.

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-Write
tags, 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.

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 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) 0x0026

Methods 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 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.

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.

Terminal window
go run ./examples/quickstart
  • 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.