EtherNet/IP
The EtherNet/IP module targets Allen-Bradley / Rockwell PLCs. It supports Register Session, SendRRData, CIP Read Tag / Write Tag, and Get Attribute Single / Set Attribute Single over TCP 44818. Current coverage focuses on scalar tags for point acquisition and write-back.
Start with a Virtual PLC
var memory = new EtherNetIpSlaveMemory();
memory.SetTag("Temperature", EtherNetIpDataType.Int, (short)253);
memory.SetTag("Running", EtherNetIpDataType.Bool, true);
await using var app = ZeusHost.Create(builder =>
{
builder.AddAcquisition(TimeSpan.FromMilliseconds(100));
builder.AddVirtualChannel("enip-link", new EtherNetIpSlaveResponder(memory));
builder.AddEtherNetIp("plc", "enip-link", points: map => map
.Int("temperature", "Temperature", scale: 0.1).Writable("temperature")
.Bool("running", "Running").Writable("running"));
});
await app.StartAsync();
var temperature = app.Points.Get<double>("temperature");
await app.Points.WriteAsync("temperature", 18.6);
Run sample: samples/Zeus.Samples.Console.EtherNetIp.
Connect to a Real PLC
await using var app = ZeusHost.Create(builder =>
{
builder.AddTcpClient("enip-link", "192.168.1.10", 44818);
builder.AddEtherNetIp("plc", "enip-link", points: map => map
.Int("temperature", "Temperature", scale: 0.1)
.Bool("running", "Running"));
});
Use the full PLC tag path for Program-scope tags, for example Program:Main.Speed.
Read/Write API
var plc = app.Devices.Get<EtherNetIpDevice>("plc");
var temperature = await plc.ReadTagAsync("Temperature", EtherNetIpDataType.Int, scale: 0.1);
await plc.WriteTagAsync("Temperature", EtherNetIpDataType.Int, 18.6, scale: 0.1);
var running = await plc.ReadTagAsync("Running", EtherNetIpDataType.Bool);
await plc.WriteTagAsync("Running", EtherNetIpDataType.Bool, true);
byte[] identityName = await plc.GetAttributeSingleAsync(classId: 0x01, instanceId: 1, attributeId: 7);
await plc.SetAttributeSingleAsync(classId: 0x64, instanceId: 1, attributeId: 3, new byte[] { 0x01 });
Scalar Types
| JSON / API | CIP type |
|---|---|
bool / Bool | BOOL 0x00C1 |
sint / SInt | SINT 0x00C2 |
int / Int | INT 0x00C3 |
dint / DInt | DINT 0x00C4 |
lint / LInt | LINT 0x00C5 |
usint / USInt | USINT 0x00C6 |
uint / UInt | UINT 0x00C7 |
udint / UDInt | UDINT 0x00C8 |
ulint / ULInt | ULINT 0x00C9 |
real / Real | REAL 0x00CA |
lreal / LReal | LREAL 0x00CB |
Points
builder.AddEtherNetIp("plc", "enip-link", points: map => map
.Int("temperature", "Temperature", scale: 0.1).Writable("temperature")
.DInt("speed", "Program:Main.Speed")
.Real("flow", "Flow")
.Bool("running", "Running").Writable("running"));
Point names can be business-friendly even when PLC tag paths are longer. scale applies only to numeric tags; BOOL points cannot use scale or alarm limits.
JSON Configuration
{
"channels": [
{ "name": "enip-link", "type": "tcp", "host": "192.168.1.10", "port": 44818 }
],
"devices": [
{
"name": "plc",
"channel": "enip-link",
"type": "ethernet-ip",
"points": [
{ "name": "temperature", "tag": "Temperature", "dataType": "int", "scale": 0.1, "writable": true },
{ "name": "running", "tag": "Running", "dataType": "bool", "writable": true },
{ "name": "speed", "tagName": "Program:Main.Speed", "dataType": "dint" }
]
}
]
}
Virtual EtherNet/IP PLC:
{ "name": "enip-link", "type": "virtual", "responder": "ethernet-ip" }
If tag / tagName is omitted, Zeus uses the point name as the PLC tag name.
Common Issues
| Symptom | Likely cause | Fix |
|---|---|---|
| Timeout | TCP 44818, firewall, or PLC EtherNet/IP service unavailable | Test with EtherNetIpSlaveResponder, then check the field network |
CIP status 0x04 | Tag path is wrong or missing | Check tag path, Program scope, and casing |
| Type mismatch | Configured dataType differs from PLC tag type | Match the PLC project type |
| Write-back missing | Point not writable or PLC tag is read-only | Mark writable and confirm PLC permissions |
| BOOL has scale/alarm | BOOL is not numeric | Remove scale, lowAlarmLimit, and highAlarmLimit |
Next: Points and Acquisition.