diff --git a/AMWD.Protocols.Modbus.Serial/ModbusSerialClient.cs b/AMWD.Protocols.Modbus.Serial/ModbusSerialClient.cs index d0251de..782981b 100644 --- a/AMWD.Protocols.Modbus.Serial/ModbusSerialClient.cs +++ b/AMWD.Protocols.Modbus.Serial/ModbusSerialClient.cs @@ -16,7 +16,7 @@ namespace AMWD.Protocols.Modbus.Serial /// /// The name of the serial port to use. public ModbusSerialClient(string portName) - : this(new ModbusSerialConnection { PortName = portName }) + : this(new ModbusSerialConnection(portName)) { } /// @@ -41,8 +41,8 @@ namespace AMWD.Protocols.Modbus.Serial Protocol = new RtuProtocol(); } - /// - public static string[] AvailablePortNames => SerialPort.GetPortNames(); + /// + public static string[] AvailablePortNames => ModbusSerialConnection.AvailablePortNames; /// public TimeSpan IdleTimeout diff --git a/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs b/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs index 94cc1fa..a091c39 100644 --- a/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs +++ b/AMWD.Protocols.Modbus.Serial/ModbusSerialConnection.cs @@ -39,10 +39,15 @@ namespace AMWD.Protocols.Modbus.Serial /// /// Initializes a new instance of the class. /// - public ModbusSerialConnection() + public ModbusSerialConnection(string portName) { + if (string.IsNullOrWhiteSpace(portName)) + throw new ArgumentNullException(nameof(portName)); + _serialPort = new SerialPortWrapper { + PortName = portName, + BaudRate = (int)BaudRate.Baud19200, DataBits = 8, Handshake = Handshake.None, @@ -59,6 +64,9 @@ namespace AMWD.Protocols.Modbus.Serial #region Properties + /// + public static string[] AvailablePortNames => SerialPort.GetPortNames(); + /// public string Name => "Serial"; @@ -68,20 +76,6 @@ namespace AMWD.Protocols.Modbus.Serial /// public virtual TimeSpan ConnectTimeout { get; set; } = TimeSpan.MaxValue; - /// - public virtual TimeSpan ReadTimeout - { - get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout); - set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds; - } - - /// - public virtual TimeSpan WriteTimeout - { - get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout); - set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds; - } - /// /// Gets or sets a value indicating whether the RS485 driver has to be enabled via software switch. /// @@ -171,6 +165,20 @@ namespace AMWD.Protocols.Modbus.Serial set => _serialPort.StopBits = value; } + /// + public virtual TimeSpan ReadTimeout + { + get => TimeSpan.FromMilliseconds(_serialPort.ReadTimeout); + set => _serialPort.ReadTimeout = (int)value.TotalMilliseconds; + } + + /// + public virtual TimeSpan WriteTimeout + { + get => TimeSpan.FromMilliseconds(_serialPort.WriteTimeout); + set => _serialPort.WriteTimeout = (int)value.TotalMilliseconds; + } + #endregion SerialPort Properties #endregion Properties diff --git a/AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs b/AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs index a631043..305ae20 100644 --- a/AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs +++ b/AMWD.Protocols.Modbus.Serial/Utils/SerialPortWrapper.cs @@ -20,6 +20,39 @@ namespace AMWD.Protocols.Modbus.Serial.Utils #endregion Fields + #region Constructor + + public SerialPortWrapper() + { + _serialPort.DataReceived += OnDataReceived; + _serialPort.PinChanged += OnPinChanged; + _serialPort.ErrorReceived += OnErrorReceived; + } + + #endregion Constructor + + #region Events + + /// + public virtual event SerialDataReceivedEventHandler DataReceived; + + /// + public virtual event SerialPinChangedEventHandler PinChanged; + + /// + public virtual event SerialErrorReceivedEventHandler ErrorReceived; + + private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) + => DataReceived?.Invoke(sender, e); + + private void OnPinChanged(object sender, SerialPinChangedEventArgs e) + => PinChanged?.Invoke(sender, e); + + private void OnErrorReceived(object sender, SerialErrorReceivedEventArgs e) + => ErrorReceived?.Invoke(sender, e); + + #endregion Events + #region Properties /// @@ -82,6 +115,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils set => _serialPort.Parity = value; } + /// + public virtual int BytesToWrite + => _serialPort.BytesToWrite; + /// public virtual int BaudRate { @@ -89,6 +126,10 @@ namespace AMWD.Protocols.Modbus.Serial.Utils set => _serialPort.BaudRate = value; } + /// + public virtual int BytesToRead + => _serialPort.BytesToRead; + #endregion Properties #region Methods @@ -101,6 +142,14 @@ namespace AMWD.Protocols.Modbus.Serial.Utils public virtual void Open() => _serialPort.Open(); + /// + public virtual int Read(byte[] buffer, int offset, int count) + => _serialPort.Read(buffer, offset, count); + + /// + public virtual void Write(byte[] buffer, int offset, int count) + => _serialPort.Write(buffer, offset, count); + /// public virtual void Dispose() => _serialPort.Dispose(); @@ -117,7 +166,7 @@ namespace AMWD.Protocols.Modbus.Serial.Utils /// /// There seems to be a bug with the async stream implementation on Windows. ///
- /// See this StackOverflow answer: + /// See this StackOverflow answer: . ///
/// The buffer to write the data into. /// The byte offset in buffer at which to begin writing data from the serial port. diff --git a/AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialClientTest.cs b/AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialClientTest.cs index e8ba84c..1d4ce78 100644 --- a/AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialClientTest.cs +++ b/AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialClientTest.cs @@ -13,13 +13,15 @@ namespace AMWD.Protocols.Modbus.Tests.Serial [TestInitialize] public void Initialize() { + string portName = "COM-42"; + _genericConnectionMock = new Mock(); _genericConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(40)); _genericConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(30)); _genericConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(20)); _genericConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(10)); - _serialConnectionMock = new Mock(); + _serialConnectionMock = new Mock(portName); _serialConnectionMock.Setup(c => c.IdleTimeout).Returns(TimeSpan.FromSeconds(10)); _serialConnectionMock.Setup(c => c.ConnectTimeout).Returns(TimeSpan.FromSeconds(20)); @@ -28,7 +30,7 @@ namespace AMWD.Protocols.Modbus.Tests.Serial _serialConnectionMock.Setup(c => c.DriverEnabledRS485).Returns(true); _serialConnectionMock.Setup(c => c.InterRequestDelay).Returns(TimeSpan.FromSeconds(50)); - _serialConnectionMock.Setup(c => c.PortName).Returns("COM-42"); + _serialConnectionMock.Setup(c => c.PortName).Returns(portName); _serialConnectionMock.Setup(c => c.BaudRate).Returns(BaudRate.Baud2400); _serialConnectionMock.Setup(c => c.DataBits).Returns(7); _serialConnectionMock.Setup(c => c.Handshake).Returns(Handshake.XOnXOff); @@ -231,5 +233,18 @@ namespace AMWD.Protocols.Modbus.Tests.Serial _serialConnectionMock.VerifyNoOtherCalls(); } + + [TestMethod] + public void ShouldPrintCleanString() + { + // Arrange + using var client = new ModbusSerialClient(_serialConnectionMock.Object); + + // Act + string str = client.ToString(); + + // Assert + SnapshotAssert.AreEqual(str); + } } } diff --git a/AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialConnectionTest.cs b/AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialConnectionTest.cs index c847fd0..d173eb7 100644 --- a/AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialConnectionTest.cs +++ b/AMWD.Protocols.Modbus.Tests/Serial/ModbusSerialConnectionTest.cs @@ -90,6 +90,21 @@ namespace AMWD.Protocols.Modbus.Tests.Serial connection.Dispose(); } + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNullExceptionOnCreate(string portName) + { + // Arrange + + // Act + using var test = new ModbusSerialClient(portName); + + // Assert - ArgumentNullException + } + [TestMethod] [ExpectedException(typeof(ObjectDisposedException))] public async Task ShouldThrowDisposedExceptionOnInvokeAsync() @@ -467,7 +482,7 @@ namespace AMWD.Protocols.Modbus.Tests.Serial return Task.FromResult(0); }); - var connection = new ModbusSerialConnection(); + var connection = new ModbusSerialConnection("some-port"); // Replace real connection with mock var connectionField = connection.GetType().GetField("_serialPort", BindingFlags.NonPublic | BindingFlags.Instance); diff --git a/AMWD.Protocols.Modbus.Tests/Serial/Snapshots/ModbusSerialClientTest/ShouldPrintCleanString.snap.bin b/AMWD.Protocols.Modbus.Tests/Serial/Snapshots/ModbusSerialClientTest/ShouldPrintCleanString.snap.bin new file mode 100644 index 0000000..f1bbcab --- /dev/null +++ b/AMWD.Protocols.Modbus.Tests/Serial/Snapshots/ModbusSerialClientTest/ShouldPrintCleanString.snap.bin @@ -0,0 +1,8 @@ +Serial Client COM-42 + BaudRate: 2400 + DataBits: 7 + StopBits: 1.5 + Parity: space + Handshake: xonxoff + RtsEnable: true + DriverEnabledRS485: true diff --git a/AMWD.Protocols.Modbus.Tests/SnapshotAssert.cs b/AMWD.Protocols.Modbus.Tests/SnapshotAssert.cs new file mode 100644 index 0000000..05f8565 --- /dev/null +++ b/AMWD.Protocols.Modbus.Tests/SnapshotAssert.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace AMWD.Protocols.Modbus.Tests +{ + // ================================================================================================================================ // + // Source: https://git.am-wd.de/am-wd/common/-/blob/fb26e441a48214aaae72003c4a5ac33d5c7b929a/src/AMWD.Common.Test/SnapshotAssert.cs // + // ================================================================================================================================ // + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed class SnapshotAssert + { + /// + /// Tests whether the specified string is equal to the saved snapshot. + /// + /// The current aggregated content string. + /// An optional message to display if the assertion fails. + /// The absolute file path of the calling file (filled automatically on compile time). + /// The name of the calling method (filled automatically on compile time). + public static void AreEqual(string actual, string message = null, [CallerFilePath] string callerFilePath = null, [CallerMemberName] string callerMemberName = null) + { + string cleanLineEnding = actual + .Replace("\r\n", "\n") // Windows + .Replace("\r", "\n"); // MacOS + AreEqual(Encoding.UTF8.GetBytes(cleanLineEnding), message, callerFilePath, callerMemberName); + } + + /// + /// Tests whether the specified byte array is equal to the saved snapshot. + /// + /// The current aggregated content bytes. + /// An optional message to display if the assertion fails. + /// The absolute file path of the calling file (filled automatically on compile time). + /// The name of the calling method (filled automatically on compile time). + public static void AreEqual(byte[] actual, string message = null, [CallerFilePath] string callerFilePath = null, [CallerMemberName] string callerMemberName = null) + => AreEqual(actual, null, message, callerFilePath, callerMemberName); + + /// + /// Tests whether the specified byte array is equal to the saved snapshot. + /// + /// + /// The past has shown, that e.g. wkhtmltopdf prints the current timestamp at the beginning of the PDF file. + /// Therefore you can specify which sequences of bytes should be excluded from the comparison. + /// + /// The current aggregated content bytes. + /// The excluded sequences. + /// An optional message to display if the assertion fails. + /// The absolute file path of the calling file (filled automatically on compile time). + /// The name of the calling method (filled automatically on compile time). + public static void AreEqual(byte[] actual, List<(int Start, int Length)> excludedSequences = null, string message = null, [CallerFilePath] string callerFilePath = null, [CallerMemberName] string callerMemberName = null) + { + string callerDirectory = Path.GetDirectoryName(callerFilePath); + string callerFileName = Path.GetFileNameWithoutExtension(callerFilePath); + + string snapshotDirectory = Path.Combine(callerDirectory, "Snapshots", callerFileName); + string snapshotFilePath = Path.Combine(snapshotDirectory, $"{callerMemberName}.snap.bin"); + + if (File.Exists(snapshotFilePath)) + { + byte[] expected = File.ReadAllBytes(snapshotFilePath); + if (actual.Length != expected.Length) + Assert.Fail(message); + + for (int i = 0; i < actual.Length; i++) + { + if (excludedSequences?.Any(s => s.Start <= i && i < s.Start + s.Length) == true) + continue; + + if (actual[i] != expected[i]) + Assert.Fail(message); + } + } + else + { + if (!Directory.Exists(snapshotDirectory)) + Directory.CreateDirectory(snapshotDirectory); + + File.WriteAllBytes(snapshotFilePath, actual); + } + } + } +}