├── .gitignore ├── README.md ├── RICADO.Omron ├── packageIcon.png ├── WriteBitsResult.cs ├── WriteClockResult.cs ├── WriteWordsResult.cs ├── Channels │ ├── SendMessageResult.cs │ ├── ReceiveMessageResult.cs │ ├── ProcessRequestResult.cs │ ├── EthernetChannel.cs │ ├── EthernetUDPChannel.cs │ └── EthernetTCPChannel.cs ├── ReadBitsResult.cs ├── ReadWordsResult.cs ├── ReadClockResult.cs ├── FINSException.cs ├── OmronException.cs ├── ReadCycleTimeResult.cs ├── Responses │ ├── WriteMemoryAreaBitResponse.cs │ ├── WriteMemoryAreaWordResponse.cs │ ├── WriteClockResponse.cs │ ├── ReadMemoryAreaBitResponse.cs │ ├── ReadMemoryAreaWordResponse.cs │ ├── ReadCycleTimeResponse.cs │ ├── ReadClockResponse.cs │ ├── ReadCPUUnitDataResponse.cs │ └── FINSResponse.cs ├── Requests │ ├── ReadClockRequest.cs │ ├── ReadCycleTimeRequest.cs │ ├── ReadCPUUnitDataRequest.cs │ ├── WriteClockRequest.cs │ ├── ReadMemoryAreaWordRequest.cs │ ├── WriteMemoryAreaWordRequest.cs │ ├── ReadMemoryAreaBitRequest.cs │ ├── WriteMemoryAreaBitRequest.cs │ └── FINSRequest.cs ├── RICADO.Omron.csproj ├── Enums.cs ├── BCDConverter.cs └── OmronPLC.cs ├── .github ├── workflows │ ├── codeql-analysis.yml │ └── main.yml └── dependabot.yml ├── LICENSE └── RICADO.Omron.sln /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | *.suo 5 | *.user -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RICADO.Omron 2 | An Omron PLC Communication Library for .NET 6+ Applications 3 | -------------------------------------------------------------------------------- /RICADO.Omron/packageIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricado-group/dotnet-omron/HEAD/RICADO.Omron/packageIcon.png -------------------------------------------------------------------------------- /RICADO.Omron/WriteBitsResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public struct WriteBitsResult 6 | { 7 | public int BytesSent; 8 | public int PacketsSent; 9 | public int BytesReceived; 10 | public int PacketsReceived; 11 | public double Duration; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RICADO.Omron/WriteClockResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public struct WriteClockResult 6 | { 7 | public int BytesSent; 8 | public int PacketsSent; 9 | public int BytesReceived; 10 | public int PacketsReceived; 11 | public double Duration; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RICADO.Omron/WriteWordsResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public struct WriteWordsResult 6 | { 7 | public int BytesSent; 8 | public int PacketsSent; 9 | public int BytesReceived; 10 | public int PacketsReceived; 11 | public double Duration; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RICADO.Omron/Channels/SendMessageResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace RICADO.Omron.Channels 8 | { 9 | internal struct SendMessageResult 10 | { 11 | internal int Bytes; 12 | internal int Packets; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RICADO.Omron/ReadBitsResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public struct ReadBitsResult 6 | { 7 | public int BytesSent; 8 | public int PacketsSent; 9 | public int BytesReceived; 10 | public int PacketsReceived; 11 | public double Duration; 12 | public bool[] Values; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RICADO.Omron/ReadWordsResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public struct ReadWordsResult 6 | { 7 | public int BytesSent; 8 | public int PacketsSent; 9 | public int BytesReceived; 10 | public int PacketsReceived; 11 | public double Duration; 12 | public Int16[] Values; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RICADO.Omron/ReadClockResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public struct ReadClockResult 6 | { 7 | public int BytesSent; 8 | public int PacketsSent; 9 | public int BytesReceived; 10 | public int PacketsReceived; 11 | public double Duration; 12 | public DateTime Clock; 13 | public int DayOfWeek; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /RICADO.Omron/Channels/ReceiveMessageResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace RICADO.Omron.Channels 8 | { 9 | internal struct ReceiveMessageResult 10 | { 11 | internal Memory Message; 12 | internal int Bytes; 13 | internal int Packets; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /RICADO.Omron/Channels/ProcessRequestResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RICADO.Omron.Responses; 3 | 4 | namespace RICADO.Omron.Channels 5 | { 6 | internal struct ProcessRequestResult 7 | { 8 | internal int BytesSent; 9 | internal int PacketsSent; 10 | internal int BytesReceived; 11 | internal int PacketsReceived; 12 | internal double Duration; 13 | internal FINSResponse Response; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /RICADO.Omron/FINSException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public class FINSException : Exception 6 | { 7 | #region Constructors 8 | 9 | internal FINSException(string message) : base(message) 10 | { 11 | } 12 | 13 | internal FINSException(string message, Exception innerException) : base(message, innerException) 14 | { 15 | } 16 | 17 | #endregion 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /RICADO.Omron/OmronException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public class OmronException : Exception 6 | { 7 | #region Constructors 8 | 9 | internal OmronException(string message) : base(message) 10 | { 11 | } 12 | 13 | internal OmronException(string message, Exception innerException) : base(message, innerException) 14 | { 15 | } 16 | 17 | #endregion 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /RICADO.Omron/ReadCycleTimeResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RICADO.Omron 4 | { 5 | public struct ReadCycleTimeResult 6 | { 7 | public int BytesSent; 8 | public int PacketsSent; 9 | public int BytesReceived; 10 | public int PacketsReceived; 11 | public double Duration; 12 | public double MinimumCycleTime; 13 | public double MaximumCycleTime; 14 | public double AverageCycleTime; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RICADO.Omron/Responses/WriteMemoryAreaBitResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RICADO.Omron.Requests; 3 | 4 | namespace RICADO.Omron.Responses 5 | { 6 | internal class WriteMemoryAreaBitResponse 7 | { 8 | #region Internal Methods 9 | 10 | internal static void Validate(WriteMemoryAreaBitRequest request, FINSResponse response) 11 | { 12 | // TODO: Consider if any Checks can be made on the FINS Response 13 | } 14 | 15 | #endregion 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /RICADO.Omron/Responses/WriteMemoryAreaWordResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RICADO.Omron.Requests; 3 | 4 | namespace RICADO.Omron.Responses 5 | { 6 | internal class WriteMemoryAreaWordResponse 7 | { 8 | #region Internal Methods 9 | 10 | internal static void Validate(WriteMemoryAreaWordRequest request, FINSResponse response) 11 | { 12 | // TODO: Consider if any Checks can be made on the FINS Response 13 | } 14 | 15 | #endregion 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /RICADO.Omron/Responses/WriteClockResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using RICADO.Omron.Requests; 5 | 6 | namespace RICADO.Omron.Responses 7 | { 8 | internal class WriteClockResponse 9 | { 10 | #region Internal Methods 11 | 12 | internal static void Validate(WriteClockRequest request, FINSResponse response) 13 | { 14 | // TODO: Consider if any Checks can be made on the FINS Response 15 | } 16 | 17 | #endregion 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # Action Name 2 | name: "CodeQL" 3 | 4 | # When the Action will Run 5 | on: 6 | schedule: 7 | - cron: '0 7 * * 5' 8 | 9 | # Workflow Jobs 10 | jobs: 11 | analyze: 12 | name: Analyze 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | steps: 21 | # Step 1 - Checkout Code 22 | - name: Checkout Code 23 | uses: actions/checkout@v4 24 | 25 | # Step 2 - Run CodeQL 26 | - name: Run CodeQL 27 | uses: ricado-group/dotnet-library-codeql-action@v1 28 | with: 29 | private-nuget-url: 'https://nuget.pkg.github.com/ricado-group/index.json' 30 | private-nuget-token: ${{ secrets.GH_PACKAGES_PAT }} 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | nuget-default: 4 | type: nuget-feed 5 | url: https://api.nuget.org/v3/index.json 6 | nuget-github: 7 | type: nuget-feed 8 | url: https://nuget.pkg.github.com/ricado-group/index.json 9 | token: ${{ secrets.GH_PACKAGES_PAT }} 10 | updates: 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | time: "07:00" 17 | timezone: "Pacific/Auckland" 18 | 19 | # Maintain dependencies for NuGet Packages 20 | - package-ecosystem: "nuget" 21 | directory: "/" 22 | registries: 23 | - nuget-default 24 | - nuget-github 25 | schedule: 26 | interval: "daily" 27 | time: "07:00" 28 | timezone: "Pacific/Auckland" -------------------------------------------------------------------------------- /RICADO.Omron/Responses/ReadMemoryAreaBitResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using RICADO.Omron.Requests; 4 | 5 | namespace RICADO.Omron.Responses 6 | { 7 | internal class ReadMemoryAreaBitResponse 8 | { 9 | #region Internal Methods 10 | 11 | internal static bool[] ExtractValues(ReadMemoryAreaBitRequest request, FINSResponse response) 12 | { 13 | if(response.Data.Length < request.Length) 14 | { 15 | throw new FINSException("The Response Data Length of '" + response.Data.Length.ToString() + "' was too short - Expecting a Length of '" + request.Length.ToString() + "'"); 16 | } 17 | 18 | return response.Data.Select(value => value == 0 ? false : true).ToArray(); 19 | } 20 | 21 | #endregion 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Action Name 2 | name: Main Automated Builds 3 | 4 | # When the Action will Run 5 | on: 6 | push: 7 | branches: 8 | - master 9 | tags: 10 | - '*.*.*' 11 | pull_request: 12 | 13 | # Workflow Jobs 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | # Step 1 - Checkout Code 19 | - name: Checkout Code 20 | uses: actions/checkout@v4 21 | 22 | # Step 2 - Build and Publish 23 | - name: Build and Publish 24 | uses: ricado-group/dotnet-library-build-release-action@v1 25 | with: 26 | project-name: 'RICADO.Omron' 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | private-nuget-url: 'https://nuget.pkg.github.com/ricado-group/index.json' 29 | private-nuget-token: ${{ secrets.GH_PACKAGES_PAT }} 30 | public-nuget-token: ${{ secrets.NUGET_APIKEY }} 31 | publish-public: true 32 | dotnet-version: 8.0.x 33 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/ReadClockRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal class ReadClockRequest : FINSRequest 8 | { 9 | #region Constructor 10 | 11 | private ReadClockRequest(OmronPLC plc) : base(plc) 12 | { 13 | } 14 | 15 | #endregion 16 | 17 | 18 | #region Internal Methods 19 | 20 | internal static ReadClockRequest CreateNew(OmronPLC plc) 21 | { 22 | return new ReadClockRequest(plc) 23 | { 24 | FunctionCode = (byte)enFunctionCode.TimeData, 25 | SubFunctionCode = (byte)enTimeDataFunctionCode.ReadClock, 26 | }; 27 | } 28 | 29 | #endregion 30 | 31 | 32 | #region Protected Methods 33 | 34 | protected override List BuildRequestData() 35 | { 36 | return new List(); 37 | } 38 | 39 | #endregion 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /RICADO.Omron/Responses/ReadMemoryAreaWordResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using RICADO.Omron.Requests; 5 | 6 | namespace RICADO.Omron.Responses 7 | { 8 | internal class ReadMemoryAreaWordResponse 9 | { 10 | #region Internal Methods 11 | 12 | internal static short[] ExtractValues(ReadMemoryAreaWordRequest request, FINSResponse response) 13 | { 14 | if (response.Data.Length < request.Length * 2) 15 | { 16 | throw new FINSException("The Response Data Length of '" + response.Data.Length.ToString() + "' was too short - Expecting a Length of '" + (request.Length * 2).ToString() + "'"); 17 | } 18 | 19 | List values = new List(); 20 | 21 | for(int i = 0; i < request.Length * 2; i += 2) 22 | { 23 | values.Add(BitConverter.ToInt16(new byte[] { response.Data[i + 1], response.Data[i] })); 24 | } 25 | 26 | return values.ToArray(); 27 | } 28 | 29 | #endregion 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 - 2024 RICADO Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/ReadCycleTimeRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal class ReadCycleTimeRequest : FINSRequest 8 | { 9 | #region Constructor 10 | 11 | private ReadCycleTimeRequest(OmronPLC plc) : base(plc) 12 | { 13 | } 14 | 15 | #endregion 16 | 17 | 18 | #region Internal Methods 19 | 20 | internal static ReadCycleTimeRequest CreateNew(OmronPLC plc) 21 | { 22 | return new ReadCycleTimeRequest(plc) 23 | { 24 | FunctionCode = (byte)enFunctionCode.Status, 25 | SubFunctionCode = (byte)enStatusFunctionCode.ReadCycleTime, 26 | }; 27 | } 28 | 29 | #endregion 30 | 31 | 32 | #region Protected Methods 33 | 34 | protected override List BuildRequestData() 35 | { 36 | List data = new List(); 37 | 38 | // Read Cycle Time 39 | data.Add(01); 40 | 41 | return data; 42 | } 43 | 44 | #endregion 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RICADO.Omron.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RICADO.Omron", "RICADO.Omron\RICADO.Omron.csproj", "{674B309E-2B8D-4539-9670-009F49C634CE}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {674B309E-2B8D-4539-9670-009F49C634CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {674B309E-2B8D-4539-9670-009F49C634CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {674B309E-2B8D-4539-9670-009F49C634CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {674B309E-2B8D-4539-9670-009F49C634CE}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {2EE1C1C7-00CC-40BB-8494-36981720DA73} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/ReadCPUUnitDataRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal class ReadCPUUnitDataRequest : FINSRequest 8 | { 9 | #region Constructor 10 | 11 | private ReadCPUUnitDataRequest(OmronPLC plc) : base(plc) 12 | { 13 | } 14 | 15 | #endregion 16 | 17 | 18 | #region Internal Methods 19 | 20 | internal static ReadCPUUnitDataRequest CreateNew(OmronPLC plc) 21 | { 22 | return new ReadCPUUnitDataRequest(plc) 23 | { 24 | FunctionCode = (byte)enFunctionCode.MachineConfiguration, 25 | SubFunctionCode = (byte)enMachineConfigurationFunctionCode.ReadCPUUnitData, 26 | }; 27 | } 28 | 29 | #endregion 30 | 31 | 32 | #region Protected Methods 33 | 34 | protected override List BuildRequestData() 35 | { 36 | List data = new List(); 37 | 38 | // Read Data 39 | data.Add(0); 40 | 41 | return data; 42 | } 43 | 44 | #endregion 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RICADO.Omron/RICADO.Omron.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net7.0;net6.0 5 | RICADO 6 | RICADO Limited 7 | An Omron PLC Communication Library for .NET 6+ Applications 8 | Copyright © RICADO Limited 2009 - 2024 9 | LICENSE 10 | packageIcon.png 11 | https://github.com/ricado-group/dotnet-omron 12 | git 13 | true 14 | snupkg 15 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 16 | 17 | 18 | 19 | 20 | True 21 | 22 | 23 | 24 | True 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /RICADO.Omron/Responses/ReadCycleTimeResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using RICADO.Omron.Requests; 5 | 6 | namespace RICADO.Omron.Responses 7 | { 8 | internal class ReadCycleTimeResponse 9 | { 10 | #region Constants 11 | 12 | internal const int CYCLE_TIME_ITEM_LENGTH = 4; 13 | 14 | #endregion 15 | 16 | 17 | #region Internal Methods 18 | 19 | internal static CycleTimeResult ExtractCycleTime(ReadCycleTimeRequest request, FINSResponse response) 20 | { 21 | if (response.Data.Length < CYCLE_TIME_ITEM_LENGTH * 3) 22 | { 23 | throw new FINSException("The Response Data Length of '" + response.Data.Length.ToString() + "' was too short - Expecting a Length of '" + (CYCLE_TIME_ITEM_LENGTH * 3).ToString() + "'"); 24 | } 25 | 26 | ReadOnlyMemory data = response.Data; 27 | 28 | return new CycleTimeResult 29 | { 30 | AverageCycleTime = getCycleTime(data.Slice(0, CYCLE_TIME_ITEM_LENGTH).ToArray()), 31 | MaximumCycleTime = getCycleTime(data.Slice(CYCLE_TIME_ITEM_LENGTH, CYCLE_TIME_ITEM_LENGTH).ToArray()), 32 | MinimumCycleTime = getCycleTime(data.Slice(CYCLE_TIME_ITEM_LENGTH * 2, CYCLE_TIME_ITEM_LENGTH).ToArray()), 33 | }; 34 | } 35 | 36 | #endregion 37 | 38 | 39 | #region Private Methods 40 | 41 | private static double getCycleTime(byte[] bytes) 42 | { 43 | if(bytes.Length != 4) 44 | { 45 | throw new ArgumentOutOfRangeException(nameof(bytes), "The Cycle Time Bytes Array Length must be 4"); 46 | } 47 | 48 | uint cycleTimeValue = BCDConverter.ToUInt32(bytes.Reverse().ToArray()); 49 | 50 | if(cycleTimeValue > 0) 51 | { 52 | return cycleTimeValue / (double)10; 53 | } 54 | 55 | return 0; 56 | } 57 | 58 | #endregion 59 | 60 | 61 | #region Structs 62 | 63 | internal struct CycleTimeResult 64 | { 65 | internal double MinimumCycleTime; 66 | internal double MaximumCycleTime; 67 | internal double AverageCycleTime; 68 | } 69 | 70 | #endregion 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /RICADO.Omron/Responses/ReadClockResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using RICADO.Omron.Requests; 5 | 6 | namespace RICADO.Omron.Responses 7 | { 8 | internal class ReadClockResponse 9 | { 10 | #region Constants 11 | 12 | internal const int DATE_LENGTH = 6; 13 | internal const int DAY_OF_WEEK_LENGTH = 1; 14 | 15 | #endregion 16 | 17 | 18 | #region Internal Methods 19 | 20 | internal static ClockResult ExtractClock(ReadClockRequest request, FINSResponse response) 21 | { 22 | if (response.Data.Length < DATE_LENGTH + DAY_OF_WEEK_LENGTH) 23 | { 24 | throw new FINSException("The Response Data Length of '" + response.Data.Length.ToString() + "' was too short - Expecting a Length of '" + (DATE_LENGTH + DAY_OF_WEEK_LENGTH).ToString() + "'"); 25 | } 26 | 27 | ReadOnlyMemory data = response.Data; 28 | 29 | return new ClockResult 30 | { 31 | ClockDateTime = getClockDateTime(data.Slice(0, DATE_LENGTH).ToArray()), 32 | DayOfWeek = BCDConverter.ToByte(data.ToArray()[DATE_LENGTH]), 33 | }; 34 | } 35 | 36 | #endregion 37 | 38 | 39 | #region Private Methods 40 | 41 | private static DateTime getClockDateTime(byte[] bytes) 42 | { 43 | byte year = BCDConverter.ToByte(bytes[0]); 44 | byte month = BCDConverter.ToByte(bytes[1]); 45 | byte day = BCDConverter.ToByte(bytes[2]); 46 | byte hour = BCDConverter.ToByte(bytes[3]); 47 | byte minute = BCDConverter.ToByte(bytes[4]); 48 | byte second = BCDConverter.ToByte(bytes[5]); 49 | 50 | if (year < 70) 51 | { 52 | return new DateTime(2000 + year, month, day, hour, minute, second); 53 | } 54 | else if (year < 100) 55 | { 56 | return new DateTime(1900 + year, month, day, hour, minute, second); 57 | } 58 | 59 | throw new FINSException("Invalid DateTime Values received from the PLC Clock"); 60 | } 61 | 62 | #endregion 63 | 64 | 65 | #region Structs 66 | 67 | internal struct ClockResult 68 | { 69 | internal DateTime ClockDateTime; 70 | internal byte DayOfWeek; 71 | } 72 | 73 | #endregion 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /RICADO.Omron/Responses/ReadCPUUnitDataResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using RICADO.Omron.Requests; 6 | 7 | namespace RICADO.Omron.Responses 8 | { 9 | internal class ReadCPUUnitDataResponse 10 | { 11 | #region Constants 12 | 13 | internal const int CONTROLLER_MODEL_LENGTH = 20; 14 | internal const int CONTROLLER_VERSION_LENGTH = 20; 15 | internal const int SYSTEM_RESERVED_LENGTH = 40; 16 | internal const int AREA_DATA_LENGTH = 12; 17 | 18 | #endregion 19 | 20 | 21 | #region Internal Methods 22 | 23 | internal static CPUUnitDataResult ExtractData(FINSResponse response) 24 | { 25 | int expectedLength = CONTROLLER_MODEL_LENGTH + CONTROLLER_VERSION_LENGTH + SYSTEM_RESERVED_LENGTH + AREA_DATA_LENGTH; 26 | 27 | if (response.Data.Length < expectedLength) 28 | { 29 | throw new FINSException("The Response Data Length of '" + response.Data.Length.ToString() + "' was too short - Expecting a Length of '" + expectedLength.ToString() + "'"); 30 | } 31 | 32 | ReadOnlyMemory data = response.Data; 33 | 34 | CPUUnitDataResult result = new CPUUnitDataResult(); 35 | 36 | result.ControllerModel = extractStringValue(data.Slice(0, CONTROLLER_MODEL_LENGTH).ToArray()); 37 | 38 | result.ControllerVersion = extractStringValue(data.Slice(CONTROLLER_MODEL_LENGTH, CONTROLLER_VERSION_LENGTH).ToArray()); 39 | 40 | return result; 41 | } 42 | 43 | #endregion 44 | 45 | 46 | #region Private Methods 47 | 48 | private static string extractStringValue(byte[] bytes) 49 | { 50 | List stringBytes = new List(bytes.Length); 51 | 52 | foreach(byte byteValue in bytes) 53 | { 54 | if(byteValue > 0) 55 | { 56 | stringBytes.Add(byteValue); 57 | } 58 | else 59 | { 60 | break; 61 | } 62 | } 63 | 64 | if(stringBytes.Count == 0) 65 | { 66 | return ""; 67 | } 68 | 69 | return ASCIIEncoding.ASCII.GetString(stringBytes.ToArray()).Trim(); 70 | } 71 | 72 | #endregion 73 | 74 | 75 | #region Structs 76 | 77 | internal struct CPUUnitDataResult 78 | { 79 | internal string ControllerModel; 80 | internal string ControllerVersion; 81 | } 82 | 83 | #endregion 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/WriteClockRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal class WriteClockRequest : FINSRequest 8 | { 9 | #region Private Properties 10 | 11 | private DateTime _dateTime; 12 | private byte _dayOfWeek; 13 | 14 | #endregion 15 | 16 | 17 | #region Internal Properties 18 | 19 | internal DateTime DateTime 20 | { 21 | get 22 | { 23 | return _dateTime; 24 | } 25 | set 26 | { 27 | _dateTime = value; 28 | } 29 | } 30 | 31 | internal byte DayOfWeek 32 | { 33 | get 34 | { 35 | return _dayOfWeek; 36 | } 37 | set 38 | { 39 | _dayOfWeek = value; 40 | } 41 | } 42 | 43 | #endregion 44 | 45 | 46 | #region Constructor 47 | 48 | private WriteClockRequest(OmronPLC plc) : base(plc) 49 | { 50 | } 51 | 52 | #endregion 53 | 54 | 55 | #region Internal Methods 56 | 57 | internal static WriteClockRequest CreateNew(OmronPLC plc, DateTime dateTime, byte dayOfWeek) 58 | { 59 | return new WriteClockRequest(plc) 60 | { 61 | FunctionCode = (byte)enFunctionCode.TimeData, 62 | SubFunctionCode = (byte)enTimeDataFunctionCode.WriteClock, 63 | DateTime = dateTime, 64 | DayOfWeek = dayOfWeek, 65 | }; 66 | } 67 | 68 | #endregion 69 | 70 | 71 | #region Protected Methods 72 | 73 | protected override List BuildRequestData() 74 | { 75 | List data = new List(); 76 | 77 | // Year (Last 2 Digits) 78 | data.Add(BCDConverter.GetBCDByte((byte)(_dateTime.Year % 100))); 79 | 80 | // Month 81 | data.Add(BCDConverter.GetBCDByte((byte)_dateTime.Month)); 82 | 83 | // Day 84 | data.Add(BCDConverter.GetBCDByte((byte)_dateTime.Day)); 85 | 86 | // Hour 87 | data.Add(BCDConverter.GetBCDByte((byte)_dateTime.Hour)); 88 | 89 | // Minute 90 | data.Add(BCDConverter.GetBCDByte((byte)_dateTime.Minute)); 91 | 92 | // Second 93 | data.Add(BCDConverter.GetBCDByte((byte)_dateTime.Second)); 94 | 95 | // Day of Week 96 | data.Add(BCDConverter.GetBCDByte(_dayOfWeek)); 97 | 98 | return data; 99 | } 100 | 101 | #endregion 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/ReadMemoryAreaWordRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal class ReadMemoryAreaWordRequest : FINSRequest 8 | { 9 | #region Private Properties 10 | 11 | private ushort _startAddress; 12 | private ushort _length; 13 | private enMemoryWordDataType _dataType; 14 | 15 | #endregion 16 | 17 | 18 | #region Internal Properties 19 | 20 | internal ushort StartAddress 21 | { 22 | get 23 | { 24 | return _startAddress; 25 | } 26 | set 27 | { 28 | _startAddress = value; 29 | } 30 | } 31 | 32 | internal ushort Length 33 | { 34 | get 35 | { 36 | return _length; 37 | } 38 | set 39 | { 40 | _length = value; 41 | } 42 | } 43 | 44 | internal enMemoryWordDataType DataType 45 | { 46 | get 47 | { 48 | return _dataType; 49 | } 50 | set 51 | { 52 | _dataType = value; 53 | } 54 | } 55 | 56 | #endregion 57 | 58 | 59 | #region Constructor 60 | 61 | private ReadMemoryAreaWordRequest(OmronPLC plc) : base(plc) 62 | { 63 | } 64 | 65 | #endregion 66 | 67 | 68 | #region Internal Methods 69 | 70 | internal static ReadMemoryAreaWordRequest CreateNew(OmronPLC plc, ushort startAddress, ushort length, enMemoryWordDataType dataType) 71 | { 72 | return new ReadMemoryAreaWordRequest(plc) 73 | { 74 | FunctionCode = (byte)enFunctionCode.MemoryArea, 75 | SubFunctionCode = (byte)enMemoryAreaFunctionCode.Read, 76 | StartAddress = startAddress, 77 | Length = length, 78 | DataType = dataType, 79 | }; 80 | } 81 | 82 | #endregion 83 | 84 | 85 | #region Protected Methods 86 | 87 | protected override List BuildRequestData() 88 | { 89 | List data = new List(); 90 | 91 | // Memory Area Data Type 92 | data.Add((byte)_dataType); 93 | 94 | // Address 95 | data.AddRange(BitConverter.GetBytes(_startAddress).Reverse()); 96 | 97 | // Reserved 98 | data.Add(0); 99 | 100 | // Length 101 | data.AddRange(BitConverter.GetBytes(_length).Reverse()); 102 | 103 | return data; 104 | } 105 | 106 | #endregion 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/WriteMemoryAreaWordRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal class WriteMemoryAreaWordRequest : FINSRequest 8 | { 9 | #region Private Properties 10 | 11 | private ushort _startAddress; 12 | private enMemoryWordDataType _dataType; 13 | private short[] _values; 14 | 15 | #endregion 16 | 17 | 18 | #region Internal Properties 19 | 20 | internal ushort StartAddress 21 | { 22 | get 23 | { 24 | return _startAddress; 25 | } 26 | set 27 | { 28 | _startAddress = value; 29 | } 30 | } 31 | 32 | internal enMemoryWordDataType DataType 33 | { 34 | get 35 | { 36 | return _dataType; 37 | } 38 | set 39 | { 40 | _dataType = value; 41 | } 42 | } 43 | 44 | internal short[] Values 45 | { 46 | get 47 | { 48 | return _values; 49 | } 50 | set 51 | { 52 | _values = value; 53 | } 54 | } 55 | 56 | #endregion 57 | 58 | 59 | #region Constructor 60 | 61 | private WriteMemoryAreaWordRequest(OmronPLC plc) : base(plc) 62 | { 63 | } 64 | 65 | #endregion 66 | 67 | 68 | #region Internal Methods 69 | 70 | internal static WriteMemoryAreaWordRequest CreateNew(OmronPLC plc, ushort startAddress, enMemoryWordDataType dataType, short[] values) 71 | { 72 | return new WriteMemoryAreaWordRequest(plc) 73 | { 74 | FunctionCode = (byte)enFunctionCode.MemoryArea, 75 | SubFunctionCode = (byte)enMemoryAreaFunctionCode.Write, 76 | StartAddress = startAddress, 77 | DataType = dataType, 78 | Values = values, 79 | }; 80 | } 81 | 82 | #endregion 83 | 84 | 85 | #region Protected Methods 86 | 87 | protected override List BuildRequestData() 88 | { 89 | List data = new List(); 90 | 91 | // Memory Area Data Type 92 | data.Add((byte)_dataType); 93 | 94 | // Address 95 | data.AddRange(BitConverter.GetBytes(_startAddress).Reverse()); 96 | 97 | // Reserved 98 | data.Add(0); 99 | 100 | // Length 101 | data.AddRange(BitConverter.GetBytes((ushort)_values.Length).Reverse()); 102 | 103 | // Word Values 104 | foreach(short value in _values) 105 | { 106 | data.AddRange(BitConverter.GetBytes(value).Reverse()); 107 | } 108 | 109 | return data; 110 | } 111 | 112 | #endregion 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/ReadMemoryAreaBitRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal class ReadMemoryAreaBitRequest : FINSRequest 8 | { 9 | #region Private Properties 10 | 11 | private ushort _address; 12 | private byte _startBitIndex; 13 | private ushort _length; 14 | private enMemoryBitDataType _dataType; 15 | 16 | #endregion 17 | 18 | 19 | #region Internal Properties 20 | 21 | internal ushort Address 22 | { 23 | get 24 | { 25 | return _address; 26 | } 27 | set 28 | { 29 | _address = value; 30 | } 31 | } 32 | 33 | internal byte StartBitIndex 34 | { 35 | get 36 | { 37 | return _startBitIndex; 38 | } 39 | set 40 | { 41 | _startBitIndex = value; 42 | } 43 | } 44 | 45 | internal ushort Length 46 | { 47 | get 48 | { 49 | return _length; 50 | } 51 | set 52 | { 53 | _length = value; 54 | } 55 | } 56 | 57 | internal enMemoryBitDataType DataType 58 | { 59 | get 60 | { 61 | return _dataType; 62 | } 63 | set 64 | { 65 | _dataType = value; 66 | } 67 | } 68 | 69 | #endregion 70 | 71 | 72 | #region Constructor 73 | 74 | private ReadMemoryAreaBitRequest(OmronPLC plc) : base(plc) 75 | { 76 | } 77 | 78 | #endregion 79 | 80 | 81 | #region Internal Methods 82 | 83 | internal static ReadMemoryAreaBitRequest CreateNew(OmronPLC plc, ushort address, byte startBitIndex, ushort length, enMemoryBitDataType dataType) 84 | { 85 | return new ReadMemoryAreaBitRequest(plc) 86 | { 87 | FunctionCode = (byte)enFunctionCode.MemoryArea, 88 | SubFunctionCode = (byte)enMemoryAreaFunctionCode.Read, 89 | Address = address, 90 | StartBitIndex = startBitIndex, 91 | Length = length, 92 | DataType = dataType, 93 | }; 94 | } 95 | 96 | #endregion 97 | 98 | 99 | #region Protected Methods 100 | 101 | protected override List BuildRequestData() 102 | { 103 | List data = new List(); 104 | 105 | // Memory Area Data Type 106 | data.Add((byte)_dataType); 107 | 108 | // Address 109 | data.AddRange(BitConverter.GetBytes(_address).Reverse()); 110 | 111 | // Bit Index 112 | data.Add(_startBitIndex); 113 | 114 | // Length 115 | data.AddRange(BitConverter.GetBytes(_length).Reverse()); 116 | 117 | return data; 118 | } 119 | 120 | #endregion 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/WriteMemoryAreaBitRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal class WriteMemoryAreaBitRequest : FINSRequest 8 | { 9 | #region Private Properties 10 | 11 | private ushort _address; 12 | private byte _startBitIndex; 13 | private enMemoryBitDataType _dataType; 14 | private bool[] _values; 15 | 16 | #endregion 17 | 18 | 19 | #region Internal Properties 20 | 21 | internal ushort Address 22 | { 23 | get 24 | { 25 | return _address; 26 | } 27 | set 28 | { 29 | _address = value; 30 | } 31 | } 32 | 33 | internal byte StartBitIndex 34 | { 35 | get 36 | { 37 | return _startBitIndex; 38 | } 39 | set 40 | { 41 | _startBitIndex = value; 42 | } 43 | } 44 | 45 | internal enMemoryBitDataType DataType 46 | { 47 | get 48 | { 49 | return _dataType; 50 | } 51 | set 52 | { 53 | _dataType = value; 54 | } 55 | } 56 | 57 | internal bool[] Values 58 | { 59 | get 60 | { 61 | return _values; 62 | } 63 | set 64 | { 65 | _values = value; 66 | } 67 | } 68 | 69 | #endregion 70 | 71 | 72 | #region Constructor 73 | 74 | private WriteMemoryAreaBitRequest(OmronPLC plc) : base(plc) 75 | { 76 | } 77 | 78 | #endregion 79 | 80 | 81 | #region Internal Methods 82 | 83 | internal static WriteMemoryAreaBitRequest CreateNew(OmronPLC plc, ushort address, byte startBitIndex, enMemoryBitDataType dataType, bool[] values) 84 | { 85 | return new WriteMemoryAreaBitRequest(plc) 86 | { 87 | FunctionCode = (byte)enFunctionCode.MemoryArea, 88 | SubFunctionCode = (byte)enMemoryAreaFunctionCode.Write, 89 | Address = address, 90 | StartBitIndex = startBitIndex, 91 | DataType = dataType, 92 | Values = values, 93 | }; 94 | } 95 | 96 | #endregion 97 | 98 | 99 | #region Protected Methods 100 | 101 | protected override List BuildRequestData() 102 | { 103 | List data = new List(); 104 | 105 | // Memory Area Data Type 106 | data.Add((byte)_dataType); 107 | 108 | // Address 109 | data.AddRange(BitConverter.GetBytes(_address).Reverse()); 110 | 111 | // Bit Index 112 | data.Add(_startBitIndex); 113 | 114 | // Length 115 | data.AddRange(BitConverter.GetBytes((ushort)_values.Length).Reverse()); 116 | 117 | // Bit Values 118 | data.AddRange(_values.Select(value => value == true ? (byte)1 : (byte)0)); 119 | 120 | return data; 121 | } 122 | 123 | #endregion 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /RICADO.Omron/Enums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace RICADO.Omron 8 | { 9 | public enum enConnectionMethod 10 | { 11 | TCP, 12 | UDP, 13 | } 14 | 15 | public enum enPLCType 16 | { 17 | NJ101, 18 | NJ301, 19 | NJ501, 20 | NX1P2, 21 | NX102, 22 | NX701, 23 | NY512, 24 | NY532, 25 | NJ_NX_NY_Series, 26 | CJ2, 27 | CP1, 28 | C_Series, 29 | Unknown, 30 | } 31 | 32 | public enum enMemoryBitDataType : byte 33 | { 34 | DataMemory = 0x2, 35 | CommonIO = 0x30, 36 | Work = 0x31, 37 | Holding = 0x32, 38 | Auxiliary = 0x33, 39 | } 40 | 41 | public enum enMemoryWordDataType : byte 42 | { 43 | DataMemory = 0x82, 44 | CommonIO = 0xB0, 45 | Work = 0xB1, 46 | Holding = 0xB2, 47 | Auxiliary = 0xB3, 48 | } 49 | 50 | internal enum enFunctionCode : byte 51 | { 52 | MemoryArea = 0x01, 53 | ParameterArea = 0x02, 54 | ProgramArea = 0x03, 55 | OperatingMode = 0x04, 56 | MachineConfiguration = 0x05, 57 | Status = 0x06, 58 | TimeData = 0x07, 59 | MessageDisplay = 0x09, 60 | AccessRights = 0x0C, 61 | ErrorLog = 0x21, 62 | FINSWriteLog = 0x21, 63 | FileMemory = 0x22, 64 | Debugging = 0x23, 65 | SerialGateway = 0x28, 66 | } 67 | 68 | internal enum enMemoryAreaFunctionCode : byte 69 | { 70 | Read = 0x01, 71 | Write = 0x02, 72 | Fill = 0x03, 73 | MultipleRead = 0x04, 74 | Transfer = 0x05, 75 | } 76 | 77 | internal enum enParameterAreaFunctionCode : byte 78 | { 79 | Read = 0x01, 80 | Write = 0x02, 81 | Fill = 0x03, 82 | } 83 | 84 | internal enum enProgramAreaFunctionCode : byte 85 | { 86 | Read = 0x06, 87 | Write = 0x07, 88 | Clear = 0x08, 89 | } 90 | 91 | internal enum enOperatingModeFunctionCode : byte 92 | { 93 | RunMode = 0x01, 94 | StopMode = 0x02, 95 | } 96 | 97 | internal enum enMachineConfigurationFunctionCode : byte 98 | { 99 | ReadCPUUnitData = 0x01, 100 | ReadConnectionData = 0x02, 101 | } 102 | 103 | internal enum enStatusFunctionCode : byte 104 | { 105 | ReadCPUUnitStatus = 0x01, 106 | ReadCycleTime = 0x20, 107 | } 108 | 109 | internal enum enTimeDataFunctionCode : byte 110 | { 111 | ReadClock = 0x01, 112 | WriteClock = 0x02, 113 | } 114 | 115 | internal enum enMessageDisplayFunctionCode : byte 116 | { 117 | Read = 0x20, 118 | } 119 | 120 | internal enum enAccessRightsFunctionCode : byte 121 | { 122 | Acquire = 0x01, 123 | ForcedAcquire = 0x02, 124 | Release = 0x03, 125 | } 126 | 127 | internal enum enErrorLogFunctionCode : byte 128 | { 129 | ClearMessages = 0x01, 130 | Read = 0x02, 131 | ClearLog = 0x03, 132 | } 133 | 134 | internal enum enFinsWriteLogFunctionCode : byte 135 | { 136 | Read = 0x40, 137 | Clear = 0x41, 138 | } 139 | 140 | internal enum enFileMemoryFunctionCode : byte 141 | { 142 | ReadFileName = 0x01, 143 | ReadSingleFile = 0x02, 144 | WriteSingleFile = 0x03, 145 | FormatMemory = 0x04, 146 | DeleteFile = 0x05, 147 | CopyFile = 0x07, 148 | ChangeFileName = 0x08, 149 | MemoryAreaTransfer = 0x0A, 150 | ParameterAreaTransfer = 0x0B, 151 | ProgramAreaTransfer = 0x0C, 152 | CreateOrDeleteDirectory = 0x15, 153 | } 154 | 155 | internal enum enDebuggingFunctionCode : byte 156 | { 157 | ForceBits = 0x01, 158 | ClearForcedBits = 0x02, 159 | } 160 | 161 | internal enum enSerialGatewayFunctionCode : byte 162 | { 163 | ConvertToCompoWayFCommand = 0x03, 164 | ConvertToModbusRTUCommand = 0x04, 165 | ConvertToModbusASCIICommand = 0x05, 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /RICADO.Omron/Requests/FINSRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using RICADO.Omron.Channels; 4 | 5 | namespace RICADO.Omron.Requests 6 | { 7 | internal abstract class FINSRequest 8 | { 9 | #region Constants 10 | 11 | internal const int HEADER_LENGTH = 10; 12 | internal const int COMMAND_LENGTH = 2; 13 | 14 | #endregion 15 | 16 | 17 | #region Private Properties 18 | 19 | private byte _localNodeId; 20 | private byte _remoteNodeId; 21 | 22 | private byte _serviceId; 23 | 24 | private byte _functionCode; 25 | private byte _subFunctionCode; 26 | 27 | #endregion 28 | 29 | 30 | #region Internal Properties 31 | 32 | internal byte LocalNodeID => _localNodeId; 33 | 34 | internal byte RemoteNodeID => _remoteNodeId; 35 | 36 | internal byte ServiceID => _serviceId; 37 | 38 | internal byte FunctionCode 39 | { 40 | get 41 | { 42 | return _functionCode; 43 | } 44 | set 45 | { 46 | _functionCode = value; 47 | } 48 | } 49 | 50 | internal byte SubFunctionCode 51 | { 52 | get 53 | { 54 | return _subFunctionCode; 55 | } 56 | set 57 | { 58 | _subFunctionCode = value; 59 | } 60 | } 61 | 62 | #endregion 63 | 64 | 65 | #region Constructors 66 | 67 | protected FINSRequest(OmronPLC plc) 68 | { 69 | if(plc.Channel is EthernetTCPChannel) 70 | { 71 | _localNodeId = (plc.Channel as EthernetTCPChannel).LocalNodeID; 72 | _remoteNodeId = (plc.Channel as EthernetTCPChannel).RemoteNodeID; 73 | } 74 | else 75 | { 76 | _localNodeId = plc.LocalNodeID; 77 | _remoteNodeId = plc.RemoteNodeID; 78 | } 79 | } 80 | 81 | #endregion 82 | 83 | 84 | #region Internal Methods 85 | 86 | internal ReadOnlyMemory BuildMessage(byte requestId) 87 | { 88 | _serviceId = requestId; 89 | 90 | List message = new List(); 91 | 92 | /** 93 | * Header Section 94 | */ 95 | 96 | // Information Control Field 97 | message.Add(0x80); 98 | 99 | // Reserved by System 100 | message.Add(0); 101 | 102 | // Permissible Number of Gateways 103 | message.Add(0x02); 104 | 105 | // Destination Network Address 106 | message.Add(0); // Local Network 107 | 108 | // Destination Node Address 109 | // 0 = Local PLC Unit 110 | // 1 to 254 = Destination Node Address 111 | // 255 = Broadcasting 112 | message.Add(_remoteNodeId); 113 | 114 | // Destination Unit Address 115 | message.Add(0); // PLC (CPU Unit) 116 | 117 | // Source Network Address 118 | message.Add(0); // Local Network 119 | 120 | // Source Node Address 121 | message.Add(_localNodeId); // Local Server 122 | 123 | // Source Unit Address 124 | message.Add(0); 125 | 126 | // Service ID 127 | message.Add(_serviceId); 128 | 129 | 130 | /** 131 | * Command Section 132 | */ 133 | 134 | // Main Function Code 135 | message.Add(_functionCode); 136 | 137 | // Sub Function Code 138 | message.Add(_subFunctionCode); 139 | 140 | 141 | /** 142 | * Data Section 143 | */ 144 | 145 | // Request Data 146 | message.AddRange(BuildRequestData()); 147 | 148 | 149 | return new ReadOnlyMemory(message.ToArray()); 150 | } 151 | 152 | #endregion 153 | 154 | 155 | #region Protected Methods 156 | 157 | protected abstract List BuildRequestData(); 158 | 159 | #endregion 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /RICADO.Omron/Channels/EthernetChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using RICADO.Omron.Requests; 5 | using RICADO.Omron.Responses; 6 | 7 | namespace RICADO.Omron.Channels 8 | { 9 | internal abstract class EthernetChannel : IDisposable 10 | { 11 | #region Private Properties 12 | 13 | private string _remoteHost; 14 | private int _port; 15 | 16 | private byte _requestId = 0; 17 | 18 | private readonly SemaphoreSlim _semaphore; 19 | 20 | #endregion 21 | 22 | 23 | #region Protected Properties 24 | 25 | protected SemaphoreSlim Semaphore => _semaphore; 26 | 27 | #endregion 28 | 29 | 30 | #region Internal Properties 31 | 32 | internal string RemoteHost 33 | { 34 | get 35 | { 36 | return _remoteHost; 37 | } 38 | } 39 | 40 | internal int Port 41 | { 42 | get 43 | { 44 | return _port; 45 | } 46 | } 47 | 48 | #endregion 49 | 50 | 51 | #region Constructors 52 | 53 | internal EthernetChannel(string remoteHost, int port) 54 | { 55 | _remoteHost = remoteHost; 56 | _port = port; 57 | 58 | _semaphore = new SemaphoreSlim(1, 1); 59 | } 60 | 61 | #endregion 62 | 63 | 64 | #region Public Methods 65 | 66 | public virtual void Dispose() 67 | { 68 | _semaphore?.Dispose(); 69 | } 70 | 71 | #endregion 72 | 73 | 74 | #region Internal Methods 75 | 76 | internal abstract Task InitializeAsync(int timeout, CancellationToken cancellationToken); 77 | 78 | internal async Task ProcessRequestAsync(FINSRequest request, int timeout, int retries, CancellationToken cancellationToken) 79 | { 80 | int attempts = 0; 81 | Memory responseMessage = new Memory(); 82 | int bytesSent = 0; 83 | int packetsSent = 0; 84 | int bytesReceived = 0; 85 | int packetsReceived = 0; 86 | DateTime startTimestamp = DateTime.UtcNow; 87 | 88 | while (attempts <= retries) 89 | { 90 | if (!_semaphore.Wait(0)) 91 | { 92 | await _semaphore.WaitAsync(cancellationToken); 93 | } 94 | 95 | try 96 | { 97 | if (attempts > 0) 98 | { 99 | await DestroyAndInitializeClient(timeout, cancellationToken); 100 | } 101 | 102 | // Build the Request into a Message we can Send 103 | ReadOnlyMemory requestMessage = request.BuildMessage(getNextRequestId()); 104 | 105 | // Send the Message 106 | SendMessageResult sendResult = await SendMessageAsync(requestMessage, timeout, cancellationToken); 107 | 108 | bytesSent += sendResult.Bytes; 109 | packetsSent += sendResult.Packets; 110 | 111 | // Receive a Response 112 | ReceiveMessageResult receiveResult = await ReceiveMessageAsync(timeout, cancellationToken); 113 | 114 | bytesReceived += receiveResult.Bytes; 115 | packetsReceived += receiveResult.Packets; 116 | responseMessage = receiveResult.Message; 117 | 118 | break; 119 | } 120 | catch (Exception) 121 | { 122 | if(attempts >= retries) 123 | { 124 | throw; 125 | } 126 | } 127 | finally 128 | { 129 | _semaphore.Release(); 130 | } 131 | 132 | // Increment the Attempts 133 | attempts++; 134 | } 135 | 136 | try 137 | { 138 | return new ProcessRequestResult 139 | { 140 | BytesSent = bytesSent, 141 | PacketsSent = packetsSent, 142 | BytesReceived = bytesReceived, 143 | PacketsReceived = packetsReceived, 144 | Duration = DateTime.UtcNow.Subtract(startTimestamp).TotalMilliseconds, 145 | Response = FINSResponse.CreateNew(responseMessage, request), 146 | }; 147 | } 148 | catch (FINSException e) 149 | { 150 | if(e.Message.Contains("Service ID") && responseMessage.Length >= 9 && responseMessage.Span[9] != request.ServiceID) 151 | { 152 | if (!_semaphore.Wait(0)) 153 | { 154 | await _semaphore.WaitAsync(cancellationToken); 155 | } 156 | 157 | try 158 | { 159 | await PurgeReceiveBuffer(timeout, cancellationToken); 160 | } 161 | catch 162 | { 163 | } 164 | finally 165 | { 166 | _semaphore.Release(); 167 | } 168 | } 169 | 170 | throw new OmronException("Received a FINS Error Response from Omron PLC '" + _remoteHost + ":" + _port + "'", e); 171 | } 172 | } 173 | 174 | #endregion 175 | 176 | 177 | #region Protected Methods 178 | 179 | protected abstract Task DestroyAndInitializeClient(int timeout, CancellationToken cancellationToken); 180 | 181 | protected abstract Task SendMessageAsync(ReadOnlyMemory message, int timeout, CancellationToken cancellationToken); 182 | 183 | protected abstract Task ReceiveMessageAsync(int timeout, CancellationToken cancellationToken); 184 | 185 | protected abstract Task PurgeReceiveBuffer(int timeout, CancellationToken cancellationToken); 186 | 187 | #endregion 188 | 189 | 190 | #region Private Methods 191 | 192 | private byte getNextRequestId() 193 | { 194 | if (_requestId == byte.MaxValue) 195 | { 196 | _requestId = byte.MinValue; 197 | } 198 | else 199 | { 200 | _requestId++; 201 | } 202 | 203 | return _requestId; 204 | } 205 | 206 | #endregion 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /RICADO.Omron/BCDConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace RICADO.Omron 8 | { 9 | public static class BCDConverter 10 | { 11 | #region Public Methods 12 | 13 | public static byte ToByte(byte bcdByte) 14 | { 15 | return convertToBinaryBytes(new byte[] { bcdByte })[0]; 16 | } 17 | 18 | public static short ToInt16(short bcdWord) 19 | { 20 | return ToInt16(BitConverter.GetBytes(bcdWord)); 21 | } 22 | 23 | public static short ToInt16(byte[] bcdBytes) 24 | { 25 | if(bcdBytes.Length != 2) 26 | { 27 | throw new ArgumentOutOfRangeException(nameof(bcdBytes), "The BCD Bytes Array Length must be '2' for conversion to Int16"); 28 | } 29 | 30 | return BitConverter.ToInt16(convertToBinaryBytes(bcdBytes)); 31 | } 32 | 33 | public static ushort ToUInt16(short bcdWord) 34 | { 35 | return ToUInt16(BitConverter.GetBytes(bcdWord)); 36 | } 37 | 38 | public static ushort ToUInt16(byte[] bcdBytes) 39 | { 40 | if (bcdBytes.Length != 2) 41 | { 42 | throw new ArgumentOutOfRangeException(nameof(bcdBytes), "The BCD Bytes Array Length must be '2' for conversion to UInt16"); 43 | } 44 | 45 | return BitConverter.ToUInt16(convertToBinaryBytes(bcdBytes)); 46 | } 47 | 48 | public static int ToInt32(short bcdWord1, short bcdWord2) 49 | { 50 | List integerBytes = new List(4); 51 | 52 | integerBytes.AddRange(BitConverter.GetBytes(bcdWord1)); 53 | integerBytes.AddRange(BitConverter.GetBytes(bcdWord2)); 54 | 55 | return ToInt32(integerBytes.ToArray()); 56 | } 57 | 58 | public static int ToInt32(byte[] bcdBytes) 59 | { 60 | if (bcdBytes.Length != 4) 61 | { 62 | throw new ArgumentOutOfRangeException(nameof(bcdBytes), "The BCD Bytes Array Length must be '4' for conversion to Int32"); 63 | } 64 | 65 | return BitConverter.ToInt32(convertToBinaryBytes(bcdBytes)); 66 | } 67 | 68 | public static uint ToUInt32(short bcdWord1, short bcdWord2) 69 | { 70 | List integerBytes = new List(4); 71 | 72 | integerBytes.AddRange(BitConverter.GetBytes(bcdWord1)); 73 | integerBytes.AddRange(BitConverter.GetBytes(bcdWord2)); 74 | 75 | return ToUInt32(integerBytes.ToArray()); 76 | } 77 | 78 | public static uint ToUInt32(byte[] bcdBytes) 79 | { 80 | if (bcdBytes.Length != 4) 81 | { 82 | throw new ArgumentOutOfRangeException(nameof(bcdBytes), "The BCD Bytes Array Length must be '4' for conversion to UInt32"); 83 | } 84 | 85 | return BitConverter.ToUInt32(convertToBinaryBytes(bcdBytes)); 86 | } 87 | 88 | public static byte GetBCDByte(byte binaryValue) 89 | { 90 | return convertToBCDBytes(binaryValue, 1)[0]; 91 | } 92 | 93 | public static short GetBCDWord(short binaryValue) 94 | { 95 | return BitConverter.ToInt16(convertToBCDBytes(binaryValue, 2)); 96 | } 97 | 98 | public static short GetBCDWord(ushort binaryValue) 99 | { 100 | return BitConverter.ToInt16(convertToBCDBytes(binaryValue, 2)); 101 | } 102 | 103 | public static short[] GetBCDWords(int binaryValue) 104 | { 105 | ReadOnlyMemory bcdBytes = convertToBCDBytes(binaryValue, 4); 106 | 107 | return new short[] { BitConverter.ToInt16(bcdBytes.Slice(0, 2).ToArray()), BitConverter.ToInt16(bcdBytes.Slice(2, 2).ToArray()) }; 108 | } 109 | 110 | public static short[] GetBCDWords(uint binaryValue) 111 | { 112 | ReadOnlyMemory bcdBytes = convertToBCDBytes(binaryValue, 4); 113 | 114 | return new short[] { BitConverter.ToInt16(bcdBytes.Slice(0, 2).ToArray()), BitConverter.ToInt16(bcdBytes.Slice(2, 2).ToArray()) }; 115 | } 116 | 117 | public static byte[] GetBCDBytes(short binaryValue) 118 | { 119 | return convertToBCDBytes(binaryValue, 2); 120 | } 121 | 122 | public static byte[] GetBCDBytes(ushort binaryValue) 123 | { 124 | return convertToBCDBytes(binaryValue, 2); 125 | } 126 | 127 | public static byte[] GetBCDBytes(int binaryValue) 128 | { 129 | return convertToBCDBytes(binaryValue, 4); 130 | } 131 | 132 | public static byte[] GetBCDBytes(uint binaryValue) 133 | { 134 | return convertToBCDBytes(binaryValue, 4); 135 | } 136 | 137 | #endregion 138 | 139 | 140 | #region Private Methods 141 | 142 | private static byte[] convertToBinaryBytes(byte[] bcdBytes) 143 | { 144 | if (bcdBytes.Length == 0) 145 | { 146 | throw new ArgumentOutOfRangeException(nameof(bcdBytes), "The BCD Bytes Length cannot be Zero"); 147 | } 148 | 149 | if (bcdBytes.Length > 4) 150 | { 151 | throw new ArgumentOutOfRangeException(nameof(bcdBytes), "The BCD Bytes Length cannot be greater than 4"); 152 | } 153 | 154 | long binaryValue = 0; 155 | 156 | foreach(byte bcdByte in bcdBytes.Reverse()) 157 | { 158 | binaryValue *= 100; 159 | binaryValue += (long)(10 * (bcdByte >> 4)); 160 | binaryValue += (long)(bcdByte & 0xF); 161 | } 162 | 163 | ReadOnlyMemory binaryBytes = BitConverter.GetBytes(binaryValue); 164 | 165 | return binaryBytes.Slice(0, bcdBytes.Length).ToArray(); 166 | } 167 | 168 | private static byte[] convertToBCDBytes(long binaryValue, int byteLength) 169 | { 170 | byte[] bcdBytes = new byte[byteLength]; 171 | 172 | for(int i = 0; i < bcdBytes.Length; i++) 173 | { 174 | long lowDigit = binaryValue % 10; 175 | long highDigit = (binaryValue % 100) - lowDigit; 176 | 177 | if(highDigit != 0) 178 | { 179 | highDigit /= 10; 180 | } 181 | 182 | lowDigit = lowDigit < 0 ? -lowDigit : lowDigit; 183 | highDigit = highDigit < 0 ? -highDigit : highDigit; 184 | 185 | bcdBytes[i] = (byte)((highDigit << 4) | lowDigit); 186 | 187 | if(binaryValue == 0) 188 | { 189 | break; 190 | } 191 | else 192 | { 193 | binaryValue /= 100; 194 | } 195 | } 196 | 197 | return bcdBytes; 198 | } 199 | 200 | #endregion 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /RICADO.Omron/Channels/EthernetUDPChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using RICADO.Sockets; 7 | using RICADO.Omron.Responses; 8 | 9 | namespace RICADO.Omron.Channels 10 | { 11 | internal class EthernetUDPChannel : EthernetChannel 12 | { 13 | #region Private Properties 14 | 15 | private UdpClient _client; 16 | 17 | #endregion 18 | 19 | 20 | #region Constructors 21 | 22 | internal EthernetUDPChannel(string remoteHost, int port) : base(remoteHost, port) 23 | { 24 | } 25 | 26 | #endregion 27 | 28 | 29 | #region Public Methods 30 | 31 | public override void Dispose() 32 | { 33 | try 34 | { 35 | _client?.Dispose(); 36 | } 37 | catch 38 | { 39 | } 40 | finally 41 | { 42 | _client = null; 43 | } 44 | } 45 | 46 | 47 | #endregion 48 | 49 | 50 | #region Internal Methods 51 | 52 | internal override async Task InitializeAsync(int timeout, CancellationToken cancellationToken) 53 | { 54 | if (!Semaphore.Wait(0)) 55 | { 56 | await Semaphore.WaitAsync(cancellationToken); 57 | } 58 | 59 | try 60 | { 61 | destroyClient(); 62 | 63 | await initializeClient(timeout, cancellationToken); 64 | } 65 | finally 66 | { 67 | Semaphore.Release(); 68 | } 69 | } 70 | 71 | #endregion 72 | 73 | 74 | #region Protected Methods 75 | 76 | protected override async Task DestroyAndInitializeClient(int timeout, CancellationToken cancellationToken) 77 | { 78 | destroyClient(); 79 | 80 | try 81 | { 82 | await initializeClient(timeout, cancellationToken); 83 | } 84 | catch (ObjectDisposedException) 85 | { 86 | throw new OmronException("Failed to Re-Connect to Omron PLC '" + RemoteHost + ":" + Port + "' - The underlying Socket Connection has been Closed"); 87 | } 88 | catch (TimeoutException) 89 | { 90 | throw new OmronException("Failed to Re-Connect within the Timeout Period to Omron PLC '" + RemoteHost + ":" + Port + "'"); 91 | } 92 | catch (System.Net.Sockets.SocketException e) 93 | { 94 | throw new OmronException("Failed to Re-Connect to Omron PLC '" + RemoteHost + ":" + Port + "'", e); 95 | } 96 | } 97 | 98 | protected override async Task SendMessageAsync(ReadOnlyMemory message, int timeout, CancellationToken cancellationToken) 99 | { 100 | SendMessageResult result = new SendMessageResult 101 | { 102 | Bytes = 0, 103 | Packets = 0, 104 | }; 105 | 106 | try 107 | { 108 | result.Bytes += await _client.SendAsync(message, timeout, cancellationToken); 109 | result.Packets += 1; 110 | } 111 | catch (ObjectDisposedException) 112 | { 113 | throw new OmronException("Failed to Send FINS Message to Omron PLC '" + RemoteHost + ":" + Port + "' - The underlying Socket Connection has been Closed"); 114 | } 115 | catch (TimeoutException) 116 | { 117 | throw new OmronException("Failed to Send FINS Message within the Timeout Period to Omron PLC '" + RemoteHost + ":" + Port + "'"); 118 | } 119 | catch (System.Net.Sockets.SocketException e) 120 | { 121 | throw new OmronException("Failed to Send FINS Message to Omron PLC '" + RemoteHost + ":" + Port + "'", e); 122 | } 123 | 124 | return result; 125 | } 126 | 127 | protected override async Task ReceiveMessageAsync(int timeout, CancellationToken cancellationToken) 128 | { 129 | ReceiveMessageResult result = new ReceiveMessageResult 130 | { 131 | Bytes = 0, 132 | Packets = 0, 133 | Message = new Memory(), 134 | }; 135 | 136 | try 137 | { 138 | List receivedData = new List(); 139 | DateTime startTimestamp = DateTime.UtcNow; 140 | 141 | while(DateTime.UtcNow.Subtract(startTimestamp).TotalMilliseconds < timeout && receivedData.Count < FINSResponse.HEADER_LENGTH + FINSResponse.COMMAND_LENGTH + FINSResponse.RESPONSE_CODE_LENGTH) 142 | { 143 | Memory buffer = new byte[4096]; 144 | TimeSpan receiveTimeout = TimeSpan.FromMilliseconds(timeout).Subtract(DateTime.UtcNow.Subtract(startTimestamp)); 145 | 146 | if (receiveTimeout.TotalMilliseconds >= 50) 147 | { 148 | int receivedBytes = await _client.ReceiveAsync(buffer, receiveTimeout, cancellationToken); 149 | 150 | if(receivedBytes > 0) 151 | { 152 | receivedData.AddRange(buffer.Slice(0, receivedBytes).ToArray()); 153 | 154 | result.Bytes += receivedBytes; 155 | result.Packets += 1; 156 | } 157 | } 158 | } 159 | 160 | if(receivedData.Count == 0) 161 | { 162 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - No Data was Received"); 163 | } 164 | 165 | if(receivedData.Count < FINSResponse.HEADER_LENGTH + FINSResponse.COMMAND_LENGTH + FINSResponse.RESPONSE_CODE_LENGTH) 166 | { 167 | throw new OmronException("Failed to Receive FINS Message within the Timeout Period from Omron PLC '" + RemoteHost + ":" + Port + "'"); 168 | } 169 | 170 | if(receivedData[0] != 0xC0 && receivedData[0] != 0xC1) 171 | { 172 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - The FINS Header was Invalid"); 173 | } 174 | 175 | result.Message = receivedData.ToArray(); 176 | } 177 | catch (ObjectDisposedException) 178 | { 179 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - The underlying Socket Connection has been Closed"); 180 | } 181 | catch (TimeoutException) 182 | { 183 | throw new OmronException("Failed to Receive FINS Message within the Timeout Period from Omron PLC '" + RemoteHost + ":" + Port + "'"); 184 | } 185 | catch (System.Net.Sockets.SocketException e) 186 | { 187 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "'", e); 188 | } 189 | 190 | return result; 191 | } 192 | 193 | protected override async Task PurgeReceiveBuffer(int timeout, CancellationToken cancellationToken) 194 | { 195 | try 196 | { 197 | if (_client.Available == 0) 198 | { 199 | await Task.Delay(timeout / 4); 200 | } 201 | 202 | DateTime startTimestamp = DateTime.UtcNow; 203 | Memory buffer = new byte[2000]; 204 | 205 | while (_client.Available > 0 && DateTime.UtcNow.Subtract(startTimestamp).TotalMilliseconds < timeout) 206 | { 207 | try 208 | { 209 | await _client.ReceiveAsync(buffer, timeout, cancellationToken); 210 | } 211 | catch 212 | { 213 | return; 214 | } 215 | } 216 | } 217 | catch 218 | { 219 | } 220 | } 221 | 222 | #endregion 223 | 224 | 225 | #region Private Methods 226 | 227 | private Task initializeClient(int timeout, CancellationToken cancellationToken) 228 | { 229 | _client = new UdpClient(RemoteHost, Port); 230 | 231 | return Task.CompletedTask; 232 | } 233 | 234 | private void destroyClient() 235 | { 236 | try 237 | { 238 | _client?.Dispose(); 239 | } 240 | finally 241 | { 242 | _client = null; 243 | } 244 | } 245 | 246 | #endregion 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /RICADO.Omron/Channels/EthernetTCPChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using RICADO.Sockets; 7 | using RICADO.Omron.Responses; 8 | 9 | namespace RICADO.Omron.Channels 10 | { 11 | internal class EthernetTCPChannel : EthernetChannel 12 | { 13 | #region Enums 14 | 15 | internal enum enTCPCommandCode : byte 16 | { 17 | NodeAddressToPLC = 0, 18 | NodeAddressFromPLC = 1, 19 | FINSFrame = 2, 20 | } 21 | 22 | #endregion 23 | 24 | 25 | #region Constants 26 | 27 | internal const int TCP_HEADER_LENGTH = 16; 28 | 29 | #endregion 30 | 31 | 32 | #region Private Properties 33 | 34 | private TcpClient _client; 35 | 36 | private byte _localNodeId; 37 | private byte _remoteNodeId; 38 | 39 | #endregion 40 | 41 | 42 | #region Internal Properties 43 | 44 | internal byte LocalNodeID => _localNodeId; 45 | 46 | internal byte RemoteNodeID => _remoteNodeId; 47 | 48 | #endregion 49 | 50 | 51 | #region Constructors 52 | 53 | internal EthernetTCPChannel(string remoteHost, int port) : base(remoteHost, port) 54 | { 55 | } 56 | 57 | #endregion 58 | 59 | 60 | #region Public Methods 61 | 62 | public override void Dispose() 63 | { 64 | try 65 | { 66 | _client?.Dispose(); 67 | } 68 | catch 69 | { 70 | } 71 | finally 72 | { 73 | _client = null; 74 | } 75 | } 76 | 77 | #endregion 78 | 79 | 80 | #region Internal Methods 81 | 82 | internal override async Task InitializeAsync(int timeout, CancellationToken cancellationToken) 83 | { 84 | if (!Semaphore.Wait(0)) 85 | { 86 | await Semaphore.WaitAsync(cancellationToken); 87 | } 88 | 89 | try 90 | { 91 | destroyClient(); 92 | 93 | await initializeClient(timeout, cancellationToken); 94 | } 95 | finally 96 | { 97 | Semaphore.Release(); 98 | } 99 | } 100 | 101 | #endregion 102 | 103 | 104 | #region Protected Methods 105 | 106 | protected override async Task DestroyAndInitializeClient(int timeout, CancellationToken cancellationToken) 107 | { 108 | destroyClient(); 109 | 110 | try 111 | { 112 | await initializeClient(timeout, cancellationToken); 113 | } 114 | catch (ObjectDisposedException) 115 | { 116 | throw new OmronException("Failed to Re-Connect to Omron PLC '" + RemoteHost + ":" + Port + "' - The underlying Socket Connection was Closed"); 117 | } 118 | catch (TimeoutException) 119 | { 120 | throw new OmronException("Failed to Re-Connect within the Timeout Period to Omron PLC '" + RemoteHost + ":" + Port + "'"); 121 | } 122 | catch (System.Net.Sockets.SocketException e) 123 | { 124 | throw new OmronException("Failed to Re-Connect to Omron PLC '" + RemoteHost + ":" + Port + "'", e); 125 | } 126 | } 127 | 128 | protected override Task SendMessageAsync(ReadOnlyMemory message, int timeout, CancellationToken cancellationToken) 129 | { 130 | return sendMessageAsync(enTCPCommandCode.FINSFrame, message, timeout, cancellationToken); 131 | } 132 | 133 | protected override Task ReceiveMessageAsync(int timeout, CancellationToken cancellationToken) 134 | { 135 | return receiveMessageAsync(enTCPCommandCode.FINSFrame, timeout, cancellationToken); 136 | } 137 | 138 | protected override async Task PurgeReceiveBuffer(int timeout, CancellationToken cancellationToken) 139 | { 140 | try 141 | { 142 | if (_client.Connected == false) 143 | { 144 | return; 145 | } 146 | 147 | if(_client.Available == 0) 148 | { 149 | await Task.Delay(timeout / 4); 150 | } 151 | 152 | DateTime startTimestamp = DateTime.UtcNow; 153 | Memory buffer = new byte[2000]; 154 | 155 | while (_client.Connected && _client.Available > 0 && DateTime.UtcNow.Subtract(startTimestamp).TotalMilliseconds < timeout) 156 | { 157 | try 158 | { 159 | await _client.ReceiveAsync(buffer, timeout, cancellationToken); 160 | } 161 | catch 162 | { 163 | return; 164 | } 165 | } 166 | } 167 | catch 168 | { 169 | } 170 | } 171 | 172 | #endregion 173 | 174 | 175 | #region Private Methods 176 | 177 | private async Task initializeClient(int timeout, CancellationToken cancellationToken) 178 | { 179 | _client = new TcpClient(RemoteHost, Port); 180 | 181 | await _client.ConnectAsync(timeout, cancellationToken); 182 | 183 | try 184 | { 185 | // Send Auto-Assign Client Node Request 186 | SendMessageResult sendResult = await sendMessageAsync(enTCPCommandCode.NodeAddressToPLC, new byte[4], timeout, cancellationToken); 187 | 188 | // Receive Client Node ID 189 | ReceiveMessageResult receiveResult = await receiveMessageAsync(enTCPCommandCode.NodeAddressFromPLC, timeout, cancellationToken); 190 | 191 | if(receiveResult.Message.Length < 8) 192 | { 193 | throw new OmronException("Failed to Negotiate a TCP Connection with Omron PLC '" + RemoteHost + ":" + Port + "' - TCP Negotiation Message Length was too Short"); 194 | } 195 | 196 | byte[] tcpNegotiationMessage = receiveResult.Message.Slice(0, 8).ToArray(); 197 | 198 | if(tcpNegotiationMessage[3] == 0 || tcpNegotiationMessage[3] == 255) 199 | { 200 | throw new OmronException("Failed to Negotiate a TCP Connection with Omron PLC '" + RemoteHost + ":" + Port + "' - TCP Negotiation Message contained an Invalid Local Node ID"); 201 | } 202 | 203 | _localNodeId = tcpNegotiationMessage[3]; 204 | 205 | if (tcpNegotiationMessage[7] == 0 || tcpNegotiationMessage[7] == 255) 206 | { 207 | throw new OmronException("Failed to Negotiate a TCP Connection with Omron PLC '" + RemoteHost + ":" + Port + "' - TCP Negotiation Message contained an Invalid Remote Node ID"); 208 | } 209 | 210 | _remoteNodeId = tcpNegotiationMessage[7]; 211 | } 212 | catch (OmronException e) 213 | { 214 | throw new OmronException("Failed to Negotiate a TCP Connection with Omron PLC '" + RemoteHost + ":" + Port + "'", e); 215 | } 216 | } 217 | 218 | private void destroyClient() 219 | { 220 | try 221 | { 222 | _client?.Dispose(); 223 | } 224 | finally 225 | { 226 | _client = null; 227 | } 228 | } 229 | 230 | private async Task sendMessageAsync(enTCPCommandCode command, ReadOnlyMemory message, int timeout, CancellationToken cancellationToken) 231 | { 232 | SendMessageResult result = new SendMessageResult 233 | { 234 | Bytes = 0, 235 | Packets = 0, 236 | }; 237 | 238 | ReadOnlyMemory tcpMessage = buildFinsTcpMessage(command, message); 239 | 240 | try 241 | { 242 | result.Bytes += await _client.SendAsync(tcpMessage, timeout, cancellationToken); 243 | result.Packets += 1; 244 | } 245 | catch (ObjectDisposedException) 246 | { 247 | throw new OmronException("Failed to Send FINS Message to Omron PLC '" + RemoteHost + ":" + Port + "' - The underlying Socket Connection was Closed"); 248 | } 249 | catch (TimeoutException) 250 | { 251 | throw new OmronException("Failed to Send FINS Message within the Timeout Period to Omron PLC '" + RemoteHost + ":" + Port + "'"); 252 | } 253 | catch (System.Net.Sockets.SocketException e) 254 | { 255 | throw new OmronException("Failed to Send FINS Message to Omron PLC '" + RemoteHost + ":" + Port + "'", e); 256 | } 257 | 258 | return result; 259 | } 260 | 261 | private async Task receiveMessageAsync(enTCPCommandCode command, int timeout, CancellationToken cancellationToken) 262 | { 263 | ReceiveMessageResult result = new ReceiveMessageResult 264 | { 265 | Bytes = 0, 266 | Packets = 0, 267 | Message = new Memory(), 268 | }; 269 | 270 | try 271 | { 272 | List receivedData = new List(); 273 | DateTime startTimestamp = DateTime.UtcNow; 274 | 275 | while (DateTime.UtcNow.Subtract(startTimestamp).TotalMilliseconds < timeout && receivedData.Count < TCP_HEADER_LENGTH) 276 | { 277 | Memory buffer = new byte[4096]; 278 | TimeSpan receiveTimeout = TimeSpan.FromMilliseconds(timeout).Subtract(DateTime.UtcNow.Subtract(startTimestamp)); 279 | 280 | if (receiveTimeout.TotalMilliseconds >= 50) 281 | { 282 | int receivedBytes = await _client.ReceiveAsync(buffer, receiveTimeout, cancellationToken); 283 | 284 | if (receivedBytes > 0) 285 | { 286 | receivedData.AddRange(buffer.Slice(0, receivedBytes).ToArray()); 287 | 288 | result.Bytes += receivedBytes; 289 | result.Packets += 1; 290 | } 291 | } 292 | } 293 | 294 | if (receivedData.Count == 0) 295 | { 296 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - No Data was Received"); 297 | } 298 | 299 | if (receivedData.Count < TCP_HEADER_LENGTH) 300 | { 301 | throw new OmronException("Failed to Receive FINS Message within the Timeout Period from Omron PLC '" + RemoteHost + ":" + Port + "'"); 302 | } 303 | 304 | if (receivedData[0] != 'F' || receivedData[1] != 'I' || receivedData[2] != 'N' || receivedData[3] != 'S') 305 | { 306 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - The TCP Header was Invalid"); 307 | } 308 | 309 | byte[] tcpHeader = receivedData.GetRange(0, TCP_HEADER_LENGTH).ToArray(); 310 | 311 | int tcpMessageDataLength = (int)BitConverter.ToUInt32(new byte[] { receivedData[7], receivedData[6], receivedData[5], receivedData[4] }) - 8; 312 | 313 | if(tcpMessageDataLength <= 0 || tcpMessageDataLength > short.MaxValue) 314 | { 315 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - The TCP Message Length was Invalid"); 316 | } 317 | 318 | if(receivedData[11] == 3 || receivedData[15] != 0) 319 | { 320 | switch(receivedData[15]) 321 | { 322 | case 1: 323 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: The FINS Identifier (ASCII Code) was Invalid."); 324 | 325 | case 2: 326 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: The Data Length is too Long."); 327 | 328 | case 3: 329 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: The Command is not Supported."); 330 | 331 | case 20: 332 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: All Connections are in Use."); 333 | 334 | case 21: 335 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: The Specified Node is already Connected."); 336 | 337 | case 22: 338 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: Attempt to Access a Protected Node from an Unspecified IP Address."); 339 | 340 | case 23: 341 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: The Client FINS Node Address is out of Range."); 342 | 343 | case 24: 344 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: The same FINS Node Address is being used by the Client and Server."); 345 | 346 | case 25: 347 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: All the Node Addresses Available for Allocation have been Used."); 348 | 349 | default: 350 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - Omron TCP Error: Unknown Code '" + receivedData[15] + "'"); 351 | } 352 | } 353 | 354 | if(receivedData[8] != 0 || receivedData[9] != 0 || receivedData[10] != 0 || receivedData[11] != (byte)command) 355 | { 356 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - The TCP Command Received '" + receivedData[11] + "' did not match Expected Command '" + (byte)command + "'"); 357 | } 358 | 359 | if(command == enTCPCommandCode.FINSFrame && tcpMessageDataLength < FINSResponse.HEADER_LENGTH + FINSResponse.COMMAND_LENGTH + FINSResponse.RESPONSE_CODE_LENGTH) 360 | { 361 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - The TCP Message Length was too short for a FINS Frame"); 362 | } 363 | 364 | receivedData.RemoveRange(0, TCP_HEADER_LENGTH); 365 | 366 | if (receivedData.Count < tcpMessageDataLength) 367 | { 368 | startTimestamp = DateTime.UtcNow; 369 | 370 | while (DateTime.UtcNow.Subtract(startTimestamp).TotalMilliseconds < timeout && receivedData.Count < tcpMessageDataLength) 371 | { 372 | Memory buffer = new byte[4096]; 373 | TimeSpan receiveTimeout = TimeSpan.FromMilliseconds(timeout).Subtract(DateTime.UtcNow.Subtract(startTimestamp)); 374 | 375 | if (receiveTimeout.TotalMilliseconds >= 50) 376 | { 377 | int receivedBytes = await _client.ReceiveAsync(buffer, receiveTimeout, cancellationToken); 378 | 379 | if (receivedBytes > 0) 380 | { 381 | receivedData.AddRange(buffer.Slice(0, receivedBytes).ToArray()); 382 | } 383 | 384 | result.Bytes += receivedBytes; 385 | result.Packets += 1; 386 | } 387 | } 388 | } 389 | 390 | if (receivedData.Count == 0) 391 | { 392 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - No Data was Received after TCP Header"); 393 | } 394 | 395 | if (receivedData.Count < tcpMessageDataLength) 396 | { 397 | throw new OmronException("Failed to Receive FINS Message within the Timeout Period from Omron PLC '" + RemoteHost + ":" + Port + "'"); 398 | } 399 | 400 | if (command == enTCPCommandCode.FINSFrame && receivedData[0] != 0xC0 && receivedData[0] != 0xC1) 401 | { 402 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - The FINS Header was Invalid"); 403 | } 404 | 405 | result.Message = receivedData.ToArray(); 406 | } 407 | catch (ObjectDisposedException) 408 | { 409 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "' - The underlying Socket Connection was Closed"); 410 | } 411 | catch (TimeoutException) 412 | { 413 | throw new OmronException("Failed to Receive FINS Message within the Timeout Period from Omron PLC '" + RemoteHost + ":" + Port + "'"); 414 | } 415 | catch (System.Net.Sockets.SocketException e) 416 | { 417 | throw new OmronException("Failed to Receive FINS Message from Omron PLC '" + RemoteHost + ":" + Port + "'", e); 418 | } 419 | 420 | return result; 421 | } 422 | 423 | private ReadOnlyMemory buildFinsTcpMessage(enTCPCommandCode command, ReadOnlyMemory message) 424 | { 425 | List tcpMessage = new List(); 426 | 427 | // FINS Message Identifier 428 | tcpMessage.Add((byte)'F'); 429 | tcpMessage.Add((byte)'I'); 430 | tcpMessage.Add((byte)'N'); 431 | tcpMessage.Add((byte)'S'); 432 | 433 | // Length of Message 434 | tcpMessage.AddRange(BitConverter.GetBytes(Convert.ToUInt32(4 + 4 + message.Length)).Reverse()); // Command + Error Code + Message Data 435 | 436 | // Command 437 | tcpMessage.Add(0); 438 | tcpMessage.Add(0); 439 | tcpMessage.Add(0); 440 | tcpMessage.Add((byte)command); 441 | 442 | // Error Code 443 | tcpMessage.Add(0); 444 | tcpMessage.Add(0); 445 | tcpMessage.Add(0); 446 | tcpMessage.Add(0); 447 | 448 | tcpMessage.AddRange(message.ToArray()); 449 | 450 | return tcpMessage.ToArray(); 451 | } 452 | 453 | #endregion 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /RICADO.Omron/Responses/FINSResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RICADO.Omron.Requests; 3 | 4 | namespace RICADO.Omron.Responses 5 | { 6 | internal class FINSResponse 7 | { 8 | #region Constants 9 | 10 | internal const int HEADER_LENGTH = 10; 11 | internal const int COMMAND_LENGTH = 2; 12 | internal const int RESPONSE_CODE_LENGTH = 2; 13 | 14 | #endregion 15 | 16 | 17 | #region Private Properties 18 | 19 | private byte _serviceId; 20 | 21 | private byte _functionCode; 22 | private byte _subFunctionCode; 23 | private byte _mainResponseCode; 24 | private byte _subResponseCode; 25 | 26 | private byte[] _data; 27 | 28 | #endregion 29 | 30 | 31 | #region Internal Properties 32 | 33 | internal byte ServiceID 34 | { 35 | get 36 | { 37 | return _serviceId; 38 | } 39 | set 40 | { 41 | _serviceId = value; 42 | } 43 | } 44 | 45 | internal byte FunctionCode 46 | { 47 | get 48 | { 49 | return _functionCode; 50 | } 51 | private set 52 | { 53 | _functionCode = value; 54 | } 55 | } 56 | 57 | internal byte SubFunctionCode 58 | { 59 | get 60 | { 61 | return _subFunctionCode; 62 | } 63 | set 64 | { 65 | _subFunctionCode = value; 66 | } 67 | } 68 | 69 | internal byte MainResponseCode 70 | { 71 | get 72 | { 73 | return _mainResponseCode; 74 | } 75 | set 76 | { 77 | _mainResponseCode = value; 78 | } 79 | } 80 | 81 | internal byte SubResponseCode 82 | { 83 | get 84 | { 85 | return _subResponseCode; 86 | } 87 | set 88 | { 89 | _subResponseCode = value; 90 | } 91 | } 92 | 93 | internal byte[] Data 94 | { 95 | get 96 | { 97 | return _data; 98 | } 99 | private set 100 | { 101 | _data = value; 102 | } 103 | } 104 | 105 | #endregion 106 | 107 | 108 | #region Constructors 109 | 110 | private FINSResponse() 111 | { 112 | } 113 | 114 | #endregion 115 | 116 | 117 | #region Internal Methods 118 | 119 | internal static FINSResponse CreateNew(Memory message, FINSRequest request) 120 | { 121 | if(message.Length < HEADER_LENGTH + COMMAND_LENGTH + RESPONSE_CODE_LENGTH) 122 | { 123 | throw new FINSException("The FINS Response Message Length was too short"); 124 | } 125 | 126 | FINSResponse response = new FINSResponse(); 127 | 128 | byte[] header = message.Slice(0, HEADER_LENGTH).ToArray(); 129 | 130 | response.ServiceID = header[9]; 131 | 132 | byte[] command = message.Slice(HEADER_LENGTH, COMMAND_LENGTH).ToArray(); 133 | 134 | if(ValidateFunctionCode(command[0]) == false) 135 | { 136 | throw new FINSException("Invalid Function Code '" + command[0].ToString() + "'"); 137 | } 138 | 139 | response.FunctionCode = command[0]; 140 | 141 | if (response.FunctionCode != request.FunctionCode) 142 | { 143 | throw new FINSException("Unexpected Function Code '" + Enum.GetName(typeof(enFunctionCode), response.FunctionCode) + "' - Expecting '" + Enum.GetName(typeof(enFunctionCode), request.FunctionCode) + "'"); 144 | } 145 | 146 | if(ValidateSubFunctionCode(command[0], command[1]) == false) 147 | { 148 | throw new FINSException("Invalid Sub Function Code '" + command[1].ToString() + "' for Function Code '" + command[0].ToString() + "'"); 149 | } 150 | 151 | response.SubFunctionCode = command[1]; 152 | 153 | if (response.SubFunctionCode != request.SubFunctionCode) 154 | { 155 | throw new FINSException("Unexpected Sub Function Code '" + getSubFunctionCodeName(response.FunctionCode, response.SubFunctionCode) + "' - Expecting '" + getSubFunctionCodeName(request.FunctionCode, request.SubFunctionCode) + "'"); 156 | } 157 | 158 | byte[] responseCode = message.Slice(HEADER_LENGTH + COMMAND_LENGTH, RESPONSE_CODE_LENGTH).ToArray(); 159 | 160 | if(hasNetworkRelayError(responseCode[0])) 161 | { 162 | throw new FINSException("A Network Relay Error has occurred"); 163 | } 164 | 165 | response.MainResponseCode = getMainResponseCode(responseCode[0]); 166 | 167 | response.SubResponseCode = getSubResponseCode(responseCode[1]); 168 | 169 | throwIfResponseError(response.MainResponseCode, response.SubResponseCode); 170 | 171 | if(request.ServiceID != response.ServiceID) 172 | { 173 | throw new FINSException("The Service ID for the FINS Request '" + request.ServiceID + "' did not match the FINS Response '" + response.ServiceID + "'"); 174 | } 175 | 176 | response.Data = message.Length > HEADER_LENGTH + COMMAND_LENGTH + RESPONSE_CODE_LENGTH ? message.Slice(HEADER_LENGTH + COMMAND_LENGTH + RESPONSE_CODE_LENGTH, message.Length - (HEADER_LENGTH + COMMAND_LENGTH + RESPONSE_CODE_LENGTH)).ToArray() : new byte[0]; 177 | 178 | return response; 179 | } 180 | 181 | internal static bool ValidateFunctionCode(byte functionCode) 182 | { 183 | return Enum.IsDefined(typeof(enFunctionCode), functionCode); 184 | } 185 | 186 | internal static bool ValidateSubFunctionCode(byte functionCode, byte subFunctionCode) 187 | { 188 | switch((enFunctionCode)functionCode) 189 | { 190 | case enFunctionCode.AccessRights: 191 | return Enum.IsDefined(typeof(enAccessRightsFunctionCode), subFunctionCode); 192 | 193 | case enFunctionCode.Debugging: 194 | return Enum.IsDefined(typeof(enDebuggingFunctionCode), subFunctionCode); 195 | 196 | case enFunctionCode.ErrorLog: 197 | return Enum.IsDefined(typeof(enErrorLogFunctionCode), subFunctionCode) || Enum.IsDefined(typeof(enFinsWriteLogFunctionCode), subFunctionCode); 198 | 199 | case enFunctionCode.FileMemory: 200 | return Enum.IsDefined(typeof(enFileMemoryFunctionCode), subFunctionCode); 201 | 202 | case enFunctionCode.MachineConfiguration: 203 | return Enum.IsDefined(typeof(enMachineConfigurationFunctionCode), subFunctionCode); 204 | 205 | case enFunctionCode.MemoryArea: 206 | return Enum.IsDefined(typeof(enMemoryAreaFunctionCode), subFunctionCode); 207 | 208 | case enFunctionCode.MessageDisplay: 209 | return Enum.IsDefined(typeof(enMessageDisplayFunctionCode), subFunctionCode); 210 | 211 | case enFunctionCode.OperatingMode: 212 | return Enum.IsDefined(typeof(enOperatingModeFunctionCode), subFunctionCode); 213 | 214 | case enFunctionCode.ParameterArea: 215 | return Enum.IsDefined(typeof(enParameterAreaFunctionCode), subFunctionCode); 216 | 217 | case enFunctionCode.ProgramArea: 218 | return Enum.IsDefined(typeof(enProgramAreaFunctionCode), subFunctionCode); 219 | 220 | case enFunctionCode.SerialGateway: 221 | return Enum.IsDefined(typeof(enSerialGatewayFunctionCode), subFunctionCode); 222 | 223 | case enFunctionCode.Status: 224 | return Enum.IsDefined(typeof(enStatusFunctionCode), subFunctionCode); 225 | 226 | case enFunctionCode.TimeData: 227 | return Enum.IsDefined(typeof(enTimeDataFunctionCode), subFunctionCode); 228 | } 229 | 230 | return false; 231 | } 232 | 233 | #endregion 234 | 235 | 236 | #region Private Methods 237 | 238 | private static string getSubFunctionCodeName(byte functionCode, byte subFunctionCode) 239 | { 240 | switch ((enFunctionCode)functionCode) 241 | { 242 | case enFunctionCode.AccessRights: 243 | return Enum.GetName(typeof(enAccessRightsFunctionCode), subFunctionCode); 244 | 245 | case enFunctionCode.Debugging: 246 | return Enum.GetName(typeof(enDebuggingFunctionCode), subFunctionCode); 247 | 248 | case enFunctionCode.ErrorLog: 249 | return Enum.IsDefined(typeof(enErrorLogFunctionCode), subFunctionCode) ? Enum.GetName(typeof(enErrorLogFunctionCode), subFunctionCode) : Enum.GetName(typeof(enFinsWriteLogFunctionCode), subFunctionCode); 250 | 251 | case enFunctionCode.FileMemory: 252 | return Enum.GetName(typeof(enFileMemoryFunctionCode), subFunctionCode); 253 | 254 | case enFunctionCode.MachineConfiguration: 255 | return Enum.GetName(typeof(enMachineConfigurationFunctionCode), subFunctionCode); 256 | 257 | case enFunctionCode.MemoryArea: 258 | return Enum.GetName(typeof(enMemoryAreaFunctionCode), subFunctionCode); 259 | 260 | case enFunctionCode.MessageDisplay: 261 | return Enum.GetName(typeof(enMessageDisplayFunctionCode), subFunctionCode); 262 | 263 | case enFunctionCode.OperatingMode: 264 | return Enum.GetName(typeof(enOperatingModeFunctionCode), subFunctionCode); 265 | 266 | case enFunctionCode.ParameterArea: 267 | return Enum.GetName(typeof(enParameterAreaFunctionCode), subFunctionCode); 268 | 269 | case enFunctionCode.ProgramArea: 270 | return Enum.GetName(typeof(enProgramAreaFunctionCode), subFunctionCode); 271 | 272 | case enFunctionCode.SerialGateway: 273 | return Enum.GetName(typeof(enSerialGatewayFunctionCode), subFunctionCode); 274 | 275 | case enFunctionCode.Status: 276 | return Enum.GetName(typeof(enStatusFunctionCode), subFunctionCode); 277 | 278 | case enFunctionCode.TimeData: 279 | return Enum.GetName(typeof(enTimeDataFunctionCode), subFunctionCode); 280 | } 281 | 282 | return "Unknown"; 283 | } 284 | 285 | private static bool hasNetworkRelayError(byte responseCode) 286 | { 287 | return (responseCode & (1 << 7)) != 0; 288 | } 289 | 290 | private static byte getMainResponseCode(byte value) 291 | { 292 | byte ignoredBits = 0x80; 293 | 294 | return (byte)(value & (byte)~ignoredBits); 295 | } 296 | 297 | private static byte getSubResponseCode(byte value) 298 | { 299 | byte ignoredBits = 0xC0; 300 | 301 | return (byte)(value & (byte)~ignoredBits); 302 | } 303 | 304 | private static void throwIfResponseError(byte mainCode, byte subCode) 305 | { 306 | if(mainCode == 0 && subCode == 0) 307 | { 308 | return; 309 | } 310 | 311 | FINSException exception = mainCode switch 312 | { 313 | 0x00 => subCode switch 314 | { 315 | 0x01 => new FINSException("Normal Completion (0x00) - Service was Canceled (0x01)"), 316 | _ => null, 317 | }, 318 | 0x01 => subCode switch 319 | { 320 | 0x01 => new FINSException("Local Node Error (0x01) - The Local Node was not found within the Network (0x01)"), 321 | _ => new FINSException("Local Node Error (0x01) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 322 | }, 323 | 0x02 => subCode switch 324 | { 325 | 0x01 => new FINSException("Destination Node Error (0x02) - The Destination Node was not found within the Network (0x01)"), 326 | 0x02 => new FINSException("Destination Node Error (0x02) - The Destination Unit could not be found (0x02)"), 327 | 0x04 => new FINSException("Destination Node Error (0x02) - The Destination Node was Busy (0x04)"), 328 | 0x05 => new FINSException("Destination Node Error (0x02) - Response Timeout (0x05)"), 329 | _ => new FINSException("Destination Node Error (0x02) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 330 | }, 331 | 0x03 => subCode switch 332 | { 333 | 0x01 => new FINSException("Controller Error (0x03) - Communications Controller Error (0x01)"), 334 | 0x02 => new FINSException("Controller Error (0x03) - CPU Unit Error (0x02)"), 335 | 0x03 => new FINSException("Controller Error (0x03) - Controller Board Error (0x03)"), 336 | 0x04 => new FINSException("Controller Error (0x03) - Unit Number Error (0x04)"), 337 | _ => new FINSException("Controller Error (0x03) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 338 | }, 339 | 0x04 => subCode switch 340 | { 341 | 0x01 => new FINSException("Service Unsupported Error (0x04) - Undefined Command (0x01)"), 342 | 0x02 => new FINSException("Service Unsupported Error (0x04) - Command Not Supported by Model/Version (0x02)"), 343 | _ => new FINSException("Service Unsupported Error (0x04) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 344 | }, 345 | 0x05 => subCode switch 346 | { 347 | 0x01 => new FINSException("Routing Table Error (0x05) - Destination Address Setting Error (0x01)"), 348 | 0x02 => new FINSException("Routing Table Error (0x05) - No Routing Tables (0x02)"), 349 | 0x03 => new FINSException("Routing Table Error (0x05) - Routing Table Error (0x03)"), 350 | 0x04 => new FINSException("Routing Table Error (0x05) - Too Many Relays (0x04)"), 351 | _ => new FINSException("Routing Table Error (0x05) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 352 | }, 353 | 0x10 => subCode switch 354 | { 355 | 0x01 => new FINSException("Command Format Error (0x10) - Command Data is too Long (0x01)"), 356 | 0x02 => new FINSException("Command Format Error (0x10) - Command Data is too Short (0x02)"), 357 | 0x03 => new FINSException("Command Format Error (0x10) - Elements Length and Values Length do not Match (0x03)"), 358 | 0x04 => new FINSException("Command Format Error (0x10) - Command Format Error (0x04)"), 359 | 0x05 => new FINSException("Command Format Error (0x10) - Header Error (0x05)"), 360 | _ => new FINSException("Command Format Error (0x10) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 361 | }, 362 | 0x11 => subCode switch 363 | { 364 | 0x01 => new FINSException("Parameter Error (0x11) - No Memory Area Specified (0x01)"), 365 | 0x02 => new FINSException("Parameter Error (0x11) - Access Size Error (0x02)"), 366 | 0x03 => new FINSException("Parameter Error (0x11) - Address Range Error (0x03)"), 367 | 0x04 => new FINSException("Parameter Error (0x11) - Address Range Exceeded (0x04)"), 368 | 0x06 => new FINSException("Parameter Error (0x11) - Program Missing (0x06)"), 369 | 0x09 => new FINSException("Parameter Error (0x11) - Relational Error (0x09)"), 370 | 0x0A => new FINSException("Parameter Error (0x11) - Duplicate Data Access (0x0A)"), 371 | 0x0B => new FINSException("Parameter Error (0x11) - Response Data is too Long (0x0B)"), 372 | 0x0C => new FINSException("Parameter Error (0x11) - Parameter Error (0x0C)"), 373 | _ => new FINSException("Parameter Error (0x11) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 374 | }, 375 | 0x20 => subCode switch 376 | { 377 | 0x02 => new FINSException("Read not Possible Error (0x20) - The Program Area is Protected (0x02)"), 378 | 0x03 => new FINSException("Read not Possible Error (0x20) - Table Missing (0x03)"), 379 | 0x04 => new FINSException("Read not Possible Error (0x20) - Data Missing (0x04)"), 380 | 0x05 => new FINSException("Read not Possible Error (0x20) - Program Missing (0x05)"), 381 | 0x06 => new FINSException("Read not Possible Error (0x20) - File Missing (0x06)"), 382 | 0x07 => new FINSException("Read not Possible Error (0x20) - Data Mismatch (0x07)"), 383 | _ => new FINSException("Read not Possible Error (0x20) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 384 | }, 385 | 0x21 => subCode switch 386 | { 387 | 0x01 => new FINSException("Write not Possible Error (0x21) - The Specified Area is Read-Only (0x01)"), 388 | 0x02 => new FINSException("Write not Possible Error (0x21) - The Program Area is Protected (0x02)"), 389 | 0x03 => new FINSException("Write not Possible Error (0x21) - Cannot Register (0x03)"), 390 | 0x05 => new FINSException("Write not Possible Error (0x21) - Program Missing (0x05)"), 391 | 0x06 => new FINSException("Write not Possible Error (0x21) - File Missing (0x06)"), 392 | 0x07 => new FINSException("Write not Possible Error (0x21) - File Name already Exists (0x07)"), 393 | 0x08 => new FINSException("Write not Possible Error (0x21) - Cannot Change (0x08)"), 394 | _ => new FINSException("Write not Possible Error (0x21) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 395 | }, 396 | 0x22 => subCode switch 397 | { 398 | 0x01 => new FINSException("Not Executable in Current Mode (0x22) - Not Possible during Execution (0x01)"), 399 | 0x02 => new FINSException("Not Executable in Current Mode (0x22) - Not Possible while Running (0x02)"), 400 | 0x03 => new FINSException("Not Executable in Current Mode (0x22) - PLC is in Program Mode (0x03)"), 401 | 0x04 => new FINSException("Not Executable in Current Mode (0x22) - PLC is in Debug Mode (0x04)"), 402 | 0x05 => new FINSException("Not Executable in Current Mode (0x22) - PLC is in Monitor Mode (0x05)"), 403 | 0x06 => new FINSException("Not Executable in Current Mode (0x22) - PLC is in Run Mode (0x06)"), 404 | 0x07 => new FINSException("Not Executable in Current Mode (0x22) - Specified Node is not a Polling Node (0x07)"), 405 | 0x08 => new FINSException("Not Executable in Current Mode (0x22) - Step Cannot be Executed (0x08)"), 406 | _ => new FINSException("Not Executable in Current Mode (0x22) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 407 | }, 408 | 0x23 => subCode switch 409 | { 410 | 0x01 => new FINSException("No Such Device (0x23) - File Device Missing (0x01)"), 411 | 0x02 => new FINSException("No Such Device (0x23) - Memory Missing (0x02)"), 412 | 0x03 => new FINSException("No Such Device (0x23) - Clock Missing (0x03)"), 413 | _ => new FINSException("No Such Device (0x23) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 414 | }, 415 | 0x24 => subCode switch 416 | { 417 | 0x01 => new FINSException("Cannot Start/Stop (0x24) - Table Missing (0x01)"), 418 | _ => new FINSException("Cannot Start/Stop (0x24) - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 419 | }, 420 | _ => new FINSException("Unknown Error - Main Response Code (0x" + mainCode.ToString("X2") + ") - Sub Response Code (0x" + subCode.ToString("X2") + ")"), 421 | }; 422 | 423 | if(exception != null) 424 | { 425 | throw exception; 426 | } 427 | } 428 | 429 | #endregion 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /RICADO.Omron/OmronPLC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using RICADO.Omron.Channels; 5 | using RICADO.Omron.Requests; 6 | using RICADO.Omron.Responses; 7 | 8 | namespace RICADO.Omron 9 | { 10 | // TODO: Add Documentation to all Classes, Interfaces, Structs and Enums 11 | 12 | public class OmronPLC : IDisposable 13 | { 14 | #region Private Properties 15 | 16 | private byte _localNodeId; 17 | private byte _remoteNodeId; 18 | private enConnectionMethod _connectionMethod; 19 | private string _remoteHost; 20 | private int _port = 9600; 21 | private int _timeout; 22 | private int _retries; 23 | 24 | private enPLCType _plcType = enPLCType.Unknown; 25 | private bool _isInitialized; 26 | private readonly object _isInitializedLock = new object(); 27 | 28 | private EthernetChannel _channel; 29 | 30 | private string _controllerModel; 31 | private string _controllerVersion; 32 | 33 | #endregion 34 | 35 | 36 | #region Internal Properties 37 | 38 | internal EthernetChannel Channel => _channel; 39 | 40 | internal bool IsNSeries => _plcType switch 41 | { 42 | enPLCType.NJ101 => true, 43 | enPLCType.NJ301 => true, 44 | enPLCType.NJ501 => true, 45 | enPLCType.NX1P2 => true, 46 | enPLCType.NX102 => true, 47 | enPLCType.NX701 => true, 48 | enPLCType.NY512 => true, 49 | enPLCType.NY532 => true, 50 | enPLCType.NJ_NX_NY_Series => true, 51 | _ => false, 52 | }; 53 | 54 | internal bool IsCSeries => _plcType switch 55 | { 56 | enPLCType.CP1 => true, 57 | enPLCType.CJ2 => true, 58 | enPLCType.C_Series => true, 59 | _ => false, 60 | }; 61 | 62 | #endregion 63 | 64 | 65 | #region Public Properties 66 | 67 | public byte LocalNodeID => _localNodeId; 68 | 69 | public byte RemoteNodeID => _remoteNodeId; 70 | 71 | public enConnectionMethod ConnectionMethod => _connectionMethod; 72 | 73 | public string RemoteHost => _remoteHost; 74 | 75 | public int Port => _port; 76 | 77 | public int Timeout 78 | { 79 | get 80 | { 81 | return _timeout; 82 | } 83 | set 84 | { 85 | _timeout = value; 86 | } 87 | } 88 | 89 | public int Retries 90 | { 91 | get 92 | { 93 | return _retries; 94 | } 95 | set 96 | { 97 | _retries = value; 98 | } 99 | } 100 | 101 | public enPLCType PLCType => _plcType; 102 | 103 | public bool IsInitialized 104 | { 105 | get 106 | { 107 | lock(_isInitializedLock) 108 | { 109 | return _isInitialized; 110 | } 111 | } 112 | } 113 | 114 | public string ControllerModel => _controllerModel; 115 | 116 | public string ControllerVersion => _controllerVersion; 117 | 118 | public ushort MaximumReadWordLength => _plcType == enPLCType.CP1 ? (ushort)499 : (ushort)999; 119 | 120 | public ushort MaximumWriteWordLength => _plcType == enPLCType.CP1 ? (ushort)496 : (ushort)996; 121 | 122 | #endregion 123 | 124 | 125 | #region Constructors 126 | 127 | public OmronPLC(byte localNodeId, byte remoteNodeId, enConnectionMethod connectionMethod, string remoteHost, int port = 9600, int timeout = 2000, int retries = 1) 128 | { 129 | if(localNodeId == 0) 130 | { 131 | throw new ArgumentOutOfRangeException(nameof(localNodeId), "The Local Node ID cannot be set to 0"); 132 | } 133 | 134 | if(localNodeId == 255) 135 | { 136 | throw new ArgumentOutOfRangeException(nameof(localNodeId), "The Local Node ID cannot be set to 255"); 137 | } 138 | 139 | _localNodeId = localNodeId; 140 | 141 | if (remoteNodeId == 0) 142 | { 143 | throw new ArgumentOutOfRangeException(nameof(remoteNodeId), "The Remote Node ID cannot be set to 0"); 144 | } 145 | 146 | if (remoteNodeId == 255) 147 | { 148 | throw new ArgumentOutOfRangeException(nameof(remoteNodeId), "The Remote Node ID cannot be set to 255"); 149 | } 150 | 151 | if(remoteNodeId == localNodeId) 152 | { 153 | throw new ArgumentException("The Remote Node ID cannot be the same as the Local Node ID", nameof(remoteNodeId)); 154 | } 155 | 156 | _remoteNodeId = remoteNodeId; 157 | 158 | _connectionMethod = connectionMethod; 159 | 160 | if (remoteHost == null) 161 | { 162 | throw new ArgumentNullException(nameof(remoteHost), "The Remote Host cannot be Null"); 163 | } 164 | 165 | if(remoteHost.Length == 0) 166 | { 167 | throw new ArgumentException("The Remote Host cannot be Empty", nameof(remoteHost)); 168 | } 169 | 170 | _remoteHost = remoteHost; 171 | 172 | if(port <= 0) 173 | { 174 | throw new ArgumentOutOfRangeException(nameof(port), "The Port cannot be less than 1"); 175 | } 176 | 177 | _port = port; 178 | 179 | if(timeout <= 0) 180 | { 181 | throw new ArgumentOutOfRangeException(nameof(timeout), "The Timeout Value cannot be less than 1"); 182 | } 183 | 184 | _timeout = timeout; 185 | 186 | if(retries < 0) 187 | { 188 | throw new ArgumentOutOfRangeException(nameof(retries), "The Retries Value cannot be Negative"); 189 | } 190 | 191 | _retries = retries; 192 | } 193 | 194 | #endregion 195 | 196 | 197 | #region Public Methods 198 | 199 | public async Task InitializeAsync(CancellationToken cancellationToken) 200 | { 201 | lock (_isInitializedLock) 202 | { 203 | if (_isInitialized == true) 204 | { 205 | return; 206 | } 207 | } 208 | 209 | // Initialize the Channel 210 | if (_connectionMethod == enConnectionMethod.UDP) 211 | { 212 | try 213 | { 214 | _channel = new EthernetUDPChannel(_remoteHost, _port); 215 | 216 | await _channel.InitializeAsync(_timeout, cancellationToken); 217 | } 218 | catch (ObjectDisposedException) 219 | { 220 | throw new OmronException("Failed to Create the Ethernet UDP Communication Channel for Omron PLC '" + _remoteHost + ":" + _port + "' - The underlying Socket Connection has been Closed"); 221 | } 222 | catch (TimeoutException) 223 | { 224 | throw new OmronException("Failed to Create the Ethernet UDP Communication Channel within the Timeout Period for Omron PLC '" + _remoteHost + ":" + _port + "'"); 225 | } 226 | catch (System.Net.Sockets.SocketException e) 227 | { 228 | throw new OmronException("Failed to Create the Ethernet UDP Communication Channel for Omron PLC '" + _remoteHost + ":" + _port + "'", e); 229 | } 230 | } 231 | else if (_connectionMethod == enConnectionMethod.TCP) 232 | { 233 | try 234 | { 235 | _channel = new EthernetTCPChannel(_remoteHost, _port); 236 | 237 | await _channel.InitializeAsync(_timeout, cancellationToken); 238 | } 239 | catch (ObjectDisposedException) 240 | { 241 | throw new OmronException("Failed to Create the Ethernet TCP Communication Channel for Omron PLC '" + _remoteHost + ":" + _port + "' - The underlying Socket Connection has been Closed"); 242 | } 243 | catch (TimeoutException) 244 | { 245 | throw new OmronException("Failed to Create the Ethernet TCP Communication Channel within the Timeout Period for Omron PLC '" + _remoteHost + ":" + _port + "'"); 246 | } 247 | catch (System.Net.Sockets.SocketException e) 248 | { 249 | throw new OmronException("Failed to Create the Ethernet TCP Communication Channel for Omron PLC '" + _remoteHost + ":" + _port + "'", e); 250 | } 251 | } 252 | 253 | await requestControllerInformation(cancellationToken); 254 | 255 | lock(_isInitializedLock) 256 | { 257 | _isInitialized = true; 258 | } 259 | } 260 | 261 | public void Dispose() 262 | { 263 | if(_channel != null) 264 | { 265 | _channel.Dispose(); 266 | 267 | _channel = null; 268 | } 269 | 270 | lock(_isInitializedLock) 271 | { 272 | _isInitialized = false; 273 | } 274 | } 275 | 276 | public Task ReadBitAsync(ushort address, byte bitIndex, enMemoryBitDataType dataType, CancellationToken cancellationToken) 277 | { 278 | return ReadBitsAsync(address, bitIndex, 1, dataType, cancellationToken); 279 | } 280 | 281 | public async Task ReadBitsAsync(ushort address, byte startBitIndex, byte length, enMemoryBitDataType dataType, CancellationToken cancellationToken) 282 | { 283 | lock(_isInitializedLock) 284 | { 285 | if(_isInitialized == false) 286 | { 287 | throw new OmronException("This Omron PLC must be Initialized first before any Requests can be Processed"); 288 | } 289 | } 290 | 291 | if (startBitIndex > 15) 292 | { 293 | throw new ArgumentOutOfRangeException(nameof(startBitIndex), "The Start Bit Index cannot be greater than 15"); 294 | } 295 | 296 | if (length == 0) 297 | { 298 | throw new ArgumentOutOfRangeException(nameof(length), "The Length cannot be Zero"); 299 | } 300 | 301 | if (startBitIndex + length > 16) 302 | { 303 | throw new ArgumentOutOfRangeException(nameof(length), "The Start Bit Index and Length combined are greater than the Maximum Allowed of 16 Bits"); 304 | } 305 | 306 | if (validateBitDataType(dataType) == false) 307 | { 308 | throw new ArgumentException("The Data Type '" + Enum.GetName(typeof(enMemoryBitDataType), dataType) + "' is not Supported on this PLC", nameof(dataType)); 309 | } 310 | 311 | if (validateBitAddress(address, dataType) == false) 312 | { 313 | throw new ArgumentOutOfRangeException(nameof(address), "The Address is greater than the Maximum Address for the '" + Enum.GetName(typeof(enMemoryBitDataType), dataType) + "' Data Type"); 314 | } 315 | 316 | ReadMemoryAreaBitRequest request = ReadMemoryAreaBitRequest.CreateNew(this, address, startBitIndex, length, dataType); 317 | 318 | ProcessRequestResult requestResult = await _channel.ProcessRequestAsync(request, _timeout, _retries, cancellationToken); 319 | 320 | return new ReadBitsResult 321 | { 322 | BytesSent = requestResult.BytesSent, 323 | PacketsSent = requestResult.PacketsSent, 324 | BytesReceived = requestResult.BytesReceived, 325 | PacketsReceived = requestResult.PacketsReceived, 326 | Duration = requestResult.Duration, 327 | Values = ReadMemoryAreaBitResponse.ExtractValues(request, requestResult.Response), 328 | }; 329 | } 330 | 331 | public Task ReadWordAsync(ushort address, enMemoryWordDataType dataType, CancellationToken cancellationToken) 332 | { 333 | return ReadWordsAsync(address, 1, dataType, cancellationToken); 334 | } 335 | 336 | public async Task ReadWordsAsync(ushort startAddress, ushort length, enMemoryWordDataType dataType, CancellationToken cancellationToken) 337 | { 338 | lock (_isInitializedLock) 339 | { 340 | if (_isInitialized == false) 341 | { 342 | throw new OmronException("This Omron PLC must be Initialized first before any Requests can be Processed"); 343 | } 344 | } 345 | 346 | if (length == 0) 347 | { 348 | throw new ArgumentOutOfRangeException(nameof(length), "The Length cannot be Zero"); 349 | } 350 | 351 | if (length > MaximumReadWordLength) 352 | { 353 | throw new ArgumentOutOfRangeException(nameof(length), "The Length cannot be greater than " + MaximumReadWordLength.ToString()); 354 | } 355 | 356 | if (validateWordDataType(dataType) == false) 357 | { 358 | throw new ArgumentException("The Data Type '" + Enum.GetName(typeof(enMemoryWordDataType), dataType) + "' is not Supported on this PLC", nameof(dataType)); 359 | } 360 | 361 | if (validateWordStartAddress(startAddress, length, dataType) == false) 362 | { 363 | throw new ArgumentOutOfRangeException(nameof(startAddress), "The Start Address and Length combined are greater than the Maximum Address for the '" + Enum.GetName(typeof(enMemoryWordDataType), dataType) + "' Data Type"); 364 | } 365 | 366 | ReadMemoryAreaWordRequest request = ReadMemoryAreaWordRequest.CreateNew(this, startAddress, length, dataType); 367 | 368 | ProcessRequestResult requestResult = await _channel.ProcessRequestAsync(request, _timeout, _retries, cancellationToken); 369 | 370 | return new ReadWordsResult 371 | { 372 | BytesSent = requestResult.BytesSent, 373 | PacketsSent = requestResult.PacketsSent, 374 | BytesReceived = requestResult.BytesReceived, 375 | PacketsReceived = requestResult.PacketsReceived, 376 | Duration = requestResult.Duration, 377 | Values = ReadMemoryAreaWordResponse.ExtractValues(request, requestResult.Response), 378 | }; 379 | } 380 | 381 | public Task WriteBitAsync(bool value, ushort address, byte bitIndex, enMemoryBitDataType dataType, CancellationToken cancellationToken) 382 | { 383 | return WriteBitsAsync(new bool[] { value }, address, bitIndex, dataType, cancellationToken); 384 | } 385 | 386 | public async Task WriteBitsAsync(bool[] values, ushort address, byte startBitIndex, enMemoryBitDataType dataType, CancellationToken cancellationToken) 387 | { 388 | lock (_isInitializedLock) 389 | { 390 | if (_isInitialized == false) 391 | { 392 | throw new OmronException("This Omron PLC must be Initialized first before any Requests can be Processed"); 393 | } 394 | } 395 | 396 | if (startBitIndex > 15) 397 | { 398 | throw new ArgumentOutOfRangeException(nameof(startBitIndex), "The Start Bit Index cannot be greater than 15"); 399 | } 400 | 401 | if(values.Length == 0) 402 | { 403 | throw new ArgumentOutOfRangeException(nameof(values), "The Values Array cannot be Empty"); 404 | } 405 | 406 | if(startBitIndex + values.Length > 16) 407 | { 408 | throw new ArgumentOutOfRangeException(nameof(values), "The Values Array Length was greater than the Maximum Allowed of 16 Bits"); 409 | } 410 | 411 | if (validateBitDataType(dataType) == false) 412 | { 413 | throw new ArgumentException("The Data Type '" + Enum.GetName(typeof(enMemoryBitDataType), dataType) + "' is not Supported on this PLC", nameof(dataType)); 414 | } 415 | 416 | if (validateBitAddress(address, dataType) == false) 417 | { 418 | throw new ArgumentOutOfRangeException(nameof(address), "The Address is greater than the Maximum Address for the '" + Enum.GetName(typeof(enMemoryBitDataType), dataType) + "' Data Type"); 419 | } 420 | 421 | WriteMemoryAreaBitRequest request = WriteMemoryAreaBitRequest.CreateNew(this, address, startBitIndex, dataType, values); 422 | 423 | ProcessRequestResult requestResult = await _channel.ProcessRequestAsync(request, _timeout, _retries, cancellationToken); 424 | 425 | WriteMemoryAreaBitResponse.Validate(request, requestResult.Response); 426 | 427 | return new WriteBitsResult 428 | { 429 | BytesSent = requestResult.BytesSent, 430 | PacketsSent = requestResult.PacketsSent, 431 | BytesReceived = requestResult.BytesReceived, 432 | PacketsReceived = requestResult.PacketsReceived, 433 | Duration = requestResult.Duration, 434 | }; 435 | } 436 | 437 | public Task WriteWordAsync(short value, ushort address, enMemoryWordDataType dataType, CancellationToken cancellationToken) 438 | { 439 | return WriteWordsAsync(new short[] { value }, address, dataType, cancellationToken); 440 | } 441 | 442 | public async Task WriteWordsAsync(short[] values, ushort startAddress, enMemoryWordDataType dataType, CancellationToken cancellationToken) 443 | { 444 | lock (_isInitializedLock) 445 | { 446 | if (_isInitialized == false) 447 | { 448 | throw new OmronException("This Omron PLC must be Initialized first before any Requests can be Processed"); 449 | } 450 | } 451 | 452 | if (values.Length == 0) 453 | { 454 | throw new ArgumentOutOfRangeException(nameof(values), "The Values Array cannot be Empty"); 455 | } 456 | 457 | if(values.Length > MaximumWriteWordLength) 458 | { 459 | throw new ArgumentOutOfRangeException(nameof(values), "The Values Array Length cannot be greater than " + MaximumWriteWordLength.ToString()); 460 | } 461 | 462 | if (validateWordDataType(dataType) == false) 463 | { 464 | throw new ArgumentException("The Data Type '" + Enum.GetName(typeof(enMemoryWordDataType), dataType) + "' is not Supported on this PLC", nameof(dataType)); 465 | } 466 | 467 | if (validateWordStartAddress(startAddress, values.Length, dataType) == false) 468 | { 469 | throw new ArgumentOutOfRangeException(nameof(startAddress), "The Start Address and Values Array Length combined are greater than the Maximum Address for the '" + Enum.GetName(typeof(enMemoryWordDataType), dataType) + "' Data Type"); 470 | } 471 | 472 | WriteMemoryAreaWordRequest request = WriteMemoryAreaWordRequest.CreateNew(this, startAddress, dataType, values); 473 | 474 | ProcessRequestResult requestResult = await _channel.ProcessRequestAsync(request, _timeout, _retries, cancellationToken); 475 | 476 | WriteMemoryAreaWordResponse.Validate(request, requestResult.Response); 477 | 478 | return new WriteWordsResult 479 | { 480 | BytesSent = requestResult.BytesSent, 481 | PacketsSent = requestResult.PacketsSent, 482 | BytesReceived = requestResult.BytesReceived, 483 | PacketsReceived = requestResult.PacketsReceived, 484 | Duration = requestResult.Duration, 485 | }; 486 | } 487 | 488 | public async Task ReadClockAsync(CancellationToken cancellationToken) 489 | { 490 | lock (_isInitializedLock) 491 | { 492 | if (_isInitialized == false) 493 | { 494 | throw new OmronException("This Omron PLC must be Initialized first before any Requests can be Processed"); 495 | } 496 | } 497 | 498 | ReadClockRequest request = ReadClockRequest.CreateNew(this); 499 | 500 | ProcessRequestResult requestResult = await _channel.ProcessRequestAsync(request, _timeout, _retries, cancellationToken); 501 | 502 | ReadClockResponse.ClockResult result = ReadClockResponse.ExtractClock(request, requestResult.Response); 503 | 504 | return new ReadClockResult 505 | { 506 | BytesSent = requestResult.BytesSent, 507 | PacketsSent = requestResult.PacketsSent, 508 | BytesReceived = requestResult.BytesReceived, 509 | PacketsReceived = requestResult.PacketsReceived, 510 | Duration = requestResult.Duration, 511 | Clock = result.ClockDateTime, 512 | DayOfWeek = result.DayOfWeek 513 | }; 514 | } 515 | 516 | public Task WriteClockAsync(DateTime newDateTime, CancellationToken cancellationToken) 517 | { 518 | return WriteClockAsync(newDateTime, (int)newDateTime.DayOfWeek, cancellationToken); 519 | } 520 | 521 | public async Task WriteClockAsync(DateTime newDateTime, int newDayOfWeek, CancellationToken cancellationToken) 522 | { 523 | lock (_isInitializedLock) 524 | { 525 | if (_isInitialized == false) 526 | { 527 | throw new OmronException("This Omron PLC must be Initialized first before any Requests can be Processed"); 528 | } 529 | } 530 | 531 | DateTime minDateTime = new DateTime(1998, 1, 1, 0, 0, 0); 532 | 533 | if (newDateTime < minDateTime) 534 | { 535 | throw new ArgumentOutOfRangeException(nameof(newDateTime), "The Date Time Value cannot be less than '" + minDateTime.ToString() + "'"); 536 | } 537 | 538 | DateTime maxDateTime = new DateTime(2069, 12, 31, 23, 59, 59); 539 | 540 | if (newDateTime > maxDateTime) 541 | { 542 | throw new ArgumentOutOfRangeException(nameof(newDateTime), "The Date Time Value cannot be greater than '" + maxDateTime.ToString() + "'"); 543 | } 544 | 545 | if(newDayOfWeek < 0) 546 | { 547 | throw new ArgumentOutOfRangeException(nameof(newDayOfWeek), "The Day of Week Value cannot be less than 0"); 548 | } 549 | 550 | if(newDayOfWeek > 6) 551 | { 552 | throw new ArgumentOutOfRangeException(nameof(newDayOfWeek), "The Day of Week Value cannot be greater than 6"); 553 | } 554 | 555 | WriteClockRequest request = WriteClockRequest.CreateNew(this, newDateTime, (byte)newDayOfWeek); 556 | 557 | ProcessRequestResult requestResult = await _channel.ProcessRequestAsync(request, _timeout, _retries, cancellationToken); 558 | 559 | WriteClockResponse.Validate(request, requestResult.Response); 560 | 561 | return new WriteClockResult 562 | { 563 | BytesSent = requestResult.BytesSent, 564 | PacketsSent = requestResult.PacketsSent, 565 | BytesReceived = requestResult.BytesReceived, 566 | PacketsReceived = requestResult.PacketsReceived, 567 | Duration = requestResult.Duration, 568 | }; 569 | } 570 | 571 | public async Task ReadCycleTimeAsync(CancellationToken cancellationToken) 572 | { 573 | lock (_isInitializedLock) 574 | { 575 | if (_isInitialized == false) 576 | { 577 | throw new OmronException("This Omron PLC must be Initialized first before any Requests can be Processed"); 578 | } 579 | } 580 | 581 | if (IsNSeries == true && _plcType != enPLCType.NJ101 && _plcType != enPLCType.NJ301 && _plcType != enPLCType.NJ501) 582 | { 583 | throw new OmronException("Read Cycle Time is not Supported on the NX/NY Series PLC"); 584 | } 585 | 586 | ReadCycleTimeRequest request = ReadCycleTimeRequest.CreateNew(this); 587 | 588 | ProcessRequestResult requestResult = await _channel.ProcessRequestAsync(request, _timeout, _retries, cancellationToken); 589 | 590 | ReadCycleTimeResponse.CycleTimeResult result = ReadCycleTimeResponse.ExtractCycleTime(request, requestResult.Response); 591 | 592 | return new ReadCycleTimeResult 593 | { 594 | BytesSent = requestResult.BytesSent, 595 | PacketsSent = requestResult.PacketsSent, 596 | BytesReceived = requestResult.BytesReceived, 597 | PacketsReceived = requestResult.PacketsReceived, 598 | Duration = requestResult.Duration, 599 | MinimumCycleTime = result.MinimumCycleTime, 600 | MaximumCycleTime = result.MaximumCycleTime, 601 | AverageCycleTime = result.AverageCycleTime, 602 | }; 603 | } 604 | 605 | #endregion 606 | 607 | 608 | #region Private Methods 609 | 610 | private bool validateBitAddress(ushort address, enMemoryBitDataType dataType) 611 | { 612 | return dataType switch 613 | { 614 | enMemoryBitDataType.DataMemory => address < (_plcType == enPLCType.NX1P2 ? 16000 : 32768), 615 | enMemoryBitDataType.CommonIO => address < 6144, 616 | enMemoryBitDataType.Work => address < 512, 617 | enMemoryBitDataType.Holding => address < 1536, 618 | enMemoryBitDataType.Auxiliary => address < (_plcType == enPLCType.CJ2 ? 11536 : 960), 619 | _ => false, 620 | }; 621 | } 622 | 623 | private bool validateBitDataType(enMemoryBitDataType dataType) 624 | { 625 | return dataType switch 626 | { 627 | enMemoryBitDataType.DataMemory => _plcType != enPLCType.CP1, 628 | enMemoryBitDataType.CommonIO => true, 629 | enMemoryBitDataType.Work => true, 630 | enMemoryBitDataType.Holding => true, 631 | enMemoryBitDataType.Auxiliary => !IsNSeries, 632 | _ => false, 633 | }; 634 | } 635 | 636 | private bool validateWordStartAddress(ushort startAddress, int length, enMemoryWordDataType dataType) 637 | { 638 | return dataType switch 639 | { 640 | enMemoryWordDataType.DataMemory => startAddress + (length - 1) < (_plcType == enPLCType.NX1P2 ? 16000 : 32768), 641 | enMemoryWordDataType.CommonIO => startAddress + (length - 1) < 6144, 642 | enMemoryWordDataType.Work => startAddress + (length - 1) < 512, 643 | enMemoryWordDataType.Holding => startAddress + (length - 1) < 1536, 644 | enMemoryWordDataType.Auxiliary => startAddress + (length - 1) < (_plcType == enPLCType.CJ2 ? 11536 : 960), 645 | _ => false, 646 | }; 647 | } 648 | 649 | private bool validateWordDataType(enMemoryWordDataType dataType) 650 | { 651 | return dataType switch 652 | { 653 | enMemoryWordDataType.DataMemory => true, 654 | enMemoryWordDataType.CommonIO => true, 655 | enMemoryWordDataType.Work => true, 656 | enMemoryWordDataType.Holding => true, 657 | enMemoryWordDataType.Auxiliary => !IsNSeries, 658 | _ => false, 659 | }; 660 | } 661 | 662 | private async Task requestControllerInformation(CancellationToken cancellationToken) 663 | { 664 | ReadCPUUnitDataRequest request = ReadCPUUnitDataRequest.CreateNew(this); 665 | 666 | ProcessRequestResult requestResult = await _channel.ProcessRequestAsync(request, _timeout, _retries, cancellationToken); 667 | 668 | ReadCPUUnitDataResponse.CPUUnitDataResult result = ReadCPUUnitDataResponse.ExtractData(requestResult.Response); 669 | 670 | if(result.ControllerModel != null && result.ControllerModel.Length > 0) 671 | { 672 | _controllerModel = result.ControllerModel; 673 | 674 | if (_controllerModel.StartsWith("NJ101")) 675 | { 676 | _plcType = enPLCType.NJ101; 677 | } 678 | else if (_controllerModel.StartsWith("NJ301")) 679 | { 680 | _plcType = enPLCType.NJ301; 681 | } 682 | else if (_controllerModel.StartsWith("NJ501")) 683 | { 684 | _plcType = enPLCType.NJ501; 685 | } 686 | else if (_controllerModel.StartsWith("NX1P2")) 687 | { 688 | _plcType = enPLCType.NX1P2; 689 | } 690 | else if (_controllerModel.StartsWith("NX102")) 691 | { 692 | _plcType = enPLCType.NX102; 693 | } 694 | else if (_controllerModel.StartsWith("NX701")) 695 | { 696 | _plcType = enPLCType.NX701; 697 | } 698 | else if(_controllerModel.StartsWith("NJ") || _controllerModel.StartsWith("NX") || _controllerModel.StartsWith("NY")) 699 | { 700 | _plcType = enPLCType.NJ_NX_NY_Series; 701 | } 702 | else if(_controllerModel.StartsWith("CJ2")) 703 | { 704 | _plcType = enPLCType.CJ2; 705 | } 706 | else if(_controllerModel.StartsWith("CP1")) 707 | { 708 | _plcType = enPLCType.CP1; 709 | } 710 | else if(_controllerModel.StartsWith("C")) 711 | { 712 | _plcType = enPLCType.C_Series; 713 | } 714 | else 715 | { 716 | _plcType = enPLCType.Unknown; 717 | } 718 | } 719 | 720 | if(result.ControllerVersion != null && result.ControllerVersion.Length > 0) 721 | { 722 | _controllerVersion = result.ControllerVersion; 723 | } 724 | } 725 | 726 | #endregion 727 | } 728 | } 729 | --------------------------------------------------------------------------------