Skip to content

Commit 925b9de

Browse files
authored
Merge pull request #65 from mycroes/read-write-errors
Read write errors
2 parents 36051aa + fba09e0 commit 925b9de

9 files changed

Lines changed: 385 additions & 30 deletions

Sally7.Tests/Protocol/CommunicationSequence.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public CommunicationSequence AddCommunicationSetup()
151151
}
152152

153153
public CommunicationSequence AddRead(Area area, int dbNumber, int address, int length, TransportSize transportSize,
154-
VariableType variableType, byte[] data)
154+
VariableType variableType, byte[] data, ReadWriteErrorCode responseErrorCode = ReadWriteErrorCode.Success)
155155
{
156156
var dataLength = 4 + data.Length;
157157

@@ -219,15 +219,15 @@ public CommunicationSequence AddRead(Area area, int dbNumber, int address, int l
219219
1, // Number of items
220220

221221
// DataItem
222-
0xff, // ErrorCode
222+
(byte) responseErrorCode, // ErrorCode
223223
(byte) transportSize, // Transport size
224224
(byte) (data.Length >> 5 & 0xff), // Data length, upper byte, in bits
225225
(byte) (data.Length << 3 & 0xff), // Data length, lower byte, in bits
226226
}.Concat(Fragment.FromBytes(data)).ToArray());
227227
}
228228

229229
public CommunicationSequence AddWrite(Area area, int dbNumber, int address, int length, TransportSize transportSize,
230-
VariableType variableType, byte[] data)
230+
VariableType variableType, byte[] data, ReadWriteErrorCode responseErrorCode = ReadWriteErrorCode.Success)
231231
{
232232
var dataLength = 4 + data.Length;
233233

@@ -301,7 +301,7 @@ public CommunicationSequence AddWrite(Area area, int dbNumber, int address, int
301301
1, // Number of items
302302

303303
// Result code per item
304-
0xff, // ErrorCode
304+
(byte) responseErrorCode, // ErrorCode
305305
}.Concat(Fragment.FromBytes(data)).ToArray());
306306
}
307307

@@ -376,4 +376,4 @@ private static Socket CreateBoundListenSocket(out int port)
376376

377377
return socket;
378378
}
379-
}
379+
}

Sally7.Tests/Protocol/CommunicationTests.cs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,110 @@ async Task Client(int port)
7979

