39 Commits

Author SHA1 Message Date
Michael Croes
ab6308eacd Merge pull request #506 from bonk-dev/s5-date
Add support for reading/writing the legacy DATE (IEC date) datatype
2023-09-07 21:26:25 +02:00
Michael Croes
130eeadbd8 Merge branch 'main' into s5-date 2023-09-07 21:20:54 +02:00
Michael Croes
76a7ea04f7 Merge pull request #507 from mycroes/clock
Add support for reading and writing PLC clock
2023-09-05 21:25:25 +02:00
Michael Croes
4764c997ed refactor: Replace DateTime.Length with Plc.DateTimeLength in Plc.Clock 2023-09-04 22:20:31 +02:00
Michael Croes
cb24e9a046 feat: Implement clock write support 2023-09-04 22:15:52 +02:00
Michael Croes
10315b4b4c style: Add missing space in WriteClockAsync summary 2023-09-04 21:55:34 +02:00
Michael Croes
0774e124bf test: Fix comments in write clock messages 2023-09-04 21:55:06 +02:00
Michael Croes
1ebffe08e7 test: Add Write_Clock_Value test 2023-08-31 19:55:57 +02:00
Michael Croes
f419df4d73 feat: Stub PLC WriteClock methods 2023-08-31 19:55:41 +02:00
Michael Croes
1969aac1b2 refactor: Rename WriteSzlRequestHeader to WriteUserDataRequest 2023-08-31 19:36:00 +02:00
Michael Croes
2f2dcf7281 test: Raise ReadClock timeout to 1 second 2023-08-29 21:38:01 +02:00
Michael Croes
07325db2fa feat: Implement clock reading 2023-08-29 21:34:09 +02:00
Michael Croes
eada47cd24 refactor(PLCHelpers): Extract WriteSzlRequestHeader 2023-08-29 20:41:55 +02:00
Michael Croes
5e1ac8c7bf test(ReadClock): Use template parameters for PDU bytes 2023-08-29 19:42:59 +02:00
Michael Croes
13544a1bcf test: Add ReadClock test 2023-08-23 23:03:45 +02:00
Michael Croes
6fc526b886 Stub PLC ReadClock methods 2023-08-23 22:40:57 +02:00
bonk-dev
f227ad4b53 Expose IecMinDate and IecMaxDate as properties 2023-08-23 12:03:22 +02:00
Dawid Pągowski
e4cc42fa51 Add support for serializing IEC date 2023-08-23 01:04:23 +02:00
Dawid Pągowski
689e7ffd96 Increase the maximum date
The spec goes up to a full 2168 year, but the PLC date type goes up to 2169 June 06 which is represented by 65535 (max ushort value).
2023-08-23 00:53:29 +02:00
Dawid Pągowski
8087b8d315 Change the thrown exceptions to ArgumentOutOfRangeException 2023-08-23 00:43:17 +02:00
Dawid Pągowski
a55ceba679 Add IEC Date VarType support to PLCHelpers.cs 2023-08-23 00:38:24 +02:00
Dawid Pągowski
eb1fad9333 Add IEC date DataType 2023-08-23 00:27:55 +02:00
Michael Croes
0de9364dee Merge pull request #504 from mycroes/remove-appveyor-config
Remove leftover appveyor.yml
2023-08-22 15:49:38 +02:00
Michael Croes
9380ea85c3 chore: Remove leftover appveyor.yml 2023-08-21 21:23:46 +02:00
Michael Croes
22451bc440 Merge pull request #501 from Himmelt/add_set_bit
Add SetBit method to modify one bit of a byte
2023-08-18 20:48:32 +02:00
Himmelt
e98ce005c5 update param description 2023-08-18 16:50:53 +08:00
Himmelt
11a40cc5e3 update summary 2023-08-18 16:37:50 +08:00
Himmelt
f79286b2d0 Apply suggestions from code review
Co-authored-by: Michael Croes <mycroes@gmail.com>
2023-08-18 15:58:35 +08:00
Himmelt
fadd7d0cb3 update params descriptions and change param name from "bitPosition" to "index" 2023-08-17 13:33:07 +08:00
Himmelt
652ff3a9bb Update S7.Net/Conversion.cs
Co-authored-by: Günther Foidl <gue@korporal.at>
2023-08-16 17:19:53 +08:00
Himmelt
9c0fea721a Update S7.Net/Conversion.cs
Co-authored-by: Günther Foidl <gue@korporal.at>
2023-08-16 17:18:33 +08:00
Himmelt
2ec73224c1 Add SetBit method to modify one bit of a byte 2023-08-15 18:08:10 +08:00
Michael Croes
a8ef47b475 Merge pull request #490 from bonk-dev/timespan
Added support for serializing TimeSpan
2023-08-04 08:55:08 +02:00
Michael Croes
55aa06a1fc Merge branch 'main' into timespan 2023-08-03 21:55:20 +02:00
Michael Croes
7e631a713f Merge pull request #498 from S7NetPlus/github-actions-logger
chore: Update GitHubActionsTestLogger
2023-08-03 21:52:03 +02:00
Dawid Pągowski
49e4d3369a Add TimeSpan serialization to Struct 2023-07-19 21:12:54 +02:00
Dawid Pągowski
ee06bec0fb Fix the documentation 2023-07-19 21:04:07 +02:00
Dawid Pągowski
05ccb05f3a Added TimeSpan tests 2023-07-19 20:57:14 +02:00
Dawid Pągowski
0d2817661e Add S7 Time type (C# TimeSpan)
Adds the S7 TIME (IEC) type (32 bits long)
It is deserialized to C# TimeSpan and serialized as S7 DInt.
2023-07-19 20:30:32 +02:00
16 changed files with 824 additions and 32 deletions

View File

@@ -0,0 +1,259 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Protocol;
namespace S7.Net.UnitTest.CommunicationTests;
[TestClass]
public class Clock
{
[TestMethod, Timeout(1000)]
public async Task Read_Clock_Value()
{
var cs = new CommunicationSequence
{
ConnectionOpenTemplates.ConnectionRequestConfirm,
ConnectionOpenTemplates.CommunicationSetup,
{
"""
// TPKT
03 00 00 1d
// COTP
02 f0 80
// S7 read clock
// UserData header
32 07 00 00 PDU1 PDU2
// Parameter length
00 08
// Data length
00 04
// Parameter
// Head
00 01 12
// Length
04
// Method (Request/Response): Req
11
// Type request (4...) Function group timers (...7)
47
// Subfunction: read clock
01
// Sequence number
00
// Data
// Return code
0a
// Transport size
00
// Payload length
00 00
""",
"""
// TPKT
03 00 00 2b
// COTP
02 f0 80
// S7 read clock response
// UserData header
32 07 00 00 PDU1 PDU2
// Parameter length
00 0c
// Data length
00 0e
// Parameter
// Head
00 01 12
// Length
08
// Method (Request/Response): Res
12
// Type response (8...) Function group timers (...7)
87
// Subfunction: read clock
01
// Sequence number
01
// Data unit reference
00
// Last data unit? Yes
00
// Error code
00 00
// Data
// Error code
ff
// Transport size: OCTET STRING
09
// Length
00 0a
// Timestamp
// Reserved
00
// Year 1
19
// Year 2
14
// Month
08
// Day
20
// Hour
11
// Minute
59
// Seconds
43
// Milliseconds: 912..., Day of week: ...4
91 24
"""
}
};
static async Task Client(int port)
{
var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4)));
await conn.OpenAsync();
var time = await conn.ReadClockAsync();
Assert.AreEqual(new DateTime(2014, 8, 20, 11, 59, 43, 912), time);
conn.Close();
}
await Task.WhenAll(cs.Serve(out var port), Client(port));
}
[TestMethod, Timeout(1000)]
public async Task Write_Clock_Value()
{
var cs = new CommunicationSequence
{
ConnectionOpenTemplates.ConnectionRequestConfirm,
ConnectionOpenTemplates.CommunicationSetup,
{
"""
// TPKT
03 00 00 27
// COTP
02 f0 80
// S7 read clock
// UserData header
32 07 00 00 PDU1 PDU2
// Parameter length
00 08
// Data length
00 0e
// Parameter
// Head
00 01 12
// Length
04
// Method (Request/Response): Req
11
// Type request (4...) Function group timers (...7)
47
// Subfunction: write clock
02
// Sequence number
00
// Data
// Return code
ff
// Transport size
09
// Payload length
00 0a
// Payload
// Timestamp
// Reserved
00
// Year 1
19
// Year 2
14
// Month
08
// Day
20
// Hour
11
// Minute
59
// Seconds
43
// Milliseconds: 912..., Day of week: ...4
91 24
""",
"""
// TPKT
03 00 00 21
// COTP
02 f0 80
// S7 read clock response
// UserData header
32 07 00 00 PDU1 PDU2
// Parameter length
00 0c
// Data length
00 04
// Parameter
// Head
00 01 12
// Length
08
// Method (Request/Response): Res
12
// Type response (8...) Function group timers (...7)
87
// Subfunction: write clock
02
// Sequence number
01
// Data unit reference
00
// Last data unit? Yes
00
// Error code
00 00
// Data
// Error code
0a
// Transport size: NONE
00
// Length
00 00
"""
}
};
static async Task Client(int port)
{
var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4)));
await conn.OpenAsync();
await conn.WriteClockAsync(new DateTime(2014, 08, 20, 11, 59, 43, 912));
conn.Close();
}
await Task.WhenAll(cs.Serve(out var port), Client(port));
}
}

