mirror of
https://github.com/S7NetPlus/s7netplus.git
synced 2026-02-17 22:38:27 +08:00
Compare commits
39 Commits
github-act
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab6308eacd | ||
|
|
130eeadbd8 | ||
|
|
76a7ea04f7 | ||
|
|
4764c997ed | ||
|
|
cb24e9a046 | ||
|
|
10315b4b4c | ||
|
|
0774e124bf | ||
|
|
1ebffe08e7 | ||
|
|
f419df4d73 | ||
|
|
1969aac1b2 | ||
|
|
2f2dcf7281 | ||
|
|
07325db2fa | ||
|
|
eada47cd24 | ||
|
|
5e1ac8c7bf | ||
|
|
13544a1bcf | ||
|
|
6fc526b886 | ||
|
|
f227ad4b53 | ||
|
|
e4cc42fa51 | ||
|
|
689e7ffd96 | ||
|
|
8087b8d315 | ||
|
|
a55ceba679 | ||
|
|
eb1fad9333 | ||
|
|
0de9364dee | ||
|
|
9380ea85c3 | ||
|
|
22451bc440 | ||
|
|
e98ce005c5 | ||
|
|
11a40cc5e3 | ||
|
|
f79286b2d0 | ||
|
|
fadd7d0cb3 | ||
|
|
652ff3a9bb | ||
|
|
9c0fea721a | ||
|
|
2ec73224c1 | ||
|
|
a8ef47b475 | ||
|
|
55aa06a1fc | ||
|
|
7e631a713f | ||
|
|
49e4d3369a | ||
|
|
ee06bec0fb | ||
|
|
05ccb05f3a | ||
|
|
0d2817661e |
259
S7.Net.UnitTest/CommunicationTests/Clock.cs
Normal file
259
S7.Net.UnitTest/CommunicationTests/Clock.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
82
S7.Net.UnitTest/TypeTests/TimeSpanTests.cs
Normal file
82
S7.Net.UnitTest/TypeTests/TimeSpanTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
23
S7.Net/Helper/DateTimeExtensions.cs
Normal file
23
S7.Net/Helper/DateTimeExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
92
S7.Net/Plc.Clock.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
82
S7.Net/Types/Date.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
97
S7.Net/Types/TimeSpan.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
appveyor.yml
13
appveyor.yml
@@ -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\*.*
|
||||
Reference in New Issue
Block a user