8080
await Task.WhenAll(communication.Serve(out var port), Client(port));
8181
}
82-
}
82+
83+
[Fact]
84+
public async Task Verify_Read_WithResults_DoesNotThrowAndReturnsErrorCode()
85+
{
86+
var sourceTsap = new Tsap(201, 202);
87+
var destinationTsap = new Tsap(203, 204);
88+
var dataItem = new DataBlockDataItem<short>(9, 6);
89+
var results = new ReadWriteErrorCode[1];
90+
91+
var communication = new CommunicationSequence(_output)
92+
.AddConnectRequest(PduSizeParameter.PduSize.Pdu1024, sourceTsap, destinationTsap)
93+
.AddCommunicationSetup()
94+
.AddRead(Area.DataBlock, 9, 6 << 3, 2, TransportSize.Byte, VariableType.Byte, [2, 1], ReadWriteErrorCode.AddressOutOfRange);
95+
96+
async Task Client(int port)
97+
{
98+
var conn = new S7Connection(IPAddress.Loopback.ToString(), port, sourceTsap, destinationTsap);
99+
await conn.OpenAsync();
100+
await conn.ReadAsync([dataItem], results);
101+
conn.Close();
102+
}
103+
104+
await Task.WhenAll(communication.Serve(out var port), Client(port));
105+
results[0].ShouldBe(ReadWriteErrorCode.AddressOutOfRange);
106+
}
107+
108+
[Fact]
109+
public async Task Verify_Read_WithoutResults_ThrowsAggregateWithDataItemReadWriteException()
110+
{
111+
var sourceTsap = new Tsap(201, 202);
112+
var destinationTsap = new Tsap(203, 204);
113+
var dataItem = new DataBlockDataItem<short>(9, 6);
114+
115+
var communication = new CommunicationSequence(_output)
116+
.AddConnectRequest(PduSizeParameter.PduSize.Pdu1024, sourceTsap, destinationTsap)
117+
.AddCommunicationSetup()
118+
.AddRead(Area.DataBlock, 9, 6 << 3, 2, TransportSize.Byte, VariableType.Byte, [2, 1], ReadWriteErrorCode.AddressOutOfRange);
119+
120+
async Task Client(int port)
121+
{
122+
var conn = new S7Connection(IPAddress.Loopback.ToString(), port, sourceTsap, destinationTsap);
123+
await conn.OpenAsync();
124+
125+
var ex = await Should.ThrowAsync<AggregateException>(() => conn.ReadAsync(dataItem));
126+
ex.InnerExceptions.Count.ShouldBe(1);
127+
var itemEx = ex.InnerExceptions[0].ShouldBeOfType<DataItemReadWriteException>();
128+
itemEx.ErrorCode.ShouldBe(ReadWriteErrorCode.AddressOutOfRange);
129+
130+
conn.Close();
131+
}
132+
133+
await Task.WhenAll(communication.Serve(out var port), Client(port));
134+
}
135+
136+
[Fact]
137+
public async Task Verify_Write_WithResults_DoesNotThrowAndReturnsErrorCode()
138+
{
139+
var sourceTsap = new Tsap(201, 202);
140+
var destinationTsap = new Tsap(203, 204);
141+
var dataItem = new DataBlockDataItem<short>(9, 6) { Value = 513 };
142+
var results = new ReadWriteErrorCode[1];
143+
144+
var communication = new CommunicationSequence(_output)
145+
.AddConnectRequest(PduSizeParameter.PduSize.Pdu1024, sourceTsap, destinationTsap)
146+
.AddCommunicationSetup()
147+
.AddWrite(Area.DataBlock, 9, 6 << 3, 2, TransportSize.Byte, VariableType.Byte, [2, 1], ReadWriteErrorCode.ObjectDoesNotExist);
148+
149+
async Task Client(int port)
150+
{
151+
var conn = new S7Connection(IPAddress.Loopback.ToString(), port, sourceTsap, destinationTsap);
152+
await conn.OpenAsync();
153+
await conn.WriteAsync([dataItem], results);
154+
conn.Close();
155+
}
156+
157+
await Task.WhenAll(communication.Serve(out var port), Client(port));
158+
results[0].ShouldBe(ReadWriteErrorCode.ObjectDoesNotExist);
159+
}
160+
161+
[Fact]
162+
public async Task Verify_Write_WithoutResults_ThrowsAggregateWithDataItemReadWriteException()
163+
{
164+
var sourceTsap = new Tsap(201, 202);
165+
var destinationTsap = new Tsap(203, 204);
166+
var dataItem = new DataBlockDataItem<short>(9, 6) { Value = 513 };
167+
168+
var communication = new CommunicationSequence(_output)
169+
.AddConnectRequest(PduSizeParameter.PduSize.Pdu1024, sourceTsap, destinationTsap)
170+
.AddCommunicationSetup()
171+
.AddWrite(Area.DataBlock, 9, 6 << 3, 2, TransportSize.Byte, VariableType.Byte, [2, 1], ReadWriteErrorCode.ObjectDoesNotExist);
172+
173+
async Task Client(int port)
174+
{
175+
var conn = new S7Connection(IPAddress.Loopback.ToString(), port, sourceTsap, destinationTsap);
176+
await conn.OpenAsync();
177+
178+
var ex = await Should.ThrowAsync<AggregateException>(() => conn.WriteAsync(dataItem));
179+
ex.InnerExceptions.Count.ShouldBe(1);
180+
var itemEx = ex.InnerExceptions[0].ShouldBeOfType<DataItemReadWriteException>();
181+
itemEx.ErrorCode.ShouldBe(ReadWriteErrorCode.ObjectDoesNotExist);
182+
183+
conn.Close();
184+
}
185+
186+
await Task.WhenAll(communication.Serve(out var port), Client(port));
187+
}
188+
}

