OpenPLC运行时web端与cpp程序端之间的信息交互详解

WEB端

web用户界面是基于flask通过以 webserver.py 为主等一系列py文件(核心py文件有 openplc.py)辅助实现的。

  1. WEB端核心文件

    1. webserver.py
    2. openplc.py
  2. 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'))
    
    1. 对于启动 plc 的功能,用户在界面中点击start plc 按钮,通过 flask 的支持(@app.route('/start_plc')),函数 start_plc 执行

      在函数中,flask_login.current_user.is_authenticated == False​ 检查用户是否登录;

      如果登录,关闭监视器 stop_monitor​,启动运行时start_runtime​​。

      阻塞1秒钟后,配置运行时 configure_runtime()​。

    2. 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 端程序

    3. 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 等模块。

    4. openplc_runtime.start_modbus(int(row[1]))​ 函数中
      取掉了 connectclose_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'。

  3. 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​ 函数被调用,进行发送消息请求时,减小西三次握手和四次挥手的时间。

  4. WEB作为TCP的发送端,发送的地址为 localhost:43628

CPP程序端

  1. CPP端的核心文件

    1. main.cpp
    2. interactivate_server.cpp
  2. CPP端的主程序启动

    main函数中,执行 pthread_create(&interactive_thread, NULL, interactiveServerThread, NULL);
    启动子线程执行 interactive_server 的 interactiveServerThread​ 函数。

  3. CPP端的 interactiveServerThread​ 启动,作为TCP接收者。

    1. interactiveServerThread​ 函数

      void *interactiveServerThread(void *arg) {
          pthread_setname_np(pthread_self(), "Interactive Server Thread");
          startInteractiveServer(43628);
      }
      

      该函数中,执行 startInteractiveServer 函数,传入端口号 43628。

    2. 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 是处理信息的方法。

  4. 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);​​

  5. 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​ 函数,参数为消息内容和客户端套接字。

  6. 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);
    }
    
  7. CPP端作为TCP的接收端:接受地址为 localhost:43628。

posted @ 2024-12-10 09:53  生产队的扛把子  阅读(495)  评论(0)    收藏  举报