Cómo hice un cliente OPC2WEB usando Google

Trabajo como ingeniero de control de procesos y soy un poco aficionado a la programación: con la ayuda de Google y Stack Overflow, hice varias calculadoras en HTML y javascript, hice un bot de telegrama en php, incluso hice un poco de programación en C # en el trabajo. Esta vez la tarea fue mucho más interesante y más difícil, aunque sonaba simple: “Quiero ver la velocidad actual de la unidad en mi navegador”. Para empezar, decidí intentar buscar un software listo para usar: por supuesto, esto se ha inventado durante mucho tiempo, hay sistemas SCADA listos para usar e incluso gratuitos que pueden funcionar como un servidor web, pero todos eran muy sofisticados y difíciles para mi comprensión, además, solo era necesario. deducir velocidad. Así que pensé que podría intentar hacerlo yo mismo, y esto es lo que resultó:



Backend



Después de decidir qué haría yo mismo, abrí el motor de búsqueda de nuevo y comencé a buscar cómo crear mi propio cliente OPC.







Las búsquedas de esto me llevaron a habr, donde descubrí la biblioteca OPCDOTNET gratuita. El archivo de la biblioteca contenía el código fuente del cliente de la consola, que compilé en mi computadora, lancé un simulador OPC simple (caja gris) ... ¡y he aquí! Vi números cambiando en la consola. Esto significa que ahora puedo enviarlos como respuesta a una solicitud web. La siguiente visita a Google fue una solicitud de un servidor web simple donde encontré un ejemplo de uso de HttpListener. Ejecuté el ejemplo en un proyecto separado, entendí cómo funciona y comencé a agregar todo esto a mi cliente OPC. Después de muchos intentos de compilación, buscando errores en Stack Overflow, aún logré ver la preciada "velocidad" en el navegador. ¡Fue una victoria! Pero de inmediato me di cuenta de que la velocidad por sí sola no es grave, después de un tiempo los tecnólogos querrán ver otros parámetros de la línea,por lo tanto, debe averiguar cómo agregar las señales necesarias sin cambiar el programa. Los archivos de configuración vinieron al rescate, donde puede configurar de antemano qué señales queremos ver, configurar el puerto de escucha del servidor, la hora de actualización, etc. Ya tenía experiencia en la creación de archivos de configuración, así que lo hice como antes y funcionó bien. Además, en el proceso, tuve que contactar a un amigo del programador, quien sugirió qué hacer para que se transmitiera el conjunto completo de datos solicitados, y no solo aquellos valores que cambiaron (en el ejemplo terminado del cliente OPC, solo los valores cambiados se mostraban en la consola).Ya tenía experiencia en la creación de archivos de configuración, así que lo hice como antes y funcionó bien. Además, en el proceso, tuve que contactar a un amigo del programador, quien sugirió qué hacer para que se transmitiera la matriz completa de datos solicitados, y no solo aquellos valores que cambiaron (en el ejemplo terminado del cliente OPC, solo los valores cambiados se mostraban en la consola).Ya tenía experiencia en la creación de archivos de configuración, así que lo hice como antes y funcionó bien. Además, en el proceso, tuve que contactar a un amigo del programador, quien sugirió qué hacer para que se transmitiera la matriz completa de datos solicitados, y no solo aquellos valores que cambiaron (en el ejemplo terminado del cliente OPC, solo los valores cambiados se mostraban en la consola).







Luego de tales cambios, el programa comenzó a generar una tabla en HTML a partir de las señales solicitadas en la configuración: al contactar la dirección del servidor donde se lanzó este cliente a través del navegador, ahora era posible ver una tabla que contiene los nombres de las señales y valores en la columna adyacente. Esto ya era bueno, pero los valores parpadearon durante la actualización, y las señales en sí se ubicaron estúpidamente una tras otra, aunque estaban estructuradas en forma de tabla. Por cierto, para que los valores se actualicen automáticamente cada segundo, y no solo cuando el usuario actualiza la página, agregué una metaetiqueta con el parámetro Refresh a la página devuelta a la solicitud. Pero realmente quería que los valores se actualizaran de forma automática y sin recargar la página, así que además del backend, ahora era necesario hacer el front: el usuario solicita una página en el servidor, dentro de la cual se produce una solicitud al cliente,y la página luego genera todo esto en una forma hermosa y comprensible, donde puede estructurar los datos como desee, cambiar colores, fuentes y tamaños; puede hacer cualquier cosa con este enfoque.



