Skip to main content

EtherNet/IP

EtherNet/IP is an industrial communications protocol used in PLC and factory-floor automation. It defines two roles:

  • Scanner — the master that initiates connections. Typically a PLC or a workstation polling I/O.
  • Adapter — the slave/device that responds. Typically an I/O module that the PLC reads from and writes to.

TinyCLR can act in either role:

  • GHIElectronics.TinyCLR.EthernetIP.Scanner NuGet — implements the Scanner side.
  • GHIElectronics.TinyCLR.EthernetIP.Adapter NuGet — implements the Adapter side.

EtherNet/IP runs on top of standard Ethernet — make sure the NetworkController is enabled before any EtherNet/IP code runs.

Scanner

A Scanner connects to one or more Adapters and exchanges messages with them. Three operations are typical:

  • List Identity — broadcast discovery to find Adapters on the local network.
  • Explicit Messaging — request/response over TCP. Used for occasional reads/writes (configuration, diagnostics).
  • Implicit Messaging — periodic I/O over UDP via a forward-open connection. Used for continuous I/O exchange (process data, sensor readings).

List Identity

Broadcast a discovery request and enumerate all Adapters that respond.

using System;
using System.Diagnostics;
using GHIElectronics.TinyCLR.Devices.Network;
using GHIElectronics.TinyCLR.EthernetIP.Scanner;

var eipClient = new ScannerController();
var devices = eipClient.ListIdentity(NetworkController, TimeSpan.FromSeconds(5));

if (devices == null){
Debug.WriteLine("No device found");
return;
}

for (int i = 0; i < devices.Length; i++){
Debug.WriteLine("EtherNet/IP device found:");
Debug.WriteLine(" Product: " + devices[i].ProductName1);
Debug.WriteLine(" IP: " + Encapsulation.CIPIdentityItem.GetIPAddress(devices[i].SocketAddress.SIN_Address));
Debug.WriteLine(" Port: " + devices[i].SocketAddress.SIN_port);
Debug.WriteLine(" Vendor ID: " + devices[i].VendorID1);
Debug.WriteLine(" Product code:" + devices[i].ProductCode1);
Debug.WriteLine(" Type code: " + devices[i].ItemTypeCode);
Debug.WriteLine(" Serial: " + devices[i].SerialNumber1);
}

Explicit messaging

Open a TCP session to a specific Adapter, send GetAttributeSingle / SetAttributeSingle requests, then close the session.

var eipClient = new ScannerController();
eipClient.IPAddress = "192.168.1.10"; // Target adapter.

const byte classId = 0x04;
const byte attributeId = 0x03;

eipClient.RegisterSession();

for (var i = 0; i < 10; i++) {
// Read.
byte[] response = eipClient.GetAttributeSingle(classId, DEMO_APP_EXPLICIT_ASSEMBLY_NUM, attributeId);

var asString = "";
foreach (byte b in response)
asString += b + ", ";
Debug.WriteLine("Read: " + asString);

// Write back (each byte incremented).
var write = new byte[response.Length];
for (var j = 0; j < write.Length; j++)
write[j] = (byte)(response[j] + 1);

eipClient.SetAttributeSingle(classId, DEMO_APP_EXPLICIT_ASSEMBLY_NUM, attributeId, write);

Thread.Sleep(1000);
}

eipClient.UnRegisterSession();

Implicit messaging

Open a UDP I/O connection via ForwardOpen, exchange O_T_IOData (output, scanner→target) and T_O_IOData (input, target→scanner) buffers continuously, then close with ForwardClose.

var eipClient = new ScannerController();
eipClient.IPAddress = "192.168.1.10";

eipClient.RegisterSession();
eipClient.ForwardOpen();

// Outgoing data (Originator → Target).
eipClient.O_T_InstanceID = DEMO_APP_OUTPUT_ASSEMBLY_NUM;
eipClient.O_T_IOData = new byte[O_T_IODataSize];
eipClient.O_T_RealTimeFormat = RealTimeFormat.Header32Bit;
eipClient.O_T_OwnerRedundant = false;
eipClient.O_T_Priority = Priority.Scheduled;
eipClient.O_T_VariableLength = false;
eipClient.O_T_ConnectionType = ConnectionType.Point_to_Point;
eipClient.RequestedPacketRate_O_T = 500_000;