View File

@@ -24,5 +24,21 @@ namespace S7.Net.UnitTest
Assert.IsFalse(dummyByte.SelectBit(7));
}
[TestMethod]
public void T01_TestSetBit()
{
byte dummyByte = 0xAA; // 1010 1010
dummyByte.SetBit(0, true);
dummyByte.SetBit(1, false);
dummyByte.SetBit(2, true);
dummyByte.SetBit(3, false);
Assert.AreEqual<byte>(dummyByte, 0xA5);// 1010 0101
dummyByte.SetBit(4, true);
dummyByte.SetBit(5, true);
dummyByte.SetBit(6, true);
dummyByte.SetBit(7, true);
Assert.AreEqual<byte>(dummyByte, 0xF5);// 1111 0101
}
}
}

View File

@@ -1006,7 +1006,7 @@ namespace S7.Net.UnitTest
var db = 2;
randomEngine.NextBytes(data);
cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5));
cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5));
try
{
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
@@ -1045,7 +1045,7 @@ namespace S7.Net.UnitTest
var db = 2;
randomEngine.NextBytes(data.Span);
cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5));
cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5));
try
{
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);

View File

@@ -0,0 +1,82 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace S7.Net.UnitTest.TypeTests
{
public static class TimeSpanTests
{
private static readonly TimeSpan SampleTimeSpan = new TimeSpan(12, 0, 59, 37, 856);
private static readonly byte[] SampleByteArray = { 0x3E, 0x02, 0xE8, 0x00 };
private static readonly byte[] SpecMinByteArray = { 0x80, 0x00, 0x00, 0x00 };
private static readonly byte[] SpecMaxByteArray = { 0x7F, 0xFF, 0xFF, 0xFF };
[TestClass]
public class FromByteArray
{
[TestMethod]
public void Sample()
{
AssertFromByteArrayEquals(SampleTimeSpan, SampleByteArray);
}
[TestMethod]
public void SpecMinimum()
{
AssertFromByteArrayEquals(Types.TimeSpan.SpecMinimumTimeSpan, SpecMinByteArray);
}
[TestMethod]
public void SpecMaximum()
{
AssertFromByteArrayEquals(Types.TimeSpan.SpecMaximumTimeSpan, SpecMaxByteArray);
}
private static void AssertFromByteArrayEquals(TimeSpan expected, params byte[] bytes)
{
Assert.AreEqual(expected, Types.TimeSpan.FromByteArray(bytes));
}
}
[TestClass]
public class ToByteArray
{
[TestMethod]
public void Sample()
{
AssertToByteArrayEquals(SampleTimeSpan, SampleByteArray);
}
[TestMethod]
public void SpecMinimum()
{
AssertToByteArrayEquals(Types.TimeSpan.SpecMinimumTimeSpan, SpecMinByteArray);
}
[TestMethod]
public void SpecMaximum()
{
AssertToByteArrayEquals(Types.TimeSpan.SpecMaximumTimeSpan, SpecMaxByteArray);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTimeBeforeSpecMinimum()
{
Types.TimeSpan.ToByteArray(TimeSpan.FromDays(-25));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTimeAfterSpecMaximum()
{
Types.TimeSpan.ToByteArray(new TimeSpan(30, 15, 15, 15, 15));
}
private static void AssertToByteArrayEquals(TimeSpan value, params byte[] expected)
{
CollectionAssert.AreEqual(expected, Types.TimeSpan.ToByteArray(value));
}
}
}
}

View File

@@ -138,19 +138,59 @@ namespace S7.Net
/// <summary>
/// Helper to get a bit value given a byte and the bit index.
/// Example: DB1.DBX0.5 -> var bytes = ReadBytes(DB1.DBW0); bool bit = bytes[0].SelectBit(5);
/// <br/>
/// <example>
/// Get the bit at DB1.DBX0.5:
/// <code>
/// byte data = ReadByte("DB1.DBB0");
/// bool bit = data.SelectBit(5);
/// </code>
/// </example>
/// </summary>
/// <param name="data"></param>
/// <param name="bitPosition"></param>
/// <returns></returns>
public static bool SelectBit(this byte data, int bitPosition)
/// <param name="data">The data to get from.</param>
/// <param name="index">The zero-based index of the bit to get.</param>
/// <returns>The Boolean value will get.</returns>
public static bool SelectBit(this byte data, int index)
{
int mask = 1 << bitPosition;
int mask = 1 << index;
int result = data & mask;
return (result != 0);
}
/// <summary>
/// Helper to set a bit value to the given byte at the bit index.
/// <br/>
/// <example>
/// Set the bit at index 4:
/// <code>
/// byte data = 0;
/// data.SetBit(4, true);
/// </code>
/// </example>
/// </summary>
/// <param name="data">The data to be modified.</param>
/// <param name="index">The zero-based index of the bit to set.</param>
/// <param name="value">The Boolean value to assign to the bit.</param>
public static void SetBit(this ref byte data, int index, bool value)
{
if ((uint)index > 7)
{
return;
}
if (value)
{
byte mask = (byte)(1 << index);
data |= mask;
}
else
{
byte mask = (byte)~(1 << index);
data &= mask;
}
}
/// <summary>
/// Converts from ushort value to short value; it's used to retrieve negative values from words
/// </summary>

View File

@@ -202,10 +202,20 @@
/// DateTIme variable type
/// </summary>
DateTime,
/// <summary>
/// IEC date (legacy) variable type
/// </summary>
Date,
/// <summary>
/// DateTimeLong variable type
/// </summary>
DateTimeLong
DateTimeLong,
/// <summary>
/// S7 TIME variable type - serialized as S7 DInt and deserialized as C# TimeSpan
/// </summary>
Time
}
}

