Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c81ab6b44 | |||
| 3e8f2cd73b | |||
| e830e43c36 | |||
| 6a63dbb739 | |||
| 1536c60336 |
@@ -1,4 +1,4 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests")]
|
[assembly: InternalsVisibleTo("AMWD.Protocols.Modbus.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100adcc4f9f5bb3ac73cb30661f6f35772b8f90a74412925764a960af06ef125bdcec05ed1d139503d5203fb72aa3fa74bab58e82ac2a6cd4b650f8cbf7086a71bc2dfc67e95b8d26d776d60856acf3121f831529b1a4dee91b34ac84f95f71a1165b7783edb591929ba2a684100c92bbed8859c7266fb507f6f55bb6f7fcac80b4")]
|
||||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ using AMWD.Protocols.Modbus.Common.Contracts;
|
|||||||
namespace AMWD.Protocols.Modbus.Common.Protocols
|
namespace AMWD.Protocols.Modbus.Common.Protocols
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of the Modbus RTU over TCP protocol.
|
/// Implementation of the Modbus RTU over Modbus TCP protocol.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The Modbus RTU over Modbus TCP is rarely used.
|
/// The Modbus RTU over Modbus TCP is rarely used.
|
||||||
/// It is a non-standard variant of Modbus TCP that includes the Modbus RTU CRC at the end of the message.
|
/// It is a non-standard variant of Modbus TCP that includes wrapps a Modbus RTU message within a Modbus TCP message.
|
||||||
|
/// <br/>
|
||||||
|
/// Definition found on <see href="https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html">Fernhill Software</see>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class RtuOverTcpProtocol : IModbusProtocol
|
public class RtuOverTcpProtocol : IModbusProtocol
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ Here you have the specific default implementations for the Modbus Protocol.
|
|||||||
|
|
||||||
- ASCII
|
- ASCII
|
||||||
- RTU
|
- RTU
|
||||||
- RTU over TCP
|
|
||||||
- TCP
|
- TCP
|
||||||
|
- [RTU over TCP]
|
||||||
|
|
||||||
**NOTE:**
|
**NOTE:**
|
||||||
The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
|
The implementations over serial line (RTU and ASCII) have a minimum unit ID of one (1) and maximum unit ID of 247 referring to the specification.
|
||||||
@@ -68,4 +68,9 @@ This validation is _not_ implemented here due to real world experience, that som
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Published under MIT License (see [**tl;dr**Legal](https://www.tldrlegal.com/license/mit-license))
|
Published under MIT License (see [**tl;dr**Legal])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[RTU over TCP]: https://www.fernhillsoftware.com/help/drivers/modbus/modbus-protocol.html
|
||||||
|
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace AMWD.Protocols.Modbus.Proxy
|
|||||||
|
|
||||||
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
||||||
|
|
||||||
if (ushort.MinValue < listenPort || listenPort < ushort.MaxValue)
|
if (listenPort < ushort.MinValue || ushort.MaxValue < listenPort)
|
||||||
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
With this package the server and client implementations will be combined as proxy.
|
With this package the server and client implementations will be combined as proxy.
|
||||||
|
|
||||||
You can use any `ModbusBasClient` implementation as target client and plug it into the implemented `ModbusTcpProxy` or `ModbusRtuProxy`, which implement the server side.
|
You can use any `ModbusBaseClient` implementation as target client and plug it into the implemented `ModbusTcpProxy` or `ModbusRtuProxy`, which implement the server side.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -41,8 +41,4 @@
|
|||||||
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
<ProjectReference Include="..\AMWD.Protocols.Modbus.Common\AMWD.Protocols.Modbus.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Extensions\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
|
|
||||||
private Task StopAsyncInternal(CancellationToken cancellationToken)
|
private Task StopAsyncInternal(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_stopCts.Cancel();
|
_stopCts?.Cancel();
|
||||||
|
|
||||||
_serialPort.Close();
|
_serialPort.Close();
|
||||||
_serialPort.DataReceived -= OnDataReceived;
|
_serialPort.DataReceived -= OnDataReceived;
|
||||||
@@ -328,7 +328,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
|
if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_DISCRETE_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -385,7 +385,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
|
if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_DISCRETE_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -442,7 +442,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
|
if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_REGISTER_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -496,7 +496,7 @@ namespace AMWD.Protocols.Modbus.Serial
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(2);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(4);
|
ushort count = requestBytes.GetBigEndianUInt16(4);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
|
if (count < RtuProtocol.MIN_READ_COUNT || RtuProtocol.MAX_REGISTER_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[1] |= 0x80;
|
responseBytes[1] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
private readonly AsyncQueue<RequestQueueItem> _requestQueue = new();
|
||||||
|
|
||||||
private TimeSpan _readTimeout = TimeSpan.FromMilliseconds(1);
|
private TimeSpan _readTimeout = TimeSpan.FromSeconds(1);
|
||||||
private TimeSpan _writeTimeout = TimeSpan.FromMilliseconds(1);
|
private TimeSpan _writeTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
private readonly ReaderWriterLockSlim _deviceListLock = new();
|
private readonly ReaderWriterLockSlim _deviceListLock = new();
|
||||||
private readonly Dictionary<byte, ModbusDevice> _devices = [];
|
private readonly Dictionary<byte, ModbusDevice> _devices = [];
|
||||||
|
|
||||||
|
private TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
@@ -49,7 +51,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
{
|
{
|
||||||
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
ListenAddress = listenAddress ?? IPAddress.Loopback;
|
||||||
|
|
||||||
if (ushort.MinValue < listenPort || listenPort < ushort.MaxValue)
|
if (listenPort < ushort.MinValue || ushort.MaxValue < listenPort)
|
||||||
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
throw new ArgumentOutOfRangeException(nameof(listenPort));
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -105,7 +107,17 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the read/write timeout.
|
/// Gets or sets the read/write timeout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReadWriteTimeout { get; set; }
|
public TimeSpan ReadWriteTimeout
|
||||||
|
{
|
||||||
|
get => _readWriteTimeout;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
|
||||||
|
_readWriteTimeout = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
|
|
||||||
@@ -151,11 +163,11 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
|
|
||||||
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
private async Task StopAsyncInternal(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_stopCts.Cancel();
|
_stopCts?.Cancel();
|
||||||
|
|
||||||
_listener.Stop();
|
_listener?.Stop();
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
_listener.Dispose();
|
_listener?.Dispose();
|
||||||
#endif
|
#endif
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -354,7 +366,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(10);
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
|
if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_DISCRETE_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -406,7 +418,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(10);
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_DISCRETE_READ_COUNT)
|
if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_DISCRETE_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -458,7 +470,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(10);
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
|
if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_REGISTER_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
@@ -507,7 +519,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
ushort firstAddress = requestBytes.GetBigEndianUInt16(8);
|
||||||
ushort count = requestBytes.GetBigEndianUInt16(10);
|
ushort count = requestBytes.GetBigEndianUInt16(10);
|
||||||
|
|
||||||
if (TcpProtocol.MIN_READ_COUNT < count || count < TcpProtocol.MAX_REGISTER_READ_COUNT)
|
if (count < TcpProtocol.MIN_READ_COUNT || TcpProtocol.MAX_REGISTER_READ_COUNT < count)
|
||||||
{
|
{
|
||||||
responseBytes[7] |= 0x80;
|
responseBytes[7] |= 0x80;
|
||||||
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
responseBytes.Add((byte)ModbusErrorCode.IllegalDataValue);
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ float voltage = registers.GetSingle();
|
|||||||
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
Console.WriteLine($"The voltage of device #{unitId} between L1 and N is: {voltage:N2}V");
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to use the `RTU over TCP` protocol instead, you can do this on initialization:
|
If you have a device speaking `RTU` connected over `TCP`, you can use it as followed:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// [...]
|
// [...]
|
||||||
|
|
||||||
using var client = new ModbusTcpClient(host, port)
|
using var client = new ModbusTcpClient(host, port)
|
||||||
{
|
{
|
||||||
Protocol = new RtuOverTcpProtocol();
|
Protocol = new RtuProtocol()
|
||||||
};
|
};
|
||||||
|
|
||||||
// [...]
|
// [...]
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
<PackageReference Include="Moq" Version="4.20.70" />
|
<PackageReference Include="Moq" Version="4.20.70" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
BIN
AMWD.Protocols.Modbus.snk
Normal file
BIN
AMWD.Protocols.Modbus.snk
Normal file
Binary file not shown.
18
CHANGELOG.md
18
CHANGELOG.md
@@ -10,6 +10,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
_no changes_
|
_no changes_
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.3.2] (2024-09-04)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Build configuration for strong named assemblies.
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.3.1] (2024-06-28)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Issues with range validation on several lines of code in server implementations.
|
||||||
|
|
||||||
|
|
||||||
## [v0.3.0] (2024-05-31)
|
## [v0.3.0] (2024-05-31)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -38,6 +52,8 @@ So this tag is only here for documentation purposes of the NuGet Gallery.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...HEAD
|
[Unreleased]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.2...HEAD
|
||||||
|
[v0.3.2]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.1...v0.3.2
|
||||||
|
[v0.3.1]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.3.0...v0.3.1
|
||||||
[v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0
|
[v0.3.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/compare/v0.2.0...v0.3.0
|
||||||
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0
|
[v0.2.0]: https://github.com/AM-WD/AMWD.Protocols.Modbus/tree/v0.2.0
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<Authors>Andreas Müller</Authors>
|
<Authors>Andreas Müller</Authors>
|
||||||
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
<Copyright>© {copyright:2018-} AM.WD</Copyright>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
||||||
|
<SignAssembly>true</SignAssembly>
|
||||||
|
<AssemblyOriginatorKeyFile>../AMWD.Protocols.Modbus.snk</AssemblyOriginatorKeyFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
|
||||||
|
|||||||
Reference in New Issue
Block a user