虚拟 Modbus
如果你想开始使用 Apache PLC4X,但手边没有任何 PLC,本教程将演示如何使用虚拟 Modbus Slave 软件来模拟与支持 Modbus 的 PLC 之间的通信。
这样一个完全开源的软件是 ModbusPal,可以从此处获取。
你需要做的只是下载名为 ModbusPal.jar 的文件。
设置虚拟 Modbus 从站
要运行该软件,只需在下载 Jar 文件所在的目录中执行以下命令:
java -jar ModbusPal.jar完成此步后,你将看到 ModbusPal 的界面,其外观如下:

图 1. ModbusPal 主界面
在此请确保 TCP port 被设置为 502(这是默认的 Modbus 端口)。
接下来点击 Add 以添加一个新的 Modbus 从站:

图 2. ModbusPal 添加从站界面
选择一个 Slave ID 以及一个 name。
点击 Add 之后,你的新 Modbus 从站便准备就绪,可以启动了。

图 3. ModbusPal 主界面,其中一个从站已启动
点击 Run 按钮后,你的从站通常就可以进行通信了。
然而到目前为止,这个新从站还没有任何可共享的数据,因为我们尚未创建任何资源,所以它将是一个空的从站。
因此作为最后的步骤,我们需要创建一些可供访问的资源。
为此,请点击你新建从站的 eye 图标。

图 4. ModbusPal 主界面,其中一个从站
这将打开一个编辑器,我们可以在其中添加一些所谓的 Holding registers 和一些 Coils。
让我们先从添加一些保持寄存器(Holding registers)开始。
请确保选中了 Holding registers,然后点击 Add 按钮:

图 5. 保持寄存器视图
工具会建议创建 65536 个新寄存器,我通常会创建 10 个左右。
我在使用此工具时,有时不得不多次点击 Add 按钮,因为编辑器并不总是在第一次就能打开。 |
|---|

图 6. 添加保持寄存器对话框
完成此步后,Holding register 标签页应如下所示:

图 7. 添加保持寄存器对话框
现在在 Coils 标签页上执行相同的操作,完成后的效果应如下所示:

图 8. 添加保持寄存器对话框
作为最后一步,我建议打开 Console。
你可以通过点击主界面的 Console 按钮来实现。
这将打开以下窗口:

图 9. 控制台
ModbusPal 将在此处记录每一个重要事件……尤其是当收到来自 PLC4X 的新连接时。
Modbus 基础知识
总的来说,Modbus 只认识两种数据类型:Coils 和 Registers。
线圈(Coils)始终是简单的 bits 或 boolean 值,而寄存器(Registers)始终是 16 bit short values。
不过,这些类型各有不同,并且有不同的名称:
Discrete Inputs(布尔值)(只读)Coils(布尔值)(读写)Input Registers(16 位短整型值)(只读)Holding Registers(16 位短整型值)(读写)
对于 Modbus 而言,地址并没有一个被普遍接受的表示法,不过由于地址非常简单,在 PLC4X 中我们按如下方式寻址:
| Modbus 类型 | PLC4X 地址语法 |
|---|---|
| 离散输入(Discrete Inputs) | discrete-input:{start-number} 或 discrete-input:{start-number}[{number-of-elements}] |
| 线圈(Coils) | coil:{start-number} 或 coil:{start-number}[{number-of-elements}] |
| 输入寄存器(Input Registers) | input-register:{start-number} 或 input-register:{start-number}[{number-of-elements}] |
| 保持寄存器(Holding Registers) | holding-register:{start-number} 或 holding-register:{start-number}[{number-of-elements}] |
表 1. 表格标题
所有地址均支持带长度和不带长度的表示法。如果未指定长度,PLC4X 将默认长度为 1。
PLC4X 的连接字符串对于所有驱动程序通常具有相同的结构:
{driver-name}:{transport-name}://{transport-config}?{parameters}片段 transport-name 和 parameters 通常是可选的,因为每个驱动都可以支持一种默认传输方式(default-transport),当省略 transport 时便会使用它。而且大多数驱动的实现方式并不要求任何参数,只有当连接需要一些非默认设置时才必须提供这些参数。
对于 Modbus,典型的传输方式有:
- tcp(transport-config 是从站的 IP 或主机名,可以通过一个端口作为后缀,端口与 IP/主机之间用
:分隔) - serial(transport-config 是串行接口的设备名称)
如果我们按照上文所述配置好了 ModbusPal 并在同台计算机上运行,那么用于连接它的连接字符串应为:
modbus-tcp://localhost如果将 ModbusPal 启动在与 Modbus 默认端口 502 不同的端口上,那么你还需要在连接字符串中提供该端口。
因此,假设你将其启动在 1502 端口上,则连接字符串如下所示:
modbus-tcp://localhost:1502编写代码
要编写一个有效的 PLC4X Java 应用,你只需添加对 api module 的依赖。使用 Maven 时,只需添加以下依赖:
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-api</artifactId>
<version>1.0.0</version>
</dependency>这将使你能够编写出一个有效的应用,它能正常编译。然而,为了真正通过某个给定协议连接到设备,你需要将该协议的实现加入到类路径中。
例如,要使用 Modbus 协议进行通信,你需要添加如下依赖:
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-driver-modbus</artifactId>
<version>1.0.0</version>
<scope>runtime</scope>
</dependency>连接
因此,一旦你的项目中具备了 API 和驱动实现,你首先需要获取一个 PlcConnection 实例。这可以通过 PlcDriverManager 来完成,由它为给定的 PLC4X connection string 创建一个实例。
String connectionString = "modbus-tcp://localhost";
try (PlcConnection plcConnection = new PlcDriverManager().getConnection(connectionString)) {
... do something with the connection here ...
}PLC4X 通常只支持非常有限的功能集,这并不是因为我们没有实现相关功能,而是因为 PLC 本身一般只支持非常有限的功能集。
PLC 所支持的、因而也是 PLC4X 所支持的基本功能包括:
- 读取数据
- 写入数据
- 订阅数据
- 在 PLC 中执行功能
- 列出 PLC 中的资源
总体而言,我们会尽可能提供更多的功能。因此,如果某种协议不支持基于订阅的通信,我们的目标是通过后台轮询来模拟这一功能,从而对用户保持透明。
但在某些情况下,我们无法进行模拟,或者某些功能被有意禁用:
- 如果 PLC 和/或协议不支持执行功能,我们就无法提供该功能。
- 我们将提供精简版的驱动程序,例如有意不支持任何数据写入和功能执行。
因此,我们使用元数据以编程方式检查某项功能是否可用:
读取数据
// Check if this connection support reading of data.
if (!plcConnection.getMetadata().isReadSupported()) {
logger.error("This connection doesn't support reading.");
return;
}一旦确认所需功能可用,即可着手构建第一个请求。具体做法是获取一个 PlcRequestBuilder:
// Create a new read request:
// - Give the single item requested the alias name "value"
PlcReadRequest.Builder builder = plcConnection.readRequestBuilder();
builder.addTagAddress("value-1", "coil:1");
builder.addTagAddress("value-2", "coil:3[4]");
builder.addTagAddress("value-3", "holding-register:1");
builder.addTagAddress("value-4", "holding-register:3[4]");
PlcReadRequest readRequest = builder.build();所以,如你所见,你需要准备一个请求,方法是向请求中添加条目,并在最后调用 build 方法。这里第一个字符串值只是你给地址起的一个别名,因为这通常是一种更具可读性的方式。第二个参数则是地址字符串本身。
在上面的示例中,我们没有添加 discrete-input 和 input-registers,因为 ModbusPal 似乎不支持这些。
通过在请求对象上执行 execute 方法,即可将请求发送到 PLC:
CompletableFuture<? extends PlcReadResponse> asyncResponse = readRequest.execute();
asyncResponse.whenComplete((response, throwable) -> {
... process the response ...
});一般来说,所有请求都是异步执行的。因此,一旦请求被完整处理,回调就会被调用:如果一切顺利,回调中将包含一个 readResponse;如果出现问题,则包含一个 throwable。
不过,如果你希望以更同步的方式来编写代码,下面的替代写法可以满足这一需求:
PlcReadResponse response = readRequest.execute().get();两种情况下响应的处理方式完全相同。下面的示例将展示您可以使用的一些选项:
for (String tagName : response.getTagNames()) {
if(response.getResponseCode(tagName) == PlcResponseCode.OK) {
int numValues = response.getNumberOfValues(tagName);
// If it's just one element, output just one single line.
if(numValues == 1) {
logger.info("Value[" + tagName + "]: " + response.getObject(tagName));
}
// If it's more than one element, output each in a single row.
else {
logger.info("Value[" + tagName + "]:");
for(int i = 0; i < numValues; i++) {
logger.info(" - " + response.getObject(tagName, i));
}
}
}
// Something went wrong, to output an error message instead.
else {
logger.error("Error[" + tagName + "]: " + response.getResponseCode(tagName).name());
}
}在 for 循环中,我们演示了用户如何遍历响应中的地址别名。对于普通的读取请求,这些别名将由请求中的项目预先确定;而在订阅响应的情况下,响应中可能只包含被订阅项目中的一部分。
在访问数据之前,建议先检查某个项目是否被正确返回。这是通过对给定别名调用 getResponseCode 方法来完成的。如果结果为 PlcResponseCode.OK,则一切正常;但也可能是以下几种情况之一:
- NOT_FOUND
- ACCESS_DENIED
- INVALID_ADDRESS
- INVALID_DATATYPE
- INTERNAL_ERROR
- RESPONSE_PENDING
假设返回码为 OK,我们便可以继续访问数据。
由于某些地址支持读取数组,用户可以通过 getNumberOfValues 方法检查返回了多少个给定类型的项目。为方便使用,响应对象提供了单参数方法用于访问数据,默认返回第一个元素。
response.getObject(fieldName)若要访问指定的元素编号,请改用双参数版本:
response.getObject(fieldName, 42)PLC4X 提供了针对多种 Java 类型的 getter 和 setter,并会自动处理类型转换。但是,例如尝试以 byte 类型获取一个 long 值,而该 long 值超出了较小类型所支持的范围时,就会抛出一个类型为 PlcIncompatibleDatatypeException 的 RuntimeException。不过,为了避免抛出该异常,存在 isValid{TypeName} 方法可供你用来检查该值是否兼容。
写入数据
写入的代码与读取的代码非常相似,只是在创建写请求时,我们需要在字段定义的同时传入数据。
虽然全部 4 种字段类型都支持读取,但只有 coil 和 holding-register 字段类型支持写入。如果你尝试向这些字段写入数据,将会导致错误。
因此,我们首先检查该驱动是否支持写入(某些驱动,例如被动模式驱动或只读版本,可能根本不支持写入):
// Check if this connection support reading of data.
if (!plcConnection.getMetadata().isWriteSupported()) {
logger.error("This connection doesn't support writing.");
return;
}一旦确认某个功能可用,你就可以着手构建第一个请求了。方法是获取一个 PlcRequestBuilder:
// Create a new read request:
// - Give the single item requested the alias name "value"
PlcWriteRequest.Builder builder = plcConnection.writeRequestBuilder();
builder.addTagAddress("value-1", "coil:1", new PlcBOOL(true);
builder.addTagAddress("value-2", "coil:3[4]", new PlcBOOL(true), new PlcBOOL(false), new PlcBOOL(true), new PlcBOOL(true));
builder.addTagAddress("value-3", "holding-register:1", new PlcINT(42);
builder.addTagAddress("value-4", "holding-register:3[4]", new PlcINT(1), new PlcINT(2), new PlcINT(3), new PlcINT(4));
PlcWriteRequest writeRequest = builder.build();你可以看到,对于每个需要写入的元素,都必须提供相同数量的参数。
PlcWriteResponse response = writeRequest.execute().get();实际执行写操作与读操作几乎完全相同,唯一的区别在于,结果仅提供状态码,而不包含数值。
for (String fieldName : response.getFieldNames()) {
if(response.getResponseCode(fieldName) == PlcResponseCode.OK) {
logger.info("Value[" + fieldName + "]: successfully written to device.");
}
// Something went wrong, to output an error message instead.
else {
logger.error("Error[" + fieldName + "]: " + response.getResponseCode(fieldName).name());
}
}评论
登录后参与评论
KnowForge