Plc Data Mapper is a library to map C# classes to plc blocks to get symbolic access to the plc.
PM> Install-Package Papper
Papper could be used with any S7 library, because it's a top level component. It convert's the given command to read or write commands for the S7 library.
To access the data you fist have to declare a class with a mapping attribute like the following one:
using Papper.Attributes;
using System;
namespace UnitTestSuit.Mappings
{
public class UDT_SafeMotionHeader_States
{
public bool ChecksumInvalid { get; set; }
public bool UpdateRequested { get; set; }
}
public class UDT_SafeMotionHeader_Commands
{
public bool UpdateAllowed { get; set; }
public bool AllSlotsLocked { get; set; }
}
public class UDT_SafeMotionSlot_Commands
{
public bool TakeoverPermitted { get; set; }
public bool TakeoverRefused { get; set; }
}
public class UDT_SafeMotionSlot_Handshake
{
public bool MotionSelected { get; set; }
public bool Button1Pressed { get; set; }
public bool Button2Pressed { get; set; }
public Int16 HandshakeTime { get; set; }
}
public class UDT_SafeMotionSlot_Motion
{
public bool ManualEnable1 { get; set; }
public bool ManualEnable2 { get; set; }
public bool ManualOperation1 { get; set; }
public bool ManualOperation2 { get; set; }
}
public class UDT_SafeMotionHeader
{
public DateTime Generated { get; set; }
public Int16 NumberOfActiveSlots { get; set; }
public UDT_SafeMotionHeader_States States { get; set; }
public UDT_SafeMotionHeader_Commands Commands { get; set; }
}
public class UDT_SafeMotionSlot
{
public Int16 SafeSlotVersion { get; set; }
public byte SlotId { get; set; }
public DateTime UnitTimestamp { get; set; }
public UInt16 UnitChecksum { get; set; }
public Int16 AggregateDBNummer { get; set; }
public Int16 AggregateOffset { get; set; }
public UInt32 HmiId { get; set; }
public UInt32 AccessRightReqFromHmiId { get; set; }
public UDT_SafeMotionSlot_Commands Commands { get; set; }
public UDT_SafeMotionSlot_Handshake Handshake { get; set; }
public UDT_SafeMotionSlot_Motion Motion { get; set; }
}
public class UDT_SafeMotion
{
public UDT_SafeMotionHeader Header { get; set; }
[ArrayBounds(0, 254)]
public UDT_SafeMotionSlot[] Slots { get; set; }
}
[Mapping("DB_Safety", "DB15", 0)]
public class DB_Safety
{
public UDT_SafeMotion SafeMotion { get; set; }
}
}
The following code snippet is a small sample for the usage of papper:
var _papper = new PlcDataMapper(960);
_papper.OnRead += Papper_OnRead;
_papper.OnWrite += Papper_OnWrite;
_papper.AddMapping(typeof(DB_Safety));
var reference = PlcReadReference.FromAddress("DB_Safety.SafeMotion.Slots[100].UnitChecksum");
PlcReadResult[] result = await _papper.ReadAsync(reference);
_papper.WriteAsync(PlcWriteReference.FromPlcReference(reference, 100));
result = await _papper.ReadAsync(PlcReadReference.FromAddress("DB_Safety.SafeMotion.Slots[100].SlotId"));
_papper.WriteAsync(PlcWriteReference.FromAddress(result[0].Address, 200));
The interface to the used S7 library have to handle the following calls
private static byte[] Papper_OnRead(string selector, int offset, int length)
{
//call s7 library to read data
}
private static bool Papper_OnWrite(string selector, int offset, byte[] data, byte bitMask = 0)
{
//call s7 library to write data
}
Syntax:
[Selector].[TYPE][OFFSET],[CountOfData]
Bool
[Selector].[TYPE][OFFSET_Byte].[OFFSET_Bit],[CountOfData]
String
[Selector].[TYPE][OFFSET_Stringlength][_CountOfData]
- IB: Input Area
- FB: Flag Area
- QB: Output Area
- TM: Timer Area
- CT: Counter Area
- DB[Number]: DataBlock Area
- Boolean: X or BIT
- Byte: B or BYTE
- Char: C or CHAR
- Date: DATE
- DateTime: DT or DATETIME
- DInt: DI or DINT
- DWord: DW or DWORD
- Int: I or INT
- LDateTime:LDT or LDATETIME
- LInt: LI or LINT
- Time: LT or LTIME
- LWord: LW or LWORD
- Real: R or REAL
- S5Time: TIMEBCD
- Counter: CT or COUNT
- SInt: SI or SINT
- String: S or STRING
- Time: T or TIme
- TimeOfDay:TOD
- UDInt: UDI or UDINT
- UInt: UI or UINT
- ULInt: ULI or ULINT
- USInt: USI or USINT
- WChar: WC or WCHAR
- Word: W or WORD
- WString: WS or WSTRING
Papper uses a couple of attributes to specify the mapping of the plc data to a dotnet class.
[Mapping("DB_Safety", "DB15", 0, 0)]
MappingAttribute(string name, string selector, int offset = 0, int observationRate = 0)
- name: The name of the block, this will used from papper to find the class by this name.
- selector: This parameter specifies the selector for the access library. (in case of dacs7 this is the absolute db name, in case of opcua, this is the symbolic name)
- offset: specify an offset in the data block, if you do not define the full block.
- observationRate: this can be used by the access lib to modify the change detection for this block.
In the case you have some parts in your block you do not like to specify, you can skip this part.
class MySymbolic
{
bool I0_0 {get; set;} // EA access this is I0.0
[MappingOffset(200)]
bool I200_0 {get; set;} // EA access this is I200.0
[MappingOffset(200, 3)]
bool I400_3 {get; set;} // EA access this is I400.3
}
MappingOffsetAttribute(int byteOffset, int bitOffset = -1)
Because the plc has no dynamic array length, you have to specify the dimensions and the length in the attribute. And additionally .net start by 0 but the plc can start anywhere.
[ArrayBounds(1, 10, 0)]
public bool[] NotFull { get; set; }
ArrayBoundsAttribute(int from, int to, int dimension = 0)
Define the length of the string.
[PlcType("WString")]
[StringLength(70)]
public string Caption { get; set; }
[PlcType("WString")]
[ArrayBounds(0, 15, 0)]
[StringLength(25)]
public string[] Position { get; set; }
To use the correct plc type especially in the case of DateTime and Time types, you are able to specify this attribute:
It can be used on a class or and a property. By using it on a property, you target the following:
The mapping of a .net datatype to a plc datatype is normally be done by an internal mapping list, the following table shows the default mappings:
.net | Plc TypeName |
---|---|
bool | bit, bool |
byte | byte |
sbyte | sint |
short | int |
int | dint |
double | lreal |
long | lint |
ushort | word |
uint | dword |
ulong | lword |
DateTime | dateandtime |
TimeSpan | time |
string | string |
float | real |
char | char |
If you need a more complex type mapping, you are able to add the PlcTypeAttribute on your properties, or use the Serializer method with the Type name in case you need to convert a single value.
Plc TypeName | .net | size in bytes |
---|---|---|
S5Time | TimeSpan | 2 |
TimeOfDay | TimeSpan | 4 |
LTimeOfDay | TimeSpan | 8 |
Bit | bool | 0.1 (single bit) |
Bool | bool | 0.1 (single bit) |
Byte | byte | 1 |
SInt | sbyte | 1 |
USInt | byte | 1 |
Int | short | 2 |
DInt | int | 4 |
UDInt | uint | 4 |
LInt | long | 8 |
ULInt | ulong | 8 |
Word | ushort | 2 |
DWord | uint | 4 |
LWord | ulong | 8 |
DateTime | DateTime | 8 |
Date | DateTime | 2 |
LDateTime | DateTime | 8 |
DateTimeL | DateTime | 12 |
LDT | DateTime | 8 |
DTL | DateTime | 12 |
Time | TimeSpan | 4 |
LTime | TimeSpan | 8 |
String | string | 2 + strLength |
WString | string | 4 + (strLength * 2) |
Real | float | 4 |
LReal | double | 8 |
Float | float | 4 |
Char | char | 1 |
WChar | string | 2 |
S7Counter | int | 2 |
In some cases, you need to know the plc type of a class (e.g. the udt name). In this case you are able to add this attribute to your class, to use the correct name. This can be used if you have a special character in your udt name, or also in case of opcua where you need the udtname + the name of a substructure.
This attribute is self describing I think.
This property is not accessible, but we had defined it for the automatic offset calculation.
You can generate a data structure from code, and if the variables names not match with the access name, you can redefine the names by adding a SymbolicAccessName on the property.
You can generate a data structure from code, and if the variables names not match with the names in your application, you can redefine the names by adding a AliasName on the property.
Transport additional information which can be used by an application.
MetaTagAttribute(string name, object value)
Additionally to access library papper also provides a data serializer. With this you are able to read your data as bytes and deserialize it to a .net class. Or you can build your .net class and serialize it to a byte array which can then be downloaded to the plc.
public class TimeTransformationRule
{
public short Bias { get; set; } = 0; // Time bias of standard local time to UTC [min]
public short DaylightBias { get; set; } = 0; //Time bias of local daylight saving time to local standard time [min]
public byte DaylightStartMonth { get; set; } = 0; //Month of change to daylight saving time
public byte DaylightStartWeek { get; set; } = 0;//Week of change to daylight saving time: 1= 1st occurrence of the weekday in the month, ..., 5= last occurrence
public byte DaylightStartWeekday { get; set; } = 0; //Weekday of change to daylight saving time: 1= Sunday
public byte DaylightStartHour { get; set; } = 0; // Hour of change to daylight saving time
public byte DaylightStartMinute { get; set; } = 0; // Minute of change to daylight saving time
public byte StandardStartMonth { get; set; } = 0; // Month of change to standard time
public byte StandardStartWeek { get; set; } = 0; // Week of change to standard time: 1= 1st occurrence of the weekday in the month, ..., 5= last occurrence
public byte StandardStartWeekday { get; set; } = 0; // Weekday of change to standard time
public byte StandardStartHour { get; set; } = 0; // Hour of change to standard time
public byte StandardStartMinute { get; set; } = 0; // Minute of change to standard time
[StringLength(80)]
public string TimeZoneName { get; set; } // Name of the used time zone like in Windows XP: "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"
}
The following sample shows how you can use the serializer to serialize and deserialize data structures.
var serializer = new PlcDataMapperSerializer();
var numberOfBytesToRead = serializer.SerializedByteSize<TimeTransformationRule>();
// read the data from the plc e.g. with:
IEnumerable<DataValue> results = await _dacs7.ReadAsync(ReadItem.Create<byte[]>("DB100", 0, (ushort)numberOfBytesToRead));
if(results.FirstOrDefault() is DataValue dv && dv.IsSuccessReturnCode)
{
// deserialize the data from the byte array
var rule = serializer.Deserialize<TimeTransformationRule>(result.Data);
// change the data
rule.DaylightStartWeekday = 2;
// serialize the class to a byte array
var writeBack = serializer.Serialize(rule);
// write it back
_dacs7.WriteAsync(WriteItem.Create("DB100", 0, writeBack));
}