// Incoming data (Target → Originator).
eipClient.T_O_InstanceID = DEMO_APP_INPUT_ASSEMBLY_NUM;
eipClient.T_O_IOData = new byte[T_O_IODataSize];
eipClient.T_O_RealTimeFormat = RealTimeFormat.ZeroLength;
eipClient.T_O_OwnerRedundant = false;
eipClient.T_O_Priority = Priority.Scheduled;
eipClient.T_O_VariableLength = false;
eipClient.T_O_ConnectionType = ConnectionType.Point_to_Point;
eipClient.RequestedPacketRate_T_O = 500_000;

eipClient.ConfigurationAssemblyInstanceID = DEMO_APP_CONFIG_ASSEMBLY_NUM;
// Set ConfigurationAssembly_Data if you want to write configuration; leave null otherwise.
// eipClient.ConfigurationAssembly_Data = new byte[Config_DataSize] { 1, 2, 3, 4, 5, 6, 7, 8 };

for (var counter = 0; counter < 10; counter++) {
// Write the outgoing buffer.
for (var i = 0; i < eipClient.O_T_IOData.Length; i++)
eipClient.O_T_IOData[i] = (byte)counter;

// Read the incoming buffer.
var asString = "";
for (var i = 0; i < eipClient.T_O_IOData.Length; i++)
asString += eipClient.T_O_IOData[i] + ", ";
Debug.WriteLine(asString);

Thread.Sleep(1000);
}

eipClient.ForwardClose();
eipClient.UnRegisterSession();

Adapter

An Adapter publishes a CIP (Common Industrial Protocol) object model and waits for Scanners to connect. Setup is verbose — you have to declare the identity object, message router, connection manager, and one or more assembly objects matching the assembly instance numbers a Scanner will reference.

Top-level setup

using System.Net;
using System.Threading;
using GHIElectronics.TinyCLR.EthernetIP.Adapter;

void DoInitAdapter(){
var adapter = new AdapterController(
deviceName: "GHI Adapter",
deviceVendorID: 3,
deviceType: 12,
deviceProductCode: 65001,
deviceSerialNumber: 87654321,
deviceMajorRevision: 4,
deviceMinorRevision: 3
);

adapter.EnableHeaderO2T(true); // Enable 32-bit O→T header.

// Define the Assembly class — covers the Input, Output, Config, and Heartbeat instances.
adapter.CreateAssemblyClass(
numberClassAttributes: 0,
highestClassAttributeNumber: 7,
numberClassServices: 1,
numberInstanceAttributes: 2,
highestInstanceAttributeNumber: 4,
numberInstanceServices: 2,
numberInstances: 0,
name: "assembly",
revision: 2
);

MessageRouterInit(adapter);
IdentityInit(adapter);
ConnectionManagerInit(adapter);

// Assembly buffers — Input (T→O), Output (O→T), Config, Explicit.
var inputBuffer = new byte[T_O_IODataSize];
var outputBuffer = new byte[O_T_IODataSize];
var configBuffer = new byte[Config_DataSize];
var explicitBuffer = new byte[ExplicitMessage_DataSize];

adapter.AddAssemblyObject(new AssemblyObject(DEMO_APP_INPUT_ASSEMBLY_NUM, inputBuffer, (ushort)inputBuffer.Length));
adapter.AddAssemblyObject(new AssemblyObject(DEMO_APP_OUTPUT_ASSEMBLY_NUM, outputBuffer, (ushort)outputBuffer.Length));
adapter.AddAssemblyObject(new AssemblyObject(DEMO_APP_CONFIG_ASSEMBLY_NUM, configBuffer, (ushort)configBuffer.Length));
adapter.AddAssemblyObject(new AssemblyObject(DEMO_APP_HEARTBEAT_INPUT_ONLY_ASSEMBLY_NUM, null, 0));
adapter.AddAssemblyObject(new AssemblyObject(DEMO_APP_HEARTBEAT_LISTEN_ONLY_ASSEMBLY_NUM, null, 0));
adapter.AddAssemblyObject(new AssemblyObject(DEMO_APP_EXPLICIT_ASSEMBLY_NUM, explicitBuffer, (ushort)explicitBuffer.Length));

// Map connection points to the assemblies above.
adapter.ConfigureExclusiveOwnerConnectionPoint(0, DEMO_APP_OUTPUT_ASSEMBLY_NUM, DEMO_APP_INPUT_ASSEMBLY_NUM, DEMO_APP_CONFIG_ASSEMBLY_NUM);
adapter.ConfigureInputOnlyConnectionPoint(0, DEMO_APP_HEARTBEAT_INPUT_ONLY_ASSEMBLY_NUM, DEMO_APP_INPUT_ASSEMBLY_NUM, DEMO_APP_CONFIG_ASSEMBLY_NUM);
adapter.ConfigureListenOnlyConnectionPoint(0, DEMO_APP_HEARTBEAT_LISTEN_ONLY_ASSEMBLY_NUM, DEMO_APP_INPUT_ASSEMBLY_NUM, DEMO_APP_CONFIG_ASSEMBLY_NUM);

// Hook lifecycle events.
adapter.RegisterSessionDetected += Adapter_RegisterSessionDetected;
adapter.UnregisterSessionDetected += Adapter_UnregisterSessionDetected;
adapter.ForwardOpenDetected += Adapter_ForwardOpenDetected;
adapter.ForwardCloseDetected += Adapter_ForwardCloseDetected;

adapter.Enable();

Thread.Sleep(Timeout.Infinite);
}

