Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
Apollo3zehn committed Sep 21, 2021
2 parents e366cab + dcf71e4 commit 39d819d
Show file tree
Hide file tree
Showing 21 changed files with 673 additions and 165 deletions.
6 changes: 3 additions & 3 deletions build/build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
</PropertyGroup>

<PropertyGroup>
<Major Condition="$(Major) == ''">3</Major>
<Minor Condition="$(Minor) == ''">2</Minor>
<Major Condition="$(Major) == ''">4</Major>
<Minor Condition="$(Minor) == ''">0</Minor>
<Revision Condition="$(Revision) == ''">0</Revision>
<VersionSuffix Condition="$(VersionSuffix) == ''"></VersionSuffix>
<VersionSuffix Condition="$(VersionSuffix) == ''">preview.1</VersionSuffix>
</PropertyGroup>

<PropertyGroup>
Expand Down
10 changes: 7 additions & 3 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ Note that in the second example, the ```Task.Delay()``` period is much lower. Si

## Modbus RTU server

When you need a Modbus RTU server, you need to instantiate it like this providing a ```unitIdentifier```, which must be in the range of 1..247 and unique for each Modbus server / Modbus slave:
When you need a Modbus RTU server, you can instantiate it like this providing a ```unitIdentifier```, which must be in the range of 1..247 and unique for each Modbus server / Modbus slave:

```cs
var server = new ModbusRtuServer(unitIdentifier: 1);
Expand All @@ -320,6 +320,8 @@ When you don't need the server anymore, dispose it:
server.Dispose();
```

The `ModbusRtuServer` also supports multiple unit identifiers, just use the corresponding constructor overload to initialize it. Alternatively you can add / remove units at runtime using `server.AddUnit(...)` or `server.RemoveUnit(...)`, respectively. Be sure to remove a unit only when no client is in the process of interacting with it to avoid unexpected errors. This feature is intended for setups where you want to simulate multiple RTU devices on a single `COM` port.

As for the TCP server, there are two options to operate the server (synchronous and asynchronous). See above for details.

## Events
Expand Down Expand Up @@ -367,14 +369,16 @@ registers.SetBigEndian<short>(address: 1, value: 99); /* recommended */
registers.SetBigEndian(address: 1, value: (short)99);
```

There are complementary methods for little-endian data and methods for reading data. The full list of `Span<short>` extension methods is:
There are complementary methods for little-endian and mid-little-endian data and methods for reading data. The full list of `Span<short>` extension methods is:

```cs
void registers.SetBigEndian<T>(...);
void registers.SetLittleEndian<T>(...);
void registers.SetMidLittleEndian<T>(...);