Sally7/DataItemException.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
3+
namespace Sally7;
4+
5+
/// <summary>
6+
/// Exception thrown for errors related to a specific data item, such as invalid configuration or read/write errors.
7+
/// </summary>
8+
/// <param name="dataItem">The data item that caused the error.</param>
9+
/// <param name="message">The error message.</param>
10+
public class DataItemException(IDataItem dataItem, string message) : Exception(
11+
message)
12+
{
13+
/// <summary>
14+
/// Gets the data item that caused the error.
15+
/// </summary>
16+
public IDataItem DataItem { get; } = dataItem;
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Sally7.Protocol.S7;
2+
3+
namespace Sally7;
4+
5+
/// <summary>
6+
/// Exception thrown when a read or write operation on a data item returns an error code.
7+
/// </summary>
8+
/// <param name="dataItem">The data item that caused the error.</param>
9+
/// <param name="errorCode">The error code returned by the operation.</param>
10+
/// <param name="operation">The operation for which the error occurred.</param>
11+
public class DataItemReadWriteException(IDataItem dataItem, ReadWriteErrorCode errorCode, string operation) :
12+
DataItemException(dataItem, $"{operation} of dataItem {dataItem} returned {errorCode}")
13+
{
14+
/// <summary>
15+
/// Gets the error code returned by the read or write operation.
16+
/// </summary>
17+
public ReadWriteErrorCode ErrorCode { get; } = errorCode;
18+
}

Sally7/Framework/CodeAnalysis.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,16 @@ namespace System.Diagnostics.CodeAnalysis
66
internal sealed class DoesNotReturnAttribute : Attribute
77
{
88
}
9+
10+
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
11+
internal sealed class NotNullWhenAttribute : Attribute
12+
{
13+
public NotNullWhenAttribute(bool returnValue)
14+
{
15+
ReturnValue = returnValue;
16+
}
17+
18+
public bool ReturnValue { get; }
19+
}
920
}
1021
#endif
Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,49 @@
11
namespace Sally7.Protocol.S7
22
{
3-
internal enum ReadWriteErrorCode : byte
3+
/// <summary>
4+
/// Defines error codes that can be returned for read and write operations on a S7 PLC.
5+
/// These error codes are defined in the S7 protocol specification and can be used to determine the reason for a failed read or write operation.
6+
/// </summary>
7+
public enum ReadWriteErrorCode : byte
48
{
9+
/// <summary>
10+
/// Indicates that the error code is reserved and should not be used.
11+
/// </summary>
512
Reserved = 0x00,
13+
14+
/// <summary>
15+
/// Indicates that a hardware fault occurred during the read or write operation.
16+
/// </summary>
617
HardwareFault = 0x01,
18+
19+
/// <summary>
20+
/// Indicates that the requested operation is not allowed on the accessed object.
21+
/// </summary>
722
AccessingObjectNotAllowed = 0x03,
23+
24+
/// <summary>
25+
/// Indicates that the address specified in the read or write request is out of range for the accessed object.
26+
/// </summary>
827
AddressOutOfRange = 0x05,
28+
29+
/// <summary>
30+
/// Indicates that the data type specified in the read or write request is not supported by the accessed object.
31+
/// </summary>
932
DataTypeNotSupported = 0x06,
33+
34+
/// <summary>
35+
/// Indicates that the data type specified in the read or write request is inconsistent with the data type of the accessed object.
36+
/// </summary>
1037
DataTypeInconsistent = 0x07,
38+
39+
/// <summary>
40+
/// Indicates that the request object does not exist.
41+
/// </summary>
1142
ObjectDoesNotExist = 0x0a,
43+
44+
/// <summary>
45+
/// Indicates that the read or write operation was successful and no error occurred.
46+
/// </summary>
1247
Success = 0xff
1348
}
1449
}

