Commit cba58e5f authored by Andreas Müller's avatar Andreas Müller

Enhancement of clients to support function 43 (0x2B).

parent 1b12bd7d
Pipeline #20 failed with stage
in 3 minutes and 12 seconds
......@@ -6,10 +6,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Modbus.Serial\Modbus.Serial.csproj" />
<ProjectReference Include="..\Modbus.Tcp\Modbus.Tcp.csproj" />
</ItemGroup>
......
using AMWD.Modbus.Common.Structures;
using AMWD.Modbus.Common.Util;
using AMWD.Modbus.Tcp.Client;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TcpClient = AMWD.Modbus.Tcp.Client.ModbusClient;
using SerialClient = AMWD.Modbus.Serial.Client.ModbusClient;
using AMWD.Modbus.Common.Interfaces;
using AMWD.Modbus.Common.Structures;
using AMWD.Modbus.Common.Util;
using AMWD.Modbus.Common;
namespace ConsoleDemo
{
......@@ -14,99 +19,176 @@ namespace ConsoleDemo
private static void Main(string[] args)
{
Console.CancelKeyPress += Console_CancelKeyPress;
MainAsync(args).Wait();
try
{
MainAsync(args).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
}
}
private static async Task MainAsync(string[] args)
{
Console.WriteLine("Console Demo Modbus TCP (Read Holding Registers)");
Console.WriteLine("Console Demo Modbus Client");
Console.WriteLine();
Console.WriteLine("Please enter the connection parameters:");
Console.Write("Hostname : ");
var host = Console.ReadLine();
Console.Write("Port : ");
var port = Convert.ToInt32(Console.ReadLine());
Console.Write("Device ID: ");
var id = Convert.ToByte(Console.ReadLine());
Console.Write("Connection Type [1] TCP, [2] RS485: ");
var cType = Convert.ToInt32(Console.ReadLine().Trim());
var client = new ModbusClient(host, port);
while (run)
IModbusClient client = null;
try
{
try
switch (cType)
{
Console.WriteLine();
Console.Write("Address : ");
var address = Convert.ToUInt16(Console.ReadLine());
Console.Write("DataType: ");
var type = Console.ReadLine();
Console.WriteLine();
case 1:
{
Console.Write("Hostname: ");
var host = Console.ReadLine().Trim();
Console.Write("Port: ");
var port = Convert.ToInt32(Console.ReadLine().Trim());
Console.Write("Result : ");
List<Register> result = null;
switch (type.Trim().ToLower())
{
case "byte":
result = await client.ReadHoldingRegisters(id, address, 1);
Console.WriteLine(result.First().GetByte());
break;
case "ushort":
result = await client.ReadHoldingRegisters(id, address, 1);
Console.WriteLine(result.First().GetUInt16());
break;
case "uint":
result = await client.ReadHoldingRegisters(id, address, 2);
Console.WriteLine(result.GetUInt32());
break;
case "ulong":
result = await client.ReadHoldingRegisters(id, address, 4);
Console.WriteLine(result.GetUInt64());
break;
case "sbyte":
result = await client.ReadHoldingRegisters(id, address, 1);
Console.WriteLine(result.First().GetSByte());
break;
case "short":
result = await client.ReadHoldingRegisters(id, address, 1);
Console.WriteLine(result.First().GetInt16());
break;
case "int":
result = await client.ReadHoldingRegisters(id, address, 2);
Console.WriteLine(result.GetInt32());
break;
case "long":
result = await client.ReadHoldingRegisters(id, address, 4);
Console.WriteLine(result.GetInt64());
break;
case "float":
result = await client.ReadHoldingRegisters(id, address, 2);
Console.WriteLine(result.GetSingle());
break;
case "double":
result = await client.ReadHoldingRegisters(id, address, 4);
Console.WriteLine(result.GetDouble());
break;
default:
Console.Write("DataType unknown");
break;
}
client = new TcpClient(host, port);
}
break;
case 2:
{
Console.Write("Interface: ");
var port = Console.ReadLine().Trim();
client = new SerialClient(port);
}
break;
default:
throw new ArgumentException("Type unknown");
}
catch (Exception ex)
await client.Connect();
while (run)
{
Console.WriteLine();
Console.WriteLine("ERROR: " + ex.Message);
Console.WriteLine();
}
}
Console.Write("Device ID: ");
var id = Convert.ToByte(Console.ReadLine().Trim());
client.Dispose();
}
Console.Write("Function [1] Read Register, [2] Device Info: ");
var fn = Convert.ToInt32(Console.ReadLine().Trim());
private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
run = false;
try
{
switch (fn)
{
case 1:
{
ushort address = 0;
ushort count = 0;
string type = "";
Console.WriteLine();
Console.Write("Address : ");
address = Convert.ToUInt16(Console.ReadLine().Trim());
Console.Write("DataType: ");
type = Console.ReadLine().Trim();
if (type == "string")
{
Console.Write("Register Count: ");
count = Convert.ToUInt16(Console.ReadLine().Trim());
}
Console.WriteLine();
Console.Write("Result : ");
List<Register> result = null;
switch (type.Trim().ToLower())
{
case "byte":
result = await client.ReadHoldingRegisters(id, address, 1);
Console.WriteLine(result?.First().GetByte());
break;
case "ushort":
result = await client.ReadHoldingRegisters(id, address, 1);
Console.WriteLine(result?.First().GetUInt16());
break;
case "uint":
result = await client.ReadHoldingRegisters(id, address, 2);
Console.WriteLine(result?.GetUInt32());
break;
case "ulong":
result = await client.ReadHoldingRegisters(id, address, 4);
Console.WriteLine(result?.GetUInt64());
break;
case "sbyte":
result = await client.ReadHoldingRegisters(id, address, 1);
Console.WriteLine(result?.First().GetSByte());
break;
case "short":
result = await client.ReadHoldingRegisters(id, address, 1);
Console.WriteLine(result?.First().GetInt16());
break;
case "int":
result = await client.ReadHoldingRegisters(id, address, 2);
Console.WriteLine(result?.GetInt32());
break;
case "long":
result = await client.ReadHoldingRegisters(id, address, 4);
Console.WriteLine(result?.GetInt64());
break;
case "float":
result = await client.ReadHoldingRegisters(id, address, 2);
Console.WriteLine(result?.GetSingle());
break;
case "double":
result = await client.ReadHoldingRegisters(id, address, 4);
Console.WriteLine(result?.GetDouble());
break;
case "string":
result = await client.ReadHoldingRegisters(id, address, count);
Console.WriteLine();
Console.WriteLine("UTF8: " + result?.GetString(count));
Console.WriteLine("Unicode: " + result?.GetString(count, 0, Encoding.Unicode));
Console.WriteLine("BigEndianUnicode: " + result?.GetString(count, 0, Encoding.BigEndianUnicode));
break;
default:
Console.Write("DataType unknown");
break;
}
}
break;
case 2:
{
var info = await client.ReadDeviceInformation(id, DeviceIDCategory.Regular);
if (info != null)
{
foreach (var kvp in info)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
}
}
break;
}
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("ERROR: " + ex.Message);
}
Console.Write("New Request? [y/N]: ");
var again = Console.ReadLine().Trim().ToLower();
if (again == "y" || again == "yes" || again == "j" || again == "ja")
{
run = true;
}
else
{
run = false;
}
}
}
finally
{
client?.Dispose();
}
}
}
}
......@@ -69,7 +69,98 @@ namespace AMWD.Modbus.Common
/// Writes multiple registers (Fn 16).
/// </summary>
[Description("Write Multiple Registers")]
WriteMultipleRegisters = 0x10
WriteMultipleRegisters = 0x10,
/// <summary>
/// Tunnels service requests and method invocations (Fn 43).
/// </summary>
/// <remarks>
/// This function code needs additional information about its type of request.
/// </remarks>
[Description("MODBUS Encapsulated Interface (MEI)")]
EncapsulatedInterface = 0x2B
}
/// <summary>
/// Lists the possible MEI types.
/// </summary>
/// <remarks>
/// MEI = MODBUS Encapsulated Interface (Fn 43).
/// </remarks>
public enum MEIType: byte
{
/// <summary>
/// The request contains data of CANopen
/// </summary>
[Description("CANopen General Reference Request and Response PDU")]
CANOpenGeneralReference = 0x0D,
/// <summary>
/// The request contains data to read specific device information.
/// </summary>
[Description("Read Device Information")]
ReadDeviceInformation = 0x0E
}
/// <summary>
/// Lists the category of the device information.
/// </summary>
public enum DeviceIDCategory : byte
{
/// <summary>
/// Read the basic information (mandatory).
/// </summary>
[Description("Basic Information Block")]
Basic = 0x01,
/// <summary>
/// Read the regular information (optional).
/// </summary>
[Description("Regular Information Block")]
Regular = 0x02,
/// <summary>
/// Read the extended information (optional, requires multiple requests).
/// </summary>
[Description("Extended Information Block")]
Extended = 0x03,
/// <summary>
/// Read an individual object.
/// </summary>
[Description("Individual Object")]
Individual = 0x04
}
/// <summary>
/// List of known object ids of the device information.
/// </summary>
public enum DeviceIDObject : byte
{
/// <summary>
/// The vendor name (mandatory).
/// </summary>
VendorName = 0x00,
/// <summary>
/// The product code (mandatory).
/// </summary>
ProductCode = 0x01,
/// <summary>
/// The major and minor revision (mandatory).
/// </summary>
MajorMinorRevision = 0x02,
/// <summary>
/// The vendor url (optional).
/// </summary>
VendorUrl = 0x03,
/// <summary>
/// The product name (optional).
/// </summary>
ProductName = 0x04,
/// <summary>
/// The model name (optional).
/// </summary>
ModelName = 0x05,
/// <summary>
/// The application name (optional).
/// </summary>
UserApplicationName = 0x06
}
/// <summary>
......
......@@ -93,6 +93,24 @@ namespace AMWD.Modbus.Common.Interfaces
/// <returns>A list of registers or null on error.</returns>
Task<List<Register>> ReadInputRegisters(byte deviceId, ushort startAddress, ushort count);
/// <summary>
/// Reads device information. (Modbus function 43).
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="categoryId">The category to read (basic, regular, extended, individual).</param>
/// <param name="objectId">The first object id to read.</param>
/// <returns>A map of device information and their content as string.</returns>
Task<Dictionary<DeviceIDObject, string>> ReadDeviceInformation(byte deviceId, DeviceIDCategory categoryId, DeviceIDObject objectId = DeviceIDObject.VendorName);
/// <summary>
/// Reads device information. (Modbus function 43).
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="categoryId">The category to read (basic, regular, extended, individual).</param>
/// <param name="objectId">The first object id to read.</param>
/// <returns>A map of device information and their content as raw bytes.</returns>
Task<Dictionary<byte, byte[]>> ReadDeviceInformationRaw(byte deviceId, DeviceIDCategory categoryId, DeviceIDObject objectId = DeviceIDObject.VendorName);
#endregion Read methods
#region Write methods
......
The MIT License
Copyright (c) 2018 Andreas Müller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
\ No newline at end of file
......@@ -52,12 +52,8 @@
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.2.5" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\LICENSE.txt" Pack="true" PackagePath="" />
</ItemGroup>
</Project>
......@@ -42,7 +42,11 @@ namespace AMWD.Modbus.Common.Util
/// <param name="bytes">New buffer as byte array.</param>
public DataBuffer(byte[] bytes)
{
Buffer = bytes ?? throw new ArgumentNullException(nameof(bytes));
if (bytes == null)
throw new ArgumentNullException(nameof(bytes));
Buffer = new byte[bytes.Length];
Array.Copy(bytes, Buffer, bytes.Length);
}
/// <summary>
......
......@@ -249,7 +249,13 @@ namespace AMWD.Modbus.Common.Util
blob[i * 2 + 1] = registers[i].LoByte;
}
return encoding.GetString(blob).Trim(new[] { ' ', '\t', '\0', '\r', '\n' });
var str = encoding.GetString(blob).Trim(new[] { ' ', '\t', '\0', '\r', '\n' });
var nullIdx = str.IndexOf('\0');
if (nullIdx >= 0)
{
return str.Substring(0, nullIdx);
}
return str;
}
#endregion To string
......
......@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
......@@ -567,6 +568,100 @@ namespace AMWD.Modbus.Serial.Client
return list;
}
/// <summary>
/// Reads device information. (Modbus function 43).
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="categoryId">The category to read (basic, regular, extended, individual).</param>
/// <param name="objectId">The first object id to read.</param>
/// <returns>A map of device information and their content as string.</returns>
public async Task<Dictionary<DeviceIDObject, string>> ReadDeviceInformation(byte deviceId, DeviceIDCategory categoryId, DeviceIDObject objectId = DeviceIDObject.VendorName)
{
var raw = await ReadDeviceInformationRaw(deviceId, categoryId, objectId);
if (raw == null)
{
return null;
}
var dict = new Dictionary<DeviceIDObject, string>();
foreach (var kvp in raw)
{
dict.Add((DeviceIDObject)kvp.Key, Encoding.ASCII.GetString(kvp.Value));
}
return dict;
}
/// <summary>
/// Reads device information. (Modbus function 43).
/// </summary>
/// <param name="deviceId">The id to address the device (slave).</param>
/// <param name="categoryId">The category to read (basic, regular, extended, individual).</param>
/// <param name="objectId">The first object id to read.</param>
/// <returns>A map of device information and their content as raw bytes.</returns>>
public async Task<Dictionary<byte, byte[]>> ReadDeviceInformationRaw(byte deviceId, DeviceIDCategory categoryId, DeviceIDObject objectId = DeviceIDObject.VendorName)
{
if (isDisposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
if (deviceId < Consts.MinDeviceId || Consts.MaxDeviceId < deviceId)
{
throw new ArgumentOutOfRangeException(nameof(deviceId));
}
try
{
var request = new Request
{
DeviceId = deviceId,
Function = FunctionCode.EncapsulatedInterface,
MEIType = MEIType.ReadDeviceInformation,
MEICategory = categoryId,
MEIObject = objectId
};
var response = await SendRequest(request);
if (response.IsTimeout)
{
throw new IOException("Request timed out");
}
if (response.IsError)
{
throw new ModbusException(response.ErrorMessage);
}
var dict = new Dictionary<byte, byte[]>();
for (int i = 0, idx = 0; i < response.ObjectCount && idx < response.Data.Length; i++)
{
byte objId = response.Data.GetByte(idx);
idx++;
byte len = response.Data.GetByte(idx);
idx++;
byte[] bytes = response.Data.GetBytes(idx, len);
idx += len;
dict.Add(objId, bytes);
}
if (response.MoreRequestsNeeded)
{
var transDict = await ReadDeviceInformationRaw(deviceId, categoryId, (DeviceIDObject)response.NextObjectId);
foreach (var kvp in transDict)
{
dict.Add(kvp.Key, kvp.Value);
}
}
return dict;
}
catch (IOException)
{
ConnectingTask = Task.Run((Action)Reconnect);
}
return null;
}
#endregion Read methods
#region Write methods
......
The MIT License
Copyright (c) 2018 Andreas Müller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
\ No newline at end of file
......@@ -53,8 +53,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="System.IO.Ports" Version="4.5.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="System.IO.Ports" Version="4.6.0-preview3.19128.7" />
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.2.5" PrivateAssets="all" />
</ItemGroup>
......@@ -62,8 +62,4 @@
<ProjectReference Include="..\Modbus.Common\Modbus.Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\LICENSE.txt" Pack="true" PackagePath="" />
</ItemGroup>
</Project>
......@@ -68,6 +68,31 @@ namespace AMWD.Modbus.Serial.Protocol
/// </summary>
internal DataBuffer Data { get; set; }
#region MODBUS Encapsulated Interface Transport
/// <summary>
/// Gets or sets the Encapsulated Interface type.
/// Only needed on <see cref="FunctionCode.EncapsulatedInterface"/>.
/// </summary>
public MEIType MEIType { get; set; }
#region Device Information
/// <summary>
/// Gets or sets the Device ID code (category).
/// Only needed on <see cref="FunctionCode.EncapsulatedInterface"/> and <see cref="MEIType.ReadDeviceInformation"/>.
/// </summary>
public DeviceIDCategory MEICategory { get; set; }
/// <summary>
/// Gets or sets the first Object ID to read.
/// </summary>
public DeviceIDObject MEIObject { get; set; }
#endregion Device Information
#endregion MODBUS Encapsulated Interface Transport
#endregion Properties
#region Serialization
......@@ -78,23 +103,23 @@ namespace AMWD.Modbus.Serial.Protocol
/// <returns></returns>
internal byte[] Serialize()
{
var buffer = new DataBuffer(4);
var buffer = new DataBuffer(2);
buffer.SetByte(0, DeviceId);
buffer.SetByte(1, (byte)Function);
buffer.SetUInt16(2, Address);
switch (Function)
{
case FunctionCode.ReadCoils:
case FunctionCode.ReadDiscreteInputs:
case FunctionCode.ReadHoldingRegisters:
case FunctionCode.ReadInputRegisters:
buffer.AddUInt16(Address);
buffer.AddUInt16(Count);
break;
case FunctionCode.WriteMultipleCoils:
case FunctionCode.WriteMultipleRegisters:
buffer.AddUInt16(Address);
buffer.AddUInt16(Count);
if (Data?.Length > 0)
{
......@@ -103,11 +128,30 @@ namespace AMWD.Modbus.Serial.Protocol
break;
case FunctionCode.WriteSingleCoil:
case FunctionCode.WriteSingleRegister:
buffer.AddUInt16(Address);
if (Data?.Length > 0)
{
buffer.AddBytes(Data.Buffer);
}
break;
case FunctionCode.EncapsulatedInterface:
buffer.AddByte((byte)MEIType);
switch (MEIType)
{
case MEIType.CANOpenGeneralReference:
if (Data?.Length > 0)
{
buffer.AddBytes(Data.Buffer);
}
break;
case MEIType.ReadDeviceInformation:
buffer.AddByte((byte)MEICategory);
buffer.AddByte((byte)MEIObject);
break;
default:
throw new NotImplementedException();
}
break;
default:
throw new NotImplementedException();
}
......@@ -123,7 +167,6 @@ namespace AMWD.Modbus.Serial.Protocol
var buffer = new DataBuffer(bytes);