Frontend



No llegué a esto de inmediato: al principio comencé a buscar en Google cómo hacer que los datos en la página se actualicen sin recargar. Al final resultó que, debe usar AJAX, es decir, cambiar los datos a través de javascript y recibirlos a través de JSON. En el cliente, hice la generación de JSON mediante una simple concatenación de cadenas y, por universalidad, decidí simplemente contar las etiquetas establecidas en la configuración en orden. Luego encontré un ejemplo en el que se solicita una cadena JSON cada segundo a través de javascript y se muestran sus valores. Al cambiar el código para que se ajuste a mis necesidades y ejecutar la página, vi que todo funciona: los datos se actualizan sin volver a cargar la página (!). Esta fue otra victoria. Ahora había poco que hacer: distribuir correctamente los datos recibidos en la página, es decir, hacer algo en forma de visualización. Al principio decidí hacer la misma mesa,pero luego me di cuenta de que la estructura de bloques se ve mejor y más funcional. Los bloques se pueden colorear y cambiar de tamaño. Y también debe hacerlo de modo que el usuario pueda agregar y cambiar la estructura de forma independiente, no volveré a escribir el archivo HTML para cada nuevo deseo. Como resultado, obtuvimos una opción como la de la siguiente imagen.







Aquí puede agregar bloques grandes que combinarán bloques pequeños con una característica. Estos bloques grandes pueden titularse según sea necesario, cambiar sus colores (si hace clic en el bloque mientras mantiene presionada la tecla Mayús) y cambie su tamaño. Los bloques con valores se agregan haciendo doble clic en un bloque grande. También puede establecer sus propios nombres y unidades de medida en ellos. Si agrega accidentalmente el elemento incorrecto o en el lugar incorrecto, puede eliminarlo. Vi esta función en un marcador, transfiriendo completamente su código a la página. Por supuesto, toda la estructura creada desaparecerá después de volver a cargar la página y, para guardarla, encontré una oportunidad como el almacenamiento local. Y para transferir la estructura terminada a otra computadora, hice la importación y exportación de la pantalla desde el almacenamiento local.



El único problema seguía siendo arrastrar los bloques: me gustaría hacer un buen arrastrar y soltar, pero para mí resultó ser demasiado. Salí de la situación de esta manera: si abres la página en el panel de desarrollador en Chrome, entonces los bloques se pueden arrastrar. Esto me dio la idea de que al usar el botón derecho del mouse, simplemente puedes intercambiar los bloques. Ahora, dicho sistema es bastante universal: para agregar una nueva señal, solo necesita agregar la etiqueta OPC requerida a la configuración y reiniciar el cliente. La etiqueta agregada se agrega automáticamente a JSON y aparece un nuevo valor en la parte inferior de la pantalla de salida, que se puede agregar con unos pocos clics a un bloque existente o nuevo en la página. En este momento, se muestran más de 60 etiquetas en la página y más de la mitad de ellas no fueron agregadas por mí, es decir, el proceso de agregar puede no ser el más fácil.pero no requiere reescribir el programa y la página de salida. Puedes probar y ver el código de esta página





