mirror of
https://github.com/S7NetPlus/s7netplus.git
synced 2026-02-17 14:28:25 +08:00
Merge pull request #507 from mycroes/clock
Add support for reading and writing PLC clock
This commit is contained in:
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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user