Sally7/ReadWriteErrorHelpers.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Runtime.InteropServices;
5+
using Sally7.Protocol.S7;
6+
7+
namespace Sally7;
8+
9+
/// <summary>
10+
/// Helper methods for handling read/write errors on data items, including checking for errors and throwing exceptions with detailed information about the failed items.
11+
/// </summary>
12+
public static class ReadWriteErrorHelpers
13+
{
14+
/// <summary>
15+
/// Checks if any of the read/write operations returned an error code other than Success.
16+
/// </summary>
17+
/// <param name="results">The results of the read/write operations.</param>
18+
/// <returns>True if any operation returned an error code other than Success; otherwise, false.</returns>
19+
public static bool HasErrors(ReadOnlySpan<ReadWriteErrorCode> results)
20+
{
21+
#if NET8_0_OR_GREATER
22+
return MemoryMarshal.AsBytes(results).IndexOfAnyExcept((byte)ReadWriteErrorCode.Success) != -1;
23+
#else
24+
for (var i = 0; i < results.Length; i++)
25+
{
26+
if (results[i] != ReadWriteErrorCode.Success) return true;
27+
}
28+
29+
return false;
30+
#endif
31+
}
32+
33+
/// <summary>
34+
/// Checks the results of read/write operations and throws an AggregateException if any operation returned an error code other than Success.
35+
/// </summary>
36+
/// <param name="operation">The operation for which the error occurred.</param>
37+
/// <param name="dataItems">The data items involved in the operations.</param>
38+
/// <param name="results">The results of the read/write operations.</param>
39+
public static void ThrowIfHasErrors(
40+
string operation,
41+
ReadOnlySpan<IDataItem> dataItems,
42+
ReadOnlySpan<ReadWriteErrorCode> results)
43+
{
44+
ThrowIfResultsLengthDoesNotMatchDataItemsLength(dataItems, results);
45+
46+
if (!HasErrors(results)) return;
47+
48+
ThrowReadWriteException(operation, dataItems, results);
49+
}
50+
51+
/// <summary>
52+
/// Throws an AggregateException containing a DataItemReadWriteException for each data item that returned an error code other than Success.
53+
/// </summary>
54+
/// <param name="operation">The operation for which the error occurred.</param>
55+
/// <param name="dataItems">The data items involved in the operations.</param>
56+
/// <param name="results">The results of the read/write operations.</param>
57+
/// <exception cref="AggregateException">Thrown with a DataItemReadWriteException for each data item that returned an error code other than Success.</exception>
58+
[DoesNotReturn]
59+
public static void ThrowReadWriteException(string operation, ReadOnlySpan<IDataItem> dataItems,
60+
ReadOnlySpan<ReadWriteErrorCode> results)
61+
{
62+
ThrowIfResultsLengthDoesNotMatchDataItemsLength(dataItems, results);
63+
64+
List<Exception> exceptions = [];
65+
66+
for (var i = 0; i < dataItems.Length; i++)
67+
{
68+
if (results[i] == ReadWriteErrorCode.Success) continue;
69+
70+
exceptions.Add(new DataItemReadWriteException(dataItems[i], results[i], operation));
71+
}
72+
73+
throw new AggregateException($"One or more errors occurred during {operation} operation.", exceptions);
74+
}
75+
76+
private static void ThrowIfResultsLengthDoesNotMatchDataItemsLength(
77+
ReadOnlySpan<IDataItem> dataItems,
78+
ReadOnlySpan<ReadWriteErrorCode> results)
79+
{
80+
if (results.Length >= dataItems.Length) return;
81+
82+
throw new ArgumentException("Results length needs to be at least the same as the data items length.", nameof(results));
83+
}
84+
}

0 commit comments

Comments
 (0)