View File

@@ -0,0 +1,23 @@
using System;
using S7.Net.Types;
using DateTime = System.DateTime;
namespace S7.Net.Helper
{
public static class DateTimeExtensions
{
public static ushort GetDaysSinceIecDateStart(this DateTime dateTime)
{
if (dateTime < Date.IecMinDate)
{
throw new ArgumentOutOfRangeException($"DateTime must be at least {Date.IecMinDate:d}");
}
if (dateTime > Date.IecMaxDate)
{
throw new ArgumentOutOfRangeException($"DateTime must be lower than {Date.IecMaxDate:d}");
}
return (ushort)(dateTime - Date.IecMinDate).TotalDays;
}
}
}

View File

@@ -56,28 +56,35 @@ namespace S7.Net
WriteS7Header(stream, s7MessageTypeUserData, parameterLength, dataLength);
}
private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort szlId, ushort szlIndex)
private static void WriteUserDataRequest(System.IO.MemoryStream stream, byte functionGroup, byte subFunction, int dataLength)
{
WriteUserDataHeader(stream, 8, 8);
WriteUserDataHeader(stream, 8, dataLength);
// Parameter
const byte szlMethodRequest = 0x11;
const byte szlTypeRequest = 0b100;
const byte szlFunctionGroupCpuFunctions = 0b100;
const byte subFunctionReadSzl = 0x01;
const byte userDataMethodRequest = 0x11;
const byte userDataTypeRequest = 0x4;
// Parameter head
stream.Write(new byte[] { 0x00, 0x01, 0x12 });
// Parameter length
stream.WriteByte(0x04);
// Method
stream.WriteByte(szlMethodRequest);
stream.WriteByte(userDataMethodRequest);
// Type / function group
stream.WriteByte(szlTypeRequest << 4 | szlFunctionGroupCpuFunctions);
stream.WriteByte((byte)(userDataTypeRequest << 4 | (functionGroup & 0x0f)));
// Subfunction
stream.WriteByte(subFunctionReadSzl);
stream.WriteByte(subFunction);
// Sequence number
stream.WriteByte(0);
}
private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort szlId, ushort szlIndex)
{
// Parameter
const byte szlFunctionGroupCpuFunctions = 0b100;
const byte subFunctionReadSzl = 0x01;
WriteUserDataRequest(stream, szlFunctionGroupCpuFunctions, subFunctionReadSzl, 8);
// Data
const byte success = 0xff;
@@ -235,6 +242,24 @@ namespace S7.Net
{
return DateTimeLong.ToArray(bytes);
}
case VarType.Time:
if (varCount == 1)
{
return TimeSpan.FromByteArray(bytes);
}
else
{
return TimeSpan.ToArray(bytes);
}
case VarType.Date:
if (varCount == 1)
{
return Date.FromByteArray(bytes);
}
else
{
return Date.ToArray(bytes);
}
default:
return null;
}
@@ -264,10 +289,12 @@ namespace S7.Net
case VarType.Timer:
case VarType.Int:
case VarType.Counter:
case VarType.Date:
return varCount * 2;
case VarType.DWord:
case VarType.DInt:
case VarType.Real:
case VarType.Time:
return varCount * 4;
case VarType.LReal:
case VarType.DateTime:
@@ -333,7 +360,7 @@ namespace S7.Net
private static byte[] BuildSzlReadRequestPackage(ushort szlId, ushort szlIndex)
{
var stream = new System.IO.MemoryStream();
WriteSzlReadRequest(stream, szlId, szlIndex);
stream.SetLength(stream.Position);

92
S7.Net/Plc.Clock.cs Normal file
View File

@@ -0,0 +1,92 @@
using System;
using System.IO;
using System.Linq;
using S7.Net.Helper;
using S7.Net.Types;
using DateTime = System.DateTime;
namespace S7.Net;
partial class Plc
{
private const byte SzlFunctionGroupTimers = 0x07;
private const byte SzlSubFunctionReadClock = 0x01;
private const byte SzlSubFunctionWriteClock = 0x02;
private const byte TransportSizeOctetString = 0x09;
private const int PduErrOffset = 20;
private const int UserDataResultOffset = PduErrOffset + 2;
/// <summary>
/// The length in bytes of DateTime stored in the PLC.
/// </summary>
private const int DateTimeLength = 10;
private static byte[] BuildClockReadRequest()
{
var stream = new MemoryStream();
WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionReadClock, 4);
stream.Write(new byte[] { 0x0a, 0x00, 0x00, 0x00 });
stream.SetLength(stream.Position);
return stream.ToArray();
}
private static DateTime ParseClockReadResponse(byte[] message)
{
const int udLenOffset = UserDataResultOffset + 2;
const int udValueOffset = udLenOffset + 2;
const int dateTimeSkip = 2;
AssertPduResult(message);
AssertUserDataResult(message, 0xff);
var len = Word.FromByteArray(message.Skip(udLenOffset).Take(2).ToArray());
if (len != DateTimeLength)
{
throw new Exception($"Unexpected response length {len}, expected {DateTimeLength}.");
}
// Skip first 2 bytes from date time value because DateTime.FromByteArray doesn't parse them.
return Types.DateTime.FromByteArray(message.Skip(udValueOffset + dateTimeSkip)
.Take(DateTimeLength - dateTimeSkip).ToArray());
}
private static byte[] BuildClockWriteRequest(DateTime value)
{
var stream = new MemoryStream();
WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionWriteClock, 14);
stream.Write(new byte[] { 0xff, TransportSizeOctetString, 0x00, DateTimeLength });
// Start of DateTime value, DateTime.ToByteArray only serializes the final 8 bytes
stream.Write(new byte[] { 0x00, 0x19 });
stream.Write(Types.DateTime.ToByteArray(value));
stream.SetLength(stream.Position);
return stream.ToArray();
}
private static void ParseClockWriteResponse(byte[] message)
{
AssertPduResult(message);
AssertUserDataResult(message, 0x0a);
}
private static void AssertPduResult(byte[] message)
{
var pduErr = Word.FromByteArray(message.Skip(PduErrOffset).Take(2).ToArray());
if (pduErr != 0)
{
throw new Exception($"Response from PLC indicates error 0x{pduErr:X4}.");
}
}
private static void AssertUserDataResult(byte[] message, byte expected)
{
var dtResult = message[UserDataResultOffset];
if (dtResult != expected)
{
throw new Exception($"Response from PLC was 0x{dtResult:X2}, expected 0x{expected:X2}.");
}
}
}

