diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4c19a31
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,39 @@
+name: CI
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v5
+
+ - uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --configuration Release --no-restore
+
+ - name: Test
+ run: dotnet test --configuration Release --no-build --verbosity normal
+
+ - name: Pack
+ run: dotnet pack src/CimonPlc/CimonPlc.csproj --configuration Release --no-build --output artifacts
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: nuget-package
+ path: artifacts/*
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index f691fe9..0000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,72 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [ "main" ]
- schedule:
- - cron: '31 3 * * 5'
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: [ 'csharp' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
-
- # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
-
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
-
- # If the Autobuild fails above, remove it and uncomment the following three lines.
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
-
- # - run: |
- # echo "Run, Build Application using script"
- # ./location_of_script_within_repo/buildscript.sh
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index ade816b..f3698fc 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,22 +1,10 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
+name: CodeQL
on:
push:
- branches: [ "main" ]
+ branches: [ main ]
pull_request:
- # The branches below must be a subset of the branches above
- branches: [ "main" ]
+ branches: [ main ]
schedule:
- cron: '42 12 * * 2'
@@ -29,44 +17,15 @@ jobs:
contents: read
security-events: write
- strategy:
- fail-fast: false
- matrix:
- language: [ 'csharp' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
-
steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
-
- # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
-
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
-
- # If the Autobuild fails above, remove it and uncomment the following three lines.
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+ - name: Checkout repository
+ uses: actions/checkout@v5
- # - run: |
- # echo "Run, Build Application using script"
- # ./location_of_script_within_repo/buildscript.sh
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: csharp
+ build-mode: none
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..f56c426
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,31 @@
+# Changelog
+
+## 2.0.0
+
+### Fixed
+- **Ethernet read requests were malformed**: the `Length` field of `ReadWordAsync`/`ReadBitAsync` frames was written as 1 byte instead of 2,
+ shifting the whole data block by one byte. Read frames now use the same `ID | FrameNo | Cmd | Res | Length(2) | Data | CheckSum` layout as write frames.
+- `TcpSocket` could not reconnect after being disconnected (a closed `Socket` was reused), so with `autoConnect` every request after the first one failed
+ with `ObjectDisposedException`. A new socket is now created for each connection.
+- Frame numbers did not wrap correctly: after 128 requests the frame number stayed `0` for the next 128 requests. It now cycles from 0 to 127.
+- `SerialSocket.ReceiveData` returned a `null` task when a read was incomplete, which caused a `NullReferenceException` when awaited.
+- Short, truncated or corrupted responses caused `IndexOutOfRangeException`; they now return `ResponseCode.WritingError`.
+- Serial `WriteWordAsync` accepted up to 63 words although the 2-char length field can only describe 61; larger requests silently produced a wrong length.
+- Opening an already open `SerialSocket` threw an exception.
+
+### Changed
+- Responses are returned as soon as a complete frame arrives, instead of always waiting for the full read timeout (1 s by default) on every request.
+- Connections opened explicitly with `Connect()` are no longer closed after each request when `autoConnect` is enabled; only connections opened automatically are closed.
+- Requests on one connector are serialized, so a connector can be shared between concurrent tasks.
+- `TcpSocket` has a new optional `usePing` parameter to skip the ICMP ping before connecting.
+- `PlcConnector`, `TcpSocket` and `SerialSocket` implement `IDisposable`.
+- Addresses are sent in upper case.
+- Targets `netstandard2.0` and `net8.0` (was `netcoreapp3.1`, which is out of support), so the package also works on .NET Framework.
+- Removed the `Ardalis.GuardClauses` dependency.
+- **Breaking:** the helper classes `Tools` and the guard extensions are now internal.
+- **Breaking:** `PlcConnector` constructor is now `protected PlcConnector(IPlcSocket socket, bool autoConnect)` and the protected `_timeout` field was removed.
+- Fixed `dotnet pack`, which referenced an icon on a local disk path.
+- Replaced duplicate CodeQL workflows with a single updated one, and added a CI workflow that builds, tests and packs the library.
+
+## 1.1.0
+- Downgrade project to .NET Core 3.1.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b7a787a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Mojtaba Kiani
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index f1b362f..2d86a01 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,224 @@
-[](https://badge.fury.io/nu/Cimon.Net)
-
# Cimon.Net
-A .NET Library for Cimon PLCs Connectivity
-
-## About The Project
-
CIMON-PLC is an industrial control device based on international standards of IEC61131. There are many tools and servers to work with PLCs and send/recieve data with them, but what if we want to use our application to communicate? I Couldn't find any library to work with Cimon PLCs, and actually I used them in many projects. So I decided to publish my library.
+[](https://www.nuget.org/packages/Cimon.Net)
+[](https://www.nuget.org/packages/Cimon.Net)
+[](https://github.com/archofthings/Cimon.Net/actions/workflows/ci.yml)
+[](https://github.com/archofthings/Cimon.Net/actions/workflows/codeql.yml)
+[](https://learn.microsoft.com/dotnet/standard/net-standard)
+[](https://dotnet.microsoft.com/download/dotnet/8.0)
+[](LICENSE)
+
+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.
+
+```csharp
+using var plc = new EthernetConnector(new TcpSocket("192.168.1.10"));
+var (code, words) = await plc.ReadWordAsync(MemoryType.D, "000100", 10);
+```
+
+## Contents
+
+- [About](#about)
+- [Features](#features)
+- [Installation](#installation)
+- [Quick start](#quick-start)
+- [Connection handling](#connection-handling)
+- [Addresses and memory types](#addresses-and-memory-types)
+- [Response codes](#response-codes)
+- [Limits per request](#limits-per-request)
+- [Supported PLCs and platforms](#supported-plcs-and-platforms)
+- [Build and test](#build-and-test)
+- [Contributing](#contributing)
+- [License](#license)
+
+## About
+
+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.
+
+## Features
+
+- **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`
+
+## Installation
+
+Install from [NuGet](https://www.nuget.org/packages/Cimon.Net) with the .NET CLI:
+
+```shell
+dotnet add package Cimon.Net
+```
+
+Or with the Package Manager Console in Visual Studio:
+
+```powershell
+Install-Package Cimon.Net
+```
+
+## Quick start
+
+Create a connector for your connection type: `EthernetConnector` for Ethernet TCP, or `SerialConnector` for RS232C/RS485.
+
+### Ethernet
+
+```csharp
+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);
+```
+
+### Serial
+
+```csharp
+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);
+```
+
+### API overview
+
+| 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`.
+
+## Connection handling
+
+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:
+
+```csharp
+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 when `autoConnect` is enabled.
+- With `autoConnect: false`, requests on a closed connection return `ResponseCode.SystemError`.
+- Requests on one connector are sent one at a time, so a single connector can be shared between tasks.
+- By default, `TcpSocket` pings the PLC before connecting and returns `ConnectionStatus.NoRouteToDestination` if there's no reply.
+ If your network blocks ICMP, turn the ping off:
+
+```csharp
+var socket = new TcpSocket("192.168.1.10", 10620, usePing: false);
+```
+
+## Addresses and memory types
+
+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 |
+
+## Response codes
+
+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 |
+
+## Limits per request
+
+| 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 |
+
+## Supported PLCs and platforms
+
+- **PLCs:** the CIMON PLC range, including the `PLC-S`, `CP` and `XP` series
+- **Frameworks:** .NET Standard 2.0 (.NET Framework 4.6.1+, .NET Core 2.0+, Mono) and .NET 8+
-## Getting Started
-### 1. Installing Cimon.Net
-You can install Cimon.Net with [NuGet Package Manager Console](https://www.nuget.org/packages/Cimon.Net):
+More documentation is in the [Wiki](https://github.com/archofthings/Cimon.Net/wiki).
- Install-Package Cimon.Net
-
-Or via the .NET Core command-line interface:
+## Build and test
- dotnet add package Cimon.Net
-
-Either commands, from Package Manager Console or .NET Core CLI, will download and install **Cimon.Net** and all required dependencies.
+You need the [.NET SDK](https://dotnet.microsoft.com/download) 10 or newer.
-### 2. Defining your Connector
-Create a Connector based on your connection type, You can choose between `EthernetConnector` to support Ethernet TCP/UDP connection or `SerialConnector` to support RS232C/RS485 serial interfaces.
+```shell
+dotnet build
+dotnet test
+```
-`EthernetConnector` usage sample for reading 10 bits from Input device memory `X` address 000001:
-
- var Plc = new EthernetConnector(new TcpSocket("192.168.1.10", 10620), true);
- var (responseCode, data) = await Plc.ReadBitAsync(MemoryType.X, "000001", 10);
+`EthernetConnector` tests run against a local mock TCP server built with [Rony.Net](https://github.com/archofthings/Rony.Net).
+`SerialConnector` tests use a fake serial socket, so no device is needed.
-`SerialConnector` usage sample for write 5 bits to Output device memory `Y` address 000010:
-
- var Plc = new SerialConnector(new SerialSocket("COM3", 9600), true);
- await Plc.WriteBitAsync(MemoryType.Y, "000010", 1, 1, 1, 0, 1);
-
-## Documentation
-Check the Wiki and feel free to edit it: https://github.com/MojtabaKiani/Cimon.Net/wiki
+## Contributing
-## Supported PLCs
-Complete range of Cimon PLC products including `PLC-S`, `CP`, `XP` series
+Issues and pull requests are welcome. Please run `dotnet test` before opening a pull request, and add tests for new behavior.
+See [CHANGELOG.md](CHANGELOG.md) for the release history.
-## Compile
-You need at least Visual Studio 2019 (you can download the Community Edition for free).
+## License
-## Running the tests
-I used my library [Rony.Net](https://github.com/MojtabaKiani/Rony.Net) in unit tests for `EthernetConnector`. But for `SerialConnector`
-I used no device or library and it only works with a fake socket.
+Cimon.Net is released under the [MIT License](LICENSE).
diff --git a/src/CimonPlc/CimonPlc.csproj b/src/CimonPlc/CimonPlc.csproj
index 13850ec..cedf90f 100644
--- a/src/CimonPlc/CimonPlc.csproj
+++ b/src/CimonPlc/CimonPlc.csproj
@@ -1,32 +1,43 @@
-
+
+
+
+ netstandard2.0;net8.0
+ latest
+ CimonPlc
+ true
+ $(NoWarn);CS1591
+ true
+
- netcoreapp3.1
Cimon.Net
+ 2.0.0
Mojtaba Kiani
Cimon.Net
- A .NET Library for Cimon PLCs Connectivity
- https://github.com/MojtabaKiani/Cimon.Net
- Cimon, Cymon, PLc, Industrial
+ A .NET Library for Cimon PLCs Connectivity over Ethernet (TCP) and Serial (RS232C/RS485)
+ Cimon;PLC;Industrial;Automation;Ethernet;Serial
en
- cimon_logo.png
MIT
- https://github.com/MojtabaKiani/Cimon.Net
+ https://github.com/archofthings/Cimon.Net
+ https://github.com/archofthings/Cimon.Net
git
- Downgarde project to .Net Core 3.1 to handle more users
- 1.1.0
+ README.md
+ true
+ true
+ snupkg
+ See https://github.com/archofthings/Cimon.Net/blob/main/CHANGELOG.md
-
-
+
+
+
+
+
-
- True
-
-
+
diff --git a/src/CimonPlc/Enums/ResponseCode.cs b/src/CimonPlc/Enums/ResponseCode.cs
index 9bcdb97..b9068d6 100644
--- a/src/CimonPlc/Enums/ResponseCode.cs
+++ b/src/CimonPlc/Enums/ResponseCode.cs
@@ -1,7 +1,7 @@
namespace CimonPlc.Enums
{
///
- /// Error Code From Cimon PLC on read & write
+ /// Error Code From Cimon PLC on read & write
///
public enum ResponseCode
{
diff --git a/src/CimonPlc/Interfaces/IEthernetSocket.cs b/src/CimonPlc/Interfaces/IEthernetSocket.cs
index ad75f93..5960bd8 100644
--- a/src/CimonPlc/Interfaces/IEthernetSocket.cs
+++ b/src/CimonPlc/Interfaces/IEthernetSocket.cs
@@ -1,6 +1,3 @@
-using CimonPlc.Enums;
-using System.Threading.Tasks;
-
namespace CimonPlc.Interfaces
{
public interface IEthernetSocket : IPlcSocket
diff --git a/src/CimonPlc/Interfaces/IPlcSocket.cs b/src/CimonPlc/Interfaces/IPlcSocket.cs
index a2ce874..f1af1a6 100644
--- a/src/CimonPlc/Interfaces/IPlcSocket.cs
+++ b/src/CimonPlc/Interfaces/IPlcSocket.cs
@@ -1,11 +1,11 @@
-using CimonPlc.Enums;
+using CimonPlc.Enums;
using System.Threading.Tasks;
namespace CimonPlc.Interfaces
{
public interface IPlcSocket
{
- public bool IsConnected { get; }
+ bool IsConnected { get; }
Task Connect(int readTimeout = 1000, int writeTimeout = 1000, int pingTimeout = 3000);
@@ -13,6 +13,11 @@ public interface IPlcSocket
Task SendData(byte[] frame);
+ ///
+ /// Waits for the response of the last sent frame.
+ /// Implementations should return as soon as a complete frame is received, or when the read timeout expires.
+ ///
+ /// The received frame, or null if nothing was received
Task ReceiveData();
}
}
diff --git a/src/CimonPlc/Interfaces/ISerialSocket.cs b/src/CimonPlc/Interfaces/ISerialSocket.cs
index fe061f8..b02c52c 100644
--- a/src/CimonPlc/Interfaces/ISerialSocket.cs
+++ b/src/CimonPlc/Interfaces/ISerialSocket.cs
@@ -1,6 +1,3 @@
-using CimonPlc.Enums;
-using System.Threading.Tasks;
-
namespace CimonPlc.Interfaces
{
public interface ISerialSocket : IPlcSocket
diff --git a/src/CimonPlc/Internal/Guard.cs b/src/CimonPlc/Internal/Guard.cs
new file mode 100644
index 0000000..a2476de
--- /dev/null
+++ b/src/CimonPlc/Internal/Guard.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace CimonPlc
+{
+ ///
+ /// Lightweight argument guards used to validate public API input.
+ ///
+ internal sealed class Guard
+ {
+ public static readonly Guard Against = new Guard();
+
+ private Guard()
+ {
+ }
+
+ public T Null(T input, string parameterName) where T : class
+ {
+ if (input is null)
+ throw new ArgumentNullException(parameterName);
+
+ return input;
+ }
+
+ public string NullOrEmpty(string input, string parameterName)
+ {
+ Null(input, parameterName);
+ if (input.Length == 0)
+ throw new ArgumentException($"Required input {parameterName} was empty.", parameterName);
+
+ return input;
+ }
+
+ public T[] NullOrEmpty(T[] input, string parameterName)
+ {
+ if (input is null)
+ throw new ArgumentNullException(parameterName);
+ if (input.Length == 0)
+ throw new ArgumentException($"Required input {parameterName} was empty.", parameterName);
+
+ return input;
+ }
+
+ public int OutOfRange(int input, string parameterName, int rangeFrom, int rangeTo)
+ {
+ if (input < rangeFrom || input > rangeTo)
+ throw new ArgumentOutOfRangeException(parameterName, $"Input {parameterName} was out of range, it must be between {rangeFrom} and {rangeTo}.");
+
+ return input;
+ }
+
+ public IEnumerable OutOfRange(IEnumerable input, string parameterName, T rangeFrom, T rangeTo) where T : IComparable
+ {
+ var comparer = Comparer.Default;
+
+ if (comparer.Compare(rangeFrom, rangeTo) >= 0)
+ throw new ArgumentException($"{nameof(rangeFrom)} should be less than {nameof(rangeTo)}");
+
+ if (input.Any(x => comparer.Compare(x, rangeFrom) < 0 || comparer.Compare(x, rangeTo) > 0))
+ throw new ArgumentOutOfRangeException(parameterName, $"Input {parameterName} was out of range, it must be between {rangeFrom} and {rangeTo}.");
+
+ return input;
+ }
+
+ public string BadFormat(string input, string parameterName, string regexPattern)
+ {
+ Null(input, parameterName);
+ if (!Regex.IsMatch(input, $"^(?:{regexPattern})$"))
+ throw new ArgumentException($"Input {parameterName} was not in required format", parameterName);
+
+ return input;
+ }
+
+ public T InvalidData(T input, string parameterName, Func predicate)
+ {
+ if (!predicate(input))
+ throw new ArgumentException($"Input {parameterName} did not satisfy the options", parameterName);
+
+ return input;
+ }
+
+ public TEnum UndefinedEnum(TEnum input, string parameterName) where TEnum : struct, Enum
+ {
+ if (!Enum.IsDefined(typeof(TEnum), input))
+ throw new ArgumentOutOfRangeException(parameterName, $"Input {parameterName} is not a defined {typeof(TEnum).Name} value.");
+
+ return input;
+ }
+ }
+}
diff --git a/src/CimonPlc/Internal/Tools.cs b/src/CimonPlc/Internal/Tools.cs
new file mode 100644
index 0000000..5b917e5
--- /dev/null
+++ b/src/CimonPlc/Internal/Tools.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Text;
+
+namespace CimonPlc
+{
+ internal static class Tools
+ {
+ public static IPStatus Ping(IPAddress ip, int pingTimeout = 300)
+ {
+ try
+ {
+ using var pingSender = new Ping();
+ var reply = pingSender.Send(ip, pingTimeout);
+ return reply.Status;
+ }
+ catch
+ {
+ return IPStatus.BadDestination;
+ }
+ }
+
+ public static int ToInt(byte byte1, byte byte2)
+ {
+ return (byte1 << 8) + byte2;
+ }
+
+ public static byte ToByte(char char1, char char2)
+ {
+ return byte.Parse(string.Concat(char1, char2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ }
+
+ public static byte[] ToDualByte(this int number)
+ {
+ number &= 0xFFFF;
+ return new byte[] { (byte)(number >> 8), (byte)(number & 0xFF) };
+ }
+
+ public static char[] ToDualChar(this byte number)
+ {
+ return number.ToString("X2", CultureInfo.InvariantCulture).ToCharArray();
+ }
+
+ public static char[] ToQuadChar(this int number)
+ {
+ number &= 0xFFFF;
+ return number.ToString("X4", CultureInfo.InvariantCulture).ToCharArray();
+ }
+
+ ///
+ /// Calculates the BCC of a serial frame and appends it as two hex chars.
+ /// The first 3 chars (header + station number) are excluded from the calculation.
+ ///
+ public static void AddBCC(this List input)
+ {
+ if (input == null || input.Count == 0)
+ return;
+
+ var sum = input.Skip(3).Sum(x => Convert.ToByte(x));
+ sum %= 256;
+
+ input.AddRange(((byte)sum).ToDualChar());
+ }
+
+ ///
+ /// Two-byte checksum used in Ethernet frames: the lower 2 bytes of the byte-wise sum.
+ ///
+ public static byte[] CheckSum(IEnumerable frame)
+ {
+ return frame.Sum(x => x).ToDualByte();
+ }
+
+ ///
+ /// Validates the header and checksum of an Ethernet response frame.
+ ///
+ public static bool IsValidResponse(byte[] buffer, byte frameNo)
+ {
+ if (buffer == null || buffer.Length < EthernetFrame.MinimumLength)
+ return false;
+
+ //[0-8] ID : This is a 9 - byte string "KDT_PLC_S".
+ if (Encoding.ASCII.GetString(buffer, 0, 9) != EthernetFrame.SlaveId)
+ return false;
+
+ //[9] Frame No : The value that 128 are added to the number of the
+ // command frame received from the Master is used
+ if (buffer[9] - 128 != frameNo)
+ return false;
+
+ //[12-13] Length : The frame must be long enough to hold the declared data
+ if (buffer.Length < EthernetFrame.ExpectedLength(buffer))
+ return false;
+
+ //Check Sum : This is 2-byte value. After the entire frame is binary-summed by the byte,
+ //the lower 2 - byte in the result value is used.
+ var checkSum = CheckSum(buffer.Take(buffer.Length - 2));
+ return checkSum[0] == buffer[buffer.Length - 2] && checkSum[1] == buffer[buffer.Length - 1];
+ }
+
+ ///
+ /// Validates the framing (STX ... ETX) and command of a serial response frame.
+ ///
+ public static bool IsValidSerialResponse(char[] receivedFrame, byte ackCommand)
+ {
+ if (receivedFrame == null || receivedFrame.Length < SerialFrame.MinimumLength)
+ return false;
+
+ if (receivedFrame[0] != SerialFrame.Stx || receivedFrame[receivedFrame.Length - 1] != SerialFrame.Etx)
+ return false;
+
+ //[3] Cmd : In Slave, 1 - byte command must be equal to sent command
+ return receivedFrame[3] == (char)ackCommand;
+ }
+
+ public static bool IsHex(char c)
+ {
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
+ }
+ }
+
+ internal static class EthernetFrame
+ {
+ public const string MasterId = "KDT_PLC_M";
+ public const string SlaveId = "KDT_PLC_S";
+
+ /// ID(9) + Frame No(1) + Cmd(1) + Res(1) + Length(2)
+ public const int HeaderLength = 14;
+
+ public const int CheckSumLength = 2;
+
+ public const int MinimumLength = HeaderLength + CheckSumLength;
+
+ /// Command used by the PLC to acknowledge writes and to report errors.
+ public const byte AckCommand = 0x41;
+
+ ///
+ /// Total length of a frame according to the length field in its header,
+ /// or -1 if the header is not complete yet.
+ ///
+ public static int ExpectedLength(IReadOnlyList buffer)
+ {
+ if (buffer.Count < HeaderLength)
+ return -1;
+
+ return HeaderLength + Tools.ToInt(buffer[12], buffer[13]) + CheckSumLength;
+ }
+ }
+
+ internal static class SerialFrame
+ {
+ public const char Enq = (char)0x05;
+ public const char Eot = (char)0x04;
+ public const char Stx = (char)0x02;
+ public const char Etx = (char)0x03;
+ public const char ErrorCommand = 'E';
+
+ /// STX(1) + Station(2) + Cmd(1) + Length/Code(2) + ETX(1)
+ public const int MinimumLength = 7;
+ }
+}
diff --git a/src/CimonPlc/Models/PlcConnector.cs b/src/CimonPlc/Models/PlcConnector.cs
index 4ef90d8..9a0a81c 100644
--- a/src/CimonPlc/Models/PlcConnector.cs
+++ b/src/CimonPlc/Models/PlcConnector.cs
@@ -1,72 +1,147 @@
-using Ardalis.GuardClauses;
using CimonPlc.Enums;
using CimonPlc.Interfaces;
using System;
+using System.Threading;
using System.Threading.Tasks;
namespace CimonPlc.Models
{
- public abstract class PlcConnector
+ public abstract class PlcConnector : IDisposable
{
- protected IPlcSocket _socket;
- protected bool _autoConnect;
- protected int _timeout;
+ protected readonly IPlcSocket _socket;
+ protected readonly bool _autoConnect;
+
+ // PLC protocols are strictly request/response, so only one exchange may be in flight per connection.
+ private readonly SemaphoreSlim _exchangeLock = new SemaphoreSlim(1, 1);
+ private bool _disposed;
public bool IsConnected => _socket.IsConnected;
- public PlcConnector(IPlcSocket socket)
+ /// The socket used to talk to the PLC
+ ///
+ /// If true, read/write functions open the connection when it is not open and
+ /// close it again after the request is completed.
+ ///
+ protected PlcConnector(IPlcSocket socket, bool autoConnect = true)
{
- _socket = socket;
+ _socket = Guard.Against.Null(socket, nameof(socket));
+ _autoConnect = autoConnect;
}
///
- /// Creates a connection to PLC using a network socket, before read or write data it must be called.
+ /// Creates a connection to PLC using a network socket. It must be called before reading or writing data,
+ /// unless the connector is created with auto connect enabled.
///
- /// Data read timeout in ms, valid rage is between 100 and 10,000
- /// Data write timeout in ms, valid rage is between 100 and 10,000
- /// Ping Timeout in ms, valid rage is between 100 and 10,000
- /// Returns success if it can connect to PLC successfully
- public virtual async Task Connect(int readTimeout = 1000, int writeTimeout = 1000, int pingTimeout = 3000)
+ /// Data read timeout in ms, valid range is between 100 and 10,000
+ /// Data write timeout in ms, valid range is between 100 and 10,000
+ /// Ping Timeout in ms, valid range is between 100 and 10,000
+ /// Returns Connected if it can connect to PLC successfully
+ public virtual Task Connect(int readTimeout = 1000, int writeTimeout = 1000, int pingTimeout = 3000)
{
Guard.Against.OutOfRange(readTimeout, nameof(readTimeout), 100, 10000);
Guard.Against.OutOfRange(writeTimeout, nameof(writeTimeout), 100, 10000);
Guard.Against.OutOfRange(pingTimeout, nameof(pingTimeout), 100, 10000);
+ ThrowIfDisposed();
- try
- {
- _timeout = readTimeout;
- return await _socket.Connect(readTimeout, writeTimeout, pingTimeout);
- }
- catch (Exception)
- {
- throw;
- }
-
+ return _socket.Connect(readTimeout, writeTimeout, pingTimeout);
}
///
- /// Drops the connection to PLC, it should call if you don't set auto connection in read/write functions.
+ /// Drops the connection to PLC. It should be called when auto connect is not used.
///
- /// Returns success if it can disconnect from PLC successfully
+ /// Returns DisConnected if it can disconnect from PLC successfully
public virtual ConnectionStatus Disconnect()
{
+ return _socket.Disconnect();
+ }
+
+ public abstract Task<(ResponseCode responseCode, int[] data)> ReadWordAsync(MemoryType memoryType, string address, int length);
+
+ public abstract Task<(ResponseCode responseCode, byte[] data)> ReadBitAsync(MemoryType memoryType, string address, int length);
+
+ public abstract Task WriteWordAsync(MemoryType memoryType, string address, params int[] data);
+
+ public abstract Task WriteBitAsync(MemoryType memoryType, string address, params byte[] data);
+
+ ///
+ /// Sends a request frame to the PLC and waits for its response, taking care of auto connect.
+ ///
+ ///
+ /// The response frame on success; otherwise a null frame and the
+ /// error code describing why the exchange failed.
+ ///
+ protected async Task<(ResponseCode error, byte[] response)> ExchangeAsync(byte[] request)
+ {
+ ThrowIfDisposed();
+
+ await _exchangeLock.WaitAsync().ConfigureAwait(false);
+ var openedHere = false;
try
{
- return _socket.Disconnect();
+ if (!IsConnected)
+ {
+ if (!_autoConnect)
+ return (ResponseCode.SystemError, null);
+
+ if (await Connect().ConfigureAwait(false) != ConnectionStatus.Connected)
+ return (ResponseCode.SystemError, null);
+
+ openedHere = true;
+ }
+
+ if (!await _socket.SendData(request).ConfigureAwait(false))
+ return (ResponseCode.WritingError, null);
+
+ var response = await _socket.ReceiveData().ConfigureAwait(false);
+ if (response == null || response.Length == 0)
+ return (ResponseCode.SystemError, null);
+
+ return (ResponseCode.Success, response);
}
- catch (Exception)
+ finally
{
- throw;
+ // Only close connections that were opened automatically, so callers who
+ // connect explicitly can keep a persistent connection.
+ if (openedHere)
+ _socket.Disconnect();
+
+ _exchangeLock.Release();
}
}
- public abstract Task<(ResponseCode responseCode, int[] data)> ReadWordAsync(MemoryType memoryType, string address, int length);
+ protected static string NormalizeAddress(string address)
+ {
+ return address.PadLeft(6, '0').ToUpperInvariant();
+ }
- public abstract Task<(ResponseCode responseCode, byte[] data)> ReadBitAsync(MemoryType memoryType, string address, int length);
+ protected void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(GetType().Name);
+ }
- public abstract Task WriteWordAsync(MemoryType memoryType, string address, params int[] data);
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
- public abstract Task WriteBitAsync(MemoryType memoryType, string address, params byte[] data);
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ return;
+ if (disposing)
+ {
+ if (_socket is IDisposable disposable)
+ disposable.Dispose();
+ else
+ _socket.Disconnect();
+
+ _exchangeLock.Dispose();
+ }
+
+ _disposed = true;
+ }
}
}
diff --git a/src/CimonPlc/PlcConnectors/EthernetConnector.cs b/src/CimonPlc/PlcConnectors/EthernetConnector.cs
index e38cc81..72d60ae 100644
--- a/src/CimonPlc/PlcConnectors/EthernetConnector.cs
+++ b/src/CimonPlc/PlcConnectors/EthernetConnector.cs
@@ -1,7 +1,6 @@
-using Ardalis.GuardClauses;
+using System;
using CimonPlc.Interfaces;
using CimonPlc.Enums;
-using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
@@ -12,13 +11,29 @@ namespace CimonPlc.PlcConnectors
{
public class EthernetConnector : PlcConnector
{
- private byte _frameNo = 0;
- public byte FrameNo => (byte)(_frameNo++ > 127 ? 0 : _frameNo);
+ private const int MaxFrameNo = 127;
+ private readonly object _frameNoLock = new object();
+ private byte _frameNo;
- public EthernetConnector(IEthernetSocket socket, bool autoConnect = true) : base(socket)
+ public EthernetConnector(IEthernetSocket socket, bool autoConnect = true) : base(socket, autoConnect)
{
- _socket = socket;
- _autoConnect = autoConnect;
+ }
+
+ ///
+ /// Number of the next frame sent to PLC, in the range from 0 to 127.
+ /// Reading this property consumes the number.
+ ///
+ public byte FrameNo
+ {
+ get
+ {
+ lock (_frameNoLock)
+ {
+ var frameNo = _frameNo;
+ _frameNo = (byte)(_frameNo >= MaxFrameNo ? 0 : _frameNo + 1);
+ return frameNo;
+ }
+ }
}
///
@@ -29,92 +44,23 @@ public EthernetConnector(IEthernetSocket socket, bool autoConnect = true) : base
/// PLC memory which required to read such as X or D
/// The word address or the card number of a corresponding device is used. That is, in case of bit device such as X/Y, the last number should be '0' and should contains 6 character, such as '000010'
/// Requested length for reading memory. Length must be in the range from 1 to 512
- /// Returns a tuple includes PLC response code and an array of byte contains read data
+ /// Returns a tuple includes PLC response code and an array of words contains read data
public override async Task<(ResponseCode responseCode, int[] data)> ReadWordAsync(MemoryType memoryType, string address, int length)
{
- Guard.Against.Null(memoryType, nameof(memoryType));
+ Guard.Against.UndefinedEnum(memoryType, nameof(memoryType));
Guard.Against.OutOfRange(length, nameof(length), 1, 512);
- Guard.Against.BadFormat(address, nameof(address), @"[0-9a-fA-F]{0,5}0");
-
-
- while (address.Length < 6)
- address = "0" + address;
-
- if (!IsConnected && !_autoConnect)
- return (ResponseCode.SystemError, null);
-
- if (!IsConnected)
- {
- var connectionStatus = await Connect();
- if (connectionStatus != ConnectionStatus.Connected)
- return (ResponseCode.SystemError, null);
- }
-
- var frame = new List();
-
- //[0-8] ID : This is a 9 - byte string “ KDT_PLC_M ”.
- frame.AddRange(Encoding.ASCII.GetBytes("KDT_PLC_M"));
-
- //[9] Frame No : This, 1 - byte data with the range from 0 to 127
- frame.Add(FrameNo);
-
- //[10] Cmd : In Master, 1 - byte command can be used and the
- //format of ‘Data’ field is selected according to each command
- frame.Add((byte)ReadCommand.WordBlockRead);
-
- //[11] Res: Reserved. (1 Byte, 00h) – Reserved device
- frame.Add(0);
-
- //[12] Length : Total number of the bytes of a frame data device
- frame.Add(10);
-
- //[13-n] Data : This is n block based on data needs to write and contains 2 parts :
- // 1- [13-20] Memory Address : 8 bytes
- frame.Add((byte)memoryType);
- frame.Add(0);
- frame.AddRange(Encoding.ASCII.GetBytes(address));
-
- // 2- [21-22] Read Length : 2 bytes
- frame.AddRange(length.ToDualByte());
-
- //Check Sum : This is 2-byte value. After the entire frame is binary-summed by the byte,
- //the lower 2 - byte in the result value is used.
- frame.AddRange(frame.Sum(x => x).ToDualByte());
-
- try
- {
- var sendState = await _socket.SendData(frame.ToArray());
- if (!sendState)
- return (ResponseCode.WritingError, null);
-
- //Read response from PLC
- await Task.Delay(_timeout);
- var receivedFrame = await _socket.ReceiveData();
- if (receivedFrame == null)
- return (ResponseCode.SystemError, null);
-
- if (_autoConnect)
- _socket.Disconnect();
+ Guard.Against.BadFormat(address, nameof(address), "[0-9a-fA-F]{0,5}0");
- //If response's cmd equals to 0x41, then must return error code
- if (receivedFrame[10] == 0x41)
- return ((ResponseCode)Tools.ToInt(receivedFrame[14], receivedFrame[15]), null);
+ var (error, response) = await SendCommandAsync((byte)ReadCommand.WordBlockRead, memoryType, address, length).ConfigureAwait(false);
+ if (error != ResponseCode.Success)
+ return (error, null);
- if (!Tools.IsValidResponse(receivedFrame, frame[9], (byte)ReadCommand.WordBlockRead))
- return (ResponseCode.WritingError, null);
+ var data = ReadData(response);
+ var words = new int[data.Length / 2];
+ for (var i = 0; i < words.Length; i++)
+ words[i] = Tools.ToInt(data[i * 2], data[i * 2 + 1]);
- var tempArray = new List();
- const int dataStartIndex = 23;
- for (var x = dataStartIndex; x < receivedFrame.Length - 2; x += 2)
- {
- tempArray.Add(Tools.ToInt(receivedFrame[x], receivedFrame[x + 1]));
- }
- return (ResponseCode.Success, tempArray.ToArray());
- }
- catch (Exception)
- {
- throw;
- }
+ return (ResponseCode.Success, words);
}
///
@@ -128,90 +74,15 @@ public EthernetConnector(IEthernetSocket socket, bool autoConnect = true) : base
/// Returns a tuple includes PLC response code and an array of byte contains read data
public override async Task<(ResponseCode responseCode, byte[] data)> ReadBitAsync(MemoryType memoryType, string address, int length)
{
- Guard.Against.Null(memoryType, nameof(memoryType));
+ Guard.Against.UndefinedEnum(memoryType, nameof(memoryType));
Guard.Against.OutOfRange(length, nameof(length), 1, 1024);
Guard.Against.BadFormat(address, nameof(address), "[0-9a-fA-F]{1,6}");
+ var (error, response) = await SendCommandAsync((byte)ReadCommand.BitBlockRead, memoryType, address, length).ConfigureAwait(false);
+ if (error != ResponseCode.Success)
+ return (error, null);
- while (address.Length < 6)
- address = "0" + address;
-
- if (!IsConnected && !_autoConnect)
- return (ResponseCode.SystemError, null);
-
- if (!IsConnected)
- {
- var connectionStatus = await Connect();
- if (connectionStatus != ConnectionStatus.Connected)
- return (ResponseCode.SystemError, null);
- }
-
- var frame = new List();
-
- //[0-8] ID : This is a 9 - byte string “ KDT_PLC_M ”.
- frame.AddRange(Encoding.ASCII.GetBytes("KDT_PLC_M"));
-
- //[9] Frame No : This, 1 - byte data with the range from 0 to 127
- frame.Add(FrameNo);
-
- //[10] Cmd : In Master, 1 - byte command can be used and the
- //format of ‘Data’ field is selected according to each command
- frame.Add((byte)ReadCommand.BitBlockRead);
-
- //[11] Res: Reserved. (1 Byte, 00h) – Reserved device
- frame.Add(0);
-
- //[12] Length : Total number of the bytes of a frame data device
- frame.Add(10);
-
- //[13-n] Data : This is n block based on data needs to write and contains 2 parts :
- // 1- [13-20] Memory Address : 8 bytes
- frame.Add((byte)memoryType);
- frame.Add(0);
- frame.AddRange(Encoding.ASCII.GetBytes(address));
-
- // 2- [21-22] Read Length : 2 bytes
- frame.AddRange(length.ToDualByte());
-
- //Check Sum : This is 2-byte value. After the entire frame is binary-summed by the byte,
- //the lower 2 - byte in the result value is used.
- frame.AddRange(frame.Sum(x => x).ToDualByte());
-
- try
- {
- var sendState = await _socket.SendData(frame.ToArray());
- if (!sendState)
- return (ResponseCode.WritingError, null);
-
- //Read response from PLC
- await Task.Delay(_timeout);
- var receivedFrame = await _socket.ReceiveData();
- if (receivedFrame == null)
- return (ResponseCode.SystemError, null);
-
- if (_autoConnect)
- _socket.Disconnect();
-
- //If response's cmd equals to 0x41, then must return error code
- if (receivedFrame[10] == 0x41)
- return ((ResponseCode)Tools.ToInt(receivedFrame[14], receivedFrame[15]), null);
-
- if (!Tools.IsValidResponse(receivedFrame, frame[9], (byte)ReadCommand.BitBlockRead))
- return (ResponseCode.WritingError, null);
-
- //Data includes : Address 9 chars + data
- var dataLength = Tools.ToInt(receivedFrame[12], receivedFrame[13]);
- const int addressLength = 9;
- var tempArray = new byte[dataLength - addressLength];
-
- const int dataStartIndex = 23;
- Array.Copy(receivedFrame, dataStartIndex, tempArray, 0, tempArray.Length);
- return (ResponseCode.Success, tempArray);
- }
- catch (Exception)
- {
- throw;
- }
+ return (ResponseCode.Success, ReadData(response));
}
///
@@ -228,84 +99,12 @@ public override async Task WriteWordAsync(MemoryType memoryType, s
Guard.Against.NullOrEmpty(data, nameof(data));
Guard.Against.OutOfRange(data, nameof(data), 0, 0xFFFF);
Guard.Against.OutOfRange(data.Length, nameof(data), 1, 64);
- Guard.Against.Null(memoryType, nameof(memoryType));
+ Guard.Against.UndefinedEnum(memoryType, nameof(memoryType));
Guard.Against.BadFormat(address, nameof(address), "[0-9a-fA-F]{0,5}0");
- while (address.Length < 6)
- address = "0" + address;
-
- if (!IsConnected && !_autoConnect)
- return ResponseCode.SystemError;
-
- if (!IsConnected)
- {
- var connectionStatus = await Connect();
- if (connectionStatus != ConnectionStatus.Connected)
- return ResponseCode.SystemError;
- }
-
- var frame = new List();
-
- //[0-8] ID : This is a 9 - byte string “ KDT_PLC_M ”.
- frame.AddRange(Encoding.ASCII.GetBytes("KDT_PLC_M"));
-
- //[9] Frame No : This, 1 - byte data with the range from 0 to 127
- frame.Add(FrameNo);
-
- //[10] Cmd : In Master, 1 - byte command can be used and the
- //format of ‘Data’ field is selected according to each command
- frame.Add((byte)WriteCommands.WordBlockWrite);
-
- //[11] Res: Reserved. (1 Byte, 00h) – Reserved device
- frame.Add(0);
-
- //[12-13] Length : This is the 2 - byte value indicating the size of ‘Data’ field. (Hexadecimal Figure)
- int dataLength = data.Length * 2 + 10;
- frame.AddRange(dataLength.ToDualByte());
-
- //[14-n] Data : This is n block based on data needs to write and contains 3 parts :
- // 1- [14-21] Memory Address : 8 bytes
- frame.Add((byte)memoryType);
- frame.Add(0);
- frame.AddRange(Encoding.ASCII.GetBytes(address));
-
- // 2- [22-23] Write Size : 2 bytes
- frame.AddRange(data.Length.ToDualByte());
-
- // 3- [24-n] Word Data : n bytes
- foreach (var item in data)
- {
- frame.AddRange(item.ToDualByte());
- }
-
- //Check Sum : This is 2-byte value. After the entire frame is binary-summed by the byte,
- //the lower 2 - byte in the result value is used.
- frame.AddRange(frame.Sum(x => x).ToDualByte());
-
- try
- {
- var sendState = await _socket.SendData(frame.ToArray());
- if (!sendState)
- return ResponseCode.WritingError;
-
- //Read response from PLC
- await Task.Delay(_timeout);
- var receivedFrame = await _socket.ReceiveData();
- if (receivedFrame == null)
- return ResponseCode.SystemError;
-
- if (_autoConnect)
- _socket.Disconnect();
-
- if (!Tools.IsValidResponse(receivedFrame, frame[9], 0x41))
- return ResponseCode.WritingError;
-
- return (ResponseCode)Tools.ToInt(receivedFrame[14], receivedFrame[15]);
- }
- catch (Exception)
- {
- throw;
- }
+ var payload = data.SelectMany(x => x.ToDualByte());
+ var (error, _) = await SendCommandAsync((byte)WriteCommands.WordBlockWrite, memoryType, address, data.Length, payload).ConfigureAwait(false);
+ return error;
}
///
@@ -315,89 +114,110 @@ public override async Task WriteWordAsync(MemoryType memoryType, s
///
/// PLC memory which required to write such as Y or D
/// The word address or the card number of a corresponding device is used, it should contains 6 characters, such as '0000D1'
- /// Data needed to write on PLC memory, The total sum of word data is not to be over 256 bits
- /// Returns a code which shows PLC response co`de
+ /// Data needed to write on PLC memory, each item must be 0 or 1. The total sum of data is not to be over 256 bits
+ /// Returns a code which shows PLC response code
public override async Task WriteBitAsync(MemoryType memoryType, string address, params byte[] data)
{
Guard.Against.NullOrEmpty(data, nameof(data));
Guard.Against.OutOfRange(data, nameof(data), 0, 1);
Guard.Against.OutOfRange(data.Length, nameof(data), 1, 256);
- Guard.Against.Null(memoryType, nameof(memoryType));
+ Guard.Against.UndefinedEnum(memoryType, nameof(memoryType));
Guard.Against.BadFormat(address, nameof(address), "[0-9a-fA-F]{1,6}");
- while (address.Length < 6)
- address = "0" + address;
+ var (error, _) = await SendCommandAsync((byte)WriteCommands.BitBlockWrite, memoryType, address, data.Length, data).ConfigureAwait(false);
+ return error;
+ }
+
+ ///
+ /// Builds a master frame, sends it and validates the PLC response.
+ ///
+ /// Success and the response frame, or the error code returned by PLC or detected locally
+ private async Task<(ResponseCode error, byte[] response)> SendCommandAsync(byte command, MemoryType memoryType, string address, int count, IEnumerable payload = null)
+ {
+ var frameNo = FrameNo;
+ var frame = BuildFrame(frameNo, command, memoryType, address, count, payload);
- if (!IsConnected && !_autoConnect)
- return ResponseCode.SystemError;
+ var (error, response) = await ExchangeAsync(frame).ConfigureAwait(false);
+ if (error != ResponseCode.Success)
+ return (error, null);
- if (!IsConnected)
+ if (!Tools.IsValidResponse(response, frameNo))
+ return (ResponseCode.WritingError, null);
+
+ //[10] Cmd : For writes the PLC always answers with 0x41 and a response code.
+ //For reads, 0x41 means the request failed and a response code is returned instead of data.
+ var isWrite = command == (byte)WriteCommands.WordBlockWrite || command == (byte)WriteCommands.BitBlockWrite;
+ if (response[10] == EthernetFrame.AckCommand)
{
- var connectionStatus = await Connect();
- if (connectionStatus != ConnectionStatus.Connected)
- return ResponseCode.SystemError;
+ if (response.Length < EthernetFrame.HeaderLength + 2 + EthernetFrame.CheckSumLength)
+ return (ResponseCode.WritingError, null);
+
+ var code = (ResponseCode)Tools.ToInt(response[14], response[15]);
+ if (!isWrite && code == ResponseCode.Success)
+ return (ResponseCode.WritingError, null);
+
+ return (code, response);
}
- var frame = new List();
+ if (isWrite || response[10] != command)
+ return (ResponseCode.WritingError, null);
- //[0-8] ID : This is a 9 - byte string “ KDT_PLC_M ”.
- frame.AddRange(Encoding.ASCII.GetBytes("KDT_PLC_M"));
+ return (ResponseCode.Success, response);
+ }
- //[9] Frame No : This, 1 - byte data with the range from 0 to 127
- frame.Add(FrameNo);
+ internal static byte[] BuildFrame(byte frameNo, byte command, MemoryType memoryType, string address, int count, IEnumerable payload = null)
+ {
+ //[14-n] Data : This is n block and contains up to 3 parts :
+ // 1- Memory Address : 8 bytes, device type + 0 + 6 chars address
+ // 2- Read/Write Size : 2 bytes
+ // 3- Data to write : n bytes (write commands only)
+ var data = new List { (byte)memoryType, 0 };
+ data.AddRange(Encoding.ASCII.GetBytes(NormalizeAddress(address)));
+ data.AddRange(count.ToDualByte());
+ if (payload != null)
+ data.AddRange(payload);
- //[10] Cmd : In Master, 1 - byte command can be used and the
- //format of ‘Data’ field is selected according to each command
- frame.Add((byte)WriteCommands.BitBlockWrite);
+ var frame = new List(EthernetFrame.MinimumLength + data.Count);
- //[11] Res: Reserved. (1 Byte, 00h) – Reserved device
- frame.Add(0);
+ //[0-8] ID : This is a 9 - byte string "KDT_PLC_M".
+ frame.AddRange(Encoding.ASCII.GetBytes(EthernetFrame.MasterId));
- //[12-13] Length : This is the 2 - byte value indicating the size of ‘Data’ field. (Hexadecimal Figure)
- int dataLength = data.Length + 10;
- frame.AddRange(dataLength.ToDualByte());
+ //[9] Frame No : This, 1 - byte data with the range from 0 to 127
+ frame.Add(frameNo);
+
+ //[10] Cmd : In Master, 1 - byte command can be used and the
+ //format of 'Data' field is selected according to each command
+ frame.Add(command);
- //[14-n] Data : This is n block based on data needs to write and contains 3 parts :
- // 1- [14-21] Memory Address : 8 bytes
- frame.Add((byte)memoryType);
+ //[11] Res: Reserved. (1 Byte, 00h)
frame.Add(0);
- frame.AddRange(Encoding.ASCII.GetBytes(address));
- // 2- [22-23] Write Size : 2 bytes
- frame.AddRange(data.Length.ToDualByte());
+ //[12-13] Length : This is the 2 - byte value indicating the size of 'Data' field.
+ frame.AddRange(data.Count.ToDualByte());
- // 3- [24-n] Word Data : n bytes
frame.AddRange(data);
//Check Sum : This is 2-byte value. After the entire frame is binary-summed by the byte,
//the lower 2 - byte in the result value is used.
- frame.AddRange(frame.Sum(x => x).ToDualByte());
-
- try
- {
- var sendState = await _socket.SendData(frame.ToArray());
- if (!sendState)
- return ResponseCode.WritingError;
-
- //Read response from PLC
- await Task.Delay(_timeout);
- var receivedFrame = await _socket.ReceiveData();
- if (receivedFrame == null)
- return ResponseCode.SystemError;
-
- if (_autoConnect)
- _socket.Disconnect();
+ frame.AddRange(Tools.CheckSum(frame));
- if (!Tools.IsValidResponse(receivedFrame, frame[9], 0x41))
- return ResponseCode.WritingError;
-
- return (ResponseCode)Tools.ToInt(receivedFrame[14], receivedFrame[15]);
+ return frame.ToArray();
+ }
- }
- catch (Exception)
- {
- throw;
- }
+ ///
+ /// Extracts read data from a response frame. The data field starts with the 9 bytes
+ /// memory address block, followed by the read values.
+ ///
+ private static byte[] ReadData(byte[] response)
+ {
+ const int addressLength = 9;
+ var dataLength = Tools.ToInt(response[12], response[13]) - addressLength;
+ if (dataLength <= 0)
+ return new byte[0];
+
+ var data = new byte[dataLength];
+ Array.Copy(response, EthernetFrame.HeaderLength + addressLength, data, 0, dataLength);
+ return data;
}
}
}
diff --git a/src/CimonPlc/PlcConnectors/SerialConnector.cs b/src/CimonPlc/PlcConnectors/SerialConnector.cs
index db9a88b..004866e 100644
--- a/src/CimonPlc/PlcConnectors/SerialConnector.cs
+++ b/src/CimonPlc/PlcConnectors/SerialConnector.cs
@@ -1,10 +1,8 @@
-using Ardalis.GuardClauses;
using CimonPlc.Interfaces;
using CimonPlc.Enums;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
-using System.Text;
using System.Linq;
using CimonPlc.Models;
@@ -12,115 +10,42 @@ namespace CimonPlc.PlcConnectors
{
public class SerialConnector : PlcConnector
{
- public SerialConnector(ISerialSocket socket, bool autoConnect = true) : base(socket)
+ public SerialConnector(ISerialSocket socket, bool autoConnect = true) : base(socket, autoConnect)
{
- _socket = socket;
- _autoConnect = autoConnect;
}
///
/// This function is to assign PLC device memory directly to read according to
/// memory data type.The total sum of the word data should be not over 63 words.
///
- /// PLC memory which required to read. Valid symbols are X, Y, M, L, K, F, Z, TC, TS, CC, CS, D, S
+ /// PLC memory which required to read. Valid symbols are X, Y, M, L, K, F, T, C, D, S
/// The word address or the card number of a corresponding device is used. That is, in case of bit device such as X/Y, the last number should be '0' and should contains 6 character, such as '000010'
/// Requested length for reading memory. Length must be in the range from 1 to 63
- /// Returns a tuple includes PLC response code and an array of byte contains read data
+ /// Returns a tuple includes PLC response code and an array of words contains read data
public override async Task<(ResponseCode responseCode, int[] data)> ReadWordAsync(MemoryType memoryType, string address, int length)
{
- Guard.Against.Null(memoryType, nameof(memoryType));
+ Guard.Against.UndefinedEnum(memoryType, nameof(memoryType));
Guard.Against.OutOfRange(length, nameof(length), 1, 63);
- Guard.Against.BadFormat(address, nameof(address), @"[0-9a-fA-F]{0,5}0");
-
-
- while (address.Length < 6)
- address = "0" + address;
+ Guard.Against.BadFormat(address, nameof(address), "[0-9a-fA-F]{0,5}0");
- if (!IsConnected && !_autoConnect)
- return (ResponseCode.SystemError, null);
+ var (error, response) = await SendCommandAsync((byte)ReadCommand.WordBlockRead, memoryType, address, length).ConfigureAwait(false);
+ if (error != ResponseCode.Success)
+ return (error, null);
- if (!IsConnected)
+ // Each word is sent as 4 hex chars
+ var data = ReadData(response);
+ var words = new int[data.Length / 4];
+ for (var i = 0; i < words.Length; i++)
{
- var connectionStatus = await Connect();
- if (connectionStatus != ConnectionStatus.Connected)
- return (ResponseCode.SystemError, null);
+ var x = i * 4;
+ words[i] = Tools.ToInt(Tools.ToByte(data[x], data[x + 1]), Tools.ToByte(data[x + 2], data[x + 3]));
}
- var frame = new List();
-
- //[0] HEADER : This is 1-byte control letter of ASCII code “ENQ”.
- frame.Add((char)0x5);
-
- //[1-2] PLC Station Number : This, 2 - byte data with the range from 0 to 127, if station numbers are assigned, multi-drop communication is available
- frame.Add('0');
- frame.Add('0');
-
- //[3] Cmd : In Master, 1 - byte command can be used and the
- //format of ‘Data’ field is selected according to each command
- frame.Add((char)ReadCommand.WordBlockRead);
-
- //[4-5] Length : Total number of the chars of a frame data device
- // It contains 8 chars for address + 2 chars for length
- frame.Add('0');
- frame.Add('A');
-
- //[6-15] Data : This is n block based on data needs to write and contains 2 parts :
- // 1- [6-13] Memory Address : 8 chars
- frame.Add(memoryType.ToString()[0]);
- frame.Add('0');
- frame.AddRange(address.ToCharArray());
-
- // 2- [14-15] Read Length : 2 chars
- frame.AddRange(((byte)length).ToDualChar());
-
- //[16-17] BCC : is the remainder value when dividing the binary-sum from Cmd to the end of data by 256.
- frame.AddBCC();
-
- //[18] End : This is 1-byte control letter of ASCII code “EOT”.
- frame.Add((char)0x4);
-
- try
- {
- var sendState = await _socket.SendData(frame.Select(Convert.ToByte).ToArray());
- if (!sendState)
- return (ResponseCode.WritingError, null);
-
- //Read response from PLC
- await Task.Delay(_timeout);
- var tempFrame = await _socket.ReceiveData();
- if (tempFrame == null || tempFrame == Array.Empty())
- return (ResponseCode.SystemError, null);
- var receivedFrame = tempFrame.Select(x => (char)x).ToArray();
-
- if (_autoConnect)
- _socket.Disconnect();
-
- //If response's cmd equals to E(0X45), then must return error code
- if (receivedFrame[3] == 'E')
- return ((ResponseCode)Tools.ToByte(receivedFrame[6], receivedFrame[7]), null);
-
- if (!Tools.IsValidSerialResponse(receivedFrame, (byte)ReadCommand.WordBlockRead))
- return (ResponseCode.WritingError, null);
-
- var tempArray = new List();
- const int dataStartIndex = 6;
- int dataLength = Tools.ToByte(receivedFrame[4], receivedFrame[5]);
- for (var x = dataStartIndex; x < dataLength + dataStartIndex; x += 4)
- {
- var byte1 = Tools.ToByte(receivedFrame[x], receivedFrame[x + 1]);
- var byte2 = Tools.ToByte(receivedFrame[x+2], receivedFrame[x + 3]);
- tempArray.Add(Tools.ToInt(byte1, byte2));
- }
- return (ResponseCode.Success, tempArray.ToArray());
- }
- catch (Exception)
- {
- throw;
- }
+ return (ResponseCode.Success, words);
}
///
- /// This function is to assign PLC device memory directly to read bit block.
+ /// This function is to assign PLC device memory directly to read bit block.
/// The total sum of the data is not to be over 126 bits.
///
/// PLC memory which required to read such as X or D
@@ -129,191 +54,43 @@ public SerialConnector(ISerialSocket socket, bool autoConnect = true) : base(soc
/// Returns a tuple includes PLC response code and an array of byte contains read data
public override async Task<(ResponseCode responseCode, byte[] data)> ReadBitAsync(MemoryType memoryType, string address, int length)
{
- Guard.Against.Null(memoryType, nameof(memoryType));
+ Guard.Against.UndefinedEnum(memoryType, nameof(memoryType));
Guard.Against.OutOfRange(length, nameof(length), 1, 126);
- Guard.Against.BadFormat(address, nameof(address), @"[0-9a-fA-F]{1,6}");
-
-
- while (address.Length < 6)
- address = "0" + address;
-
- if (!IsConnected && !_autoConnect)
- return (ResponseCode.SystemError, null);
-
- if (!IsConnected)
- {
- var connectionStatus = await Connect();
- if (connectionStatus != ConnectionStatus.Connected)
- return (ResponseCode.SystemError, null);
- }
-
- var frame = new List();
-
- //[0] HEADER : This is 1-byte control letter of ASCII code “ENQ”.
- frame.Add((char)0x5);
-
- //[1-2] PLC Station Number : This, 2 - byte data with the range from 0 to 127, if station numbers are assigned, multi-drop communication is available
- frame.Add('0');
- frame.Add('0');
-
- //[3] Cmd : In Master, 1 - byte command can be used and the
- //format of ‘Data’ field is selected according to each command
- frame.Add((char)ReadCommand.BitBlockRead);
-
- //[4-5] Length : Total number of the chars of a frame data device
- // It contains 8 chars for address + 2 chars for length
- frame.Add('0');
- frame.Add('A');
-
- //[6-15] Data : This is n block based on data needs to write and contains 2 parts :
- // 1- [6-13] Memory Address : 8 chars
- frame.Add(memoryType.ToString()[0]);
- frame.Add('0');
- frame.AddRange(address.ToCharArray());
-
- // 2- [14-15] Read Length : 2 chars
- frame.AddRange(((byte)length).ToDualChar());
-
- //[16-17] BCC : is the remainder value when dividing the binary-sum from Cmd to the end of data by 256.
- frame.AddBCC();
+ Guard.Against.BadFormat(address, nameof(address), "[0-9a-fA-F]{1,6}");
- //[18] End : This is 1-byte control letter of ASCII code “EOT”.
- frame.Add((char)0x4);
+ var (error, response) = await SendCommandAsync((byte)ReadCommand.BitBlockRead, memoryType, address, length).ConfigureAwait(false);
+ if (error != ResponseCode.Success)
+ return (error, null);
- try
- {
- var sendState = await _socket.SendData(frame.Select(Convert.ToByte).ToArray());
- if (!sendState)
- return (ResponseCode.WritingError, null);
-
- //Read response from PLC
- await Task.Delay(_timeout);
- var tempFrame = await _socket.ReceiveData();
- if (tempFrame == null || tempFrame == Array.Empty())
- return (ResponseCode.SystemError, null);
- var receivedFrame = tempFrame.Select(x => (char)x).ToArray();
-
- if (_autoConnect)
- _socket.Disconnect();
-
- //If response's cmd equals to E(0X45), then must return error code
- if (receivedFrame[3] == 'E')
- return ((ResponseCode)Tools.ToByte(receivedFrame[6], receivedFrame[7]), null);
-
- if (!Tools.IsValidSerialResponse(receivedFrame, (byte)ReadCommand.BitBlockRead))
- return (ResponseCode.WritingError, null);
-
- var tempArray = new List();
- const int dataStartIndex = 6;
- int dataLength = Tools.ToByte(receivedFrame[4], receivedFrame[5]);
- for (var x = dataStartIndex; x < dataLength + dataStartIndex; x += 2)
- {
- tempArray.Add(Tools.ToByte(receivedFrame[x], receivedFrame[x + 1]));
- }
- return (ResponseCode.Success, tempArray.Select(x => (byte)x).ToArray());
+ // Each bit is sent as 2 hex chars
+ var data = ReadData(response);
+ var bits = new byte[data.Length / 2];
+ for (var i = 0; i < bits.Length; i++)
+ bits[i] = Tools.ToByte(data[i * 2], data[i * 2 + 1]);
- }
- catch (Exception)
- {
- throw;
- }
+ return (ResponseCode.Success, bits);
}
///
/// This function is to assign PLC device memory directly to write according to
- /// memory data type. The total sum of word data is not to be over 63 words.
+ /// memory data type. The total sum of word data is not to be over 61 words.
///
/// PLC memory which required to write such as Y or D
/// The word address or the card number of a corresponding device is used. That is, in case of bit device such as X/Y, the last number should be '0' and should contains 6 characters, such as '000010'
- /// Data needed to write on PLC memory, The total sum of word data is not to be over 64 words
+ /// Data needed to write on PLC memory. The frame length field is limited to 255 chars, so up to 61 words can be written at once
/// Returns a code which shows PLC response code
public override async Task WriteWordAsync(MemoryType memoryType, string address, params int[] data)
{
Guard.Against.NullOrEmpty(data, nameof(data));
Guard.Against.OutOfRange(data, nameof(data), 0, 0xFFFF);
- Guard.Against.OutOfRange(data.Length, nameof(data), 1, 63);
- Guard.Against.Null(memoryType, nameof(memoryType));
+ Guard.Against.OutOfRange(data.Length, nameof(data), 1, 61);
+ Guard.Against.UndefinedEnum(memoryType, nameof(memoryType));
Guard.Against.BadFormat(address, nameof(address), "[0-9a-fA-F]{0,5}0");
- while (address.Length < 6)
- address = "0" + address;
-
- if (!IsConnected && !_autoConnect)
- return ResponseCode.SystemError;
-
- if (!IsConnected)
- {
- var connectionStatus = await Connect();
- if (connectionStatus != ConnectionStatus.Connected)
- return ResponseCode.SystemError;
- }
-
- var frame = new List();
-
- //[0] HEADER : This is 1-byte control letter of ASCII code “ENQ”.
- frame.Add((char)0x5);
-
- //[1-2] PLC Station Number : This, 2 - byte data with the range from 0 to 127, if station numbers are assigned, multi-drop communication is available
- frame.Add('0');
- frame.Add('0');
-
- //[3] Cmd : In Master, 1 - byte command can be used and the
- //format of ‘Data’ field is selected according to each command
- frame.Add((char)WriteCommands.WordBlockWrite);
-
- //[4-5] Length : Total number of the chars of a frame data device
- // It contains 8 chars for address + 2 chars word count + 4 chars for each word
- var length = 10 + data.Length * 4 ;
- frame.AddRange(((byte)length).ToDualChar());
-
- //[6-15] Data : This is n block based on data needs to write and contains 3 parts :
- // 1- [6-13] Memory Address : 8 chars
- frame.Add(memoryType.ToString()[0]);
- frame.Add('0');
- frame.AddRange(address.ToCharArray());
-
- // 2- [14-15] Read Length : 2 chars
- frame.AddRange(((byte)data.Length).ToDualChar());
-
- // 3- [15-n] Words to write : 4 chars per word
- foreach (var word in data)
- frame.AddRange(word.ToQuadChar());
-
- //[^3-^2] BCC : is the remainder value when dividing the binary-sum from Cmd to the end of data by 256.
- frame.AddBCC();
-
- //[^1] End : This is 1-byte control letter of ASCII code “EOT”.
- frame.Add((char)0x4);
-
- try
- {
- var sendState = await _socket.SendData(frame.Select(Convert.ToByte).ToArray());
- if (!sendState)
- return ResponseCode.WritingError;
-
- //Read response from PLC
- await Task.Delay(_timeout);
- var tempFrame = await _socket.ReceiveData();
- if (tempFrame == null || tempFrame == Array.Empty())
- return ResponseCode.SystemError;
- var receivedFrame = tempFrame.Select(x => (char)x).ToArray();
-
- if (_autoConnect)
- _socket.Disconnect();
-
- //If response's cmd equals to E(0X45), then must return error code
- if (receivedFrame[3] == 'E')
- return (ResponseCode)Tools.ToByte(receivedFrame[6], receivedFrame[7]);
-
- if (!Tools.IsValidSerialResponse(receivedFrame, (byte)WriteCommands.WordBlockWrite))
- return ResponseCode.WritingError;
-
- return (ResponseCode)Tools.ToByte(receivedFrame[4], receivedFrame[5]);
- }
- catch (Exception)
- {
- throw;
- }
+ // 4 chars per word
+ var payload = data.SelectMany(x => x.ToQuadChar());
+ var (error, _) = await SendCommandAsync((byte)WriteCommands.WordBlockWrite, memoryType, address, data.Length, payload).ConfigureAwait(false);
+ return error;
}
///
@@ -322,95 +99,111 @@ public override async Task WriteWordAsync(MemoryType memoryType, s
///
/// PLC memory which required to write such as Y or D
/// The word address or the card number of a corresponding device is used, it should contains 6 characters, such as '0000D1'
- /// Data needed to write on PLC memory, The total sum of word data is not to be over 256 bits
- /// Returns a code which shows PLC response co`de
+ /// Data needed to write on PLC memory, each item must be 0 or 1. The total sum of data is not to be over 126 bits
+ /// Returns a code which shows PLC response code
public override async Task WriteBitAsync(MemoryType memoryType, string address, params byte[] data)
{
Guard.Against.NullOrEmpty(data, nameof(data));
Guard.Against.OutOfRange(data, nameof(data), 0, 1);
Guard.Against.OutOfRange(data.Length, nameof(data), 1, 126);
- Guard.Against.Null(memoryType, nameof(memoryType));
+ Guard.Against.UndefinedEnum(memoryType, nameof(memoryType));
Guard.Against.BadFormat(address, nameof(address), "[0-9a-fA-F]{1,6}");
- while (address.Length < 6)
- address = "0" + address;
+ // 1 char per bit
+ var payload = data.Select(x => (char)x);
+ var (error, _) = await SendCommandAsync((byte)WriteCommands.BitBlockWrite, memoryType, address, data.Length, payload).ConfigureAwait(false);
+ return error;
+ }
- if (!IsConnected && !_autoConnect)
- return ResponseCode.SystemError;
+ ///
+ /// Builds a master frame, sends it and validates the PLC response.
+ ///
+ /// Success and the response frame, or the error code returned by PLC or detected locally
+ private async Task<(ResponseCode error, char[] response)> SendCommandAsync(byte command, MemoryType memoryType, string address, int count, IEnumerable payload = null)
+ {
+ var frame = BuildFrame(command, memoryType, address, count, payload);
- if (!IsConnected)
- {
- var connectionStatus = await Connect();
- if (connectionStatus != ConnectionStatus.Connected)
- return ResponseCode.SystemError;
- }
+ var (error, rawResponse) = await ExchangeAsync(frame).ConfigureAwait(false);
+ if (error != ResponseCode.Success)
+ return (error, null);
+
+ var response = rawResponse.Select(x => (char)x).ToArray();
- var frame = new List();
+ //If response's cmd equals to E(0x45), then the error code is in [6-7]
+ if (response.Length >= 8 && response[3] == SerialFrame.ErrorCommand)
+ return (ParseCode(response[6], response[7]), null);
- //[0] HEADER : This is 1-byte control letter of ASCII code “ENQ”.
- frame.Add((char)0x5);
+ if (!Tools.IsValidSerialResponse(response, command))
+ return (ResponseCode.WritingError, null);
- //[1-2] PLC Station Number : This, 2 - byte data with the range from 0 to 127, if station numbers are assigned, multi-drop communication is available
+ var isWrite = command == (byte)WriteCommands.WordBlockWrite || command == (byte)WriteCommands.BitBlockWrite;
+ if (isWrite)
+ return (ParseCode(response[4], response[5]), response);
+
+ //[4-5] Length : number of data chars, which must all be present before ETX
+ if (!Tools.IsHex(response[4]) || !Tools.IsHex(response[5]))
+ return (ResponseCode.WritingError, null);
+
+ var dataLength = Tools.ToByte(response[4], response[5]);
+ if (response.Length < 6 + dataLength + 1 || response.Skip(6).Take(dataLength).Any(x => !Tools.IsHex(x)))
+ return (ResponseCode.WritingError, null);
+
+ return (ResponseCode.Success, response);
+ }
+
+ internal static byte[] BuildFrame(byte command, MemoryType memoryType, string address, int count, IEnumerable payload = null)
+ {
+ //[6-n] Data : This is n block and contains up to 3 parts :
+ // 1- Memory Address : 8 chars, device symbol + 0 + 6 chars address
+ // 2- Read/Write Size : 2 chars
+ // 3- Data to write : n chars (write commands only)
+ var data = new List { memoryType.ToString()[0], '0' };
+ data.AddRange(NormalizeAddress(address));
+ data.AddRange(((byte)count).ToDualChar());
+ if (payload != null)
+ data.AddRange(payload);
+
+ var frame = new List(data.Count + 9);
+
+ //[0] HEADER : This is 1-byte control letter of ASCII code "ENQ".
+ frame.Add(SerialFrame.Enq);
+
+ //[1-2] PLC Station Number : multi-drop communication is available if station numbers are assigned
frame.Add('0');
frame.Add('0');
//[3] Cmd : In Master, 1 - byte command can be used and the
- //format of ‘Data’ field is selected according to each command
- frame.Add((char)WriteCommands.BitBlockWrite);
+ //format of 'Data' field is selected according to each command
+ frame.Add((char)command);
- //[4-5] Length : Total number of the chars of a frame data device
- // It contains 8 chars for address + 2 chars word count + 1 char per bit
- var length = 10 + data.Length ;
- frame.AddRange(((byte)length).ToDualChar());
+ //[4-5] Length : Total number of the chars of the data field
+ frame.AddRange(((byte)data.Count).ToDualChar());
- //[6-15] Data : This is n block based on data needs to write and contains 3 parts :
- // 1- [6-13] Memory Address : 8 chars
- frame.Add(memoryType.ToString()[0]);
- frame.Add('0');
- frame.AddRange(address.ToCharArray());
+ frame.AddRange(data);
- // 2- [14-15] Read Length : 2 chars
- frame.AddRange(((byte)data.Length).ToDualChar());
+ //BCC : is the remainder value when dividing the binary-sum from Cmd to the end of data by 256.
+ frame.AddBCC();
- // 3- [15-n] Words to write : 1 char per bit
- foreach (var bit in data)
- frame.Add((char)bit);
+ //End : This is 1-byte control letter of ASCII code "EOT".
+ frame.Add(SerialFrame.Eot);
- // [^3-^2]BCC : 2 chars is the remainder value when dividing the binary-sum from Cmd to the end of data by 256.
- frame.AddBCC();
+ return frame.Select(Convert.ToByte).ToArray();
+ }
+
+ private static char[] ReadData(char[] response)
+ {
+ var dataLength = Tools.ToByte(response[4], response[5]);
+ var data = new char[dataLength];
+ Array.Copy(response, 6, data, 0, dataLength);
+ return data;
+ }
- //[^1] End : This is 1-byte control letter of ASCII code “EOT”.
- frame.Add((char)0x4);
+ private static ResponseCode ParseCode(char char1, char char2)
+ {
+ if (!Tools.IsHex(char1) || !Tools.IsHex(char2))
+ return ResponseCode.WritingError;
- try
- {
- var sendState = await _socket.SendData(frame.Select(Convert.ToByte).ToArray());
- if (!sendState)
- return ResponseCode.WritingError;
-
- //Read response from PLC
- await Task.Delay(_timeout);
- var tempFrame = await _socket.ReceiveData();
- if (tempFrame == null || tempFrame == Array.Empty())
- return ResponseCode.SystemError;
- var receivedFrame = tempFrame.Select(x => (char)x).ToArray();
-
- if (_autoConnect)
- _socket.Disconnect();
-
- //If response's cmd equals to E(0X45), then must return error code
- if (receivedFrame[3] == 'E')
- return (ResponseCode)Tools.ToByte(receivedFrame[6], receivedFrame[7]);
-
- if (!Tools.IsValidSerialResponse(receivedFrame, (byte)WriteCommands.BitBlockWrite))
- return ResponseCode.WritingError;
-
- return (ResponseCode)Tools.ToByte(receivedFrame[4], receivedFrame[5]);
- }
- catch (Exception)
- {
- throw;
- }
+ return (ResponseCode)Tools.ToByte(char1, char2);
}
}
}
diff --git a/src/CimonPlc/Public/Guards.cs b/src/CimonPlc/Public/Guards.cs
deleted file mode 100644
index f56834d..0000000
--- a/src/CimonPlc/Public/Guards.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.RegularExpressions;
-using JetBrainsNotNullAttribute = JetBrains.Annotations.NotNullAttribute;
-
-namespace Ardalis.GuardClauses
-{
- public static class GuardClauseExtensions
- {
- public static string BadFormat([JetBrainsNotNull] this IGuardClause guardClause, [JetBrainsNotNull] string input, [JetBrainsNotNull] string parameterName, [JetBrainsNotNull] string regexPattern)
- {
- if (input != Regex.Match(input, regexPattern).Value)
- throw new ArgumentException($"Input {parameterName} was not in required format", parameterName);
-
- return input;
- }
-
- public static IEnumerable OutOfRange([JetBrainsNotNull] this IGuardClause guardClause, [JetBrainsNotNull] IEnumerable input, [JetBrainsNotNull] string parameterName, T rangeFrom, T rangeTo) where T : IComparable
- {
- Comparer comparer = Comparer.Default;
-
- if (comparer.Compare(rangeFrom, rangeTo) >= 0)
- {
- throw new ArgumentException($"{nameof(rangeFrom)} should be less or equal than {nameof(rangeTo)}");
- }
-
- if (input.Any(x => comparer.Compare(x, rangeFrom) < 0 || comparer.Compare(x, rangeTo) > 0))
- {
- throw new ArgumentOutOfRangeException(parameterName, $"Input {parameterName} was out of range, it must be between {rangeFrom} and {rangeTo}.");
- }
-
- return input;
-
- }
-
- public static T InvalidData([JetBrainsNotNull] this IGuardClause guardClause, [JetBrainsNotNull] T input, [JetBrainsNotNull] string parameterName, Func predicate)
- {
- if (!predicate(input))
- throw new ArgumentException($"Input {parameterName} did not satisfy the options", parameterName);
-
- return input;
- }
- }
-}
diff --git a/src/CimonPlc/Public/Tools.cs b/src/CimonPlc/Public/Tools.cs
deleted file mode 100644
index 0974dfc..0000000
--- a/src/CimonPlc/Public/Tools.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Text;
-
-namespace CimonPlc
-{
- public static class Tools
- {
- public static IPStatus Ping(IPAddress ip,int pingTimeout=300)
- {
- try
- {
- var pingSender = new Ping();
- var reply = pingSender.Send(ip, pingTimeout);
- return reply.Status;
- }
- catch
- {
- return IPStatus.BadDestination;
- }
- }
-
- public static int ToInt(byte byte1, byte byte2)
- {
- return (byte1 << 8) + byte2;
- }
-
- public static byte ToByte(char char1, char char2)
- {
- return byte.Parse(string.Concat(char1, char2), System.Globalization.NumberStyles.HexNumber);
- }
-
- public static byte[] ToDualByte(this int number)
- {
- number &= 0xFFFF;
- return new byte[] { (byte)(number >> 8), (byte)(number & 0xFF) };
- }
-
- public static char[] ToDualChar(this byte number)
- {
- number &= 0xFF;
- return number.ToString("X2").ToArray();
- }
-
- public static char[] ToQuadChar(this int number)
- {
- number &= 0xFFFF;
- return number.ToString("X4").ToArray();
- }
-
- public static void AddBCC(this List input)
- {
- if (input == null || input.Count == 0)
- return;
-
- // Exclude 3 start chars during BCC calculation
- var sum = input.Skip(3).Sum(x => Convert.ToByte(x));
- sum %= 256;
-
- input.AddRange(((byte)sum).ToDualChar());
- }
-
- public static bool IsValidResponse(byte[] buffer, byte frameNo, int ackCommand)
- {
- //[0-8] ID : This is a 9 - byte string “ KDT_PLC_S ”.
- if (Encoding.UTF8.GetString(buffer, 0, 9) != "KDT_PLC_S")
- return false;
-
- //[9] Frame No : This is 1 - byte data.The value that 128 are added to the number of the
- // command frame received from the Master is used
- if (buffer[9] - 128 != frameNo)
- return false;
-
- //[10] Cmd :In Slave, 1 - byte command as specified in parameter can be used and the
- // format of ‘Data’ field is selected according to command
- if (buffer[10] != ackCommand)
- return false;
-
- //Check Sum : This is 2-byte value. After the entire frame is binary-summed by the byte,
- //the lower 2 - byte in the result value is used.
- var tempArray = new byte[buffer.Length - 2];
- Array.Copy(buffer, 0, tempArray, 0, tempArray.Length);
- if (!tempArray.Sum(x => x).ToDualByte().SequenceEqual(new byte[] { buffer[^2], buffer[^1] }))
- return false;
-
- return true;
-
- }
-
- internal static bool IsValidSerialResponse(char[] receivedFrame, byte ackCommand)
- {
- if (receivedFrame[0]!=(char)2 || receivedFrame[^1] != (char)3)
- return false;
-
- //[3] Cmd :In Slave, 1 - byte command must be equal to sent command
- if (receivedFrame[3] != (char)ackCommand)
- return false;
-
- return true;
- }
- }
-}
diff --git a/src/CimonPlc/Sockets/SerialSocket.cs b/src/CimonPlc/Sockets/SerialSocket.cs
index 542a4ee..006d0e3 100644
--- a/src/CimonPlc/Sockets/SerialSocket.cs
+++ b/src/CimonPlc/Sockets/SerialSocket.cs
@@ -1,52 +1,95 @@
-using Ardalis.GuardClauses;
using CimonPlc.Enums;
using CimonPlc.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.IO.Ports;
using System.Threading.Tasks;
namespace CimonPlc.Sockets
{
- public class SerialSocket : ISerialSocket
+ public class SerialSocket : ISerialSocket, IDisposable
{
- private readonly SerialPort _socket;
+ private const int PollInterval = 5;
- public bool IsConnected => _socket.IsOpen;
+ private readonly SerialPort _port;
+
+ public bool IsConnected => _port.IsOpen;
public SerialSocket(string portName, int baudRate = 9600)
{
Guard.Against.NullOrEmpty(portName, nameof(portName));
Guard.Against.OutOfRange(baudRate, nameof(baudRate), 75, 256000);
- _socket = new SerialPort(portName, baudRate);
+ _port = new SerialPort(portName, baudRate);
}
public Task Connect(int readTimeout = 1000, int writeTimeout = 1000, int pingTimeout = 3000)
{
- _socket.ReadTimeout = readTimeout;
- _socket.WriteTimeout = writeTimeout;
+ _port.ReadTimeout = readTimeout;
+ _port.WriteTimeout = writeTimeout;
+
+ if (!_port.IsOpen)
+ _port.Open();
- _socket.Open();
- return Task.FromResult(_socket.IsOpen ? ConnectionStatus.Connected : ConnectionStatus.DisConnected);
+ return Task.FromResult(_port.IsOpen ? ConnectionStatus.Connected : ConnectionStatus.DisConnected);
}
public ConnectionStatus Disconnect()
{
- _socket.Close();
+ if (_port.IsOpen)
+ _port.Close();
+
return ConnectionStatus.DisConnected;
}
public Task SendData(byte[] frame)
{
- _socket.Write(frame,0,frame.Length);
+ if (!_port.IsOpen)
+ return Task.FromResult(false);
+
+ _port.DiscardInBuffer();
+ _port.Write(frame, 0, frame.Length);
return Task.FromResult(true);
}
- public Task ReceiveData()
+ ///
+ /// Waits for a complete response frame (terminated by ETX) from PLC, up to the read timeout.
+ ///
+ /// The received frame, or null if nothing was received
+ public async Task ReceiveData()
+ {
+ if (!_port.IsOpen)
+ return null;
+
+ var frame = new List();
+ var stopwatch = Stopwatch.StartNew();
+
+ while (stopwatch.ElapsedMilliseconds < _port.ReadTimeout)
+ {
+ var available = _port.BytesToRead;
+ if (available > 0)
+ {
+ var buffer = new byte[available];
+ var received = _port.Read(buffer, 0, available);
+ for (var i = 0; i < received; i++)
+ frame.Add(buffer[i]);
+
+ if (frame[frame.Count - 1] == SerialFrame.Etx)
+ break;
+
+ continue;
+ }
+
+ await Task.Delay(PollInterval).ConfigureAwait(false);
+ }
+
+ return frame.Count == 0 ? null : frame.ToArray();
+ }
+
+ public void Dispose()
{
- var frameLength = _socket.BytesToRead;
- var frame = new byte[frameLength];
- var result = _socket.Read(frame,0, frameLength);
- return result == frameLength ? Task.FromResult(frame) : null;
+ _port.Dispose();
}
}
}
diff --git a/src/CimonPlc/Sockets/TcpSocket.cs b/src/CimonPlc/Sockets/TcpSocket.cs
index 91a8516..4ba6191 100644
--- a/src/CimonPlc/Sockets/TcpSocket.cs
+++ b/src/CimonPlc/Sockets/TcpSocket.cs
@@ -1,6 +1,8 @@
-using Ardalis.GuardClauses;
using CimonPlc.Enums;
using CimonPlc.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
@@ -8,58 +10,145 @@
namespace CimonPlc.Sockets
{
- public class TcpSocket : IEthernetSocket
+ public class TcpSocket : IEthernetSocket, IDisposable
{
- private readonly Socket _socket;
+ private const int PollInterval = 5;
+
private readonly IPAddress _ip;
private readonly int _port;
+ private readonly bool _usePing;
+ private Socket _socket;
+ private int _readTimeout = 1000;
- public bool IsConnected => _socket.Connected;
+ public bool IsConnected => _socket?.Connected == true;
- public TcpSocket(string ip, int port = 10620)
+ /// IP address of the PLC
+ /// TCP port of the PLC, default is 10620
+ ///
+ /// If true, the PLC is pinged before connecting and
+ /// is returned when it does not answer. Disable it on networks that block ICMP.
+ ///
+ public TcpSocket(string ip, int port = 10620, bool usePing = true)
{
Guard.Against.OutOfRange(port, nameof(port), 1, 65535);
Guard.Against.NullOrEmpty(ip, nameof(ip));
- Guard.Against.InvalidData(ip, nameof(ip), x => IPAddress.TryParse(x, out IPAddress tempIp));
+ Guard.Against.InvalidData(ip, nameof(ip), x => IPAddress.TryParse(x, out _));
- _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_ip = IPAddress.Parse(ip);
_port = port;
+ _usePing = usePing;
}
public async Task Connect(int readTimeout = 1000, int writeTimeout = 1000, int pingTimeout = 3000)
{
- if (Tools.Ping(_ip, pingTimeout) != IPStatus.Success)
- {
+ if (IsConnected)
+ return ConnectionStatus.Connected;
+
+ if (_usePing && Tools.Ping(_ip, pingTimeout) != IPStatus.Success)
return ConnectionStatus.NoRouteToDestination;
- }
- var remoteEp = new IPEndPoint(_ip, _port);
- _socket.ReceiveTimeout = readTimeout;
- _socket.SendTimeout = writeTimeout;
+ // A closed socket can't be reused, so a new one is created for every connection.
+ CloseSocket();
+ _readTimeout = readTimeout;
+ _socket = new Socket(_ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
+ {
+ ReceiveTimeout = readTimeout,
+ SendTimeout = writeTimeout,
+ NoDelay = true
+ };
+
+ try
+ {
+ await _socket.ConnectAsync(new IPEndPoint(_ip, _port)).ConfigureAwait(false);
+ }
+ catch
+ {
+ CloseSocket();
+ throw;
+ }
- await _socket.ConnectAsync(remoteEp);
return _socket.Connected ? ConnectionStatus.Connected : ConnectionStatus.DisConnected;
}
public ConnectionStatus Disconnect()
{
- _socket.Close();
+ CloseSocket();
return ConnectionStatus.DisConnected;
}
public async Task SendData(byte[] frame)
{
- var result= await _socket.SendAsync(frame, SocketFlags.None);
- return (result == frame.Length);
+ if (!IsConnected)
+ return false;
+
+ var result = await _socket.SendAsync(new ArraySegment(frame), SocketFlags.None).ConfigureAwait(false);
+ return result == frame.Length;
}
+ ///
+ /// Waits for a complete response frame from PLC, up to the read timeout.
+ ///
+ /// The received frame, or null if nothing was received
public async Task ReceiveData()
{
- var frameLength = _socket.Available;
- var frame = new byte[frameLength];
- var result = await _socket.ReceiveAsync(frame, SocketFlags.None);
- return result == frameLength ? frame : null;
+ if (!IsConnected)
+ return null;
+
+ var frame = new List();
+ var stopwatch = Stopwatch.StartNew();
+
+ while (stopwatch.ElapsedMilliseconds < _readTimeout)
+ {
+ var available = _socket.Available;
+ if (available > 0)
+ {
+ var buffer = new byte[available];
+ var received = _socket.Receive(buffer, 0, available, SocketFlags.None);
+ for (var i = 0; i < received; i++)
+ frame.Add(buffer[i]);
+
+ var expectedLength = EthernetFrame.ExpectedLength(frame);
+ if (expectedLength > 0 && frame.Count >= expectedLength)
+ break;
+
+ continue;
+ }
+
+ // Readable with nothing to read means the remote side closed the connection.
+ if (_socket.Poll(0, SelectMode.SelectRead) && _socket.Available == 0)
+ break;
+
+ await Task.Delay(PollInterval).ConfigureAwait(false);
+ }
+
+ return frame.Count == 0 ? null : frame.ToArray();
+ }
+
+ public void Dispose()
+ {
+ CloseSocket();
+ }
+
+ private void CloseSocket()
+ {
+ var socket = _socket;
+ _socket = null;
+ if (socket == null)
+ return;
+
+ try
+ {
+ if (socket.Connected)
+ socket.Shutdown(SocketShutdown.Both);
+ }
+ catch (SocketException)
+ {
+ // The connection is already gone
+ }
+ finally
+ {
+ socket.Dispose();
+ }
}
}
}
diff --git a/tests/CimonPlc.UnitTests/CimonPlc.UnitTests.csproj b/tests/CimonPlc.UnitTests/CimonPlc.UnitTests.csproj
index a67c1fd..a8c99e0 100644
--- a/tests/CimonPlc.UnitTests/CimonPlc.UnitTests.csproj
+++ b/tests/CimonPlc.UnitTests/CimonPlc.UnitTests.csproj
@@ -1,20 +1,20 @@
-
+
- netcoreapp3.1
-
+ net10.0
false
+ true
-
+
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/tests/CimonPlc.UnitTests/CimonPlc/EthernetConnectorTests.cs b/tests/CimonPlc.UnitTests/CimonPlc/EthernetConnectorTests.cs
index 8370c12..a3d7608 100644
--- a/tests/CimonPlc.UnitTests/CimonPlc/EthernetConnectorTests.cs
+++ b/tests/CimonPlc.UnitTests/CimonPlc/EthernetConnectorTests.cs
@@ -1,4 +1,5 @@
-using System;
+using System;
+using System.Threading.Tasks;
using Xunit;
using CimonPlc.PlcConnectors;
using CimonPlc.Enums;
@@ -19,7 +20,7 @@ public class EthernetConnectorTests
[InlineData(1000, 1000, 1000)]
[InlineData(5000, 4000, 3000)]
[InlineData(1000, 1000, 5000)]
- public async void Connect_Should_Return_Connected_On_Valid_Data(int readTimeout, int writeTimeout, int pingTimeout)
+ public async Task Connect_Should_Return_Connected_On_Valid_Data(int readTimeout, int writeTimeout, int pingTimeout)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10619));
@@ -39,7 +40,7 @@ public async void Connect_Should_Return_Connected_On_Valid_Data(int readTimeout,
[InlineData(0, 1000, 1000)]
[InlineData(1000, 0, 1000)]
[InlineData(1000, 1000, 0)]
- public async void Connect_Should_Return_Error_On_Incorrect_Data(int readTimeout, int writeTimeout, int pingTimeout)
+ public async Task Connect_Should_Return_Error_On_Incorrect_Data(int readTimeout, int writeTimeout, int pingTimeout)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10620));
@@ -54,7 +55,7 @@ public async void Connect_Should_Return_Error_On_Incorrect_Data(int readTimeout,
[InlineData(MemoryType.D, "000F0", 6)]
[InlineData(MemoryType.M, "0F010", 100)]
[InlineData(MemoryType.L, "000F10", 100)]
- public async void ReadWordAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, int length)
+ public async Task ReadWordAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, int length)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10620));
@@ -90,7 +91,7 @@ public async void ReadWordAsync_Should_Return_Value_On_Correct_Data(MemoryType m
[InlineData(MemoryType.D, "000051", 6)]
[InlineData(MemoryType.M, "0F01", 600)]
[InlineData(MemoryType.Y, "000F1", 0)]
- public async void ReadWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, int length)
+ public async Task ReadWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, int length)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10620));
@@ -105,7 +106,7 @@ public async void ReadWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType
[InlineData(MemoryType.D, "000F5", 6)]
[InlineData(MemoryType.M, "0F01", 100)]
[InlineData(MemoryType.L, "000F1", 100)]
- public async void ReadBitAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, int length)
+ public async Task ReadBitAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, int length)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10621));
@@ -140,7 +141,7 @@ public async void ReadBitAsync_Should_Return_Value_On_Correct_Data(MemoryType me
[InlineData(MemoryType.D, "000x5", 6)]
[InlineData(MemoryType.M, "0F01", 1060)]
[InlineData(MemoryType.Y, "000F1", 0)]
- public async void ReadBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, int length)
+ public async Task ReadBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, int length)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10620));
@@ -155,7 +156,7 @@ public async void ReadBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType
[InlineData(MemoryType.D, "000F0", 16050)]
[InlineData(MemoryType.M, "0F010", 100, 100, 10000, 1200, 1400)]
[InlineData(MemoryType.L, "000F0", 1010, 65000, 3403, 2302)]
- public async void WriteWordAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, params int[] data)
+ public async Task WriteWordAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, params int[] data)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10622));
@@ -187,7 +188,7 @@ public async void WriteWordAsync_Should_Return_Value_On_Correct_Data(MemoryType
[InlineData(MemoryType.M, "0F00", 70000, 30000, 40000)]
[InlineData(MemoryType.Y, "000F0", -10, 100, 1000)]
[InlineData(MemoryType.Y, "0")]
- public async void WriteWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, params int[] data)
+ public async Task WriteWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, params int[] data)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10620));
@@ -203,7 +204,7 @@ public async void WriteWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryTyp
[InlineData(MemoryType.D, "000F1", (byte)1, (byte)1)]
[InlineData(MemoryType.M, "0F011", (byte)1, (byte)1, (byte)0, (byte)1, (byte)1)]
[InlineData(MemoryType.L, "000F0", (byte)1, (byte)1, (byte)1, (byte)0)]
- public async void WriteBitAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, params byte[] data)
+ public async Task WriteBitAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, params byte[] data)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10623));
@@ -234,7 +235,7 @@ public async void WriteBitAsync_Should_Return_Value_On_Correct_Data(MemoryType m
[InlineData(MemoryType.D, "000x5", (byte)1)]
[InlineData(MemoryType.D, "000F5", (byte)2)]
[InlineData(MemoryType.Y, "0")]
- public async void WriteBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, params byte[] data)
+ public async Task WriteBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, params byte[] data)
{
//Arrange
var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10623));
diff --git a/tests/CimonPlc.UnitTests/CimonPlc/EthernetProtocolTests.cs b/tests/CimonPlc.UnitTests/CimonPlc/EthernetProtocolTests.cs
new file mode 100644
index 0000000..94830c4
--- /dev/null
+++ b/tests/CimonPlc.UnitTests/CimonPlc/EthernetProtocolTests.cs
@@ -0,0 +1,194 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CimonPlc.Enums;
+using CimonPlc.PlcConnectors;
+using CimonPlc.Sockets;
+using CimonPlc.UnitTests.FakeClasses;
+using Rony.Listeners;
+using Rony.Net;
+using Xunit;
+
+namespace CimonPlc.UnitTests
+{
+ public class EthernetProtocolTests
+ {
+ private static byte[] WriteAck(byte[] request, int code = 0)
+ {
+ var response = new List();
+ response.AddRange(Encoding.ASCII.GetBytes("KDT_PLC_S"));
+ response.Add((byte)(request[9] + 128));
+ response.Add(0x41);
+ response.Add(0);
+ response.AddRange(new byte[] { 0, 2 });
+ response.AddRange(code.ToDualByte());
+ response.AddRange(response.Sum(x => x).ToDualByte());
+ return response.ToArray();
+ }
+
+ [Fact]
+ public void FrameNo_Should_Cycle_From_0_To_127()
+ {
+ //Arrange
+ var connector = new EthernetConnector(new FakeEthernetSocket(x => WriteAck(x)));
+
+ //Act
+ var frameNumbers = Enumerable.Range(0, 300).Select(_ => (int)connector.FrameNo).ToArray();
+
+ //Assert
+ var expected = Enumerable.Range(0, 300).Select(x => x % 128).ToArray();
+ Assert.Equal(expected, frameNumbers);
+ }
+
+ [Fact]
+ public void BuildFrame_Should_Use_Two_Bytes_Length_For_Read_Requests()
+ {
+ //Act
+ var frame = EthernetConnector.BuildFrame(5, (byte)ReadCommand.WordBlockRead, MemoryType.D, "100", 3);
+
+ //Assert
+ var expected = new List();
+ expected.AddRange(Encoding.ASCII.GetBytes("KDT_PLC_M"));
+ expected.AddRange(new byte[] { 5, 0x52, 0, 0, 10, (byte)MemoryType.D, 0 });
+ expected.AddRange(Encoding.ASCII.GetBytes("000100"));
+ expected.AddRange(new byte[] { 0, 3 });
+ expected.AddRange(expected.Sum(x => x).ToDualByte());
+ Assert.Equal(expected.ToArray(), frame);
+ }
+
+ [Fact]
+ public void BuildFrame_Should_Append_Word_Data_For_Write_Requests()
+ {
+ //Act
+ var frame = EthernetConnector.BuildFrame(0, (byte)WriteCommands.WordBlockWrite, MemoryType.D, "0", 2, new byte[] { 0x12, 0x34, 0xAB, 0xCD });
+
+ //Assert
+ Assert.Equal(14 + 10 + 4 + 2, frame.Length);
+ Assert.Equal(new byte[] { 0, 14 }, frame.Skip(12).Take(2));
+ Assert.Equal(new byte[] { 0x12, 0x34, 0xAB, 0xCD }, frame.Skip(24).Take(4));
+ }
+
+ [Fact]
+ public async Task WriteWordAsync_Should_Return_PLC_Error_Code()
+ {
+ //Arrange
+ var connector = new EthernetConnector(new FakeEthernetSocket(x => WriteAck(x, (int)ResponseCode.InvalidDeviceAddress)));
+
+ //Act
+ var result = await connector.WriteWordAsync(MemoryType.D, "0", 1);
+
+ //Assert
+ Assert.Equal(ResponseCode.InvalidDeviceAddress, result);
+ }
+
+ [Fact]
+ public async Task ReadWordAsync_Should_Return_Error_On_Truncated_Response()
+ {
+ //Arrange
+ var connector = new EthernetConnector(new FakeEthernetSocket(x => x.Take(10).ToArray()));
+
+ //Act
+ var (responseCode, data) = await connector.ReadWordAsync(MemoryType.D, "0", 2);
+
+ //Assert
+ Assert.Equal(ResponseCode.WritingError, responseCode);
+ Assert.Null(data);
+ }
+
+ [Fact]
+ public async Task ReadWordAsync_Should_Return_Error_On_Wrong_Checksum()
+ {
+ //Arrange
+ var connector = new EthernetConnector(new FakeEthernetSocket(x =>
+ {
+ var response = WriteAck(x);
+ response[response.Length - 1]++;
+ return response;
+ }));
+
+ //Act
+ var result = await connector.WriteWordAsync(MemoryType.D, "0", 1);
+
+ //Assert
+ Assert.Equal(ResponseCode.WritingError, result);
+ }
+
+ [Fact]
+ public async Task ReadWordAsync_Should_Return_SystemError_On_No_Response()
+ {
+ //Arrange
+ var connector = new EthernetConnector(new FakeEthernetSocket(x => null));
+
+ //Act
+ var (responseCode, data) = await connector.ReadWordAsync(MemoryType.D, "0", 2);
+
+ //Assert
+ Assert.Equal(ResponseCode.SystemError, responseCode);
+ Assert.Null(data);
+ }
+
+ [Fact]
+ public async Task Operations_Should_Return_SystemError_When_Not_Connected_Without_AutoConnect()
+ {
+ //Arrange
+ var connector = new EthernetConnector(new FakeEthernetSocket(x => WriteAck(x)), autoConnect: false);
+
+ //Act
+ var result = await connector.WriteWordAsync(MemoryType.D, "0", 1);
+
+ //Assert
+ Assert.Equal(ResponseCode.SystemError, result);
+ }
+
+ [Fact]
+ public async Task AutoConnect_Should_Keep_Explicitly_Opened_Connection()
+ {
+ //Arrange
+ var socket = new FakeEthernetSocket(x => WriteAck(x));
+ var connector = new EthernetConnector(socket);
+ await connector.Connect();
+
+ //Act
+ await connector.WriteWordAsync(MemoryType.D, "0", 1);
+ await connector.WriteWordAsync(MemoryType.D, "0", 2);
+
+ //Assert
+ Assert.True(connector.IsConnected);
+ Assert.Equal(1, socket.ConnectCount);
+ }
+
+ [Fact]
+ public async Task AutoConnect_Should_Close_Automatically_Opened_Connection()
+ {
+ //Arrange
+ var socket = new FakeEthernetSocket(x => WriteAck(x));
+ var connector = new EthernetConnector(socket);
+
+ //Act
+ await connector.WriteWordAsync(MemoryType.D, "0", 1);
+
+ //Assert
+ Assert.False(connector.IsConnected);
+ }
+
+ [Fact]
+ public async Task TcpSocket_Should_Reconnect_After_AutoDisconnect()
+ {
+ //Arrange
+ using var connector = new EthernetConnector(new TcpSocket("127.0.0.1", 10624));
+ using var mockServer = new MockServer(new TcpServer(10624));
+ mockServer.Start();
+ mockServer.Mock.Send("").Receive(x => WriteAck(x));
+
+ //Act
+ var first = await connector.WriteWordAsync(MemoryType.D, "0", 1);
+ var second = await connector.WriteWordAsync(MemoryType.D, "0", 2);
+ mockServer.Stop();
+
+ //Assert
+ Assert.Equal(ResponseCode.Success, first);
+ Assert.Equal(ResponseCode.Success, second);
+ }
+ }
+}
diff --git a/tests/CimonPlc.UnitTests/CimonPlc/SerialConnectorTests.cs b/tests/CimonPlc.UnitTests/CimonPlc/SerialConnectorTests.cs
index 0eb3f21..0bd86a5 100644
--- a/tests/CimonPlc.UnitTests/CimonPlc/SerialConnectorTests.cs
+++ b/tests/CimonPlc.UnitTests/CimonPlc/SerialConnectorTests.cs
@@ -1,4 +1,7 @@
-using System;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
using Xunit;
using CimonPlc.PlcConnectors;
using CimonPlc.Enums;
@@ -22,7 +25,7 @@ public SerialConnectorTests()
[InlineData(1000, 1000, 1000)]
[InlineData(5000, 4000, 3000)]
[InlineData(1000, 1000, 5000)]
- public async void Connect_Should_Return_Connected_On_Valid_Data(int readTimeout, int writeTimeout, int pingTimeout)
+ public async Task Connect_Should_Return_Connected_On_Valid_Data(int readTimeout, int writeTimeout, int pingTimeout)
{
//Act
var result = await _connector.Connect(readTimeout, writeTimeout, pingTimeout);
@@ -37,7 +40,7 @@ public async void Connect_Should_Return_Connected_On_Valid_Data(int readTimeout,
[InlineData(0, 1000, 1000)]
[InlineData(1000, 0, 1000)]
[InlineData(1000, 1000, 0)]
- public async void Connect_Should_Return_Error_On_Incorrect_Data(int readTimeout, int writeTimeout, int pingTimeout)
+ public async Task Connect_Should_Return_Error_On_Incorrect_Data(int readTimeout, int writeTimeout, int pingTimeout)
{
//Assert
await Assert.ThrowsAnyAsync(() => _connector.Connect(readTimeout, writeTimeout, pingTimeout));
@@ -49,7 +52,7 @@ public async void Connect_Should_Return_Error_On_Incorrect_Data(int readTimeout,
[InlineData(MemoryType.D, "000F0", 6)]
[InlineData(MemoryType.M, "0F010", 50)]
[InlineData(MemoryType.L, "000F10", 30)]
- public async void ReadWordAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, int length)
+ public async Task ReadWordAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, int length)
{
//Act
var (responseCode, data) = await _connector.ReadWordAsync(memoryType, address, length);
@@ -65,7 +68,7 @@ public async void ReadWordAsync_Should_Return_Value_On_Correct_Data(MemoryType m
[InlineData(MemoryType.D, "000051", 6)]
[InlineData(MemoryType.M, "0F01", 65)]
[InlineData(MemoryType.Y, "000F1", 0)]
- public async void ReadWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, int length)
+ public async Task ReadWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, int length)
{
//Assert
await Assert.ThrowsAnyAsync(() => _connector.ReadWordAsync(memoryType, address, length));
@@ -78,7 +81,7 @@ public async void ReadWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType
[InlineData(MemoryType.D, "000F5", 6)]
[InlineData(MemoryType.M, "0F01", 126)]
[InlineData(MemoryType.L, "000F1", 100)]
- public async void ReadBitAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, int length)
+ public async Task ReadBitAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, int length)
{
//Act
var (responseCode, data) = await _connector.ReadBitAsync(memoryType, address, length);
@@ -93,7 +96,7 @@ public async void ReadBitAsync_Should_Return_Value_On_Correct_Data(MemoryType me
[InlineData(MemoryType.D, "000x5", 6)]
[InlineData(MemoryType.M, "0F01", 256)]
[InlineData(MemoryType.Y, "000F1", 0)]
- public async void ReadBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, int length)
+ public async Task ReadBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, int length)
{
//Assert
await Assert.ThrowsAnyAsync(() => _connector.ReadBitAsync(memoryType, address, length));
@@ -105,7 +108,7 @@ public async void ReadBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType
[InlineData(MemoryType.D, "000F0", 16050)]
[InlineData(MemoryType.M, "0F010", 100, 100, 10000, 1200, 1400)]
[InlineData(MemoryType.L, "000F0", 1010, 65000, 3403, 2302)]
- public async void WriteWordAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, params int[] data)
+ public async Task WriteWordAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, params int[] data)
{
//Act
var result = await _connector.WriteWordAsync(memoryType, address, data);
@@ -120,7 +123,7 @@ public async void WriteWordAsync_Should_Return_Value_On_Correct_Data(MemoryType
[InlineData(MemoryType.M, "0F00", 70000, 30000, 40000)]
[InlineData(MemoryType.Y, "000F0", -10, 100, 1000)]
[InlineData(MemoryType.Y, "0")]
- public async void WriteWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, params int[] data)
+ public async Task WriteWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, params int[] data)
{
//Assert
await Assert.ThrowsAnyAsync(() => _connector.WriteWordAsync(memoryType, address, data));
@@ -133,7 +136,7 @@ public async void WriteWordAsync_Should_Return_Error_On_Incorrect_Data(MemoryTyp
[InlineData(MemoryType.D, "000F1", (byte)1, (byte)1)]
[InlineData(MemoryType.M, "0F011", (byte)1, (byte)1, (byte)1, (byte)0, (byte)1)]
[InlineData(MemoryType.L, "000F0", (byte)1, (byte)1, (byte)1, (byte)1)]
- public async void WriteBitAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, params byte[] data)
+ public async Task WriteBitAsync_Should_Return_Value_On_Correct_Data(MemoryType memoryType, string address, params byte[] data)
{
//Act
var result = await _connector.WriteBitAsync(memoryType, address, data);
@@ -147,10 +150,45 @@ public async void WriteBitAsync_Should_Return_Value_On_Correct_Data(MemoryType m
[InlineData(MemoryType.D, "000x5", (byte)1)]
[InlineData(MemoryType.D, "000F5", (byte)2)]
[InlineData(MemoryType.Y, "0")]
- public async void WriteBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, params byte[] data)
+ public async Task WriteBitAsync_Should_Return_Error_On_Incorrect_Data(MemoryType memoryType, string address, params byte[] data)
{
//Assert
await Assert.ThrowsAnyAsync(() => _connector.WriteBitAsync(memoryType, address, data));
}
+
+
+ [Fact]
+ public async Task WriteWordAsync_Should_Reject_More_Words_Than_Frame_Length_Can_Hold()
+ {
+ //Arrange
+ var data = new int[62];
+
+ //Assert
+ await Assert.ThrowsAnyAsync(() => _connector.WriteWordAsync(MemoryType.D, "0", data));
+ }
+
+ [Fact]
+ public async Task WriteWordAsync_Should_Accept_Maximum_Word_Count()
+ {
+ //Act
+ var result = await _connector.WriteWordAsync(MemoryType.D, "0", new int[61]);
+
+ //Assert
+ Assert.Equal(ResponseCode.Success, result);
+ }
+
+ [Fact]
+ public void BuildFrame_Should_Create_Read_Request()
+ {
+ //Act
+ var frame = SerialConnector.BuildFrame((byte)ReadCommand.WordBlockRead, MemoryType.D, "a0", 16);
+
+ //Assert
+ var expected = new List();
+ expected.AddRange("\u000500R0AD00000A010");
+ expected.AddBCC();
+ expected.Add((char)4);
+ Assert.Equal(expected.Select(Convert.ToByte).ToArray(), frame);
+ }
}
}
diff --git a/tests/CimonPlc.UnitTests/FakeClasses/FakeEthernetSocket.cs b/tests/CimonPlc.UnitTests/FakeClasses/FakeEthernetSocket.cs
new file mode 100644
index 0000000..357f961
--- /dev/null
+++ b/tests/CimonPlc.UnitTests/FakeClasses/FakeEthernetSocket.cs
@@ -0,0 +1,51 @@
+using CimonPlc.Enums;
+using CimonPlc.Interfaces;
+using System;
+using System.Threading.Tasks;
+
+namespace CimonPlc.UnitTests.FakeClasses
+{
+ ///
+ /// Ethernet socket which answers every request using the given response factory.
+ ///
+ public class FakeEthernetSocket : IEthernetSocket
+ {
+ private readonly Func _responseFactory;
+ private byte[] _lastRequest;
+
+ public FakeEthernetSocket(Func responseFactory)
+ {
+ _responseFactory = responseFactory;
+ }
+
+ public bool IsConnected { get; private set; }
+
+ public int ConnectCount { get; private set; }
+
+ public byte[] LastRequest => _lastRequest;
+
+ public Task Connect(int readTimeout = 1000, int writeTimeout = 1000, int pingTimeout = 3000)
+ {
+ ConnectCount++;
+ IsConnected = true;
+ return Task.FromResult(ConnectionStatus.Connected);
+ }
+
+ public ConnectionStatus Disconnect()
+ {
+ IsConnected = false;
+ return ConnectionStatus.DisConnected;
+ }
+
+ public Task SendData(byte[] frame)
+ {
+ _lastRequest = frame;
+ return Task.FromResult(true);
+ }
+
+ public Task ReceiveData()
+ {
+ return Task.FromResult(_responseFactory(_lastRequest));
+ }
+ }
+}
diff --git a/tests/CimonPlc.UnitTests/FakeCalsses/FakeSerialSocket.cs b/tests/CimonPlc.UnitTests/FakeClasses/FakeSerialSocket.cs
similarity index 98%
rename from tests/CimonPlc.UnitTests/FakeCalsses/FakeSerialSocket.cs
rename to tests/CimonPlc.UnitTests/FakeClasses/FakeSerialSocket.cs
index fc80ce8..addb22e 100644
--- a/tests/CimonPlc.UnitTests/FakeCalsses/FakeSerialSocket.cs
+++ b/tests/CimonPlc.UnitTests/FakeClasses/FakeSerialSocket.cs
@@ -1,4 +1,4 @@
-using CimonPlc.Enums;
+using CimonPlc.Enums;
using CimonPlc.Interfaces;
using System.Threading.Tasks;
using System.Collections.Generic;
diff --git a/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstBadFormatTests.cs b/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstBadFormatTests.cs
index 68f47cd..6e7d60c 100644
--- a/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstBadFormatTests.cs
+++ b/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstBadFormatTests.cs
@@ -1,4 +1,4 @@
-using Ardalis.GuardClauses;
+using CimonPlc;
using System;
using Xunit;
diff --git a/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstByteOutOfRangeTests.cs b/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstByteOutOfRangeTests.cs
index 920fdb8..001fcab 100644
--- a/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstByteOutOfRangeTests.cs
+++ b/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstByteOutOfRangeTests.cs
@@ -1,4 +1,4 @@
-using Ardalis.GuardClauses;
+using CimonPlc;
using System;
using System.Collections;
using System.Collections.Generic;
diff --git a/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstIntOutOfRangeTests.cs b/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstIntOutOfRangeTests.cs
index 9aba3de..088086d 100644
--- a/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstIntOutOfRangeTests.cs
+++ b/tests/CimonPlc.UnitTests/GuardClauses/GuardAgainstIntOutOfRangeTests.cs
@@ -1,4 +1,4 @@
-using Ardalis.GuardClauses;
+using CimonPlc;
using System;
using System.Collections;
using System.Collections.Generic;
diff --git a/tests/CimonPlc.UnitTests/PublicFunctions/AddBCCTests.cs b/tests/CimonPlc.UnitTests/PublicFunctions/AddBCCTests.cs
index f61505f..e95f062 100644
--- a/tests/CimonPlc.UnitTests/PublicFunctions/AddBCCTests.cs
+++ b/tests/CimonPlc.UnitTests/PublicFunctions/AddBCCTests.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using Xunit;
namespace CimonPlc.UnitTests.PublicFunctions
diff --git a/tests/CimonPlc.UnitTests/PublicFunctions/ToDualCharTests.cs b/tests/CimonPlc.UnitTests/PublicFunctions/ToDualCharTests.cs
index ab95d2e..c22aebc 100644
--- a/tests/CimonPlc.UnitTests/PublicFunctions/ToDualCharTests.cs
+++ b/tests/CimonPlc.UnitTests/PublicFunctions/ToDualCharTests.cs
@@ -1,4 +1,4 @@
-using Xunit;
+using Xunit;
namespace CimonPlc.UnitTests.PublicFunctions
{
diff --git a/tests/CimonPlc.UnitTests/PublicFunctions/ToQuadCharTests.cs b/tests/CimonPlc.UnitTests/PublicFunctions/ToQuadCharTests.cs
index 47fe4dc..4478f36 100644
--- a/tests/CimonPlc.UnitTests/PublicFunctions/ToQuadCharTests.cs
+++ b/tests/CimonPlc.UnitTests/PublicFunctions/ToQuadCharTests.cs
@@ -1,4 +1,4 @@
-using Xunit;
+using Xunit;
namespace CimonPlc.UnitTests.PublicFunctions
{