| // |
| // 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. |