View File

@@ -312,6 +312,35 @@ namespace S7.Net
return dataItems;
}
/// <summary>
/// Read the PLC clock value.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous operation, with it's result set to the current PLC time on completion.</returns>
public async Task<System.DateTime> ReadClockAsync(CancellationToken cancellationToken = default)
{
var request = BuildClockReadRequest();
var response = await RequestTsduAsync(request, cancellationToken);
return ParseClockReadResponse(response);
}
/// <summary>
/// Write the PLC clock value.
/// </summary>
/// <param name="value">The date and time to set the PLC clock to</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task WriteClockAsync(System.DateTime value, CancellationToken cancellationToken = default)
{
var request = BuildClockWriteRequest(value);
var response = await RequestTsduAsync(request, cancellationToken);
ParseClockWriteResponse(response);
}
/// <summary>
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
/// </summary>

View File

@@ -492,6 +492,30 @@ namespace S7.Net
}
}
/// <summary>
/// Read the PLC clock value.
/// </summary>
/// <returns>The current PLC time.</returns>
public System.DateTime ReadClock()
{
var request = BuildClockReadRequest();
var response = RequestTsdu(request);
return ParseClockReadResponse(response);
}
/// <summary>
/// Write the PLC clock value.
/// </summary>
/// <param name="value">The date and time to set the PLC clock to.</param>
public void WriteClock(System.DateTime value)
{
var request = BuildClockWriteRequest(value);
var response = RequestTsdu(request);
ParseClockWriteResponse(response);
}
/// <summary>
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
/// </summary>