Span<short> registers.GetBigEndian<T>(...);
Span<short> registers.GetLittleEndian<T>(...);
Span<short> registers.GetMidLittleEndian<T>(...);
```

## Coils and Discrete Inputs
Expand Down Expand Up @@ -405,7 +409,7 @@ It might happen that a server should not support all Modbus functions or only a

var server = new ModbusTcpServer()
{
RequestValidator = (functionCode, address, quantityOfRegisters) =>
RequestValidator = (unitIdentifier, functionCode, address, quantityOfRegisters) =>
{
if (functionCode == ModbusFunctionCode.WriteSingleRegister)
return ModbusExceptionCode.IllegalFunction;
Expand Down
1 change: 1 addition & 0 deletions doc/samples/modbus_validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var server = new ModbusTcpServer()
};

private ModbusExceptionCode ModbusValidator(
byte unitIdentifier,
ModbusFunctionCode functionCode,
ushort address,
ushort quantityOfRegisters)
Expand Down
36 changes: 18 additions & 18 deletions sample/SampleServerClientRtu/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class Program
static async Task Main(string[] args)
{
/* Modbus RTU uses a COM port for communication. Therefore, to run
* this sample, you need to make sure that there are real or virtual
* this sample, you need to make sure that there are real or virtual
* COM ports available. The easiest way is to install one of the free
* COM port bridges available in the internet. That way, the Modbus
* COM port bridges available in the internet. That way, the Modbus
* server can connect to e.g. COM1 which is virtually linked to COM2,
* where the client is connected to.
*
*
* When you only want to use the client and communicate to an external
* Modbus server, simply remove all server related code parts in this
* Modbus server, simply remove all server related code parts in this
* sample and connect to real COM port using only the client.
*/

Expand Down Expand Up @@ -45,7 +45,7 @@ static async Task Main(string[] args)
/* subscribe to the 'RegistersChanged' event (in case you need it) */
server.RegistersChanged += (sender, registerAddresses) =>
{
// the variable 'registerAddresses' contains a list of modified register addresses
// the variable 'registerAddresses' contains the unit ID and a list of modified register addresses
};

/* create Modbus RTU client */
Expand Down Expand Up @@ -109,23 +109,23 @@ static void DoServerWork(ModbusRtuServer server)

// Option A: normal performance version, more flexibility

/* get buffer in standard form (Span<short>) */
var registers = server.GetHoldingRegisters();
registers.SetLittleEndian<int>(address: 5, random.Next());
/* get buffer in standard form (Span<short>) */
var registers = server.GetHoldingRegisters();
registers.SetLittleEndian<int>(address: 5, random.Next());

// Option B: high performance version, less flexibility

/* interpret buffer as array of bytes (8 bit) */
var byte_buffer = server.GetHoldingRegisterBuffer<byte>();
byte_buffer[20] = (byte)(random.Next() >> 24);
/* interpret buffer as array of bytes (8 bit) */
var byte_buffer = server.GetHoldingRegisterBuffer<byte>();
byte_buffer[20] = (byte)(random.Next() >> 24);

/* interpret buffer as array of shorts (16 bit) */
var short_buffer = server.GetHoldingRegisterBuffer<short>();
short_buffer[30] = (short)(random.Next(0, 100) >> 16);
/* interpret buffer as array of shorts (16 bit) */
var short_buffer = server.GetHoldingRegisterBuffer<short>();
short_buffer[30] = (short)(random.Next(0, 100) >> 16);

/* interpret buffer as array of ints (32 bit) */
var int_buffer = server.GetHoldingRegisterBuffer<int>();
int_buffer[40] = random.Next(0, 100);
/* interpret buffer as array of ints (32 bit) */
var int_buffer = server.GetHoldingRegisterBuffer<int>();
int_buffer[40] = random.Next(0, 100);
}

static void DoClientWork(ModbusRtuClient client, ILogger logger)
Expand Down Expand Up @@ -172,4 +172,4 @@ static void DoClientWork(ModbusRtuClient client, ILogger logger)
logger.LogInformation("FC06 - WriteSingleRegister: Done");
}
}
}
}
55 changes: 51 additions & 4 deletions src/FluentModbus/IModbusRtuSerialPort.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,71 @@

namespace FluentModbus
{
internal interface IModbusRtuSerialPort
/// <summary>
/// A serial port for Modbus RTU communication.
/// </summary>
public interface IModbusRtuSerialPort
{
#region Methods

/// <summary>
/// Reads a number of bytes from the serial port input buffer and writes those bytes into a byte array at the specified offset.
/// </summary>
/// <param name="buffer">The byte array to write the input to.</param>
/// <param name="offset">The offset in <paramref name="buffer"/> at which to write the bytes.</param>
/// <param name="count">The maximum number of bytes to read. Fewer bytes are read if <paramref name="count"/> is greater than the number of bytes in the input buffer.</param>
/// <returns>The number of bytes read.</returns>
int Read(byte[] buffer, int offset, int count);

/// <summary>
/// Asynchronously reads a number of bytes from the serial port input buffer and writes those bytes into a byte array at the specified offset.
/// </summary>
/// <param name="buffer">The byte array to write the input to.</param>
/// <param name="offset">The offset in <paramref name="buffer"/> at which to write the bytes.</param>
/// <param name="count">The maximum number of bytes to read. Fewer bytes are read if <paramref name="count"/> is greater than the number of bytes in the input buffer.</param>
/// <returns>The number of bytes read.</returns>
Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken token);

/// <summary>
/// Writes a specified number of bytes to the serial port using data from a buffer.
/// </summary>
/// <param name="buffer">The byte array that contains the data to write to the port.</param>
/// <param name="offset">The zero-based byte offset in the <paramref name="buffer"/> parameter at which to begin copying bytes to the port.</param>
/// <param name="count">The number of bytes to write.</param>
void Write(byte[] buffer, int offset, int count);

/// <summary>
/// Asynchronously writes a specified number of bytes to the serial port using data from a buffer.
/// </summary>
/// <param name="buffer">The byte array that contains the data to write to the port.</param>
/// <param name="offset">The zero-based byte offset in the <paramref name="buffer"/> parameter at which to begin copying bytes to the port.</param>
/// <param name="count">The number of bytes to write.</param>
Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token);

/// <summary>
/// Opens a new serial port connection.
/// </summary>
void Open();

/// <summary>
/// Closes the port connection, sets the <see cref="IsOpen"/> property to <see langword="true"/>, and disposes of the internal Stream object.
/// </summary>
void Close();

#endregion
#endregion Methods

#region Properties

/// <summary>
/// Gets the port for communications, including but not limited to all available COM ports.
/// </summary>
string PortName { get; }

/// <summary>
/// Gets a value indicating the open or closed status of the serial port object.
/// </summary>
bool IsOpen { get; }

#endregion
#endregion Properties
}
}
}
8 changes: 8 additions & 0 deletions src/FluentModbus/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel;

// workaround class for records and "init" keyword
namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal class IsExternalInit { }
}
2 changes: 1 addition & 1 deletion src/FluentModbus/ModbusFrameBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ public void Dispose()

#endregion
}
}
}
37 changes: 30 additions & 7 deletions src/FluentModbus/ModbusUtils.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System;
using System.Globalization;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace FluentModbus
Expand Down Expand Up @@ -67,23 +68,21 @@ public static ushort CalculateCRC(Memory<byte> buffer)

public static bool DetectFrame(byte unitIdentifier, Memory<byte> frame)
{
byte newUnitIdentifier;

/* Correct response frame (min. 6 bytes)
* 00 Unit Identifier
* 01 Function Code
* 02 Byte count
* 03 Minimum of 1 byte
* 04 CRC Byte 1
* 05 CRC Byte 2
* 05 CRC Byte 2
*/

/* Error response frame (5 bytes)
* 00 Unit Identifier
* 01 Function Code + 0x80
* 02 Exception Code
* 03 CRC Byte 1
* 04 CRC Byte 2
* 04 CRC Byte 2
*/

var span = frame.Span;
Expand All @@ -93,7 +92,7 @@ public static bool DetectFrame(byte unitIdentifier, Memory<byte> frame)

if (unitIdentifier != 255) // 255 means "skip unit identifier check"
{
newUnitIdentifier = span[0];
var newUnitIdentifier = span[0];

if (newUnitIdentifier != unitIdentifier)
return false;
Expand Down Expand Up @@ -130,6 +129,30 @@ public static T SwitchEndianness<T>(T value) where T : unmanaged
return data[0];
}

public static T ConvertBetweenLittleEndianAndMidLittleEndian<T>(T value) where T : unmanaged
{
// from DCBA to CDAB

if (Unsafe.SizeOf<T>() == 4)
{
Span<T> data = stackalloc T[] { value, default };

var dataset_bytes = MemoryMarshal.Cast<T, byte>(data);
var offset = 4;

dataset_bytes[offset + 0] = dataset_bytes[1];
dataset_bytes[offset + 1] = dataset_bytes[0];
dataset_bytes[offset + 2] = dataset_bytes[3];
dataset_bytes[offset + 3] = dataset_bytes[2];

return data[1];
}
else
{
throw new Exception($"Type {value.GetType().Name} cannot be represented as mid-little-endian.");
}
}

public static void SwitchEndianness<T>(Memory<T> dataset) where T : unmanaged
{
ModbusUtils.SwitchEndianness(dataset.Span);
Expand All @@ -154,4 +177,4 @@ public static void SwitchEndianness<T>(Span<T> dataset) where T : unmanaged
}
}
}
}
}
18 changes: 18 additions & 0 deletions src/FluentModbus/Resources/ErrorMessage.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/FluentModbus/Resources/ErrorMessage.resx
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@
<data name="ModbusServer_InvalidUnitIdentifier" xml:space="preserve">
<value>The unit identifier is invalid. Valid node addresses are in the range of 1 - 247.</value>
</data>
<data name="ModbusServer_UnitIdentifierNotFound" xml:space="preserve">
<value>No unit found for the specified unit identifier.</value>
</data>
<data name="ModbusServer_ZeroUnitOverloadOnlyApplicableInSingleUnitMode" xml:space="preserve">
<value>A unit identifier of 0 is only applicable in single unit mode.</value>
</data>
<data name="ModbusTcpRequestHandler_NoValidRequestAvailable" xml:space="preserve">
<value>There is no valid request available.</value>
</data>
Expand Down
Loading

0 comments on commit 39d819d

Please sign in to comment.