OpenPLC运行时web端与cpp程序端之间的信息交互详解
WEB端
web用户界面是基于flask通过以 webserver.py 为主等一系列py文件(核心py文件有 openplc.py)辅助实现的。
-
WEB端核心文件
- webserver.py
- openplc.py
-
WEB用户界面端的启动
webserver.py 文件中# 初始化Flash应用 app = flask.Flask(__name__) # 设置应用密钥,生成16字节的随机数; app.secret_key = str(os.urandom(16)) login_manager = flask_login.LoginManager() login_manager.init_app(app) # 创建一个 Flask-Login 的 LoginManager 实例,用于管理用户登录状态。 # 使用 init_app(app) 方法将登录管理器与 Flask 应用实例关联起来。 # openplc的运行时实例(信息发送者) openplc_runtime = openplc.runtime() # flask 启动,注册ip和端口号 app.run(debug=False, host='0.0.0.0', threaded=True, port=8080)并且,webserver.py 文件中有几个响应函数绑定到了WEB用户界面的按钮上,例如启动plc:
@app.route('/start_plc') def start_plc(): global openplc_runtime if (flask_login.current_user.is_authenticated == False): return flask.redirect(flask.url_for('login')) else: monitor.stop_monitor() openplc_runtime.start_runtime() time.sleep(1) configure_runtime() monitor.cleanup() monitor.parse_st(openplc_runtime.project_file) return flask.redirect(flask.url_for('dashboard'))-
对于启动 plc 的功能,用户在界面中点击start plc 按钮,通过 flask 的支持(
@app.route('/start_plc')),函数 start_plc 执行。在函数中,
flask_login.current_user.is_authenticated == False 检查用户是否登录;如果登录,关闭监视器
stop_monitor,启动运行时start_runtime。阻塞1秒钟后,配置运行时
configure_runtime()。 -
在
openplc.start_runtime() 函数中,def start_runtime(self): if (self.status() == "Stopped"): # 实例化一个子进程,运行openplc core程序。./core/openplc 是一个可执行文件。 # openplc 是由 complie_program.sh 编译生成的,调用 compile_program 函数。 # complie_program.sh 脚本使用了 iet2c 和 g++ 编译器。 self.theprocess = subprocess.Popen(['./core/openplc']) # XXX: iPAS self.runtime_status = "Running"
self.theprocess = subprocess.Popen(['./core/openplc']) 执行 CPP 端程序。 -
在
configure_runtime 函数中,def configure_runtime(): global openplc_runtime openplc_runtime.init_socket() # 连接名字为 openplc.db 的Sqlite数据库 database = "openplc.db" conn = create_connection(database) if (conn != None): try: print("Openning database") # 查询 Settings 表中所有数据行 cur = conn.cursor() cur.execute("SELECT * FROM Settings") rows = cur.fetchall() cur.close() conn.close() for row in rows: if (row[0] == "Modbus_port"): if (row[1] != "disabled"): # 数据行的第一列和第二列匹配成功,执行 openplc_runtime.start_modbus 方法 print("Enabling Modbus on port " + str(int(row[1]))) openplc_runtime.start_modbus(int(row[1])) else: print("Disabling Modbus") openplc_runtime.stop_modbus() elif (row[0] == "Dnp3_port"): if (row[1] != "disabled"): # 执行 start_dnp3 print("Enabling DNP3 on port " + str(int(row[1]))) openplc_runtime.start_dnp3(int(row[1])) else: print("Disabling DNP3") openplc_runtime.stop_dnp3() elif (row[0] == "Enip_port"): if (row[1] != "disabled"): print("Enabling EtherNet/IP on port " + str(int(row[1]))) openplc_runtime.start_enip(int(row[1])) else: print("Disabling EtherNet/IP") openplc_runtime.stop_enip() elif (row[0] == "Pstorage_polling"): if (row[1] != "disabled"): print("Enabling Persistent Storage with polling rate of " + str(int(row[1])) + " seconds") openplc_runtime.start_pstorage(int(row[1])) else: print("Disabling Persistent Storage") openplc_runtime.stop_pstorage() delete_persistent_file() except Error as e: print("error connecting to the database" + str(e)) else: print("Error opening DB")基于 openplc.db 数据库文件中的 Settings 表的数据行,(实质上 Settings 作为配置表),选择性的开启 modbus,dnp3,enip,persistent 等模块。
-
在
openplc_runtime.start_modbus(int(row[1])) 函数中
取掉了connect 和close 的_rpc 函数def start_modbus(self, port_num): return self._rpc(f'start_modbus({port_num})') def _rpc(self, msg, timeout=1000): # 该函数被我这改进了,所以有点区别 data = "" if not self.runtime_status == "Running": return data if not self.socket: print("Socket not initialized") return data try: self.socket.send(f'{msg}\n'.encode('utf-8')) data = self.socket.recv(timeout).decode('utf-8') self.runtime_status = "Running" except socket.error as serr: print(f'Socket error during {msg}, is the runtime active?') self.runtime_status = "Stopped" return data调用了
_rpc 方法,该方法调用了OS系统的 TCP 协议栈发送信息,目的地是 ip-port_num,发送的消息内容为 'start_modbus502'。
-
-
WEB端的发送信息
# 在webserver.py 中进行了openplc的运行时类的实例化(信息发送者) openplc_runtime = openplc.runtime()主要通过调用 openplc_runtime 实例中的
_rpc 函数进行发送消息。原版的
_rpc 函数,如下:def _rpc(self, msg, timeout=1000): data = "" if not self.runtime_status == "Running": return data try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('localhost', 43628)) s.send(f'{msg}\n'.encode('utf-8')) data = s.recv(timeout).decode('utf-8') s.close() self.runtime_status = "Running" except socket.error as serr: print(f'Socket error during {msg}, is the runtime active?') self.runtime_status = "Stopped" return data在 try 语句中,发送者每次发送信息时,会依次执行
connectsendrecvclose,四个函数。完整的三次握手和四次挥手都进行了。在取掉了
connect 和close 的_rpc 函数中,将connect 操作放在了启动 plc CPP端时,将close 操作放到了关闭 plc CPP端时。
因此,每次_rpc 函数被调用,进行发送消息请求时,减小西三次握手和四次挥手的时间。 -
WEB作为TCP的发送端,发送的地址为 localhost:43628
CPP程序端
-
CPP端的核心文件
- main.cpp
- interactivate_server.cpp
-
CPP端的主程序启动
main函数中,执行
pthread_create(&interactive_thread, NULL, interactiveServerThread, NULL);
启动子线程执行 interactive_server 的interactiveServerThread 函数。 -
CPP端的
interactiveServerThread 启动,作为TCP接收者。-
interactiveServerThread 函数void *interactiveServerThread(void *arg) { pthread_setname_np(pthread_self(), "Interactive Server Thread"); startInteractiveServer(43628); }该函数中,执行 startInteractiveServer 函数,传入端口号 43628。
-
startInteractiveServer 函数void startInteractiveServer(int port) { ... socket_fd = createSocket_interactive(port); // 当run_openplc为真时,循环执行 while(run_openplc) { // 等待客户端连接 client_fd = waitForClient_interactive(socket_fd); //block until a client connects ... // 如果连接成功 // 创建线程,调用handleConnections_interactive函数,读取 client_fd 的输入 // client_fd 是 accept 返回的文件描述符。 ret = pthread_create(&thread, NULL, handleConnections_interactive, arguments); ... printf("Terminating interactive server thread\n"); }为了方便阅读,此处只保留了核心的函数调用,用添加了注释帮助理解。
首先,通过socket_fd = createSocket_interactive(port);执行,通过系统的TCP协议栈,子线程 interactivate_server 会获取端口号为port的TCP的套接字。
此时子线程作为 TCP 的服务器。client_fd = waitForClient_interactive(socket_fd),等待客户端连接,如果连接成功,返回客户端的套接字。ret = pthread_create(&thread, NULL, handleConnections_interactive, arguments);启动新的子线程handleConnections_interactive 去处理客户端的信息。
参数 arguments 内封装了 client_fd,handleConnections_interactive 是处理信息的方法。
-
-
interactive_server.cpp 子线程,接收消息并处理的核心方法
handleConnections_interactive为了方便阅读,下面只保留了核心的函数调用,用添加了注释帮助理解。
void *handleConnections_interactive(void *arguments) { ... // 当run_openplc为真时,循环执行 while(run_openplc) { // 监听客户端消息 messageSize = listenToClient_interactive(client_fd, buffer); ... // 如果 messageSize 信息有效,则处理消息 processMessage_interactive(buffer, messageSize, client_fd); } // 关闭客户端套接字 closeSocket(client_fd); // 打印终止信息 printf("Terminating interactive server connections\r\n"); // 退出线程 pthread_exit(NULL); }只要运行时在执行,run_openplc 为 true。
interactivate_server 线程作为 TCP 服务器收到了客户端的信息,messageSize = listenToClient_interactive(client_fd, buffer);
且检验消息有效后,处理消息processMessage_interactive(buffer, messageSize, client_fd); -
processMessage_interactive 函数如下void processMessage_interactive(unsigned char *buffer, int bufferSize, int client_fd) { for (int i = 0; i < bufferSize; i++) { if (buffer[i] == '\r' || buffer[i] == '\n' || command_index >= 1024) { processCommand(server_command, client_fd); command_index = 0; break; } server_command[command_index] = buffer[i]; command_index++; server_command[command_index] = '\0'; } }该函数用于字符处理,server_command 是收到的消息内容。
下面来看
processCommand 函数,参数为消息内容和客户端套接字。 -
processCommand 函数,该函数实现了信息处理的核心业务。
只保留核心内容和解释的注释。通过字符匹配,实现功能的执行。
void processCommand(unsigned char *buffer, int client_fd) { ... // 如果命令是quit(),则停止所有服务器并退出 if (strncmp(buffer, "quit()", 6) == 0) { processing_command = true; ... // run_openplc 标志量置为0,表示退出。主循环会检测到这个标志量,然后退出。 run_openplc = 0; processing_command = false; } // 如果命令是start_ethercat(),则启动ethercat服务器 else if (strncmp(buffer, "start_ethercat(", 15) == 0) { processing_command = true; ... //Configure ethercat ethercat_configured = configureEthercat(); processing_command = false; } // 如果命令是start_modbus(),则启动modbus服务器 else if (strncmp(buffer, "start_modbus(", 13) == 0) { processing_command = true; ... //Start Modbus server run_modbus = 1; pthread_create(&modbus_thread, NULL, modbusThread, NULL); processing_command = false; } // 如果命令是stop_modbus(),则停止modbus服务器 else if (strncmp(buffer, "stop_modbus()", 13) == 0) { processing_command = true; ... run_modbus = 0; pthread_join(modbus_thread, NULL); ... processing_command = false; } // 如果命令是start_dnp3(),则启动dnp3服务器 else if (strncmp(buffer, "start_dnp3(", 11) == 0) { processing_command = true; ... //Start DNP3 server run_dnp3 = 1; pthread_create(&dnp3_thread, NULL, dnp3Thread, NULL); processing_command = false; } // 如果命令是stop_dnp3(),则停止dnp3服务器 else if (strncmp(buffer, "stop_dnp3()", 11) == 0) { processing_command = true; ... run_dnp3 = 0; pthread_join(dnp3_thread, NULL); ... processing_command = false; } // 如果命令是start_enip(),则启动enip服务器 else if (strncmp(buffer, "start_enip(", 11) == 0) { processing_command = true; ... //Start Enip server run_enip = 1; pthread_create(&enip_thread, NULL, enipThread, NULL); processing_command = false; } // 如果命令是stop_enip(),则停止enip服务器 else if (strncmp(buffer, "stop_enip()", 11) == 0) { processing_command = true; ... run_enip = 0; pthread_join(enip_thread, NULL); ... processing_command = false; } // 如果命令是start_pstorage(),则启动pstorage服务器 else if (strncmp(buffer, "start_pstorage(", 15) == 0) { processing_command = true; ... //Start Enip server run_pstorage = 1; pthread_create(&pstorage_thread, NULL, pstorageThread, NULL); processing_command = false; } // 如果命令是stop_pstorage(),则停止pstorage服务器 else if (strncmp(buffer, "stop_pstorage()", 15) == 0) { processing_command = true; ... run_pstorage = 0; ... processing_command = false; } // 如果命令是runtime_logs(),则返回运行时日志 else if (strncmp(buffer, "runtime_logs()", 14) == 0) { processing_command = true; printf("Issued runtime_logs() command\n"); write(client_fd, log_buffer, log_index); processing_command = false; return; } // 如果命令是exec_time(),则返回执行时间 else if (strncmp(buffer, "exec_time()", 11) == 0) { processing_command = true; time(&end_time); count_char = sprintf(buffer, "%llu\n", (unsigned long long)difftime(end_time, start_time)); write(client_fd, buffer, count_char); processing_command = false; return; } // 如果命令不识别,则返回错误信息 else { processing_command = true; count_char = sprintf(buffer, "Error: unrecognized command\n"); write(client_fd, buffer, count_char); processing_command = false; return; } // 返回OK count_char = sprintf(buffer, "OK\n"); write(client_fd, buffer, count_char); } -
CPP端作为TCP的接收端:接受地址为 localhost:43628。
浙公网安备 33010602011771号