View File

@@ -26,6 +26,11 @@ namespace S7.Net.Protocol
_ => Types.String.ToByteArray(s, dataItem.Count)
};
if (dataItem.VarType == VarType.Date)
{
return Date.ToByteArray((System.DateTime)dataItem.Value);
}
return SerializeValue(dataItem.Value);
}

82
S7.Net/Types/Date.cs Normal file
View File

@@ -0,0 +1,82 @@
using System;
using S7.Net.Helper;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Words from S7 plc to C#.
/// </summary>
public static class Date
{
/// <summary>
/// Minimum allowed date for the IEC date type
/// </summary>
public static System.DateTime IecMinDate { get; } = new(year: 1990, month: 01, day: 01);
/// <summary>
/// Maximum allowed date for the IEC date type
/// <remarks>
/// Although the spec allows only a max date of 31-12-2168, the PLC IEC date goes up to 06-06-2169 (which is the actual
/// WORD max value - 65535)
/// </remarks>
/// </summary>
public static System.DateTime IecMaxDate { get; } = new(year: 2169, month: 06, day: 06);
private static readonly ushort MaxNumberOfDays = (ushort)(IecMaxDate - IecMinDate).TotalDays;
/// <summary>
/// Converts a word (2 bytes) to IEC date (<see cref="System.DateTime"/>)
/// </summary>
public static System.DateTime FromByteArray(byte[] bytes)
{
if (bytes.Length != 2)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes.");
}
var daysSinceDateStart = Word.FromByteArray(bytes);
if (daysSinceDateStart > MaxNumberOfDays)
{
throw new ArgumentException($"Read number exceeded the number of maximum days in the IEC date (read: {daysSinceDateStart}, max: {MaxNumberOfDays})",
nameof(bytes));
}
return IecMinDate.AddDays(daysSinceDateStart);
}
/// <summary>
/// Converts a <see cref="System.DateTime"/> to word (2 bytes)
/// </summary>
public static byte[] ToByteArray(System.DateTime dateTime) => Word.ToByteArray(dateTime.GetDaysSinceIecDateStart());
/// <summary>
/// Converts an array of <see cref="System.DateTime"/>s to an array of bytes
/// </summary>
public static byte[] ToByteArray(System.DateTime[] value)
{
var arr = new ByteArray();
foreach (var date in value)
arr.Add(ToByteArray(date));
return arr.Array;
}
/// <summary>
/// Converts an array of bytes to an array of <see cref="System.DateTime"/>s
/// </summary>
public static System.DateTime[] ToArray(byte[] bytes)
{
var values = new System.DateTime[bytes.Length / sizeof(ushort)];
for (int i = 0; i < values.Length; i++)
{
values[i] = FromByteArray(
new[]
{
bytes[i], bytes[i + 1]
});
}
return values;
}
}
}

