blob: bed4d3c73cff4c273c262bbb1d7fa009accfbe58 [file] [log] [blame]
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
:imagesdir: ./../img/
== Implementing Drivers
All Java drivers are implemented using https://netty.io/[Netty] which sort of evolved from https://mina.apache.org/[Apache Mina].
Usually the protocols used in industrial controllers are layered protocols in which one protocol is embedded in another.
Some times several layers can be involved. Also some times, depending on the mode a protocol is used in, one protocol can be embedded in different containers.
We decided not to implement each driver in one monolithic block of code, but modeled each protocol layer separately.
This makes the implementation and testing a lot simpler and makes the entire architecture much more flexible.
Two examples would be `Modbus` and the `S7` protocol:
The TCP variant of the `Modbus` Protocol is nothing more than adding a TCP header to a normal serial Modbus message.
So by layering the implementations we can serve both the serial as the TCP variant with the same driver logic for the `Modbus` protocol.
The `S7` Protocol is built to be transported using a protocol called `ISO TP`.
When using the normal `TCP` communication, this is again embedded in a `ISO over TCP` protocol frame, which is then included in a `TCP` packet.
However it is also possible to use it on native `Ethernet` Frames.
This high-performance and reduced latency implementation would directly transport `ISO TP` packets in native `Ethernet` frames.
Keeping the drivers layered, gives us the flexibility to support this.
While the layers control `what` is communicated with the outside world, we have also defined a set of connectors that implement `how` our driver communicates with it.
All is then put together in a so-called `pipeline`.
A PLC4X driver is responsible for creating a connection.
This connection is a synonym for a new instance of a Netty pipeline which is terminated with a corresponding connector.
[ditaa,netty-pipeline]
....
+------------------------------------------+
|c0BA |
| Application | Application
| |
+---------+--------------------------------+
| ^
- - - - - -|- - - - - - - - - - - | - - - - - - - - - - -
v |
+--------------------------------+---------+
|c05A |
| PLC4X |
| |
+---------+--------------------------------+
| | ^ |
| v | |
| +------------------------------------+ |
| |cAAA | |
| | PLC4X Driver Connection | |
| | | |
| +------+-----------------------------+ |
| | | ^ | |
| | v | | |
| | +--------------------------+---+ | |
| | |cAAA | | |
| | | PLC4X Protocol Layer | | |
| | | | | |
| | +---+--------------------------+ | |
| | | ^ | |
| | v | | |
| | +--------------------------+---+ | |
| | : Optional | | | PLC4X
| | | Protocol Layer(s) | | | Netty
| | | | | | Pipeline
| | +---+--------------------------+ | |
| | | ^ | |
| | v | | |
| | +--------------------------+---+ | |
| | |cAAA | | |
| | | Protocol Layer | | |
| | | | | |
| | +---+--------------------------+ | |
| | | ^ | |
| | v | | |
| +-----------------------------+------+ |
| |cAAA | |
| | Connector | |
| | | |
+--+------+-----------------------------+--+
| ^
- - - - - -|- - - - - - - - - - - | - - - - - - - - - - -
v |
+--------------------------------+---------+
|cF6F |
| PLC | Device
| |
+------------------------------------------+
....
== Default Layers
Each driver should consist of at least 2 layers:
- The PLC4X Layer
- The Protocol Layer
=== The PLC4X Layer
The main objective of this layer is to translate PLC4X requests into messages the target protocol understands.
It also acts as the bridge to link the requests and corresponding responses.
This is also the layer on which emulation of functionality should be implemented.
For example S7 controllers allow reading of multiple addresses in one request.
However they do not support writing of multiple values at once.
The `Plc4XS7Protocol` takes care of sending multiple single-value requests to the PLC to simulate writing of multiple values.
This simulation however should be transparent from the underlying protocol implementations.
=== The Protocol Layer(s)
The protocol layer(s) are responsible for encoding and decoding messages of the protocol they belong to.
So usually we require at least one of these layers for every driver.
== Connectors
We currently have defined 4 different types of connectors:
- Tcp Socket
- Raw Socket
- Serial
- Test
But this list can easily be extended as needed.
=== Tcp Sockets
This is the default type of connector used when implementing protocols using the normal TCP protocol.
It is what comes with Netty out of the box and should be used if possible.
=== Raw Sockets
This is a special form of connector that allows implementing protocols below the TCP level.
This is also where things start getting a little more complicated.
As Java doesn't support communication below TCP and UDP, this option makes use of the `Java Native Interface (JNI)` to access native libs that then implement the functionality on OS level.
The library used for this is called `libpcap` (for Linux and Mac) or `winpcap` for Windows.
Also as creating of raw sockets requires elevated user permissions the application has to be run as `root` or (preferred option) the library has to be setup to run with root privileges (`setuid`).
When setup correctly the raw socket connector allows implementing protocols right down to manually constructing `Ehternet` frames.
This is currently treated as a temporary solution as we have to collect experience with this approach. Eventually native transports implemented as part of the PLC4X project might be the more performant solution.
=== Serial
This connector doesn't open any form of network interface, but uses the operating systems serial ports for communication.
It is used by some of the protocols that don't support Networking, such as the serial variant of the Modbus protocol.
=== Test
This connector is used for testing purposes.
Instead of opening a connection to a device using a normal communication channel, this connector is made to be used inside unit- and integration-tests.
It allows to manually send and receive (binary) data to and from a pipeline for testing.
== Implementing a custom driver
PLC4X's `DriverManager` finds it's drivers, by using the default `Java ServiceLoader`.
This requires a file called `org.apache.plc4x.java.spi.PlcDriver` in the `META-INF/services` directory of the drivers JAR file.
For each type of driver provided inside this JAR, one line has to be added to this file containing the fully qualified class name of the driver implementation.
For the S7 driver for example all it contains is this line:
org.apache.plc4x.java.s7.S7PlcDriver
A driver implementation must implement the `org.apache.plc4x.java.spi.PlcDriver` interface.
This defines the necessary methods for the `DriverManager` to find the correct implementation and create a new connection instance.
The important methods here are:
- getProtocolCode
- connect(String url)
- connect(String url, PlcAuthentication authentication)
`getProtocolCode` is used to find a driver suitable for providing a connection mathing the prefix of the PLC4X connection string.
So if for example the connection string is:
s7://192.168.0.1/1/
The DriverManager will look if he can find a PlcDriver implementation for which `getProtocolCode` method returns the string "s7".
If no form of `PlcAuthentication` is provided, the normal `connect` method is then used to create a new connection instance.
If however authentication information is provided, the second connect method is used.
However we still have to find and implement a protocol that actually supports authentication.
The probably simplest way to implement a custom connection is to extend `org.apache.plc4x.java.base.connection.NettyPlcConnection`.
This allows passing in a `ChannelFactory` instance, which allows overriding the default communication channel used by the driver.
An `AbstractPlcConnection` is required to implement a method called `getChannelHandler`.
This is responsible for constructing the communication pipeline.
Here is an example of the connection for the TCP variant of the `Modbus` protocol:
....
public class ModbusTcpPlcConnection extends BaseModbusPlcConnection {
private static final int MODBUS_TCP_PORT = 502;
public ModbusTcpPlcConnection(InetAddress address, String params) {
this(new TcpSocketChannelFactory(address, MODBUS_TCP_PORT), params);
logger.info("Configured ModbusTcpPlcConnection with: host-name {}", address.getHostAddress());
}
ModbusTcpPlcConnection(ChannelFactory channelFactory, String params) {
super(channelFactory, params);
}
@Override
protected ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture) {
return new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) {
// Build the protocol stack for communicating with the modbus protocol.
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new ModbusTcpProtocol());
pipeline.addLast(new ModbusProtocol());
pipeline.addLast(new Plc4XModbusProtocol());
}
};
}
}
....
As you can see in above example there are two constructors.
The first one is the default, which establishes a connection using the default connector.
As the TCP variant of the `Modbus` protocol uses normal TCP, a `TcpSocketChannelFactory` instance is used.
However in order to test the driver, a unit- or integration-test can use the second constructor to inject a different `ChannelFactory`.
Notice that this constructor can be package-private if the test-case is in the same package.
Here the `TestConnectionFactory` will allow creating tests without having to worry about the physical connection and all problems that come with it.
The pipeline itself is created in the `getChannelHandler` method.
Here you have to keep in mind that the layer that is closest to the connection has to be added first, the `PLC4X Layer` last.