Identity object

The Identity object reports the device's vendor, type, product code, revision, status, serial number, and product name to Scanners during enumeration.

void IdentityInit(AdapterController adapter) {
var identityClass = new CIPClass(
AdapterController.ClassId.Identity,
numberClassAttributes: 0,
highestClassAttributeNumber: 7,
numberClassServices: 2,
numberInstanceAttributes: 7,
highestInstanceAttributeNumber: 7,
numberInstanceServices: 5,
numberInstances: 1,
name: "identity",
revision: 1
);

adapter.AddCipClass(identityClass);
var cipInstance = adapter.GetCipInstance(identityClass, 1);

var vendorId = new byte[] { 12, 0 };
var deviceType = new byte[] { 34, 0 };
var productCode = new byte[] { 56, 0 };
var revision = new byte[] { 78, 87 };
var status = new byte[] { 90, 0 };
var serial = new byte[] { 11, 0, 0, 0 };
var productName = System.Text.Encoding.UTF8.GetBytes("GHI custom");

var getAllFlag = AdapterController.CIPAttributeFlag.kGetableSingleAndAll;

adapter.InsertAttribute(cipInstance, 1, AdapterController.CIPDataType.kCipUint, AdapterController.CipAttributeEncodeInMessage.EncodeCipUint, AdapterController.CipAttributeDecodeFromMessage.None, vendorId, getAllFlag);
adapter.InsertAttribute(cipInstance, 2, AdapterController.CIPDataType.kCipUint, AdapterController.CipAttributeEncodeInMessage.EncodeCipUint, AdapterController.CipAttributeDecodeFromMessage.None, deviceType, getAllFlag);
adapter.InsertAttribute(cipInstance, 3, AdapterController.CIPDataType.kCipUint, AdapterController.CipAttributeEncodeInMessage.EncodeCipUint, AdapterController.CipAttributeDecodeFromMessage.None, productCode, getAllFlag);
adapter.InsertAttribute(cipInstance, 4, AdapterController.CIPDataType.kCipUsintUsint, AdapterController.CipAttributeEncodeInMessage.EncodeCipUint, AdapterController.CipAttributeDecodeFromMessage.None, revision, getAllFlag);
adapter.InsertAttribute(cipInstance, 5, AdapterController.CIPDataType.kCipUint, AdapterController.CipAttributeEncodeInMessage.EncodeCipUint, AdapterController.CipAttributeDecodeFromMessage.None, status, getAllFlag);
adapter.InsertAttribute(cipInstance, 6, AdapterController.CIPDataType.kCipUdint, AdapterController.CipAttributeEncodeInMessage.EncodeCipUdint, AdapterController.CipAttributeDecodeFromMessage.None, serial, getAllFlag);
adapter.InsertAttribute(cipInstance, 7, AdapterController.CIPDataType.kCipShortString, AdapterController.CipAttributeEncodeInMessage.EncodeCipShortString, AdapterController.CipAttributeDecodeFromMessage.None, productName, getAllFlag);

adapter.InsertService(identityClass, AdapterController.CIPServiceCode.kGetAttributeSingle, AdapterController.CipServiceFunctionCode.kGetAttributeSingle, "GetAttributeSingle");
adapter.InsertService(identityClass, AdapterController.CIPServiceCode.kGetAttributeAll, AdapterController.CipServiceFunctionCode.kGetAttributeAll, "GetAttributeAll");
adapter.InsertService(identityClass, AdapterController.CIPServiceCode.kReset, AdapterController.CipServiceFunctionCode.kReset, "Reset");
adapter.InsertService(identityClass, AdapterController.CIPServiceCode.kGetAttributeList, AdapterController.CipServiceFunctionCode.kGetAttributeList, "GetAttributeList");
adapter.InsertService(identityClass, AdapterController.CIPServiceCode.kSetAttributeList, AdapterController.CipServiceFunctionCode.kSetAttributeList, "SetAttributeList");
}