View File

@@ -45,6 +45,7 @@ namespace S7.Net.Types
break;
case "Int32":
case "UInt32":
case "TimeSpan":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
@@ -215,6 +216,21 @@ namespace S7.Net.Types
numBytes += sData.Length;
break;
case "TimeSpan":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
info.SetValue(structValue, TimeSpan.FromByteArray(new[]
{
bytes[(int)numBytes + 0],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 3]
}));
numBytes += 4;
break;
default:
var buffer = new byte[GetStructSize(info.FieldType)];
if (buffer.Length == 0)
@@ -311,6 +327,9 @@ namespace S7.Net.Types
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
};
break;
case "TimeSpan":
bytes2 = TimeSpan.ToByteArray((System.TimeSpan)info.GetValue(structValue));
break;
}
if (bytes2 != null)
{

97
S7.Net/Types/TimeSpan.cs Normal file
View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert between <see cref="T:System.TimeSpan"/> and S7 representation of TIME values.
/// </summary>
public static class TimeSpan
{
/// <summary>
/// The minimum <see cref="T:System.TimeSpan"/> value supported by the specification.
/// </summary>
public static readonly System.TimeSpan SpecMinimumTimeSpan = System.TimeSpan.FromMilliseconds(int.MinValue);
/// <summary>
/// The maximum <see cref="T:System.TimeSpan"/> value supported by the specification.
/// </summary>
public static readonly System.TimeSpan SpecMaximumTimeSpan = System.TimeSpan.FromMilliseconds(int.MaxValue);
/// <summary>
/// Parses a <see cref="T:System.TimeSpan"/> value from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>A <see cref="T:System.TimeSpan"/> object representing the value read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of
/// <paramref name="bytes"/> is not 4 or any value in <paramref name="bytes"/>
/// is outside the valid range of values.</exception>
public static System.TimeSpan FromByteArray(byte[] bytes)
{
var milliseconds = DInt.FromByteArray(bytes);
return System.TimeSpan.FromMilliseconds(milliseconds);
}
/// <summary>
/// Parses an array of <see cref="T:System.TimeSpan"/> values from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>An array of <see cref="T:System.TimeSpan"/> objects representing the values read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of
/// <paramref name="bytes"/> is not a multiple of 4 or any value in
/// <paramref name="bytes"/> is outside the valid range of values.</exception>
public static System.TimeSpan[] ToArray(byte[] bytes)
{
const int singleTimeSpanLength = 4;
if (bytes.Length % singleTimeSpanLength != 0)
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing an array of {nameof(System.TimeSpan)} requires a multiple of {singleTimeSpanLength} bytes of input data, input data is '{bytes.Length}' long.");
var result = new System.TimeSpan[bytes.Length / singleTimeSpanLength];
var milliseconds = DInt.ToArray(bytes);
for (var i = 0; i < milliseconds.Length; i++)
result[i] = System.TimeSpan.FromMilliseconds(milliseconds[i]);
return result;
}
/// <summary>
/// Converts a <see cref="T:System.TimeSpan"/> value to a byte array.
/// </summary>
/// <param name="timeSpan">The TimeSpan value to convert.</param>
/// <returns>A byte array containing the S7 date time representation of <paramref name="timeSpan"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value of
/// <paramref name="timeSpan"/> is before <see cref="P:SpecMinimumTimeSpan"/>
/// or after <see cref="P:SpecMaximumTimeSpan"/>.</exception>
public static byte[] ToByteArray(System.TimeSpan timeSpan)
{
if (timeSpan < SpecMinimumTimeSpan)
throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan,
$"Time span '{timeSpan}' is before the minimum '{SpecMinimumTimeSpan}' supported in S7 time representation.");
if (timeSpan > SpecMaximumTimeSpan)
throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan,
$"Time span '{timeSpan}' is after the maximum '{SpecMaximumTimeSpan}' supported in S7 time representation.");
return DInt.ToByteArray(Convert.ToInt32(timeSpan.TotalMilliseconds));
}
/// <summary>
/// Converts an array of <see cref="T:System.TimeSpan"/> values to a byte array.
/// </summary>
/// <param name="timeSpans">The TimeSpan values to convert.</param>
/// <returns>A byte array containing the S7 date time representations of <paramref name="timeSpans"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any value of
/// <paramref name="timeSpans"/> is before <see cref="P:SpecMinimumTimeSpan"/>
/// or after <see cref="P:SpecMaximumTimeSpan"/>.</exception>
public static byte[] ToByteArray(System.TimeSpan[] timeSpans)
{
var bytes = new List<byte>(timeSpans.Length * 4);
foreach (var timeSpan in timeSpans) bytes.AddRange(ToByteArray(timeSpan));
return bytes.ToArray();
}
}
}

View File

@@ -1,13 +0,0 @@
image: Visual Studio 2022
configuration: Release
install:
- choco install gitversion.portable -y
before_build:
- cmd: gitversion /l console /output buildserver
- dotnet restore
build_script:
msbuild /nologo /v:m /p:AssemblyVersion=%GitVersion_AssemblySemVer% /p:FileVersion=%GitVersion_MajorMinorPatch% /p:InformationalVersion=%GitVersion_InformationalVersion% /p:Configuration=%CONFIGURATION% S7.sln
after_build:
- dotnet pack S7.Net -c %CONFIGURATION% /p:Version=%GitVersion_NuGetVersion% --no-build -o artifacts
artifacts:
- path: artifacts\*.*