OpenPLC Runtime v3 项目核心详解
OpenPLC Runtime v3 项目阅读记录
- OpenPLC Runtime v3 项目阅读记录
概述
关键点简述:
openplc 项目按粗粒度可划分为两层:1、python web服务器。2、cpp runtime 服务器。

- 对于 python web 服务器,可以细分为三个功能模块:前端页面模块、后端接口模块、runtime 通信模块;
- 对于 cpp runtime 服务器,细分暂定为三个模块:客户端交互模块、plc服务模块(包含下面的协议模块)、main 主循环模块;
- 一些值得关注的文件和脚本。
说明:
Webserver 层直接面向使用者,通过前端组件与之交互。
凡是涉及数据的组件均依赖后端接口提供的调用服务,包括不限于route、button、href、text等。
后端接口模块负责的主要功能可以见Python Web 页面的菜单项,即“dashboard”、“programs”、“monitoring”等。
后端接口模块又依赖 Openplc业务模块提供的py函数,包括不限于 runtime 生命周期管理,通信协议管理,程序编译管理。
Core 层为 webserver 提供12种函数接口,主要在 interacitve_server.cpp 中实现。
通信方式为TCP/IP的socket套接字。
Compile_program.sh 脚本生成 openplc 可执行文件。输入是 *.st,core/*.cpp,使用 iec2c和g++。
示例:
上传 st 文件作为 program,编辑信息后,点击编译会在 core 目录下得到 openplc可执行文件。将 core 目录下的 c/cpp 文件编译链接后得到,函数入口即是main.cpp。
当点击 Web 页面的“start plc”按钮,实际会执行core目录下的 openplc可执行文件。
main.cpp主函数创建线程执行 interactive_server.cpp、接着主线程进入循环,循环执行PLC程序。
interactive_server 线程监听 socket,再收到Python 服务器 openplc.py 发来的信息后,匹配到对应的命令,依次调用 server.cpp 提供的接口执行命令。例如start_modbus。
server 执行 start_modbus 也会创建线程去启动modbus 服务,监听 modbus 协议要求的端口号,再收到请求后,读取配置的缓冲区,处理命令,再写出到缓冲区。具体实现均在 modbus.cpp。
除了modbus.cpp 外还有类似的 enip.cpp。
架构

documentation/
EtherNet-IP: 包含与EtherNet/IP协议相关的文档。EtherNet-IP文件夹存放了实现EtherNet/IP的所有相关文档。
utils/
apt-cyg/: 包含[apt-cyg]的Cygwin安装工具。dnp3_src/: 包含DNP3协议相关的源代码和文档。ethercat_src/: 包含EtherCAT协议相关的源代码。(目前为空)glue_generator_src/: 包含生成代码粘合层的工具。libmodbus_src/: 包含libmodbus库的源代码,支持Raspberry Pi的GPIO功能。matiec_src/: 包含[matiec]编译器的源代码,这是一个用于IEC61131-3 编程语言的编译器。python2/: python2 源码。st_optimizer_src/: ST优化器。本程序负责从PLCOpen Editor初始编译后的优化过程。它所做的只是扫描ST文件中的第一级IF语句,并将相同的语句连接在一起。
webserver/
基于 Flask app 实现的用于浏览器的服务端
webserver.py pages.py 前段组件页面;
webserver.py 访问页面的响应函数,供前端访问;
openplc.py 文件提供与 runtime 可执行程序通信的接口,供响应函数调用;
active_program: 存储正在执行的或者上一个执行的 st 的文件名称。check_openplc_db.py: 检查和初始化OpenPLC项目需要的默认数据库。core/: 核心功能模块,将 st 程序转为基于 C/C++ 的可执行的程序。dnp3.cfg: DNP3协议的配置文件。lib/: 依赖库。monitoring.py: 监控脚本。openplc.py: Python的OpenPLC类实现,实现了客户端的交互接口,同 core 可执行程序提供的服务端接口(interactive_server)进行交互。pages.py: python web html 组件页面。scripts/: 用于 python 程序调用的脚本,启动程序,编译程序,改变硬件层等。static/:静态资源,前端logo,图片等。st_files: 保存上传的 st 文件的目录。webserver.py:openplc python web 实现的主文件。
webserver/core
生成 openplc 可执行程序的工作目录。
core 目录下所有的 *.c *.cpp 文件经过编译、链接生成可执行文件 openplc;
该可执行文件负责执行具体的业务,为 openplc.py 服务层提供可调用的函数。
只不过调用的方法是通过 socket 匹配字符串的形式。
具体实现:
hardware_layers
支持scripts文件夹中change_hardware_layer.sh中有关操作。
执行硬件层选择。lib
txt 文件,作用未知,猜测是 st 程序语法分析相关。psm
猜测:与可执行程序 openplc 通信交互使用。主要实现与 modbus.cpp 的通信?main.original/main.py
与OpenPLC Python SubModule (PSM)有关。
PSM是连接 OpenPLC 内核与 Python 程序的桥梁。
PSM 允许使用 Python 直接连接 OpenPLC IO,甚至使用普通 Python 为扩展板编写驱动程序。
PSM API 非常简单,只有几个函数。
给出了一个用PSM的示例代码,可以通过PSM与OpenPLC核心连接,实现对OpenPLC的IO操作。psm.py
出了一个使用pymodbus库实现的 Modbus TCP 服务器的示例。
注1:Modbus TCP服务器是一种实现了Modbus通信协议的服务器,通过TCP/IP网络提供数据交换服务。
Modbus是一种用于工业自动化领域的通信协议,它通常用于连接和控制自动化设备,例如传感器、执行器、PLC等。
在Modbus TCP服务器负责处理来自客户端的请求,执行相应的操作,并返回结果。
注2:pymodbus是一个用于处理 Modbus 协议的 Python 库,括 Modbus TCP 和 Modbus RTU 的支持。
client.cpp这是OpenPLC的网络例程文件。它具有创建套接字并连接到服务器的过程。dnp3.cpp,dnp3协议实现主文件,参考更详细的文档 OpenPLCRuntimeDnp3Cpp。enip.cpp,enip协议实现主文件,参考更详细的文档 OpenPLCRuntimeEnipCpp。interactive_server.cpp
这是用于交互式服务器的文件。它具有创建套接字、绑定套接字、启动网络通信和处理命令的过程。交互式服务器仅响应本地主机,并且仅用于与 Python web服务器GUI 通信。- 启动服务器的命令。它接收端口号作为参数,并创建一个无限循环来监听和解析客户端发送的信息。本服务提供了12个命令供客户端调用。
- 匹配到命令后,调用 server.cpp 提供的接口,执行具体的业务。
main.cpp
openplc 程序主入口,实现循环执行PLC,记录日志,计算时间,提供12种函数作为业务层,供 openplc 服务层调用。
main.cpp 、server.cpp、interactive_server.cpp 配合使用;- main 线程会创建与上层 openplc 通信的专用线程
interactiveServerThread; - 配置硬件内容;
- 启动主循环,循环执行PLC程序;通过RES0方式运行。
- 配置全局变量 run_openplc 等,作为标志量,信号量在线程间通信。全局变量会被 interactive_server 线程修改。
- main 线程会创建与上层 openplc 通信的专用线程
modbus.cpp,实现 modbus 协议主文件,参考 OpenPLCRuntimeModbusCpp 文档。modbus_master.cpp,openplc可执行文件好像没用到此文件实现的函数。pccc.cpp,支持enip协议实现,参考更详细文档 OpenPLCPcccCpp。persistent_storage.cpp,openplc 持久化工作,参考 OpenPLCRuntimePersistentStorageCpp。server.cpp
这是OpenPLC的网络例程文件。它具有创建套接字、绑定套接字,启动网络通信功能。- 启动服务器的命令。它接收端口号作为参数,并创建一个无限循环来解析客户端发来的消息。
- 通用的启动服务器命令,modbus,dnp3等能够统一调用
startServer函数。根据传入的端口号和协议参数,创建 socket 去分别监听和解析。 - 处理请求时,调用 modbus.cpp、enip.cpp 提供的服务。
文件详读
解释 core 目录下关键文件。
dnp3.cpp, modbus.cpp, enip.cpp, pccc.cpp, persistent_storage.cpp 相关记录请移步到具体的文档。
core/client.cpp
core/client.cpp
简述:创建套接字、连接到服务器、发送和接收消息以及关闭连接的功能。
角色:
头文件定义在 core/lib/openplc_networking.h。
支持 core/lib/communication.h 的静态函数 TCP_CONNECT_body__、TCP_SEND_body__、TCP_RECEIVE_body__、TCP_CLOSE_body__ 实现,但是函数也没被OpenPLC项目用到。
常量:
METHOD_UDP,值为1,使用UDP协议。METHOD_TCP,值为0,使用TCP协议。
方法:
connect_to_tcp_server
此函数根据指定的方法建立与TCP或UDP服务器的连接。
它有三个参数:ip_address(服务器的ip地址)、port(服务器的端口号)和method(指示是使用TCP还是UDP)。socket creation
根据方法参数的不同,它创建一个 TCP (SOCK_STREAM)或 UDP (SOCK_DGRAM)套接字。error handling
如果套接字创建失败,它将记录一条错误消息并返回 -1。socket configuration
它使用提供的 IP 地址和端口配置套接字地址结构(sockaddr_in)。server connection
尝试连接到服务器。如果连接失败,它将记录一条错误消息,关闭套接字,并返回 -1。non-blocking mode
使用fcntl将套接字设置为非阻塞模式。如果此操作失败,它将记录一条错误消息并返回-1。return
如果成功,它将返回套接字文件描述符。
send_tcp_message
此函数通过已建立的TCP连接发送消息。
它有三个参数:msg(要发送的消息)、msg_size(消息的大小)和socket_id(套接字文件描述符)。sending message
使用send函数通过套接字发送消息。error handling
如果发送失败,它会记录一条错误消息
receive_tcp_message
此函数从已建立的TCP连接接收消息。
它有三个参数:msg_buffer(存储接收到的消息的缓冲区)、buffer_size(缓冲区的大小)和socket_id(套接字文件描述符)。receiving message
使用recv函数从套接字接收消息。error handling
如果接收失败并且错误不是EAGAIN或EWOULDBLOCK,则会记录一条错误消息。
否则,它将处理接收到的消息。
close_tcp_connection
此函数用于关闭已建立的TCP连接。 它接受一个参数:socket_id(套接字文件描述符)。closing connection
使用 close 函数关闭套接字
core/debug.h
已知 debug.cpp 是基于 st 文件,过滤掉部分字符生成的。
但是,debug.h 文件是项目作者手动写的。
规定了接口,且都被 debug.cpp 实现。
set_endiannessget_var_countget_var_sizeget_var_addrforce_varset_tracetrace_reset
此文件声明的函数,均被 modbus.cpp 文件调用,且调用路径为:
(modbus.cpp)handleConnections -> processMessage -> processModbusMessage -> debugGetTrace -> (debug.cpp)get_var_count、get_var_addr
core/debug.cpp
core/debug.cpp
简述:
该文件内容与st文件内容一致,是基于上传st程序生成的。

所选文件debug.cpp是OpenPLC运行时项目的一部分。
它的主要作用是促进OpenPLC环境中的调试和变量操作。
虽然由matiec 生成,但是 debug.cpp (其中实现的函数)支持 modbus.cpp 函数的调试(在调试函数中被调用)。
core/interactive_server.cpp
创建套接字、绑定套接字、启动网络通信和处理命令的过程。响应 Python web 服务器(openplc.py的函数交互)。
- 启动服务器的命令。它接收端口号作为参数,并创建一个无限循环来监听和解析客户端发送的信息。本服务提供了12个命令供客户端调用。
- stop_server 命令,会修改全局变量 run_server 的状态为 0。
- 匹配到命令后,调用 server.cpp 提供的接口,执行具体的业务
core/server.cpp
这是OpenPLC的网络例程文件。它具有创建套接字、绑定套接字,启动网络通信功能。支持 interactive_server 服务,提供了 modbus, dnp3, enip 服务的启动。这些服务一般并基于全局变量 run_server 状态,循环执行。
webserver/monitoring.py
webserver/monitoring.py
作用是在使用 Modbus TCP 通信的 OpenPLC (可编程序控制器)环境中监视和管理调试变量。
该文件包含解析结构化文本(ST)文件的功能,以提取调试变量,从 Modbus 服务器读取它们的值,并持续监视这些值。
依赖:
time时间相关函数、threading创建管理线程;from struct import *:在 python 中用 C 风格的数据结构工作;from pymodbus.client.sync import ModbusTcpClient:
Pymodbus 是一个用 Python 提供 Modbus 协议实现的库。
ModbusTcpClient 类允许通过 TCP/IP 与 Modbus 服务器进行同步通信。
这对于需要与 Modbus 兼容的设备(如工业控制器、传感器和执行器)交互的应用程序来说是必不可少的。
具体实现:
parse_st:
语法/句法解析一个结构化文本文件(ST),提取相关的调试信息,存储在名为debug_vars的全局列表里。cleanup:
清空全局列表debug_vars的内容。modbus_monitor:
Modbus_monitor函数用于从 Modbus 客户机读取各种类型的数据,并相应地更新debug_vars列表中的 debug`_data 对象。下面是其功能的详细分类:mb_client全局变量,指向一个 Modbus 客户端实例,能够执行读操作。debug_vars列表,对列表的每一项debug_data执行迭代,每一个对象debug_data对象包含被读数据的位置和类型信息。Reading Input Status:%IX0.0-%IX0.7,Reading Coils:%QX0.0-%QX0.3Reading Input Registers%IW1-%IW7Reading Holding Registers%QW0-%QW3Reading Word Memory%MW0-%MW1023Reading Double Memory%MD0-%MD1023Reading Long Memory%ML0-%ML1023
start_monitor
初始化监视器进程,监视 Modbus TCP 客户端。
modbus_port_cfg制定了Modbus连接的端口号。
设置monitor_active全局变量为 True
实例化mb_client全局变量,指向一个ModbusTcpClient。
接着调用modbus_monitor方法, 设置调试信息的value,(虚拟地址计算为内存地址)。stop_monitor
关闭mb_client指向的ModbusTcpClient实例。
webserver/openplc.py
webserver/openplc.py
openplc.py
- 定义了 runtime 类,管理 runtime 生命周期; 启动运行时,关闭运行时,启动和关闭运行时的服务模块。
- 定义了 _rpc函数,实现了基于TCP的 socket 的双向通信。基于 _rpc 函数与可执行文件实现了 12 种函数调用;
- 管理 runtime 的开始、结束,管理runtime 负责的 modbus、dnp3、等协议的启动和结束。
- 编译st程序(通过执行 complie_program.sh 文件),启动程序(通过运行程序)。
- 提供接口,供
webserver.py调用;
improt前提:
- subprocess 进城管理模块;
- socket 网络管理模块;
- threading 线程管理模块;
- queue 队列;
- os.path 文件操作;
实现功能简述:
- 管理 OpenPLC 运行时;
管理 OpenPLC 运行时环境的生命周期和操作。OpenPLC 是一个开源程序逻辑控制器,用于工业自动化。
同时,会与运行时进行交互。运行时监听了一个ip:port,接受并处理信息。 - 编译 ST 程序;
- 处理交流协议;
- 检索日志和执行时间;
具体实现:
- Function
display_time;
此函数将以秒为单位的时间持续时间转换为人类可读的格式,并将其分解为星期、天、小时、分钟和秒。 - Class
NoBlockingStreamReader;
此类采用非阻塞的方式从流中读取数据行(例如子进程的 stdout 或 stderr);使用一个单独的线程从流中读取,并将行放入队列中,可以在不阻塞主线程的情况下读取队列;?Ques:线程执行方法是一个条件判断的循环,循环中会抛出异常。线程执行时如果抛出了异常会发生什么。 - Class
UnexpectedEndOfStream;
此类继承Exception,自定义的异常,当流出现不期望的终点时,该异常被抛出。 - Class
runtime;
此类管理OpenPLC运行时环境。提供以下API:start_runtime:
启动 OpenPLC 运行时,通过启动核心程序,程序以2进制的形式存放在文件系统(./core/openplc);_rpc:
发送信息到运行时,通过网络套接口并返回响应;
socket 网络套接口打开 localhost:43628 发送消息并接受响应。stop_runtime:
停止 OpenPLC 运行时,通过发送quit信息并等待运行时进程被终止;compile_program(st_file):
编译指定的 ST(Structed Text) 文件,尽可能地提取调试信息;
停止运行时,打开 st 文件,读取每一行,按行格式,分为 debug 和 program 两个文件,保存 debug 信息到文件中。启动编译程序./scripts/compile_program.sh。compilation_status:
返回编译进程的状态;status:
返回当前运行时的状态;它执行一系列检查以确定系统是在编译还是在运行,并相应地更新状态。start_modbus:启动Modbus交流协议;stop_modbus: 停止Modbus交流协议;start_dnp3: 启动DNP3交流协议;stop_dnp3:停止NP3交流协议;start_enip:启动EtherNet/IP交流协议;stop_enip:停止EtherNet/IP交流协议;start_pstorage:以指定的投票率启动持久化存储;stop_pstorage:停止持久化存储;logs:检索执行日志;exec_time检索运行时的执行时间;
注意:
subuprocess.Popen方法,作用是执行一个 shell 脚本;[compile_program];
方法参数是 @1 一个列表,第一项为脚本的位置 str, 第二项为传入的 st 文件名称 str;@2 stdout 表明脚本的标准输出被捕获;@3 stderr 表明重定向标准错误到标准输出;确保所有的输出均被捕获。NonBlockingStreamReader是一个非阻塞输出,记录执行脚本的日志;[compile_program];
传入一个标准输出流作为参数,目的是从输出流中非阻塞的去读取所有的执行输出。有利于去实时看进程的标准输出,尤其是长进程执行时。compilation_object.readline()从输出流中读取实时日志,引用来自定义的类compilation_object = NonBlockingStreamReader(a.stdout)[compilation_status]globals()在哪里定义过? [status]
webserver/pages.py
webserver/pages.py
pages.py 专门处理 OpenPLC webserver 的 HTML 内容和 JavaScript 功能。
该文件包含用于登录页面的 HTML 模板,以及用于处理表单验证和 UI 交互的 JavaScript 函数。
/users/add-user/edit-user?table_id=10/dashboard/programs/compile-program?file=/update-program?id=/remove-program?id=/start_plc/stop_plc
webserver/webserver.py
webserver/webserver.py
webserver.py
- web 交互界面,app 持续监听着 localhost:8080;
- 所有的后端响应接口均在该文件中,为前端按钮 button,路由 route 提供服务;
- 具体实现功能模块(该功能模块意味着与web交互的按钮和路由,以及对请求的响应):plc 启动,关闭,modbus,dnp3等协议启动和关闭;st 程序增删改上传编译;用户管理;硬件管理;监视器管理等;
功能
- 初始化 Flash 应用
- 设置应用密钥,生成随机数密钥
- 创建登录管理示例与 Flash 应用绑定
- 调用
webserver/openplc.py执行,创建 openplc 运行时实例,实例后续被配置和启动,用于接收和处理消息。monitor.parse_st负责发送 st 相关消息。 - main 方法实例化和运行一个 OpenPLC 的 web服务,方法内:
- 定位 st 文件,读取
active_program文件内容作为 st 文件名称。 - 连接数据库
- 查询 st 文件名称关联的的程序信息表
- 查询设置信息表,
- 初始化和启动 OpenPLC 运行时;
- 解析 st 文件名称对应的 st 文件。
- 定位 st 文件,读取
实现函数:
文件中实现的具体函数详细解读,针对关键的函数和稍微难读懂的函数。
-
configure_runtime查出数据库 settings 表,根据配置去启动 runtime 服务,去调用openplc.py的服务启动函数,实质上是 openplc 通过 rpc 的将字符串发送到 openplc 执行程序监听的 socket; -
generate_mbconfig查出数据库settings表和Slave_dev表,输出固定配置信息内容,生成mbconfig.cfg文件。 -
draw_top_div绘制页面顶部导航栏; -
draw_status绘制页面左侧导航栏下面的文字和按钮; -
draw_blank_page绘制空白页,用于错误展示; -
draw_compiling_page绘制正在编译界面; -
user_loader
简述:@login_manager.user_loader它将user_loader函数注册为用户加载程序回调。此函数的用途是从 SQLite 数据库检索用户详细信息,并在找到用户时返回 User 对象。负责从数据库加载用户信息,并为 Flask-Login 创建一个 User 对象来管理用户会话。
具体实现:- 连接
sqlit openplc数据库 - 读出
Users表所有数据; - 逐行检索
username是否对应;- 成功的话,将 user 实例化,赋值;
- 连接
-
request_loader
简述:用于加载 Flask-Login 的用户信息,Flask 应用程序的用户会话管理扩展。这个函数用@login_manager.request_loader将函数注册为请求加载程序回调。此函数的用途是从 SQLite 数据库检索用户详细信息,并在找到并验证用户时返回 User 对象。负责从数据库加载用户信息,验证用户身份,并为 Flask-Login 创建一个 User 对象来管理用户会话。 -
before_request
该函数在 Flask 应用程序中的每个请求之前执行。这个函数用@app.before_request标注,它将函数注册为要在每个请求被处理之前运行的回调。- 将会话设置为永久的,这意味着当用户关闭浏览器时会话不会过期;
- 它将永久会议的会期设置为5分钟。这意味着,如果5分钟内没有活动,会话将到期;
- 该函数将会话标记为已修改,从而确保会话的过期时间随每个请求而更新;
-
index
代码为 Flask web 应用程序的根 URL (/)定义一个路由。此路由由 index 函数处理,该函数根据当前用户的身份验证状态确定响应。- 首先检查当前用户身份,使用
flask_login.current_user.is_authenticated验证。如果用户通过了身份验证,函数会将用户重定向到仪表板页面。如果用户没有通过身份验证,函数会将用户重定向到登录页面。
- 首先检查当前用户身份,使用
-
login
通过根据数据库验证凭据并使用Flask login管理用户会话来处理用户登录。 -
start_plc
调用 openplc.py 脚本的start_rumtime方法,通过启动 openplc 可执行文件,启动 openplc 运行时。
openplc可执行文件生成过程:上传 .st 文件后,执行compile_program.sh脚本来处理 .st 文件,实质上是依次经过 iet2c、编译、链接后生成的。
调用configure_runtime()函数以根据数据库中存储的设置(是否启动某些服务),去向 OpenPLC运行时发送消息去启动服务。 -
stop_plc
停止 openplc runtime 正在执行的程序。
调用 openplc.py 脚本的stop_runtime方法,通过 rpc 发送 'quit()' 字符串到 runtime 监听的 socket。 -
runtime_logs,获取编译st文件的日志。
-
dashboard
处理仪表板页面的显示,确保只有经过身份验证的用户才能访问它。它根据OpenPLC运行时的当前状态和详细信息动态生成内容。
定义 Flask Web 应用程序的/dashboardURL 的路由。此路由由dashboard函数处理,该函数根据用户的身份验证状态管理dashboard页面的显示。
如果用户通过了身份验证,函数将通过调用monitor.stop_monitor()来停止监视进程。然后它检查 OpenPLC 运行时状态是否为“编译”。如果是,函数返回一个页面,指示正在编译程序;
如果OpenPLC运行时没有编译,该函数将为仪表板页面构造HTML内容。它首先添加必要的样式和标题信息;
添加了带有各种导航选项的侧边栏,如仪表板、程序、Modbus、监控、硬件、用户、设置和注销;
调用draw_status()将OpenPLC运行时的当前状态添加到侧边栏;
添加主要内容区域,其中包括OpenPLC运行时的状态、当前程序名称、描述、文件和运行时执行时间;
附加结束HTML标签并返回构造的HTML内容; -
programs,- 检查用户登录。
- 查询数据库:
cur.execute("SELECT Prog_ID, Name, File, Date_upload FROM Programs ORDER BY Date_upload DESC")。 - 按行展示。
-
reload_programcur.execute("SELECT * FROM Programs WHERE Prog_ID = ?", (int(prog_id),)),查询展示。
-
update_programprog_id = flask.request.args.get('id')- 展示更新程序表单。
-
update_program_actionprog_file = flask.request.files['file']- 保存上传的 st 文件。
prog_id = flask.request.form['prog_id']cur.execute("SELECT * FROM Programs WHERE Prog_ID = ?", (int(prog_id),))prog_file.save(os.path.join('st_files', filename))
-
remove_programprog_id = flask.request.args.get('id')cur.execute("DELETE FROM Programs WHERE Prog_ID = ?", (int(prog_id),))- 按 id 删除程序。
-
upload_programprog_file = flask.request.files['file']prog_file.save(os.path.join('st_files', filename))- 保存上传的文件,并展示一些文件信息。
-
upload_program_action
- 获取文件信息
prog_name = flask.request.form['prog_name'] prog_descr = flask.request.form['prog_descr'] prog_file = flask.request.form['prog_file'] epoch_time = flask.request.form['epoch_time']cur.execute("INSERT INTO Programs (Name, Description, File, Date_upload) VALUES (?, ?, ?, ?)", (prog_name, prog_descr, prog_file, epoch_time))保存。
-
compile_programst_file = flask.request.args.get('file')cur.execute("SELECT * FROM Programs WHERE File=?", (st_file,))- 实例赋值
openplc_runtime.project_name = str(row[1]) openplc_runtime.project_description = str(row[2]) openplc_runtime.project_file = str(row[3])openplc_runtime.compile_program(st_file),启动编译。
-
compilation_logsopenplc_runtime.compilation_status()
-
modbuscur.execute("SELECT dev_id, dev_name, dev_type, di_size, coil_size, ir_size, hr_read_size, hr_write_size FROM Slave_dev")- 查表,按行展示。
-
add_modbus_device- get 请求:
ports = [comport.device for comport in serial.tools.list_ports.comports()]- 按行展示。
- post 请求:
- 获取设备信息
devname = flask.request.form.get('device_name') ... aow_size = flask.request.form.get('aow_size')- 插入数据库,
cur.execute("INSERT INTO Slave_dev (dev_name, dev_type, slave_id, com_port, baud_rate, parity, data_bits, stop_bits, ip_address, ip_port, di_start, di_size, coil_start, coil_size, ir_start, ir_size, hr_read_start, hr_read_size, hr_write_start, hr_write_size, pause) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (devname, devtype, devid, devcport, devbaud, devparity, devdata, devstop, devip, devport, di_start, di_size, do_start, do_size, ai_start, ai_size, aor_start, aor_size, aow_start, aow_size, devpause))。
- get 请求:
-
modbus_edit_device- get
dev_id = flask.request.args.get('table_id')cur.execute("SELECT * FROM Slave_dev WHERE dev_id = ?", (int(dev_id),))ports = [comport.device for comport in serial.tools.list_ports.comports()]- 按行展示
- post
- 从会话中获取数据。
cur.execute("UPDATE Slave_dev SET dev_name = ?, dev_type = ?, slave_id = ?, com_port = ?, baud_rate = ?, parity = ?, data_bits = ?, stop_bits = ?, ip_address = ?, ip_port = ?, di_start = ?, di_size = ?, coil_start = ?, coil_size = ?, ir_start = ?, ir_size = ?, hr_read_start = ?, hr_read_size = ?, hr_write_start = ?, hr_write_size = ?, pause = ? WHERE dev_id = ?", (devname, devtype, devid, devcport, devbaud, devparity, devdata, devstop, devip, devport, di_start, di_size, do_start, do_size, ai_start, ai_size, aor_start, aor_size, aow_start, aow_size, devpause, int(devid_db)))- 更新数据库。
- get
-
delete_devicedevid_db = flask.request.args.get('dev_id')cur.execute("DELETE FROM Slave_dev WHERE dev_id = ?", (int(devid_db),))- 按 id 删除设备。
-
monitoring- 检查运行状态
if (openplc_runtime.status() == "Running"): cur.execute("SELECT * FROM Settings")- 检查 modbus 状态
if modbus_enabled == True: monitor.start_monitor(modbus_port_cfg), 启动监视for debug_data in monitor.debug_vars:,检查每一个值
- 检查运行状态
-
monitor_updatemb_port_cfg = flask.request.args.get('mb_port')monitor.start_monitor(int(mb_port_cfg)),启动监视。mb_client = ModbusTcpClient('127.0.0.1', port=modbus_port_cfg),启动一个 MODBUS TCP 客户端。
for debug_data in monitor.debug_vars:,检查每个值。
-
point_infopoint_id = flask.request.args.get('table_id'),debug_data = monitor.debug_vars[int(point_id)],检查debug 值。
-
point_update -
hardwarewith open('./scripts/openplc_driver') as f: current_driver = f.read().rstrip(),展示文件内容。
-
restore_custom_hardwarewith open('./core/psm/main.original') as f: original_code = f.read(),with open('./core/psm/main.py', 'w+') as f: f.write(original_code),
-
users,用户列表展示。 -
add_user,新增用户。 -
edit_user,编辑用户信息。 -
delete_user,用户删除。 -
settings,设置相关的 CRUD。 -
logout,退出登录。 -
create_connection
输入数据库文件,创建并返回 SQLite 数据库的连接。openplc.db文件是 db 文件,数据直接存储在里面。 -
if __name__ == '__main__'
代码是与 OpenPLC 运行时交互的基于 Flask 的 Web 应用程序的入口点。
它初始化 Web 服务器并从 SQLite 数据库加载当前 PLC 程序配置。- 它首先从名为
active_program的文件中读取活动PLC程序的名称; - 它使用
create_connect函数建立到SQLite数据库的连接。如果连接成功,它将从数据库中检索活动程序的详细信息和一些设置;根据文件名查询Programs表的project_name``、project_description、project_file。 - 查出
Settings表。默认Start_run_mode为 false。 - 启动 web app。
0.0.0.0:8080。
- 它首先从名为
注意
-
monitorimport monitoring as monitor ### monitoring 为文件 -
参与 Flask web 页面交互的函数,在函数头上会标注注解。此文件用到的注解只有两种
@app、@login_manager。 -
首先函数中包含很多重要的函数,例如
star_plc()。 -
查找项目中文件是否包含
onclick。找到对应的函数。 -
Flask 启动原理。
-
Flash 中注解使用规范,自定义注解使用。
webserver/core/main.cpp 主函数启动流程
- 主循环读取缓冲区输入,
RES0执行,缓冲区输出。通过Modbus。 - PLC 启动流程。通过套接字接受字符串命令,然后匹配执行。执行的命令种类有限,主要是启动服务,退出,打印时间、日志等。
- main 函数启动流程,命令行执行
./start_openplc.sh,脚本内执行python3 webserver.py,其实只启动了 Flask app web 交互界面,通过命令app.run(debug=False, host='0.0.0.0', threaded=True, port=8080)。没有启动 openplc_runtime,因为数据库settings表中的配置项start_run默认为 false。现在访问localhost:8080会进入 Flask 交互界面。启动命令中threaded配置为 True,意味着每个请求交给单独线程处理。Flask 服务一直开着。Flask 服务中主要是两个文件,webserver.py和openplc.py。
PLC INITIALIZATION
-
main.cppint main(int argc, char **argv)// PLC INITIALIZATION,创建线程执行 interactiveServerThread pthread_create(&interactive_thread, NULL, interactiveServerThread, NULL); // void *interactiveServerThread,执行 interactive_server::startInteractiveServer startInteractiveServer(43628); -
main.cpp main->interactive_server.cpp startInteractiveServer// 创建socket while(run_openplc) { // 等待客户端连接 client_fd = waitForClient_interactive(socket_fd); // 如果连接成功 // 创建线程,arguments[0] = client_fd; // 去执行 handleConnections_interactive ret = pthread_create(&thread, NULL, handleConnections_interactive, arguments); } -
interactive_server.cppvoid *handleConnections_interactive(void *arguments)while(run_openplc) { // 处理消息 processMessage_interactive(buffer, messageSize, client_fd); } // 关闭客户端套接字,退出线程 -
interactive_server.cppvoid processMessage_interactive(unsigned char *buffer, int bufferSize, int client_fd)for (int i = 0; i < bufferSize; i++) { // 每次for 循环,从 buffer 中读取一条可被识别的命令 call。 // "quit", "start_modbus" 等,以 '\n','\r' 分割; processCommand(server_command, client_fd); } -
interactive_server.cppvoid processCommand(unsigned char *buffer, int client_fd)// 12 个 if-else 通过字符串匹配的方式,去分别执行指定逻辑。 // quit(),检查并关掉 modbus、dnp3等;关闭方法是将状态信号量 run_modbus, run_dnp3 置为0,(此处没加锁。) // start_modbus(, 创建线程执行 modbusThread pthread_create(&modbus_thread, NULL, modbusThread, NULL); -
interactive_server.cppvoid processCommand(unsigned char *buffer, int client_fd)// 执行 server startserver, 启动 modbus 服务 startServer(modbus_port, MODBUS_PROTOCOL); -
interactive_server.cpp void processCommand(unsigned char *buffer, int client_fd) -> server.cpp void startServer(uint16_t port, int protocol_type)// 循环运行,例如 modbus 监听线程。 while(*run_server) { //等待客户端连接 client_fd = waitForClient(socket_fd, protocol_type); //block until a client connects //如果客户端连接成功 //设置线程参数,客户端连接符、协议类型 arguments[0] = client_fd; arguments[1] = protocol_type; //创建线程 ret = pthread_create(&thread, NULL, handleConnections, (void*)arguments); } // 关闭socket和客户端连接
MUTEX INITIALIZATION
信号量初始化。
HARDWARE INITIALIZATION
硬件初始化。
PERSISTENT STORAGE INITIALIZATION
存储持久化初始化。
主循环 Main Loop
主循环处理输入和输出缓冲区的更新、特殊功能的执行以及PLC程序逻辑的运行。
- 调用 updateCustomIn 函数来更新自定义输入变量。此函数可能在代码的其他地方定义,负责处理特定于应用程序的任何自定义输入逻辑。
- updateBuffersIn_MB 函数用来自从属设备的数据更新输入图像表,通信方式是Modbus。此函数从连接的设备读取输入数据,并相应地更新内部缓冲区;
- 调用 handleSpecialFunctions 函数来执行每个周期中需要处理的任何特殊函数。这可能包括更新当前时间或处理特定计数器等任务;
Config_run__函数用递增的__tick变量调用,以执行 PLC 程序逻辑。这个功能运行 PLC 程序的主要逻辑,处理输入数据和更新输出数据。RES0_run__负责执行。- 在执行
PLC逻辑之后,调用updateCustomOut函数来更新自定义输出变量。 UpdateBuffersOut_MB函数使用输出映像表中的数据更新从设备。此功能将输出数据写入连接的设备,确保物理输出根据 PLC 程序逻辑更新;
iec2c 程序作用
在脚本 compile_program.sh 中,执行 ./iec2c -f -l -p -r -R -a ./st_files/xxx.st 命令会生成7个文件,存储在 webserver/core 目录下。
POUS.c、POUS.h、LOCATED_VARIABLES.h、VARIABLES.csv、Config0.c、Config0.h、Res0.c。
这 7 个文件会和 main.cpp 等其他文件一起,被编译成 openplc 可执行程序。
研究 C 语言文件,由 .st 文件生成的
主要文件如下:
- POUS.c
- POUS.h
- LOCATED_VARIABLES.h
- VARIABLES.csv
- Config0.c
- Config0.h
- Res0.c
POUS.h
POUS依赖
#include "accessor.h"定义了很多宏方法。#include "iec_std_lib.h"标准的 iec 依赖库。
POUS实现
- 结构体
ARDUINOSEG定义。- 例如
ArduinoSeg_ex.st文件的首行为PROGRAM ArduinoSeg。 - 该结构体名称与 .st 文件中,定义的程序名的大写形式一致。
- 结构体中属性的类型和命令与 st 文件中相似。
![ARDUINOSEG结构体定义和st文件中的程序定义]()
- 如上图所示,图片中左边为OpenPLC Runtime 项目的 POUS.h,文件的代码;图片中右边为 ArduinoSeg_ex.st 文件的一部分代码。
TON0,TOF0,CTU0,R_TRIG1这些属性的类型和命令与 st 文件中是一致的。类型的定义来自依赖iec_std_FB.h。TON,TOF,CTU,R_TRIG这些类型均是结构体定义。结构体中属性的声明也使用了__DECLARE_VAR等宏。疑问:OpenPLC Runtime 项目的边界如何定义?core/lib 目录下的文件是否需要与Runtime 项目的文件一起编写?iec_std_FB.h和accessor.h文件是开源的还是项目作者自己写的?
下面三个变量通过accessor.h的宏方法进行声明:__DECLARE_VAR(BOOL,SQUARE_WAVE),__DECLARE_EXTERNAL(INT,CURRENT_COUNT),__DECLARE_VAR(BOOL,CTU_RESET),- 给出两种宏的定义
#define __DECLARE_VAR(type, name) __IEC_##type##_t name;#define __DECLARE_EXTERNAL(type, name) __IEC_##type##_p name;- 疑问:
__IEC_##type##_t name和__IEC_##type##_p name语句中的类型是否也在代码仓库的某个地方为定义?
- 例如
- 方法
ARDUINOSEG_init__头声明。 - 方法
ARDUINOSEG_body__头声明。
POUS.c
实现了在 POUS.h 文件中声明的方法:
ARDUINOSEG_init__,方法名暗示职责为初始化变量。#define INSTANCE0 RES0__INSTANCE0ARDUINOSEG RES0__INSTANCE0;声明了一个全局变量,ARDUINOSEG类型的定义在POUS.h文件中。- 在
RES0_init__方法中,调用了ARDUINOSEG_init__方法,初始化了变量RES0__INSTANCE0,传入了ARDUINOSEG类型的实参INSTANCE0和 BOOL 类型的实参retain,初始值为 0。
ARDUINOSEG_body__,为初始化的变量赋值。- 传入
ARDUINOSEG类型的实参INSTANCE0。 - 该方法内部执行的大部分是
__SET_VAR宏,宏定义在accessor.h文件。 __SET_VAR(data__->TON0.,EN,,__BOOL_LITERAL(TRUE));TON_body__(&data__->TON0);TOF_body__(&data__->TOF0);R_TRIG_body__(&data__->R_TRIG1);CTU_body__(&data__->CTU0);__SET_EXTERNAL(data__->,CURRENT_COUNT,,__GET_VAR(data__->CTU0.CV,));- 这些方法定义在
iec_std_FB.h头文件中。 - 疑问:POUS.c 和 POUS.h 均没有
#include iec_std_FB.h文件,除非iec_std_lib.h中 有#include iec_std_FB.h语句。 确实有。
- 传入
依次解释 ARDUINOSEG_body__ 方法中各行代码
- 该方法负责执行 ARDUINOSEG 逻辑程序
- ``,启用 TON timer。
- ,设置 TON timer 输入。
- ,设置 TON timer 预制时间。
TON_body__(TON *data__),执行 TON timer 逻辑。- ,启用 TOF timer。
- ,设置 TOF timer 输入。
- ,设置 TOF timer 预制时间。
- ,执行 TOF timer 逻辑。
- ,更新
SQUARE_WAVE变量。 - ,设置上升边缘触发器的时钟输入。
- ,执行上升边缘触发器的逻辑。
- ,设置计数器的累计输入。
- ,设置计数器的重置输入。
- ,设置计数器初始值。
- ,执行计数器逻辑。
- ,更新
CTU_RESET变量。 - ,更新外部
CURRENT_COUNT变量。

根据图片可以发现,st 文件中的一行 TON0(EN := TRUE, IN := NOT(square_wave), PT := T#200ms);,被转换为了 3 行 C 语言代码。
__SET_VAR(data__->TON0.,EN,,__BOOL_LITERAL(TRUE));
__SET_VAR(data__->TON0.,IN,,!(__GET_VAR(data__->SQUARE_WAVE,)));
__SET_VAR(data__->TON0.,PT,,__time_to_timespec(1, 200, 0, 0, 0, 0));
TON_body__(TON *data__)逻辑过程
- 函数管理 TON 函数块的时间逻辑,更新 TON 状态和输出,基于输入的条件和经过的时间。函数实现了定时器在延时功能块上的行为。
- ``,函数定义,定义了静态函数
TON_body__,输入 TON 结构指针。 - ``,控制执行,检查 EN 输入是 FALSE,如果是,设置 ENO 为 FALSE 并且返回。如果不是,设置 ENO 为 TRUE。
- ``,初始化局部变量,设置
CURRENT_TIME变量到系统时间,使用getting 和 setting 宏。 - ``,状态机制,
- 如果 STATE 是 0, PREV_IN 是 FALSE, IN 是 TRUE,它设置 STATE 为 1,Q 为 FALSE 和 START_TIME 为 CURRENT_TIME。
- 如果 IN 是 FALSE,它重置 ET,Q 和 STATE。
- 如果 STATE 是 1,经过时间大于等于 PT,它设置 STATE 为 2, Q 为 TREU ,ET 为 PT。否则,它设置 ET 为经过时间。
- ``,更新上一步输入,用当前值 IN 更新 PREV_IN。
Res0.c
Res0依赖
#include "iec_std_lib.h"标准的 iec 依赖库。extern unsigned long long common_ticktime__;引入Config0.c文件定义的unsigned long long类型的common_ticktime__。#include "POUS.h"定义了程序的结构体,声明了初始化和运行方法。
Res0实现
- 全局变量定义
BOOL TASK0;ARDUINOSEG RES0__INSTANCE0;
- 宏
#define INSTANCE0 RES0__INSTANCE0
- 方法
void RES0_init__(void),TASK0 = __BOOL_LITERAL(FALSE);,为全局变量TASK0赋值。#define __BOOL_LITERAL(value) __literal(BOOL,value)#define __literal(type,value,...) __lit(type,value,__VA_ARGS__)#define __lit(type,value,...) (type)value##__VA_ARGS__
ARDUINOSEG_init__(&INSTANCE0,retain);,调用POUS.h头文件声明的ARDUINOSEG_init__方法。实参INSTANCE0为ARDUINOSEG类型的全局变量; 实参retain为BOOL类型的局部变量,初始值为 0。
void RES0_run__(unsigned long tick)ARDUINOSEG_body__(&INSTANCE0);,调用POUS.h头文件声明的ARDUINOSEG_body__方法,实参INSTANCE0为ARDUINOSEG类型的全局变量。
Config0.h
- 声明了全局函数原型
__DECLARE_GLOBAL_PROTOTYPE(INT,CURRENT_COUNT) - 给出宏定义
#define __DECLARE_GLOBAL_PROTOTYPE(type, name) extern type* __GET_GLOBAL_##name(void);- 给出实际函数原型
extern INT* __GET_GLOBAL_CURRENT_COUNT(void)
Config0.c
Config0依赖
iec_std_lib.h,IEC 61131-3 standard function library。accessor.h定义了宏,负责变量声明、初始化、设置和获取(getter、setter)。
Config0实现
- 全局变量
__DECLARE_GLOBAL(INT,CONFIG0,CURRENT_COUNT),声明类型为INT,域为CONFIG0的全局变量。accessor.h中的宏__DECLARE_GLOBAL定义了全局变量、静态变量和三个相关的函数。函数给出了具体的实现。#define __DECLARE_GLOBAL(type, domain, name)
unsigned long long common_ticktime__ = 20000000ULL; /*ns*/unsigned long greatest_tick_count__ = 0UL; /*tick*/- 用于其他文件的
extern语句。
- 函数
- 声明了
void RES0_init__(void)。 void config_init__(void)函数实现。-
__INIT_GLOBAL(INT,CURRENT_COUNT,__INITIAL_VALUE(0),retain),对INT类型CONFIG0域的全局变量CURRENT_COUNT进行初始化。- retain 为
BOOL类型局部变量,初始值为 0。
#define __INIT_GLOBAL(type, name, initial, retained)\ {\ static const type temp = initial;\ __INIT_GLOBAL_##name(temp);\ __INIT_RETAIN((*GLOBAL__##name), retained)\ } #define __INIT_RETAIN(name, retained)\ name.flags |= retained?__IEC_RETAIN_FLAG:0; - retain 为
-
RES0_init__();
-
- 声明了
void RES0_run__(unsigned long tick)。 void config_run__(unsinged long tick)函数实现RES0_run__(tick);,调用Res0.c的方法。
- 声明了
main.cpp 函数执行 plc 逻辑程序
config_init__();while (run_openplc),进入主循环config_run__(__tick++); // execute plc program logic,执行 plc 逻辑程序。- 此处注释表明该命令负责执行 plc 逻辑程序。但是依次点进去看每一个具体实现的函数,直到最底层
ARDUINOSEG_body__函数,只是一些 set 方法和 body 方法。
- 此处注释表明该命令负责执行 plc 逻辑程序。但是依次点进去看每一个具体实现的函数,直到最底层
- 计算一次执行的时间等统计量。
debug python 项目webserver
逐步查看执行流程:
上传 st 文件程序
- debug 模式启动 flask 服务,浏览器访问 program 页面,上传 st 文件。debug 进入方法
openplc.py/runtime.compile_program。 - 该方法会读取 st 文件内容,将内容分离为2种命令行的列表。
- C 语言代码列表:
![DegbuCalls]()
- 保存到
core/debug.cpp和st_files/xxx.st.dbg。 - ST 程序代码列表:
![CompileProgramsProgramCalls]()
保存到st_files/xxx.st。
- C 语言代码列表:
- 启动新进程去执行
scriptes/compile_program.sh脚本,参数为xxx.st。并且打印执行日志到输出流。a = subprocess.Popen(['./scripts/compile_program.sh', str(st_file)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) compilation_object = NonBlockingStreamReader(a.stdout)
compile_program.sh
./iec2c -f -l -p -r -R -a ./st_files/"$1",编译结构化文本文件到 C 代码。- iec2c 工具,用于转换 ST 文件到 C 代码。
- 参数用于控制行为,
- ./st_files/"$1",为输入的 ST 文件名。
mv -f POUS.c POUS.h LOCATED_VARIABLES.h VARIABLES.csv Config0.c Config0.h Res0.c ./core/,移动 .c 文件到core目录下。- 基于
OPENPLC_DRIVER配置项编译不同版本的Config0.c。该变量决定了指定用于 OPENPLC 项目的驱动。sl_rp4:g++ -std=gnu++11 -I ./lib -c Config0.c -lasiodnp3 -lasiopal -lopendnp3 -lopenpal -w -DSL_RP4-std=gnu++11制定了使用的C++标准。-I ./lib增加./lib目录到头文件检索的目录列表。-c Config0.c无链接只编译Config0.c文件。-lasiodnp3 -lasiopal -lopendnp3 -lopenpal链接到指定的依赖库。-w隐藏警告。-DSL_RP4指定了SL_RP4宏,用于代码中条件化编译特定的sl_rp4驱动。
g++ -std=gnu++11 -I ./lib -c Res0.c -lasiodnp3 -lasiopal -lopendnp3 -lopenpal -w $ETHERCAT_INC -DSL_RP4-c Res0.c无链接只编译Res0.c文件。
- else:
g++ -std=gnu++11 -I ./lib -c Config0.c -lasiodnp3 -lasiopal -lopendnp3 -lopenpal -wg++ -std=gnu++11 -I ./lib -c Res0.c -lasiodnp3 -lasiopal -lopendnp3 -lopenpal -w $ETHERCAT_INC
./glue_nenerator执行?文件负责功能是什么?g++ -std=gnu++11 *.cpp *.o -o openplc -I ./lib -pthread -fpermissive \pkg-config --cflags --libs libmodbus` -lasiodnp3 -lasiopal -lopendnp3 -lopenpal -w $ETHERCAT_INC -DSL_RP4`- 执行 g++ 编译器去生成最终的的可执行文件,
openplc。 *.cpp *.o包含当前目录下所有的 C++ 源文件和目标文件。-pthread启动多线程执行,使用 POSIX 线程依赖库。-fpermissive允许不符合规范的代码进行编译。pkg-config --cflags --libs libmodbus包含用于libmodbus的必要标志和依赖库。
- 执行 g++ 编译器去生成最终的的可执行文件,
启动 PLC 程序
- debug 模式启动 flask 服务,浏览器点击
start_plc按钮,debug 进入方法webserver.py/start_plc。 monitor.stop_monitor()关闭,原本没开。openplc_runtime.start_runtime(),启动 PLC 。self.theprocess = subprocess.Popen(['./core/openplc'])开启子进程执行 openplc 可执行文件。self.runtime_status = "Running"设置状态为运行中。- 阻塞一秒,等可执行PLC程序启动。
configure_runtime(),向 PLC 程序发送 rpc 请求,逐步启动配置项。cur.execute("SELECT * FROM Settings")查询配置信息。- 启动 modbus:
查询到信息 'Modbus_port', '502',调用openplc.py的start_modbus方法。
![]()
openplc.py的start_modbus方法调用自身的_rpc远程调用方法。参数为start_modbus(502)。
![]()
_rpc方法创建 socket 连接,发送信息后,解码响应。
![]()
响应数据为OK\n。
![]()
- 启动 dnp3 过程与 modbus 类似,只是参数为
start_dnp3(20000) - 启动 enip 过程与 modbus 类似,参数为
start_enip(44818) - 停止 potorage,参数为
stop[_pstorage(),删除持久化文件delete_persistent_file()。
monitor.cleanup():del debug_vars[:]。monitor.parse_st(openplc_runtime.project_file):检索 st 文件中代码块,找到具有 AT 的代码行。但是没找到。- 重定向到仪表盘。
问题
- Ques:
interactive_server.cpp文件中waitForClient_interactive方法是非阻塞调用?在上一个方法createSocket_interactive中,设置非阻塞?SetSocketBlockingEnabled(socket_fd, false);
设置非阻塞调用意味着在第一次连接建立之前,会在while循环保持CPU忙等待。 - Ques:
interactive_server.cpp文件中waitForClient_interactive方法如何跳出while循环? - 由问题2延伸,
run_openplc标志信号量在哪修改?stop_plc 函数中修改,在 interactive 模块。







浙公网安备 33010602011771号