Message router

void MessageRouterInit(AdapterController adapter) {
var messageRouterClass = new CIPClass(
AdapterController.ClassId.MessageRouter,
numberClassAttributes: 7,
highestClassAttributeNumber: 7,
numberClassServices: 2,
numberInstanceAttributes: 0,
highestInstanceAttributeNumber: 0,
numberInstanceServices: 1,
numberInstances: 1,
name: "message router",
revision: 1
);

adapter.AddCipClass(messageRouterClass);
adapter.InsertService(messageRouterClass, AdapterController.CIPServiceCode.kGetAttributeSingle, AdapterController.CipServiceFunctionCode.kGetAttributeSingle, "GetAttributeSingle");
}

Connection manager

void ConnectionManagerInit(AdapterController adapter) {
var connectionManagerClass = new CIPClass(
AdapterController.ClassId.ConnectionManager,
numberClassAttributes: 0,
highestClassAttributeNumber: 7,
numberClassServices: 2,
numberInstanceAttributes: 0,
highestInstanceAttributeNumber: 14,
numberInstanceServices: 8,
numberInstances: 1,
name: "connection manager",
revision: 1
);

adapter.AddCipClass(connectionManagerClass);

adapter.InsertService(connectionManagerClass, AdapterController.CIPServiceCode.kGetAttributeSingle, AdapterController.CipServiceFunctionCode.kGetAttributeSingle, "GetAttributeSingle");
adapter.InsertService(connectionManagerClass, AdapterController.CIPServiceCode.kGetAttributeAll, AdapterController.CipServiceFunctionCode.kGetAttributeAll, "GetAttributeAll");
adapter.InsertService(connectionManagerClass, AdapterController.CIPServiceCode.kForwardOpen, AdapterController.CipServiceFunctionCode.kForwardOpen, "ForwardOpen");
adapter.InsertService(connectionManagerClass, AdapterController.CIPServiceCode.kLargeForwardOpen, AdapterController.CipServiceFunctionCode.kLargeForwardOpen, "LargeForwardOpen");
adapter.InsertService(connectionManagerClass, AdapterController.CIPServiceCode.kForwardClose, AdapterController.CipServiceFunctionCode.kForwardClose, "ForwardClose");
adapter.InsertService(connectionManagerClass, AdapterController.CIPServiceCode.kGetConnectionOwner, AdapterController.CipServiceFunctionCode.kGetConnectionOwner, "GetConnectionOwner");
adapter.InsertService(connectionManagerClass, AdapterController.CIPServiceCode.kGetConnectionData, AdapterController.CipServiceFunctionCode.kGetConnectionData, "GetConnectionData");
adapter.InsertService(connectionManagerClass, AdapterController.CIPServiceCode.kSearchConnectionData, AdapterController.CipServiceFunctionCode.kSearchConnectionData, "SearchConnectionData");
}