Dado que este artículo debería ser como una instrucción sobre cómo un no programador como yo puede hacer algo útil con la ayuda de los motores de búsqueda, entonces probablemente necesite agregar algunas palabras acerca de cómo exactamente estaba buscando información. Aquí es correcto decir como en la imagen al principio: piensa lo que quiere obtener y pregunta a Google al respecto, y si algo no funciona en alguna parte, mira los códigos de error y vuelve a preguntar. La búsqueda en inglés ayuda mucho; incluso escribiendo solo palabras clave, puede obtener un enlace a un problema resuelto similar en el Stackerflow con una probabilidad del 80%. Para buscar ejemplos listos para usar, el código desde el cual puede tomar y transferir estúpidamente a su programa, puede agregar palabras clave como "ejemplo" o "ejemplo" en ruso. Se encontraron varias buenas ideas en habr, es decir, puede intentar insertar la palabra clave "habr" en la solicitud,pero utilicé esto solo cuando supe con certeza que vi la solución que estaba buscando en Habré. Casi cualquier pequeña tarea de todo lo que se hizo se resolvió a través de un motor de búsqueda: "cambiar div color shift click js", "hacer div redimensionable", "cómo editar una página web" ... cientos de variaciones de diferentes consultas. Quizás en los comentarios los profesionales puedan compartir sus consejos.



Y sí, ya que estamos hablando de consejos, también me gustaría recibir críticas constructivas y consejos útiles de su parte. Quizás alguien quiera estirar su cerebro y pueda ofrecer una solución mucho más funcional en un par de horas. O tal vez este post le dé a alguien algunas ideas interesantes, porque de esta manera puedes aceptar cualquier solicitud JSON y hacer cualquier estructura visual en base a ella. Sería muy bueno tener una solución universal similar en la que puedas distribuir cualquier dato como más te convenga, administrando formas visuales simples, arrastrar y soltar, cambiar el tamaño y todo eso para hacerlo hermoso y funcional, pero no eso es todo. Aunque resultó bien, creo. La velocidad de la unidad, según lo solicitado por el cliente, ahora se puede observar desde el navegador y agregar algo nuevo no será difícil.



Enlace acódigo de cliente en C #



O debajo del spoiler
/*=====================================================================
  File:      OPCCSharp.cs

  Summary:   OPC sample client for C#

-----------------------------------------------------------------------
  This file is part of the Viscom OPC Code Samples.

  Copyright(c) 2001 Viscom (www.viscomvisual.com) All rights reserved.

THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.
======================================================================*/

using System;
using System.Threading;
using System.Runtime.InteropServices;
using System.Configuration;
using OPC.Common;
using OPC.Data;
using System.Net;
using System.Globalization;
using System.Data.SqlClient;
using System.Data;
using System.Net.Sockets;


namespace CSSample
{
    class Tester
    {
        // ***********************************************************	EDIT THIS :
        string serverProgID = ConfigurationManager.AppSettings["opcID"];         // ProgID of OPC server

        private OpcServer theSrv;
        private OpcGroup theGrp;
        private static float[] currentValues;
        private static string responseStringG ="";
        private static HttpListener listener = new HttpListener();

        private static string consoleOut = ConfigurationManager.AppSettings["consoleOutput"];
        private static string answerType = ConfigurationManager.AppSettings["answerType"];
        private static string portNumb = ConfigurationManager.AppSettings["portNumber"];
        private static int timeref = Int32.Parse(ConfigurationManager.AppSettings["refreshTime"]);
        private static string[] tagsNames = ConfigurationManager.AppSettings["tagsNames"].Split(','); // tags from config
        private static string[] ratios = ConfigurationManager.AppSettings["ratios"].Split(',');

        private static string sqlSend = ConfigurationManager.AppSettings["sqlSend"];
        private static string udpSend = ConfigurationManager.AppSettings["udpSend"];
        private static string webSend = ConfigurationManager.AppSettings["webSend"];
        private static string table_name = ConfigurationManager.AppSettings["table"]; //    ;
        private static string column_name = ConfigurationManager.AppSettings["column"];
        private static int sendtags = Int32.Parse(ConfigurationManager.AppSettings["tags2send"]);
        
        private static IPAddress remoteIPAddress = IPAddress.Parse(ConfigurationManager.AppSettings["remoteIP"]); // Ip from config
        private static int remotePort = Convert.ToInt16(ConfigurationManager.AppSettings["remotePort"]); // remote port from config

        public static SqlConnection myConn = new SqlConnection(ConfigurationManager.ConnectionStrings["connstr"].ConnectionString); //   SQL    
        SqlCommand myCommand = new SqlCommand("Command String", myConn);

