S7plc EPICS driver
S7plc EPICS driver documentation
Contents
- Introduction
- Theory of Operation
- Driver Configuration
- Device Support
- Connection Status
- Analog Input
- Analog Output
- Binary Input
- Binary Output
- Multibit Binary Input
- Multibit Binary Output
- Multibit Binary Input Direct
- Multibit Binary Output Direct
- Long Input
- Long Output
- String Input
- String Output
- Waveform Input
- Calculation Output
- Driver Functions
1 Introduction
This driver is intended to connect a Siemens S7 PLC (programmable
logic controller) via TCP/IP to an EPICS IOC, using the so called
"send/receive" protocol.
However, it can be used for any device sending and receiving blocks of
process variables (PVs) in the same way.
I highly recommend to connect to the PLC on a separate physical
network using a second network interface to avoid connection problems.
The driver was originally developped for SLS (Swiss Light Source) in 2000.
Later is has been modified by DESY (Deutsches Elektronen Synchrotron). The
current version has been completely rewritten for PPT (Puls-Plasmatechnik GmbH)
to run on a R3.14.6 PC based system, but it can also run on R3.13 vxWorks
system. Author of the current version is
Dirk
Zimoch <dirk.zimoch@psi.ch>.
In this document, it is assumed that the reader is familiar with EPICS, the
record concept and meanings of the fields of the standard records.
Recommended documentation: EPICS Record Refecrence Manual.
2 Theory of Operation
The driver and the PLC periodically exchange data (process variables)
over the network.
For each direction (IOC to PLC and PLC to IOC), there is one fixed size
data block that bundles all process variables for this direction.
The process variables are identified by their bytes offset in the data
block.
Process variables can be 8, 16, or 32 bit wide signed or unsigned integers
or bit fields, single or double precision floating point values,
or arrays of these, as well as strings of single byte characters.
Both byte orders, big endian and little endian, are supported.
The IOC programmer typically connects one input record to each process
variable sent from the PLC to the IOC and one output record for each
process variable sent from the IOC to the PLC.
This requires that the programmer of the PLC and the programmer of the IOC
agree on the data block sizes and layouts.
The layout is not negotiated between IOC and PLC in any automated way.
When the IOC starts, the driver tries to connect to the PLC
which must run a TCP server.
If connection cannot be established (e.g. because the PLC is off)
the driver periodically retries to set up the connection.
Once connected, the driver waits for data blocks sent by the PLC.
The PLC must be set up to send its data periodically.
If the driver does not receive any data within a configurable timeout
(which should be 2 to 10 times the send period of the PLC) or the
data block does not have the correct size, the driver considers the
communication broken and closes the connection.
After a short time it tries to reconnect.
Upon reciving the data block, the driver copies the process variables
from this block into input records and then triggers processing of the
records.
To allow this triggering, the input records should use "I/O Intr" scanning
(or alternatively be processed by another record that uses "I/O Intr"
and is conencted to the same PLC).
All input records connected to the same PLC are triggered at the same time
after all input values have been updated.
The order of processing is undefined.
On the other hand, the driver periodically checks if any of the output
records connected to this PLC has processed since the last cycle.
If and only if this is the case, a data block containing all output
variables is sent to the PLC.
Sending output only starts after the "PINI" run at IOC startup.
This allows to initialize all output records before any data is sent
to the IOC.
Thus all output records should have the "PINI" field set to "YES" (or
alternatively be processed by another record that uses "PINI").
Any output that has not been processed before the data block is sent
to the PLC transfers only 0 bytes (usually meaning integer 0, floating
point 0.0, or empty string, depending on the data type).
Data blocks are always transfered completely.
That means all process variables in that block are transfered, even
if they have not changed since the last cycle.
There is no way to write or read only a sub-set of process variables.
In between two cycles, any number of process variables may have changed.
These changes are only transfered to the other communication partner
in the next cycle.
Thus, there is always a delay until the values update on the other side
of the connection.
Also if values change too quickly, faster than the transfer cycle,
intermediate values are lost.
3 Driver Configuration
In the IOC startup script, the s7plc driver needs to be configured:
s7plcConfigure (PLCname, IPaddr, port, inSize,
outSize, bigEndian, recvTimeout, sendIntervall)
PLCname is an arbitrary symbolic name for the PLC running
a server TCP socket on IPaddr:port .
The records reference the PLC with this name in their INP or
OUT link. PLCname must not contain the
slash character (/ ).
inSize and outSize are the data block
sizes in bytes read from and sent to the PLC, respectively. Any of them can
be 0 . Byte order is defined by bigEndian . If
this is 1 , the IOC expects the PLC to send and receive any
multibyte PV (word, float, etc) most significant byte first. If it is
0 , the data is least significant byte first. This is independent
of the byte order of the IOC.
If the IOC does not receive new data from the PLC for
recvTimeout milliseconds, it closes the connection and
tries to reopen it after a few seconds. recvTimeout
should be 2 to 10 times the send intervall of the PLC.
The IOC checks for data to send every sendIntervall
milliseconds. If any output record has been processed in this time, the
complete buffer is sent to the PLC. If no new output is available, nothing
is sent.
Example:
s7plcConfigure ("vak-4", "192.168.0.10", 2000, 1024, 32, 1, 500, 100)
In the vxWorks target shell, PLCname , and
IPaddr must be quoted. In the iocsh, quotes are
optional.
The variable s7plcDebug can be set in the statup script or
at any time on the command line to change the amount or debug output.
The following levels are supported:
-1: fatal errors only
0: errors only
1: startup messages
2:+ output record processing
3:+ inputput record processing
4:+ driver calls
5:+ io printout
Be careful using level>1 since many messages can introduce considerable
delays which may result in connection losses. Default level is 0.
On vxWorks, s7plcDebug can be set with
s7plcDebug=level
In the iocsh use
var s7plcDebug level
4 Device Support
The driver supports the standard record types ai,
ao, bi, bo,
mbbi, mbbo,
mbbiDirect, mbboDirect,
longin, longout,
stringin, stringout,
and waveform. With EPICS R3.14,
calcout is supported, too.
The DTYP is "S7plc" . If the record processes when
the PLC is not connected (off, down, unreachable), the record raises an
alarm with SEVR="INVALID" and STAT="CONN" .
There is also a connection status support for bi. The
DTYP is "S7plc stat" . This record does not
raise an alarm when the PLC is disconnected. It just changes to
0 state in that case.
SCAN="I/O Intr" is supported. Whenever input data is received
from a PLC, all "I/O Intr" input records connected to this PLC
are processed. In each output cyle, all "I/O Intr" output
records are processed.
The general form of the INP or OUT link is
"@PLCname/offset T=type L=low H=high B=bit"
Not all parameters T , L , H , and
B are required for each record type and parameters equal to the
default value may be omitted. The default values depend on the record type.
PLCname is the PLC name as defined by
s7plcConfigure in the startup script.
offset is the byte offset of the PV relative to the
beginning of the input or output data block for this PLC. It must be an
integer number or a sum of integer numbers like 20+3+2 .
T=type defines the data type for transmitting the PV
from or to the PLC. It is not case sensitive and has several aliases (see
table below).
The default is T=INT16 for most record types.
L=low and H=high are used in analog
input and output records if LINR is set to "LINEAR"
to convert analog values to integer values and back. They define the raw
values which correspond to EGUL and EGUF ,
respectively.
Analog output records will never write integer values lower than
L or higher than H . If necessary, the raw output
value is truncated to the nearest limit. The default values for
L and H depend on T .
T= | Data Type | Default L= | Default H= |
INT8
| 8 bit (1 byte) signed integer number |
-0x7F -127 |
0x7F 127 |
UINT8 UNSIGN8 BYTE CHAR
| 8 bit (1 byte) unsigned integer number |
0x00 0 |
0xFF 255 |
INT16 SHORT |
16 bit (2 bytes) signed integer number |
-0x7FFF -32767 |
0x7FFF 32767 |
UINT16 UNSIGN16 WORD |
16 bit (2 bytes) unsigned integer number |
0x0000 0 |
0xFFFF 65535 |
INT32 LONG |
32 bit (4 bytes) signed integer number |
-0x7FFFFFFF -2147483647 |
0x7FFFFFFF 2147483647 |
UINT32 UNSIGN32 DWORD |
32 bit (4 bytes) unsigned integer number |
0x00000000 0 |
0xFFFFFFFF 4294967295 |
REAL32 FLOAT32 FLOAT |
32 bit (4 bytes) floating point number |
N/A | N/A |
REAL64 FLOAT64 DOUBLE |
64 bit (8 bytes) floating point number |
N/A | N/A |
STRING |
character array |
40 | N/A |
If T=STRING , L means length, not low.
The default value is the length of the VAL field.
In the case of the stringin and stringout records, this is 40 (including
the terminating null byte).
B=bit is only used for bi and bo records to define the
bit number within the data byte, word, or doubleword (depending on
T ). Bit number 0 is the least significant bit.
Note that in big endian byte order (also known as motorola format) bit 0 is
in the last byte, while in little endian byte order (intel format) bit 0 is
in the first byte. If in doubt, use T=BYTE to avoid all
byte order problems when handling single bits.
Note that the output buffer is initialised with null bytes at startup and
any output record that has not been processed after reboot will send null
values to the PLC. The driver does not send anything before the global
variable interruptAccept has been set TRUE at the
end of iocInit . All records with PINI set to
"YES" have already been processed by that time. The driver
does not change the VAL field of any output record at
initialisation. Thus, auto save and restore can be used in combination with
PINI="YES" .
4.1 Connection Status
record (bi, "$(RECORDNAME)") {
field (DTYP, "S7plc stat")
field (INP, "@$(PLCNAME)")
field (SCAN, "I/O Intr")
}
The record value is 1 if a connection to the PLC is established and 0 if not.
Disconnect does not raise an alarm.
4.2 Analog Input
With conversion from integer data type:
record (ai, "$(RECORDNAME)") {
field (DTYP, "S7plc")
field (INP, "@$(PLCNAME)/$(OFFSET) T=$(T) L=$(L) H=$(H)")
field (SCAN, "I/O Intr")
field (LINR, "Linear")
field (EGUL, "$(MINVAL)")
field (EGUF, "$(MAXVAL)")
}
Using floating point data type:
record (ai, "$(RECORDNAME)") {
field (DTYP, "S7plc")
field (INP, "@$(PLCNAME)/$(OFFSET) T=$(T)")
field (SCAN, "I/O Intr")
}
Default type is T=INT16 .
Defaults for L and H depend
on T (see table above).
If T is an integer type, the PV is read into
RVAL . If LINR is set to "LINEAR" ,
then the record support converts RVAL to VAL
so that RVAL=L converts to VAL=EGUL and
RVAL=H converts to VAL=EGUF .
VALtemp=(RVAL-L)*(EGUF-EGUL)/(H-L)+EGUL
After this conversion, VALtemp is still
subject to scaling and smoothing.
VAL=(VALtemp*ASLO+AOFF)*(1-SMOO)+VALold*SMOO .
If T=FLOAT or T=DOUBLE ,
the PV is read directly into VAL and L ,
H , EGUL and EGUF are ignored.
The device support emulates scaling and smoothing which is otherwise done
by the record support during conversion.
VAL=(PV*ASLO+AOFF)*(1-SMOO)+VALold*SMOO
T=STRING is not valid for ai records.
4.3 Analog Output
With conversion to integer data type:
record (ao, "$(RECORDNAME)") {
field (DTYP, "S7plc")
field (OUT, "@$(PLCNAME)/$(OFFSET) T=$(T) L=$(L) H=$(H)")
field (LINR, "Linear")
field (PINI, "YES")
field (EGUL, "$(MINVAL)")
field (EGUF, "$(MAXVAL)")
}
Using floating point data type:
record (ao, "$(RECORDNAME)") {
field (DTYP, "S7plc")
field (OUT, "@$(PLCNAME)/$(OFFSET) T=$(T)")
field (PINI, "YES")
}
Default type is T=INT16 .
Defaults for L and H depend
on T (see table above).
If T is an integer type, RVAL is
written to the PV. If LINR is set to "LINEAR" ,
then the record support first scales OVAL .
OVALtemp=(OVAL-AOFF)/ASLO
After that, the value is converted to RVAL so that
OVALtemp=EGUL converts to RVAL=L and
OVALtemp=EGUF converts to RVAL=H .
RVAL=(OVALtemp-EGUL)*(H-L)/(EGUF-EGUL)+L
If RVAL is higher than H or lower than
L , the value is truncated to the nearest limit.
If T=FLOAT or T=DOUBLE ,
OVAL is written directly to the PV. L ,
H , EGUL and EGUF are ignored.
The device support emulates scaling which is otherwise done by the
record support during conversion.
PV=(OVAL-AOFF)/ASLO
T=STRING is not valid for ao records.
4.4 Binary Input
record(bi, "$(NAME)") {
field (DTYP, "S7plc")
field (INP, "@$(PLCNAME)/$(OFFSET) T=$(T) B=$(B)")
field (SCAN, "I/O Intr")
}
Default type is T=INT16 . Default bit is B=0 .
Depending on T , B can vary from 0 to 7, 15, or 31.
Bit 0 is the least significant bit. In little endian byte order, bit 0 is in
the first byte, in big endian byte order it is in the last byte of the PV.
If in doubt, use T=BYTE to avoid all byte order problems when
handling single bits.
The PV is read to RVAL and masked with
2B .
VAL is 1 if RVAL is not 0.
RVAL=PV&(1<<B); VAL=(RVAL!=0)?1:0
T=STRING , T=FLOAT or T=DOUBLE are not
valid for bo records. Signed and unsigned types are equivalent.
4.5 Binary Output
record(bo, "$(NAME)") {
field (DTYP, "S7plc")
field (OUT, "@$(PLCNAME)/$(OFFSET) T=$(T) B=bit")
field (PINI, "YES")
}
Default type is T=INT16 . Default bit is B=0 .
Depending on T , B can vary from 0 to 7, 15, or 31.
Bit 0 is the least significant bit. In little endian byte order, bit 0 is in
the first byte, in big endian byte order it is in the last byte of the PV.
If in doubt, use T=BYTE to avoid all byte order problems when
handling single bits.
If VAL is not 0, then RVAL is set to
2B , else RVAL is set to 0.
Only the referenced bit of the PV is changed while all other bits remain
untouched. Thus, other output records can write to different bits of the
same PV.
RVAL=(VAL!=0)?(1<<bit):0;
PV=(PVold&~(1<<bit))|RVAL
T=STRING , T=FLOAT or T=DOUBLE are not
valid for bo records. Signed and unsigned types are equivalent.
4.6 Multibit Binary Input
record(mbbi, "$(NAME)") {
field (DTYP, "S7plc")
field (INP, "@$(PLCNAME)/$(OFFSET) T=$(T)")
field (SCAN, "I/O Intr")
field (NOBT, "$(NUMBER_OF_BITS)")
field (SHFT, "$(RIGHT_SHIFT)")
}
Default type is T=INT16 .
The PV is read to RVAL , shifted right by SHFT bits
and masked with NOBT bits. Valid values for NOBT
and SHFT depend on T :
NOBT +SHFT must not exceed the number of bits of
the type.
Bit 0 is the least significant bit. In little endian byte order, bit 0 is in
the first byte, in big endian byte order it is in the last byte of the PV.
Example: Use bits 4 to 9 out of 16.
T=INT16 , NOBT=6 , SHFT=4
PV |
15 |
14 |
13 |
12 |
11 |
10 |
9 |
8 |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
RVAL |
|
|
|
|
|
|
|
|
|
|
9 |
8 |
7 |
6 |
5 |
4 |
T=STRING , T=FLOAT or T=DOUBLE are not
valid for mbbi records. Signed and unsigned types are equivalent.
4.7 Multibit Binary Output
record(mbbo, "$(NAME)") {
field (DTYP, "S7plc")
field (OUT, "@$(PLCNAME)/$(OFFSET) T=$(T)")
field (PINI, "YES")
field (NOBT, "$(NUMBER_OF_BITS)")
field (SHFT, "$(LEFT_SHIFT)")
}
Default type is T=INT16 .
RVAL is masked with NOBT bits, shifted left by
SHFT bits and written to the PV. Valid values for
NOBT and SHFT depend on T :
NOBT +SHFT must not exceed the number of bits of
the type.
Bit 0 is the least significant bit. In little endian byte order, bit 0 is in
the first byte, in big endian byte order it is in the last byte of the PV.
Only the referenced NOBT bits of the PV are changed. All other
bits remain untouched. Thus, other output records can write to different bits
of the same PV.
Example: Use bits 5 to 8 out of 16.
T=INT16 , NOBT=4 , SHFT=5
RVAL |
|
|
|
|
|
|
|
|
|
|
|
|
8 |
7 |
6 |
5 |
PV |
15 |
14 |
13 |
12 |
11 |
10 |
9 |
8 |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
T=STRING , T=FLOAT or T=DOUBLE are not
valid for mbbo records. Signed and unsigned types are equivalent.
4.8 Multibit Binary Input Direct
record(mbbiDirect, "$(NAME)") {
field (DTYP, "S7plc")
field (INP, "@$(PLCNAME)/$(OFFSET) T=$(T)")
field (SCAN, "I/O Intr")
field (NOBT, "$(NUMBER_OF_BITS)")
field (SHFT, "$(RIGHT_SHIFT)")
}
Default type is T=INT16 .
The PV is read to VAL , shifted right by SHFT
bits and masked with NOBT bits (see mbbi).
Valid values for NOBT and SHFT depend
on T : NOBT +SHFT must
not exceed the number of bits of the type.
Bit 0 is the least significant bit. In little endian byte order, bit 0 is in
the first byte, in big endian byte order it is in the last byte of the PV.
T=STRING , T=FLOAT or T=DOUBLE are not
valid for mbbiDirect records. Signed and unsigned types are equivalent.
4.9 Multibit Binary Output Direct
record(mbboDirect, "$(NAME)") {
field (DTYP, "S7plc")
field (OUT, "@$(PLCNAME)/$(OFFSET) T=$(T)")
field (PINI, "YES")
field (NOBT, "$(NUMBER_OF_BITS)")
field (SHFT, "$(LEFT_SHIFT)")
}
Default type is T=INT16 .
VAL is masked with NOBT bits, shifted left by SHFT
bits and written to the PV (see mbbo). Valid values for
NOBT and SHFT depend on T :
NOBT +SHFT must not exceed the number of bits of
the type.
Bit 0 is the least significant bit. In little endian byte order, bit 0 is in
the first byte, in big endian byte order it is in the last byte of the PV.
Only the referenced NOBT bits of the PV are changed. All other
bits remain untouched. Thus, other output records can write to different bits
of the same PV.
T=STRING , T=FLOAT or T=DOUBLE are not
valid for mbboDirect records. Signed and unsigned types are equivalent.
4.10 Long Input
record(longin, "$(NAME)") {
field (DTYP, "S7plc")
field (INP, "@$(PLCNAME)/$(OFFSET) T=$(T)")
field (SCAN, "I/O Intr")
}
Default type is T=INT16 .
The PV is read to VAL . If the type has less than 32 bits, the
value is zero extended or sign extended depending on the signedness of
the type.
T=STRING , T=FLOAT or T=DOUBLE are not
valid for longin records.
4.11 Long Output
record(longout, "$(NAME)") {
field (DTYP, "S7plc")
field (OUT, "@$(PLCNAME)/$(OFFSET) T=$(T)")
field (PINI, "YES")
}
Default type is T=INT16 .
Depending on T , the least significant 8, 16, or 32 bytes
of VAL are written to the PV.
T=STRING , T=FLOAT or T=DOUBLE are not
valid for longout records.
4.12 String Input
record(stringin, "$(NAME)") {
field (DTYP, "S7plc")
field (INP, "@$(PLCNAME)/$(OFFSET) L=$(LENGTH)")
field (SCAN, "I/O Intr")
}
Default and only valid type is T=STRING .
Default length is L=40 .
L bytes are read from the PV to VAL and null
terminated. Thus, the effective string length is maximal
L -1 bytes.
4.13 String Output
record(stringout, "$(NAME)") {
field (DTYP, "S7plc")
field (OUT, "@$(PLCNAME)/$(OFFSET) L=$(LENGTH)")
field (PINI, "YES")
}
Default and only valid type is T=STRING .
Default length is L=40 .
L bytes are written from VAL to the PV.
If the actual string length of VAL is shorter than
L , the remaining space is filled with null bytes. If
it is longer than L , the string is truncated and not
null terminated
4.14 Waveform Input
record(waveform, "$(NAME)") {
field (DTYP, "S7plc")
field (INP, "@$(PLCNAME)/$(OFFSET)")
field (SCAN, "I/O Intr")
field (NELM, "$(NUMBER_OF_ELEMENTS)")
field (FTVL, "$(DATATYPE)")
}
NELM elements are read from the PV to VAL .
The default type depends on FTVL . For example
FTVL=LONG results in T=INT32 .
T and FTVL must match but can differ
in signedness. In most cases, better just specify FTVL and
leave T to the default.
If T=STRING , FTVL must be "CHAR" or
"UCHAR" . L=length can be specified but
defaults to and must not exceed NELM .
If L is less than NELM , the
remaining elements are left untouched.
FTVL="STRING" is not supported.
The special type T=TIME is supported for
waveforms records only. FTVL must be
"CHAR" or "UCHAR" and NELM should be
"8" . The input bytes are converted from BCD (binary coded decimal)
to integer values in the range from 0 to 99 each. This type is intended
to transfer BCD coded real time clock timestamps.
The Siemens "STEP 7" manual defines the 8 byte PLC timetamp as follows:
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
year |
month |
day |
hour |
minute |
second |
msec(hi) |
msec(lo)*10+day of week |
Years 90 to 99 mean 1990 to 1999, years 0 to 89 mean 2000 to 2089. Months
and days start with 1. Hour is 0 to 23, minute and second 0 to 59. Msec are
milliseconds in the range 0 to 999. The first two digits (0-99 hundredth of
a second) are in msec(hi). The last digit (0-9 thousandth of a second)
is multiplyed by 10 and added to the day of week (Sunday=1 to Saturday=7).
If you want to have the unconverted BCD bytes, do not use
T=TIME .
4.15 Calculation Output
record(calcout, "$(NAME)") {
field (DTYP, "S7plc")
field (OUT, "@$(PLCNAME)/$(OFFSET) T=$(T) L=$(L) H=$(H)")
field (PINI, "YES")
}
Default type is T=INT16 .
Defaults for L and H depend
on T (see table above).
OVAL (the result of CALC or OCAL ,
depending on DOPT ) is written to the PV. If
T is an integer type, the value is truncated to an
integer and compared to L and H .
If OVAL is lower than L or higher than
H , it is truncated to the nearest limit.
If T=FLOAT or T=DOUBLE ,
OVAL is written to the PV directly without any conversion.
T=STRING is not valid for calcout records.
To use this device support with calcout records, you need EPICS R3.14.
5 Driver Functions
Device support for other record types can be written with calls to the
following driver functions:
s7plcStation* s7plcOpen (char* PLCname);
int s7plcRead (s7plcStation* station,
unsigned int offset, unsigned int dlen,
void* pdata);
int s7plcReadArray (s7plcStation* station,
unsigned int offset, unsigned int dlen,
unsigned int nelem, void* pdata);
int s7plcWrite (s7plcStation* station,
unsigned int offset, unsigned int dlen,
void* pdata);
int s7plcWriteMasked (s7plcStation* station,
unsigned int offset, unsigned int dlen,
void* pdata, void* pmask);
int s7plcWriteArray (s7plcStation* station,
unsigned int offset, unsigned int dlen,
unsigned int nelem, void* pdata);
int s7plcWriteMaskedArray (s7plcStation* station,
unsigned int offset, unsigned int dlen,
unsigned int nelem, void* pdata, void* pmask);
The functions s7plcRead() , s7plcWrite() ,
s7plcWriteMasked() , and s7plcWriteArray()
are actually macros for
s7plcReadArray() and s7plcWriteMaskedArray() with
nelem=1 and/or mask=NULL .
station is a handle previously obtained by a call to
s7plcOpen() .
offset is the byte offset of the PV relative
to the beginning to the data block.
dlen is the length of the PV in bytes (one element in case of
arrays). If the endianess of the PLC differs from the IOC, the byte order of
the dlen bytes is swapped by the driver.
nelem is the number of elements in an array.
pdata is a pointer to a buffer of
nelem *dlen bytes.
PVs are read to or written from this buffer.
mask is a pointer to a bitmask of dlen bytes.
Only those bits are changed where the mask contains 1 bits. All other bits
remain untouched.
For strings, use array functions with dlen=1 and
nelem=buffersize .
Dirk Zimoch, March 2005 - February 2012
Updated: 06.04.2021
Source: /afs/psi.ch/project/epics/webhosting/software/s7plc/s7plc.html
|