Shared assembly numbers

Both the Scanner and the Adapter need to agree on the assembly instance numbers and buffer sizes. Define them once in a shared file:

public const byte DEMO_APP_INPUT_ASSEMBLY_NUM = 100;
public const byte DEMO_APP_OUTPUT_ASSEMBLY_NUM = 150;
public const byte DEMO_APP_CONFIG_ASSEMBLY_NUM = 151;
public const byte DEMO_APP_HEARTBEAT_INPUT_ONLY_ASSEMBLY_NUM = 152;
public const byte DEMO_APP_HEARTBEAT_LISTEN_ONLY_ASSEMBLY_NUM = 153;
public const byte DEMO_APP_EXPLICIT_ASSEMBLY_NUM = 154;

public const ushort O_T_IODataSize = 8;
public const ushort T_O_IODataSize = 8;
public const ushort Config_DataSize = 8;
public const ushort ExplicitMessage_DataSize = 8;

Event handlers

Beyond the lifecycle events wired up in DoInitAdapter, more granular events are available for low-level traffic and assembly data flow:

adapter.RegisterSessionDetected += Adapter_RegisterSessionDetected;
adapter.UnregisterSessionDetected += Adapter_UnregisterSessionDetected;
adapter.ForwardOpenDetected += Adapter_ForwardOpenDetected;
adapter.ForwardCloseDetected += Adapter_ForwardCloseDetected;

adapter.ReceivedExplictTcpData += Adapter_ReceivedExplicitTcpData;
adapter.ReceivedExplictUdpData += Adapter_ReceivedExplicitUdpData;
adapter.NotifyClass += Adapter_NotifyClass;
adapter.AfterAssemblyDataReceived += Adapter_AfterAssemblyDataReceived;
adapter.BeforeAssemblyDataSend += Adapter_BeforeAssemblyDataSend;

Sample implementations:

void Adapter_RegisterSessionDetected(AdapterController adapter, IPAddress ip)
=> Debug.WriteLine("RegisterSession from " + ip);

void Adapter_UnregisterSessionDetected(AdapterController adapter, IPAddress ip)
=> Debug.WriteLine("UnregisterSession from " + ip);

void Adapter_ForwardOpenDetected(AdapterController adapter, IPAddress ip, bool large)
=> Debug.WriteLine("ForwardOpen from " + ip + " (large=" + large + ")");

void Adapter_ForwardCloseDetected(AdapterController adapter, IPAddress ip)
=> Debug.WriteLine("ForwardClose from " + ip);

void Adapter_BeforeAssemblyDataSend(AdapterController adapter, ushort instanceNumber)
=> Debug.WriteLine("Before send: instance=" + instanceNumber);

void Adapter_AfterAssemblyDataReceived(AdapterController adapter, ushort instanceNumber)
=> Debug.WriteLine("After receive: instance=" + instanceNumber);

void Adapter_NotifyClass(AdapterController adapter, uint classCode, ushort instanceNumber, ushort attributeNumber, IPAddress ip) {
Debug.WriteLine("NotifyClass: class=" + classCode + " instance=" + instanceNumber
+ " attribute=" + attributeNumber + " from=" + ip);
}

void Adapter_ReceivedExplicitTcpData(AdapterController adapter, ushort commandCode, IPAddress ip)
=> Debug.WriteLine("TCP: command=" + commandCode + " from=" + ip);

void Adapter_ReceivedExplicitUdpData(AdapterController adapter, ushort commandCode, IPAddress ip, bool unicast)
=> Debug.WriteLine("UDP: command=" + commandCode + " from=" + ip + " unicast=" + unicast);

API reference

NamespaceDescription
GHIElectronics.TinyCLR.EthernetIP.ScannerEtherNet/IP scanner (master) classes
GHIElectronics.TinyCLR.EthernetIP.AdapterEtherNet/IP adapter (slave) classes