        public void Work()
        {
            /*	try						// disabled for debugging
                {	*/

            theSrv = new OpcServer();
            theSrv.Connect(serverProgID);
            Thread.Sleep(500);              // we are faster then some servers!

            // add our only working group
            theGrp = theSrv.AddGroup("OPCCSharp-Group", false, timeref);

            string[] tags = ConfigurationManager.AppSettings["tags"].Split(','); // tags from config
            if (sendtags > tags.Length) sendtags = tags.Length;

                var itemDefs = new OPCItemDef[tags.Length];
            for (var i = 0; i < tags.Length; i++)
            {
                itemDefs[i] = new OPCItemDef(tags[i], true, i, VarEnum.VT_EMPTY);
            }

            OPCItemResult[] rItm;
            theGrp.AddItems(itemDefs, out rItm);
            if (rItm == null)
                return;
            if (HRESULTS.Failed(rItm[0].Error) || HRESULTS.Failed(rItm[1].Error))
            {
                Console.WriteLine("OPC Tester: AddItems - some failed"); theGrp.Remove(true); theSrv.Disconnect(); return;

            };

            var handlesSrv = new int[itemDefs.Length];
            for (var i = 0; i < itemDefs.Length; i++)
            {
                handlesSrv[i] = rItm[i].HandleServer;
            }

            currentValues = new Single[itemDefs.Length];

            // asynch read our two items
            theGrp.SetEnable(true);
            theGrp.Active = true;
            theGrp.DataChanged += new DataChangeEventHandler(this.theGrp_DataChange);
            theGrp.ReadCompleted += new ReadCompleteEventHandler(this.theGrp_ReadComplete);


            int CancelID;

            int[] aE;
            theGrp.Read(handlesSrv, 55667788, out CancelID, out aE);

            // some delay for asynch read-complete callback (simplification)
            Thread.Sleep(500);

            while (webSend=="yes")
            {
                HttpListenerContext context = listener.GetContext();
                HttpListenerRequest request = context.Request;
                HttpListenerResponse response = context.Response;
                context.Response.AddHeader("Access-Control-Allow-Origin", "*");


                byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseStringG);
                // Get a response stream and write the response to it.
                response.ContentLength64 = buffer.Length;
                System.IO.Stream output = response.OutputStream;
                output.Write(buffer, 0, buffer.Length);
                // You must close the output stream.
                output.Close();
            }
            // disconnect and close
            Console.WriteLine("************************************** hit <return> to close...");
            Console.ReadLine();
            theGrp.ReadCompleted -= new ReadCompleteEventHandler(this.theGrp_ReadComplete);
            theGrp.RemoveItems(handlesSrv, out aE);
            theGrp.Remove(false);
            theSrv.Disconnect();
            theGrp = null;
            theSrv = null;


            /*	}
            catch( Exception e )
                {
                Console.WriteLine( "EXCEPTION : OPC Tester " + e.ToString() );
                return;
                }	*/
        }

        // ------------------------------ events -----------------------------

