A lightweight .NET library for communicating with CIMON PLCs over Ethernet (TCP) and Serial (RS232C / RS485). Read and write bits and words in PLC device memory with a simple async API.
using var plc = new EthernetConnector(new TcpSocket("192.168.1.10"));
var (code, words) = await plc.ReadWordAsync(MemoryType.D, "000100", 10);- About
- Features
- Installation
- Quick start
- Connection handling
- Addresses and memory types
- Response codes
- Limits per request
- Supported PLCs and platforms
- Build and test
- Contributing
- License
CIMON PLCs are industrial controllers based on the IEC 61131 standard. There are many tools and servers to exchange data with PLCs, but what if you want your own application to talk to them directly? I couldn't find any library for CIMON PLCs, even though I used them in many projects, so I decided to publish mine.
- Ethernet (TCP) and Serial (RS232C / RS485) connectors with the same API
- Read and write bits and words in any device memory (
X,Y,M,L,K,F,T,C,S,D) - Fully async, and safe to share one connector between concurrent tasks
- Responses are validated (header, frame number, checksum and length) before data is returned
- Automatic or manual connection management
- Targets .NET Standard 2.0 and .NET 8, so it runs on .NET Framework 4.6.1+ as well as modern .NET
- No third-party dependencies besides
System.IO.Ports
Install from NuGet with the .NET CLI:
dotnet add package Cimon.NetOr with the Package Manager Console in Visual Studio:
Install-Package Cimon.NetCreate a connector for your connection type: EthernetConnector for Ethernet TCP, or SerialConnector for RS232C/RS485.
using CimonPlc.Enums;
using CimonPlc.PlcConnectors;
using CimonPlc.Sockets;
using var plc = new EthernetConnector(new TcpSocket("192.168.1.10", 10620));
// Read 10 bits from input memory X, starting at address 000001
var (responseCode, bits) = await plc.ReadBitAsync(MemoryType.X, "000001", 10);
if (responseCode == ResponseCode.Success)
Console.WriteLine(string.Join(", ", bits));
// Write 3 words to data register D, starting at address 000100
var writeResult = await plc.WriteWordAsync(MemoryType.D, "000100", 1200, 35, 65535);using var plc = new SerialConnector(new SerialSocket("COM3", 9600));
// Write 5 bits to output memory Y, starting at address 000010
var responseCode = await plc.WriteBitAsync(MemoryType.Y, "000010", 1, 1, 1, 0, 1);
// Read 4 words from data register D
var (code, words) = await plc.ReadWordAsync(MemoryType.D, "000200", 4);| Method | Returns | Description |
|---|---|---|
ReadWordAsync(memoryType, address, length) |
(ResponseCode, int[]) |
Reads length 16-bit words |
ReadBitAsync(memoryType, address, length) |
(ResponseCode, byte[]) |
Reads length bits, each value is 0 or 1 |
WriteWordAsync(memoryType, address, params int[] data) |
ResponseCode |
Writes words, each value in 0–65535 |
WriteBitAsync(memoryType, address, params byte[] data) |
ResponseCode |
Writes bits, each value 0 or 1 |
Connect(readTimeout, writeTimeout, pingTimeout) |
ConnectionStatus |
Opens the connection (timeouts in ms, 100–10,000) |
Disconnect() |
ConnectionStatus |
Closes the connection |
Invalid arguments (bad address format, out-of-range length or values) throw ArgumentException.
Communication problems are reported through the returned ResponseCode.
With autoConnect enabled (the default), each read or write opens the connection if needed and closes it afterwards.
That's convenient for occasional requests. For frequent polling it's much faster to keep one connection open:
using var plc = new EthernetConnector(new TcpSocket("192.168.1.10"));
var status = await plc.Connect(readTimeout: 1000, writeTimeout: 1000, pingTimeout: 3000);
if (status != ConnectionStatus.Connected)
return;
while (!cancellationToken.IsCancellationRequested)
{
var (code, words) = await plc.ReadWordAsync(MemoryType.D, "000100", 10);
// ...
await Task.Delay(500);
}
plc.Disconnect();- A connection opened with
Connect()stays open, even whenautoConnectis enabled. - With
autoConnect: false, requests on a closed connection returnResponseCode.SystemError. - Requests on one connector are sent one at a time, so a single connector can be shared between tasks.
- By default,
TcpSocketpings the PLC before connecting and returnsConnectionStatus.NoRouteToDestinationif there's no reply. If your network blocks ICMP, turn the ping off:
var socket = new TcpSocket("192.168.1.10", 10620, usePing: false);Addresses are hexadecimal strings of up to 6 characters. Shorter addresses are padded with leading zeros, so "A0" becomes "0000A0".
- Word functions need a word address, so the last digit must be
0(for example"000010"). - Bit functions accept any bit address (for example
"0000A1").
| Memory | Name | Description |
|---|---|---|
X |
Input | Inputs received directly from devices |
Y |
Output | Outputs written to devices |
M |
Internal memory | Internal relays used by the logic |
L |
Link | Data link with upper and lower devices |
K |
Keep | Like M, but retained when power is off |
F |
Internal flag | System state, clock contacts and flags |
T |
Timer | Timer values |
C |
Counter | Counter values |
S |
Step control | Step control relays |
D |
Data register | Internal data storage |
Every request returns a ResponseCode. Anything other than Success means the request failed.
| Code | Value | Meaning |
|---|---|---|
Success |
0 | Request processed successfully |
SystemError |
1 | System error, no connection or no response from the PLC |
InvalidDevicePrefix |
2 | Invalid device prefix |
InvalidDeviceAddress |
3 | Invalid device address |
UdpErrorReadDataSize |
4 | Error in requested data size |
UdpErrorBlockSize |
5 | More than 16 requested blocks |
BufferError |
6 | Buffer memory reported an error in data or size |
OverBufferCapacity |
7 | Receive buffer capacity exceeded |
OverSendingTime |
8 | Sending time exceeded |
UdpInvalidHeader |
9 | Invalid header |
ChecksumError |
10 | Checksum error in the received data |
FrameSizeError |
11 | Error in frame length |
UdpDataSizeError |
12 | Error in the size of data to write |
UnknownBitValue |
13 | Invalid bit value in write data |
UnknownCommand |
14 | Unknown command |
WritingError |
15 | Writing is disabled, or the response was invalid |
CpuError |
16 | CPU processing error |
| Function | Ethernet | Serial |
|---|---|---|
ReadWordAsync |
1–512 words | 1–63 words |
ReadBitAsync |
1–1024 bits | 1–126 bits |
WriteWordAsync |
1–64 words | 1–61 words |
WriteBitAsync |
1–256 bits | 1–126 bits |
- PLCs: the CIMON PLC range, including the
PLC-S,CPandXPseries - Frameworks: .NET Standard 2.0 (.NET Framework 4.6.1+, .NET Core 2.0+, Mono) and .NET 8+
More documentation is in the Wiki.
You need the .NET SDK 10 or newer.
dotnet build
dotnet testNo PLC is needed. EthernetConnector tests talk over real TCP sockets to a PLC simulated with the
Rony.Net mock server, and SerialConnector tests use a fake serial socket.
The Ethernet tests are also a sample of testing a binary protocol client with Rony.Net:
PlcMockServerTestusesRony.Net.Xunitto give every test its own server on a free port, with its log in the test output.CimonFramingis a customIMessageFramingthat splits the TCP stream into Cimon frames.CimonPlcSimulatoranswers like a PLC and keeps its memory, so written values can be read back.EthernetFailureTestssimulates slow, chunked, truncated, corrupted and dropped responses.
Plc.Attach(Server); // answer like a PLC
using var connector = CreateConnector(); // EthernetConnector for 127.0.0.1:Server.Port
await connector.WriteWordAsync(MemoryType.D, "100", 1, 2, 3);
var (code, words) = await connector.ReadWordAsync(MemoryType.D, "100", 3); // [1, 2, 3]
Server.Should().HaveAcceptedConnections(Times.Exactly(2)) // autoConnect: one connection per request
.And.HaveReceivedInOrder(
r => r.Body[10] == (byte)WriteCommands.WordBlockWrite,
r => r.Body[10] == (byte)ReadCommand.WordBlockRead);Issues and pull requests are welcome. Please run dotnet test before opening a pull request, and add tests for new behavior.
See CHANGELOG.md for the release history.
Cimon.Net is released under the MIT License.