Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 Capítulo 10 Programación de un servidor OPC Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 201 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 10.1 Introducción al estándar de comunicación OPC En el capítulo anterior conseguimos comunicar la mbed con un ordenador personal. Sin embargo, tuvimos que escribir una aplicación específica para suministrare set-points set desde el PC. El objetivo de este capítulo es crear un servidor OPC en el ordenador que reciba los datos de la mbed a través de Ethernet y que nos sirva de puente para conectar el microcontrolador a cualquier equipo que incluya el estándar estándar OPC sin necesidad de realizar ninguna adaptación adicional. OPC (OLE OLE for Process Control) Control es uno de los estándares industriales de comunicación más populares que existen. Está basado en la tecnología DCOM (Distributed ( Component Objetc Model) Model de Microsoft®, una tecnología desarrollada para la comunicación entre distintas aplicaciones presentes en un mismo computador o en una red de ellos. Esta tecnología es el resultado de la evolución de OLE (Object (Object Linking and Embedding)) en su versión 1.0, que permitía el enlace e incrustación de documentos y otros objetos entre distintas aplicaciones de Windows. Windows Se utiliza OPC para resolver uno de los problemas más habituales e incómodos de la industria de la automatización: lograr la comunicación entre múltiples dispositivos, controladores, hardware y aplicaciones software sin sufrir problemas de conectividad derivados dos de los drivers propietarios de cada elemento. El secreto del éxito de OPC, OPC, desarrollado por la organización sin ánimo de lucro OPC Foundation™, es crear un estándar de comunicaciones independiente de los fabricantes que abstrae la implementación en el origen y el destino de los datos de forma que puedan intercambiar información sin que ninguno de los lados conozca nada sobre el protocolo de comunicación ya la organización interna de datos del otro lado. Así pues, el uso de OPC permite eliminar la necesidad necesidad de emplear drivers propietarios para interconectar cada elemento de una jerarquía de automatización. En su lugar cada elemento de la red incluirá un servidor o cliente OPC, de modo que se simplifica Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 202 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 notablemente la arquitectura de la comunicación. Véase Véase la siguiente figura para comprender el gran avance que supone la interconexión de dispositivos vía OPC: Drivers propietarios Solución OPC La solución que no incluye OPC requiere que cada dispositivo implemente un driver específico para conectarse con cada uno de los otros elementos de la red. Sin embargo, la solución OPC requiere un único “driver” OPC estandarizado por dispositivo que le permite la interconexión con cada uno de los otros elementos. e Además de simplificar la arquitectura, arqui tanto clientes como fabricantes obtienen ventajas muy notables con esta solución: - El cliente no tiene que modificar sus drivers cada vez que realice alguna modificación o agregue algún nuevo equipo a su instalación. - El fabricante no tiene que desarrollar un driver específico para distintos equipos para hacer a su producto accesible en el mercado. Le basta con desarrollar la tecnología OPC para ofertar una conectividad sencilla y fiable con todos los productos del mercado que también incluyen OPC, que en la actualidad son la mayoría de dispositivos y software del mundo de la automatización. Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 203 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 Una vez expuesta la motivación de usar OPC vamos a introducir los conceptos fundamentales de este estándar. En primer lugar, la tecnología OPC funciona utilizando la conectividad servidor/cliente.. Un servidor puede recibir la conexión de múltiples clientes, así como un cliente puede conectarse a distintos servidores a la vez. • El servidor OPC se asocia a las fuentes es de datos, es decir, es un driver estandarizado que interactúa con la información de los dispositivos (PLC, RTU, páginas web, bases de datos, etc) y la pone a disposición de otros elementos de la red. En definitiva, un servidor OPC es un conector entre el el mundo real y el mundo OPC. Por ello programaremos un servidor OPC y no un cliente para conectar a la mbed con cualquier otro elemento. • El cliente OPC es un software programado para conectarse a los servidores OPC. Conceptualmente representan un sumidero de datos, aunque la conexión servidor/cliente es bidireccional. Técnicamente son aplicaciones incrustadas en otras como HMI’s (Human-Machine (Human Machine Interface) o históricos de datos. Otro punto a tener en cuenta es la existencia de múltiples especificaciones OPC, OPC que se escogerán en función del objetivo de la aplicación o sistema. Es habitual que los servidores puedan soportar varias especificaciones mientras que los clientes solo sean programados para una de ellas. Las especificaciones existentes de OPC son las siguientes: − OPC Data Access (OPC DA): el servidor proporciona acceso a datos en tiempo real. El cliente puede demandar el valor más reciente de cualquier variable del proceso. − OPC Historical Data Access (OPC HDA): se emplea para recuperar y analizar datos históricos del proceso, que habitualmente estarán almacenados en una base de datos o una RTU. − OPC Alarm and Events (OPC A&E): proporciona una interfaz donde los clientes son informados de alarmas y eventos del proceso. Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 204 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 − OPC Security:: especificación que que concierne el uso de seguridad en la programación de servidores OPC. Está basado en el modelo Windows NT Security. − OPC Batch:: especificación muy similar a OPC Data Access (OPC DA), pero especialmente diseñada y optimizada para procesos de fabricación por po lotes. − OPC Data eXchange (OPC DX): esta especificación ón define un conjunto de interfaces que hace posible la conexión e intercambio de datos entre servidores. OPC DX permite a servidores OPC DA intercambiar datos sin necesidad de clientes intermedios. − OPC eXtensible Markup Language (OPC XML): encapsula los datos del proceso de manera que los hace disponibles desde cualquier sistema operativo. Ofrece además una interfaz SOAP (Simple Object Application Protocol) que permiten escribir los clientes en otros leguajes de programación distintos a C++ como Java, Perl o Python. Se utiliza HTTP como protocolo de transferencia de datos para el tráfico con base en Internet, lo cual es más adecuado que DCOM. − OPC Unified Architecture (OPC UA): es la siguiente generaci ción de estándar OPC, en el cual se sustituye la tecnología DCOM por .NET, también de Microsoft®. Esto permite unificar en esta especificación todas las anteriores: OPC DA, OPC HDA, OPC DX, OPC XML, etc. Se emplea también SOAP/HTTP, lo cual permite un sencillo sencillo desarrollo de clientes y servidores en entornos que no sean de Microsoft®. Por último .NET proporciona una base más segura que DCOM. Es de esperar que sea la especificación que se acabe imponiendo. Para este proyecto nos vamos a quedar con la primera especificación, especificaci OPC Data Access,, pues nuestra necesidad es la comunicación en tiempo real con la mbed. Pasamos ahora, pues, a comentar la arquitectura básica de OPC DA. Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 205 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 Arquitectura OPC Data Access En primer lugar definimos la jerarquía del servidor OPC. Los datos y propiedades del proceso representan el nivel de jerarquía más bajo y se asocian a objetos denominados ítems o tags. A su vez, estos ítems se agrupan en los denominados grupos, que representan el siguiente nivel de jerarquía. jerarquí . La agrupación se realiza por motivos lógicos (mismo tipo de variables, datos de una parte específica del proceso, etc.) etc o por motivos dinámicos (variables que cambian a una determinada frecuencia común en el proceso). proceso) El nivel superior de jerarquía es el propio servidor,, que es el encargado de obtener y traducir la información real del proceso y almacenarla en los distintos ítems. Esta jerarquía puede observarse de forma gráfica en la siguiente figura: Por su parte, el cliente puede crear varios objetos en el servidor para definir su visión del proceso, es decir, puede formar sus propios grupos compuestos por los ítems disponibles en el servidor. Al crear un grupo y asociar a él algún ítem, el cliente puede establecer los siguientes parámetros: - Frecuencia de actualización (Update Rate): fija el intervalo en el que el cliente toma datos del servidor. - Zona muerta (Dead Band): fija la variación mínima que tiene que tener el ítem para recibir un nuevo valor val en el cliente. Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 206 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 Cada vez que se actualiza un ítem seleccionado por el cliente, éste recibe del servidor tres datos: - El valor del ítem.. Puede ser del tipo char, short, long, boolean, float, double, etc. - La hora de actualización del ítem (Time Stamp). Formato fecha y hora. - La calidad del dato.. Bueno (Good) , malo (Bad) o incierto (Uncertain). (Uncertain) De este modo queda definida la arquitectura básica de la especificación OPC Data Access. En el siguiente apartado procederemos a la programación del servidor. Objetivo Para este proyecto vamos a crear un servidor OPC DA orientado al control de un depósito de agua perteneciente a la planta de los cuatro tanques ubicado en el laboratorio de control de la Escuela Superior de Ingenieros de la Universidad de Sevilla. Necesitaremos monitorizar la altura del agua en el tanque, así sí como la referencia marcada por el usuario y el voltaje aplicado a la bomba por el microcontrolador. Crearemos por tanto cuatro tags o ítems,, uno de entrada (referencia) y tres de salida (altura, voltaje y referencia actual). Agruparemos los tags de entrada rada y salida en dos grupos homónimos. Diferenciamos “referencia” y “referencia referencia actual” actual porque al introducir un nuevo set-point point a través del cliente OPC, la mbed está programada para realizar dicho cambio de referencia transcurridos N tiempos de muestreo. De este modo aprovechamos la capacidad de anticipación al cambio de set-point set point del control predictivo. El motivo por el que creamos el tag referencia actual es para poder saber en tiempo real cuándo ha cambiado realmente el set-point. set point. Esto nos será fundamental fundamen también para la representación de la gráfica de y(t), donde siempre incluimos la referencia en función del tiempo. Pasemos ahora a la descripción del servidor OPC programado. Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 207 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 10.2 Programación de un servidor OPC Para programar el servidor OPC DA vamos a utilizar el toolkit gratuito de Graybox Software (se adjunta en el CD, ficheros del capítulo 10), 10), que nos aporta las herramientas necesarias para la construcción del servidor. Este toolkit nos ofrece una serie de clases para la implementación OPC, así como una serie de ejemplos que nos sirven de punto de partida. Tomaremos el ejemplo GraySim, GraySim que trabaja además con clases ATL (Active Template Library). ATL es un conjunto de conjunto de clases basadas en plantillas C++ desarrolladas por Microsoft® para simplificar la programación de objetos COM. Este ejemplo crea una clase derivada por herencia múltiple de una clase padre ATL y otra contenida en la librería del toolkit. A partir de ahí vamos a eliminar todos los tags y su tratamiento por defecto para para diseñar el servidor a nuestro antojo, utilizando las funciones elementales de la librería para tal fin. Para programar este servidor necesitamos usar Microsoft® Visual C++ 2005 o superior, una versión que además contenga ATL7. Al igual que en el capítulo lo anterior se utilizará Microsoft® Visual Studio 2010. Para tener acceso a la librería del toolkit debemos incluir el fichero gbda3.lib .lib al proyecto (Proyecto Proyecto > Propiedades del proyecto > Propiedades de configuración > Vinculador > Entrada > Dependencias adicionales). Además habrá que tener el fichero gbda3.dll en el mismo directorio que el ejecutable del programa. Del mismo modo incluiremos los ficheros ws2_32.lib y ws2_32.dll, necesarios para la implantación del socket TCP/IP que nos servirá para comunicar comunic la mbed con el servidor OPC. Hay que tener en cuenta que, para que los cambios realizados en el servidor tengan efecto, no basta con generar un nuevo ejecutable (graysim.exe), ( ), sino que hemos de volver a registrarlo. Para ello utilizaremos las funciones –unregserver y –service en la consola del Símbolo del sistema: Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 208 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 Una vez aclarado este punto pasamos a la explicación del código del servidor OPC. Para tener acceso a todos los ficheros del proyecto de Visual Studio se recomienda recurrir al CD, donde se adjunta en su totalidad. Veamos ahora la estructura del programa y sus funciones más importantes.. El fichero principal del programa (graysim.cpp) ( ) contiene la definición de la clase CGraySimulatorModule, la cual incluye las funciones que implementan el servidor OPC, así como el punto de entrada al programa. Los métodos o funciones de dicha clase se explicarán a continuación, pero primero vamos a observar cómo empieza a funcionar el programa. El código de este fichero, omitiendo omitiendo la definición de la clase, es el siguiente: #include "stdafx.h" #include "resource.h" #include "tagdescr.h" #include "graysim.h" #include <GB_OPCDA.h> #include <stdio.h> #define _USE_MATH_DEFINES #include <cmath> #include <winsock2.h> #define SCANRATE 50 #define TAGDESCRCOUNT (sizeof sizeof(TagDescr)/sizeof(TagDescr[0])) Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 209 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 //Definimos la clase con herencia múltiple class CGraySimulatorModule : public CAtlServiceModuleT< CGraySimulatorModule, IDS_SERVICENAME >, public GBDataAccess { /* Definición de los métodos no heredados de la clase CGraySimulatorModule */ }; CGraySimulatorModule _AtlModule; //Declaramos un objeto de la clase CGraySimulatorModule //Punto de entrada del program extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, /*hPrevInstance*/ LPTSTR /*lpCmdLine*/, int nShowCmd) { return _AtlModule.WinMain(nShowCmd); } El punto de entrada del programa se encuentra en función WinMain del objeto _AtlModule _tWinMain, de la clase que a su vez llama a la CGraySimulatorModule que hemos declarado previamente. Esta sta función está definida en el fichero atlbase.h, atlbase.h y a su vez llama a la función Start. Esta función de ATL realiza una serie de inicializaciones relacionadas con Win32 (Windows) para acabar llamando a la función Run, también en atlbase.h. El código de esta función es el siguiente: HRESULT Run(_In_ int nShowCmd = SW_HIDE) throw() { HRESULT hr = S_OK; T* pT = static_cast<T*>(this); static_cast hr = pT->PreMessageLoop PreMessageLoop(nShowCmd); // LLama a RunMessageLoop solo si PreMessageLoop devuelve S_OK if (hr == S_OK) { pT->RunMessageLoop RunMessageLoop(); } // Llama a PostMessageLoop si PreMessageLoop sale con éxito if (SUCCEEDED(hr)) { hr = pT->PostMessageLoop(); pT } ATLASSERT(SUCCEEDED(hr)); return hr; } Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 210 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 Esta función es la que gestiona el funcionamiento del servidor: en primer lugar llama a PreMessageLoop (inicialización), luego invoca a RunMessageLoop (bucle de funcionamiento normal del programa) y acaba llamando a PostMessageLoop (finalización del programa). Estas tres funciones aparecen definidas en graysim.cpp, dentro de la definición de la clase CGraySimulatorModule. Su código será mostrado y explicado un poco más adelante, junto al resto de métodos de la clase, que pasamos a explicar e ahora. Clase CGraySimulatorModule Clase definida por herencia múltiple pública de las siguientes clases: - GBDataAccess, derivada de GBOPCDataAccessBase, que a su vez es derivada de GBClassFactory, - todas ellas pertenecientes al toolkit de Graybox. CAtlServiceModuleT, derivada de CAtlExeModule, que hereda de CAtlModuleT que, a su vez, es derivada de CAtlModule, todas ellas pertenecientes a ATL. Métodos públicos: InitializeSecurity() () Llama a la función CoInitializeSecurity, que registra la seguridad y define los valores por defecto de seguridad de un sistema COM. HRESULT InitializeSecurity() throw() { return CoInitializeSecurity(NULL, -1, 1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IDENTIFY, NULL, EOAC_NONE, NULL); } Al escribir junto al prototipo de la función función indicamos que no se permite lanzar excepciones desde dentro de la función. InitializeCom( ) Llama a la función CoInitializeEx CoInitialize , que inicializa la librería COM que gestiona el uso de hilos, definiendo el modelo delo de concurrencia de éstos. Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 211 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 static HRESULT InitializeCom( ) throw( ) { // COM debe inicializarse como COINIT_MULTITHREADED return CoInitializeEx(NULL, COINIT_MULTITHREADED); } PreMessageLoop(int nShowCmd) Función que realiza el registro e inicialización del servidor, así como la creación de los tags a través de la llamada a Initialize(). En esta función abrimos también un fichero (Traza.txt) en el que recogeremos los datos que lleguen al servidor. La otra tarea que realizamos en esta funciónn es la creación de un socket cliente TCP/IP y su conexión al socket servidor ubicado en la mbed. El argumento nShowCmd simplemente sirve para especificar cómo se muestra la aplicación cuando se abre. Este parámetro llega a la función desde el punto de entrada del programa (_tWinMain) y se define como SW_HIDE (ocultar ventana) en la función Run. HRESULT PreMessageLoop(int nShowCmd) throw( ) { //Abrimos el fichero donde guardaremos los datos recibidos mific=fopen("Traza.txt" "Traza.txt","wt"); //Inicializamos la variables de recepción de datos recvbuf = new char[100]; [100]; y_mbed=0; u_mbed=0; ref_mbed=10.5; sprintf(recvbuf,""); ); //Variables para inicialización del servidor OPC HRESULT ret; CLSID clsid; //Configuración del socket TCP/IP int iResult = WSAStartup(MAKEWORD(2,2), &wsaData); if (iResult != NO_ERROR) printf("TCP: "TCP: Error en WSAStartup().\n"); WSAStartup(). else printf("TCP: "TCP: WSAStartup() OK.\n"); OK. ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (ConnectSocket == INVALID_SOCKET) { printf("TCP: "TCP: Error en socket(): %ld\n", %ld WSAGetLastError()); WSACleanup(); } //Especificamos la dirección del servidor TCP/IP sockaddr_in clientService; clientService.sin_family = AF_INET; Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 212 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 clientService.sin_addr.s_addr = inet_addr("192.168.1.33"); inet_addr( clientService.sin_port = htons(12345); //Programamos un timeout en la recepción de datos de 0.1 segundos tv.tv_sec = 0; tv.tv_usec = 100000; setsockopt(ConnectSocket, SOL_SOCKET, SO_RCVTIMEO, (char ( *)&tv, sizeof tv); //Nos conectamos al servidor if (connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService)) (clientService)) == SOCKET_ERROR) { WSACleanup(); } //Fin Fin de la configuración TCP/IP //Llamamos a PreMessageLoop de CAtlServiceModule if FAILED( ret = CAtlServiceModuleT::PreMessageLoop(nShowCmd) ) return ret; //Obtenemos el CLSID CLSIDFromString((LPOLESTR)GetAppId(), &clsid); //Inicializamos el servidor if FAILED( ret = GBInitialize(&clsid, SCANRATE, SCANRATE, GB_SRV_NOACCESSPATH, L'.',100) L ) return ret; //Creamos los tags if FAILED( ret = Initialize() ) return ret; //Registramos el servidor OPC if FAILED( ret = GBRegisterClassObject() ) return ret; //Actualizamos el estado del servidor a SERVICE_RUNNING if (m_bService) { m_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE; SetServiceStatus(SERVICE_RUNNING); } return S_OK; } La función CAtlServiceModuleT::PreMessageLoop InitializeSecurity, realiza una llamada a comentada previamente. La inicialización del servidor OPC se realiza mediante la función GBInitialize, que recibe como primer argumento el puntero al identificador (CLSID) del objeto COM (Component Object Model) que representará representa al servidor. Por su parte, la función GBRegisterClassObject registra el servidor OPC con OLE (Object Linking and Embedding), Embedding) de modo que ue los clientes OPC puedan conectarse a él. La función Initialize, encargada de la creación de los tags, se presenta más adelante. Al salir de PreMessageLoop el servidor está listo para empezar a funcionar. Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 213 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 RunMessageLoop( ) Función en la que discurre la ejecución ón normal del servidor. Dentro de ella entramos en un bucle while(1) en el que realizamos las siguientes acciones: - Esperar la llegada de un mensaje o evento. Si no llega salta un timeout igual al SCANRATE. - Si hay mensaje, procesarlo mediante TranslateMessage y DispatchMessage. Si el mensaje que llega es WM_QUIT salimos del bucle while mediante un break. - Si no hay mensajes, leemos la información que llega desde la mbed, los almacenamos y lo registramos en el fichero (Traza.txt), (Traza.txt) y llamamos a la función Update, donde se actualizan los tags. Esta función se explica más adelante. void RunMessageLoop( ) throw( ) { MSG msg; BOOL bMsg; int bytesRecv; float y,u; //Repetiremos este bucle hasta la condición de fin (llegada de WM_QUIT) while (1) { //Esperamos la llegada de mensajes. Esta función tiene timeout MsgWaitForMultipleObjects(0, NULL, FALSE, SCANRATE, QS_ALLINPUT); bMsg = PeekMessage(&msg, NULL, 0, 0, PM_REMOVE); //Si llega algún mensaje lo procesamos procesam if (bMsg) { if (msg.message == WM_QUIT) break; //Salimos de while(1) TranslateMessage(&msg); DispatchMessage(&msg); } //Si no, realizamos las acciones habituales else { //Leemos el socket, que nos envía datos desde la mbed bytesRecv = recv(ConnectSocket, recvbuf, 100, 0); //Si hay datos los guardamos y escribimos en el fichero if(bytesRecv) { sscanf(recvbuf,"%lf %lf %lf",&y_mbed,&u_mbed,&ref_mbed); ,&y_mbed,&u_mbed,&ref_mbed); fprintf(mific,"%lf %lf %lf\n",y_mbed,u_mbed,ref_mbed); ,y_mbed,u_mbed,ref_mbed); } //Llamamos a Update(), que actualiza el valor de los tags Update(); } }; } Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 214 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 Los datos que recibimos de la mbed se mandan desde el microcontrolador en cada iteración del algoritmo de control mediante las siguientes sentencias (programa mbed): mbed) //Copiamos los datos en el buffer sprintf(buf,"%lf "%lf %lf %lf",(m.y[0]+y_eq),(uk+u_eq),(m.w[0]+y_eq)); %lf",(m.y[0]+y_eq),(uk+u_eq),(m.w[0]+y_eq)); //Y los enviamos pConnectedSock->send send(buf, strlen(buf)); Como podemos observar, construimos una cadena de caracteres con los datos que queremos enviar mediante la función sprintf. Cuando recibimos los datos en el PC (en el servidor OPC) volvemos a separarlos y guardarlos en variables double independientes mediante la función sscanf. PostMessageLoop( ) Función a la que llamamos justo antes de cerrar el servidor. servidor. En ella cerramos el fichero, liberamos la memoria reservada dinámicamente y cerramos el socket. Finalmente llamamos a la función GBRevokeClassObject, que informa al OLE de que el servidor registrado ado previamente con GBRegisterClassObject ya no está disponible para su uso. Salimos a través de CAtlServiceModuleT::PostMessageLoop, donde deshabilitamos también el objeto COM. HRESULT PostMessageLoop( ) throw( ) { //Cerramos el fichero fclose(mific); //Liberamos la memoria reservada dinámicamente free(recvbuf); //Cerramos el socket closesocket(ConnectSocket); //Cerramos ws2_32.dll WSACleanup(); //Suspendemos el uso del servidor GBRevokeClassObj GBRevokeClassObject(); //Salimos a través de PostMessageLoop() de CATLServiceModule, // que finaliza la ejecución return CAtlServiceModuleT::PostMessageLoop(); } OnPause( ) Con esta función se indica que el servidor está pausado. Para ello llamamos a las funciones SetServiceStatus y GBSetState. Cambiamos el valor del flag Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 215 m_bPaused Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 dentro de una sección crítica (mutex),, de modo que ningún otro hilo pueda cambiar su valor en ell preciso instante en el que está siendo modificado. void OnPause( ) throw( throw ) { SetServiceStatus(SERVICE_PAUSE_PENDING); EnterCriticalSection(&m_crit); m_bPaused = true; LeaveCriticalSection(&m_crit); GBSetState(OPC_STATUS_SUSPENDED); SetServiceStatus(SERVICE_PAUSED); } OnContinue( ) Con esta función se indica que la ejecución del servidor ha sido reanudada. Para ello llamamos a las funciones SetServiceStatus y GBSetState. Cambiamos el valor del flag m_bPaused dentro de una sección crítica (mutex), de modo que ningún otro hilo pueda cambiar su valor en el preciso instante en el que está siendo modificado. void OnContinue( ) throw( throw ) { SetServiceStatus(SERVICE_CONTINUE_PENDING); EnterCriticalSection(&m_crit); m_bPaused = false; LeaveCriticalSection(&m_crit); GBSetState(OPC_STATUS_RUNNING); SetServiceStatus(SERVICE_RUNNING); } Métodos privados: Initialize() Función en la que se crean los tags mediante la función GBCreateItem. Para la creación de cada tag creamos una estructura de tipo VARIANT, estructura usada por los objetos COM que pueden almacenar diversos tipos de variables (float, int, char, etc). Esta variable contendrá el tipo de datos que va a manejar el tag, y su puntero puntero se pasa como argumento a GBCreateItem. Otra serie de argumentos los tomamos de una tabla de estructuras TAGDESCR. DESCR. Esta estructura está definida en el fichero TagDescr.h y tiene los siguientes campos: Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 216 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 struct TAGDESCR { const wchar_t* * Name; VARTYPE Type; DWORD Rights; int Min; int Max; DWORD Flags; const wchar_t* * Descr; }; El vector de estructuras donde definimos los tags que vamos a crear es el siguiente: static const TAGDESCR TagDescr[] = { "Altura del deposito"}, deposito" {L"salida.altura", VT_R8, 3, 0, 0, GBS_Y, L"Altura {L"salida.voltaje", VT_R8, 3, 0, 0, GBS_U, L"Voltaje "Voltaje aplicado a la bomba"}, bomba" {L"salida.ref_actual" "salida.ref_actual", VT_R8, 3, 0, 0, GBS_CREF, L"Referencial "Referencial actual"}, actual" {L"entrada.referencia" "entrada.referencia", VT_R8, 3, 0, 0, GBS_REF, L"Referencia "Referencia de altura"}, altura" }; Para crear los tags pasaremos desde esta tabla como argumentos a la función GBCreateItem el nombre del tag, tag el tipo de dato (VT_R8) y sus derechos (lectura/escritura, es el valor de 3). El resto de argumentos de GBCreateItem serán sus identificadores dentroo del programa y un flag de opciones. Más adelante, dentro de la propia Initialize, usaremos el último campo de la estructura TAGDESCR para añadir la descripción al tag mediante la función GBAddProperty. Sin más dilación, el código de Initialize es el siguiente: inline HRESULT Initialize() { unsigned i; m_bPaused = false; m_iActive = 0; memset(m_pbActive, 0, sizeof(m_pbActive)); InitializeCriticalSection(&m_crit); //Creamos TAGDESCRCOUNT tags for (i = 0; i<TAGDESCRCOUNT; i++) { //Declaremos una variable tipo VARIANT para crear cada tag VARIANT v; memset(&v, 0, sizeof(v)); v.vt = TagDescr[i].Type; Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 217 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 if (v.vt == VT_BSTR) v.bstrVal = SysAllocString(L""); SysAllocString(L //Creamos el tag con GBCreateItem GBCreateItem(&m_puTagIds[i], i, TagDescr[i].Name, TagDescr[i].Rights, GB_TAG_DONTCOPYSTR, &v); //Borramos la variable VARIANT VariantClear(&v); v.vt = VT_BSTR; //Añadimos la descripción del tag a la variable v.bstrVal = SysAllocString(TagDescr[i].Descr); //Añadimos la descripción al tag con GBAddProperty GBAddProperty(m_puTagIds[i], OPC_PROPERTY_DESCRIPTION, &v, NULL, NULL, 0); //Borramos la variable VARIANT VariantClear(&v); } //Damos valor inicial a la referencia FILETIME ft; VARIANT vaux; DWORD Id = m_puTagIds[3]; HRESULT Error=S_OK; WORD Quality = OPC_QUALITY_GOOD; GetSystemTimeAsFileTime(&ft) GetSystemTimeAsFileTime(&ft); vaux.dblVal = 10.5; vaux.vt = VT_R8; VariantChangeType(&vaux, &vaux, 0, TagDescr[3].Type); //Actualizamos el tag referencia con los datos especificados GBUpdateItems(1, &Id, &vaux, &Quality, &ft, &ft, &Error, FALSE); return S_OK; } Nótese que se ha dado un valor inicial al tercer tag (referencia) mediante la función GBUpdateItems, la cual describiremos en dentro de la siguiente función que vamos a explicar. Update() Función en la que se actualiza el valor de los tags de lectura. Para ello hacemos uso de la función GBUpdateItems. La actualización se realiza dentro de una sección crítica para evitar que otros hilos cambien el valor de los tags en este intervalo de tiempo. tiemp Para actualizar los tags primero vamos a rellenar mediante un bucle for un vector de estructuras VARIANT donde almacenaremos tanto el nuevo valor (campo dblVal) como el tipo de variable (campo vt) de cada tag,, distinguiéndolos mediante una sentencia switch y el flag definido para cada tag en TagDescr.h. TagDescr.h. En este bucle también Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 218 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 rellenaremos un vector que contenga la hora de la actualización (Timestamp), otro con la calidad del dato (Quality) y otros dos últimos con los identificadores de cada tag (Id) y el estado de error (Error). Una vez tengamos todos estos vectores rellenos se los pasaremos como argumentos a la función GBUpdateItems, junto con el número de tags a actualizar (primer argumento) y un flag (último argumento) indicando si queremos que la operación operación se realice de forma síncrona (TRUE) o asíncrona (FALSE). Una vez volvamos de la llamada a la función los tags habrán sido actualizados. Borramos entonces las estructuras VARIANT y salimos de la función. El código de este método es el siguiente: inline void Update() { unsigned i, c = 0; int k, l; //Creamos un vector de variables VARIANT para el valor VARIANT Value[TAGDESCRCOUNT]; //Creamos un vector de variables WORD para la calidad WORD Quality[TAGDESCRCOUNT]; //Creamos mos un vector de FILETIME para el tiempo FILETIME Timestamp[TAGDESCRCOUNT], ft; HRESULT Error[TAGDESCRCOUNT]; DWORD Id[TAGDESCRCOUNT]; double t, A, n, T; bool clear = false; //Entramos en la sección crítica (mutex) EnterCriticalSection(&m_crit); if (m_iActive <= = 0 || m_bPaused) goto Skip; //Si el servidor está pausado saltamos a Skip GetSystemTimeAsFileTime(&ft); t = (double)GetTickCount() )GetTickCount() / 1000.0; for (i = 0; i<TAGDESCRCOUNT; i++) { if (!m_pbActive[i]) continue; //Asociamos el valor y el tipo a la variable VARIANT switch (TagDescr[i].Flags & GBS_FUNC_MASK) { case GBS_CREF: //Referencia actual Value[c].vt = VT_R8; Value[c].dblVal=ref_mbed; VariantChangeType(&Value[c], &Value[c], 0, TagDescr[i].Type); break; case GBS_Y: //Altura Value[c].vt = VT_R8; Value[c].dblVal=y_mbed; VariantChangeType(&Value[c], &Value[c], 0, TagDescr[i].Type); break; Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 219 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 case GBS_U: //Voltaje Value[c].vt = VT_R8; Value[c].dblVal=u_mbed; VariantChangeType(&Value[c], &Value[c], 0, TagDescr[i].Type); break; default: continue; default } //Añadimos la ID, el etado de error, la calidad y el tiempo Id[c] = m_puTagIds[i]; Error[c] = S_OK; Timestamp[c] = ft; Quality[c] = OPC_QUALITY_GOOD; OPC_QUA c++; } Skip: //Salimos de la sección crítica (mutex) LeaveCriticalSection(&m_crit); if (c) { //Actualizamos los tags con GBUpdateItems GBUpdateItems(c, Id, Value, Quality, Timestamp, Error, FALSE); if (clear) for (i = 0; i<c; c; i++) VariantClear(&Value[i]); } } El resto de funciones privadas (private) de la clase CGraySimulatorModule son una serie de manejadores anejadores de eventos, eventos entre los que cabe destacar el que gestiona la interrupción de escritura desde un cliente de un ítem: GBOnWriteItems Este manejador salta cuando se escribe en un ítem desde un cliente. En el servidor que estamos programando trataremos solo el caso de que se escriba en el tag “referencia”. “refe El resto de casos serán ignorados y no producirán ningún efecto en el servidor, ni tampoco fallo. Además de actualizar el valor del tag “referencia”, enviaremos el nuevo set-point set a la mbeb directamente desde esta función. De la larga lista de argumentos rgumentos de la función GBOnWriteItems sólo nos quedaremos con el valor (pValues), que copiaremos a una estructura VARIANT que utilizaremos para actualizar el tag mediante la función GBUpdateItems del mismo modo que explicamos en el punto anterior. Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 220 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 Por su parte, para el envío del nuevo set-point set point a la mbed, debemos realizar una conversión de entero a cadena de caracteres mediante la función itoa y enviarlo a través del socket TCP/IP mediante la función send. Finalmente, el código del manejador es el siguiente: siguie DWORD __stdcall GBOnWriteItems(DWORD dwCount, GBItemID* pTags, VARIANT* pValues, p WORD* pwQualities, FILETIME* pftTimestamps, HRESULT* pErrors, HRESULT* pMasterError, LCID dwLcid, DWORD dwClientID) { VARIANT v; HRESULT hr; FILETIME ft; DWORD Id = m_puTagIds[3]; HRESULT Error=S_OK; WORD Quality = OPC_QUALITY_GOOD; GetSystemTimeAsFileTime(&ft); //Creamos un buffer para enviar la referencia a la mbed char buf[100]; for (unsigned i = 0; i<dwCount; i++) { if (!pTags[i].dwTagID) continue; switch (TagDescr[pTags[i].dwUserID].Flags & GBS_ATTR_MASK) { case GBS_REF: //Tag: referencia v.vt = VT_EMPTY; //Copiamos el valor recibido (pValues) en v hr = VariantChangeTypeEx(&v, &pValues[i], dwLcid, 0, VT_R8); if SUCCEEDED(hr) { //Pasamos el dato de entero a string itoa( itoa(int(v.dblVal),buf,10); //Lo enviamos por TCP/IP a la mbed send(ConnectSocket,buf,strlen(buf),0); //Actualizamos el tag en el servidor OPC con el nuevo valor GBUpdateItems(1, &Id, &v, &Quality, &ft, &Error, FALSE); } if FAILED(hr) { pErrors[i] = hr; *pMasterError = S_FALSE; } } } return GB_RET_CACHE; } Hasta aquí la clase CGraySimulatorModule, donde han quedado explicadas todas las funciones que intervienen en la programación del servidor. Para consultar el código Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 221 Ramón amón Jesús Cuesta Pérez Curso 2011/2012 Proyecto Fin de Carrera Implementación de algoritmos MPC con restricciones en mbed NXP LPC1768 completo consultar el CD (ficheros capítulo 10), donde se adjunta el proyecto de Visual Studio 2010 que contienee al servidor OPC. En el siguiente capítulo presentamos los dos clientes OPC que hemos utilizado para comprobar el correcto funcionamiento de este servidor, así como para realizar los cambios de referencia en el control de la planta de los cuatro tanques. 10.3 Bibliografía del capítulo Iwanitz, F. Lange, J. OPC. Fundamentals, Implementation, and Application Heidelberg : Hüthig, cop. 2002 2nd rev. ed. Balagurusamy, E. Programación gramación orientada a objetos con C++ Madrid [etc.] : McGraw-Hill/Interamericana Hill/Interamericana de España, 2007 3ª ed. Graybox Software. Graybox OPC Server Toolkit - Reference http://gray-box.net/opc_server_toolkit.php box.net/opc_server_toolkit.php MSDN (Microsoft Developer Network). Network) Component Object Model (COM) http://msdn.microsoft.com/en us/library/windows/desktop/ms680573(v=vs.85).aspx http://msdn.microsoft.com/en-us/library/windows/desktop/ms680573(v=vs.85).aspx Matrikon OPC. OPC Tutorials http://www.matrikonopc.com/resources/opc http://www.matrikonopc.com/resources/opc-tutorials.aspx OPC Foundation. OPC Fundation Website http://www.opcfoundation.org Dpto. Ing. de Sistemas y Automática Universidad de Sevilla Página 222 Ramón amón Jesús Cuesta Pérez Curso 2011/2012