        public void theGrp_DataChange(object sender, DataChangeEventArgs e)
        {

            foreach (OPCItemState s in e.sts)
            {
                if (HRESULTS.Succeeded(s.Error))
                {
                    if (consoleOut == "yes")
                    {
                        Console.WriteLine(" ih={0} v={1} q={2} t={3}", s.HandleClient, s.DataValue, s.Quality, s.TimeStamp); //      
                    }
                    currentValues[s.HandleClient] = Convert.ToSingle(s.DataValue) * Single.Parse(ratios[s.HandleClient], CultureInfo.InvariantCulture.NumberFormat); //     
                }
                else
                    Console.WriteLine(" ih={0}    ERROR=0x{1:x} !", s.HandleClient, s.Error);
            }
            string responseString = "{";
            if (answerType == "table")
            {
                responseString = "<HTML><head><meta charset=\"UTF-8\"><meta http-equiv=\"Refresh\" content=\"" + timeref / 1000 + "\"/></head>" +
            "<BODY><table border><tr><td>" + string.Join("<br>", tagsNames) + "</td><td >" + string.Join("<br>", currentValues) + "</td></tr></table></BODY></HTML>";
                responseStringG = responseString;
            }
            else
            {
                for (int i = 0; i < currentValues.Length - 1; i++) responseString = responseString + "\"tag" + i + "\":\"" + currentValues[i] + "\", ";
                responseString = responseString + "\"tag" + (currentValues.Length - 1) + "\":\"" + currentValues[currentValues.Length - 1] + "\"}";
                responseStringG = responseString;
            }
            byte[] byteArray = new byte[sendtags * 4];
            Buffer.BlockCopy(currentValues, 0, byteArray, 0, byteArray.Length);
            if (sqlSend == "yes")
            {
                try
                {
                    SqlCommand cmd = new SqlCommand("INSERT INTO " + table_name + " (" + column_name + ") values (@bindata)", myConn);
                    myConn.Open();
                    var param = new SqlParameter("@bindata", SqlDbType.Binary)
                    { Value = byteArray };
                    cmd.Parameters.Add(param);
                    cmd.ExecuteNonQuery();
                    myConn.Close();
                }
                catch (Exception err)
                {
                    Console.WriteLine("SQL-exception: " + err.ToString());
                    return;
                }
            }

            if (udpSend == "yes")  UDPsend(byteArray);
        }

        private static void UDPsend(byte[] datagram)
        {
            //  UdpClient
            UdpClient sender = new UdpClient();

            //  endPoint     
            IPEndPoint endPoint = new IPEndPoint(remoteIPAddress, remotePort);

            try
            {

                sender.Send(datagram, datagram.Length, endPoint);
                //Console.WriteLine("Sended", datagram);
            }
            catch (Exception ex)
            {
                Console.WriteLine(" : " + ex.ToString() + "\n  " + ex.Message);
            }
            finally
            {
                //  
                sender.Close();
            }
        }
        public void theGrp_ReadComplete(object sender, ReadCompleteEventArgs e)
        {
            Console.WriteLine("ReadComplete event: gh={0} id={1} me={2} mq={3}", e.groupHandleClient, e.transactionID, e.masterError, e.masterQuality);
            foreach (OPCItemState s in e.sts)
            {
                if (HRESULTS.Succeeded(s.Error))
                {
                    Console.WriteLine(" ih={0} v={1} q={2} t={3}", s.HandleClient, s.DataValue, s.Quality, s.TimeStamp);
                }
                else
                    Console.WriteLine(" ih={0}    ERROR=0x{1:x} !", s.HandleClient, s.Error);
            }
        }

        static void Main(string[] args)
        {
            string url = "http://*";
            string port = portNumb;
            string prefix = String.Format("{0}:{1}/", url, port);
            listener.Prefixes.Add(prefix);
            listener.Start();
            
            Tester tst = new Tester();
            tst.Work();
        }
    }
}

/* add this code to app.exe.config file
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>
  <appSettings>
    <add key="opcID" value="Graybox.Simulator" />
    <add key="tagsNames" value="Line Speed,Any name, " />
    <add key="tags" value="numeric.sin.int16,numeric.sin.int16,numeric.sin.int16" />
    <!-- ratios for tags -->
    <add key="ratios" value="1,0.5,0.1" />
    <add key="portNumber" value="45455" />
    <add key="refreshTime" value="1000" />
    <!-- "yes" or no to show values in console-->
    <add key="consoleOutput" value="yes" />
    <add key="webSend" value="no" /> 
    <!-- "table" or json (actually any other word for json)-->
    <add key="answerType" value="json" />

    <add key="sqlSend" value="no" />
    <add key="table" value="raw_tbl" />
    <add key="column" value="data" />
    
    <add key="udpSend" value="yes" />
    <add key="remotePort" value="3310"/>
    <add key="remoteIP" value="127.0.0.1"/>

    <add key="tags2send" value="2" />
    
  </appSettings>
  
  <connectionStrings>
    <add connectionString="Password=12345;Persist Security Info=True;User ID=user12345;Initial Catalog=amt;Data Source=W7-VS2017" name="connstr" />
  </connectionStrings>
   
</configuration>
     */






All Articles