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.ScannerNuGet — implements the Scanner side.GHIElectronics.TinyCLR.EthernetIP.AdapterNuGet — 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
| Namespace | Description |
|---|---|
| GHIElectronics.TinyCLR.EthernetIP.Scanner | EtherNet/IP scanner (master) classes |
| GHIElectronics.TinyCLR.EthernetIP.Adapter | EtherNet/IP adapter (slave) classes |