Como fiz um cliente OPC2WEB usando o Google

Trabalho como engenheiro de controle de processos e gosto um pouco de programação: com ajuda do Google e Stack Overflow, fiz várias calculadoras em HTML e javascript, fiz um bot de telegrama em php, até programei um pouco em c # no trabalho. Desta vez a tarefa era muito mais interessante e mais difícil, embora parecesse simples: “Quero ver a velocidade atual do aparelho no meu navegador”. Para começar, resolvi procurar um software pronto: claro, isso está inventado há muito tempo, existem sistemas SCADA ready-made e até gratuitos que podem funcionar como servidor web, mas eram todos muito sofisticados e difíceis de entender, além disso, era apenas necessário deduzir velocidade. Então, pensei que poderia tentar fazer sozinho, e aqui está o que resultou:



Processo interno



Depois de decidir o que faria sozinho, abri o mecanismo de busca novamente e comecei a procurar como fazer meu próprio cliente OPC sozinho.







Pesquisas por isso me levaram a habr, onde descobri sobre a biblioteca OPCDOTNET gratuita. O arquivo da biblioteca continha o código-fonte do cliente do console, que eu compilei no meu computador, lançou um simulador OPC simples (caixa cinza) ... e eis que! Eu vi números mudando no console. Isso significa que agora posso enviá-los como resposta a uma solicitação da web. A próxima visita ao Google foi uma solicitação de um servidor web simples, onde me deparei com um exemplo de uso do HttpListener. Executei o exemplo em um projeto separado, entendi como funciona e comecei a adicionar tudo isso ao meu cliente OPC. Depois de muitas tentativas de compilar, procurando erros no Stack Overflow, ainda consegui ver a querida "velocidade" do navegador. Foi uma vitória! Mas imediatamente percebi que velocidade por si só não é grave, depois de um tempo os tecnólogos vão querer ver outros parâmetros da linhaportanto, você precisa descobrir como adicionar os sinais necessários sem alterar o programa. Os arquivos de configuração vieram para o resgate, onde você pode definir antecipadamente quais sinais queremos ver, definir a porta de escuta do servidor, o tempo de atualização e assim por diante. Já tinha experiência em criar arquivos de configuração, então fiz como antes e funcionou bem. Além disso, no processo, tive que entrar em contato com um amigo do programador, que sugeriu o que fazer para que toda a matriz de dados solicitados fosse transmitida, e não apenas os valores que mudaram (no exemplo final do cliente OPC, apenas os valores alterados foram exibidos no console).Já tinha experiência em criar arquivos de configuração, então fiz como antes e funcionou bem. Além disso, no processo, tive que entrar em contato com um amigo do programador, que sugeriu o que fazer para que toda a matriz de dados solicitados fosse transmitida, e não apenas os valores que mudaram (no exemplo final do cliente OPC, apenas os valores alterados foram exibidos no console).Já tinha experiência em criar arquivos de configuração, então fiz como antes e funcionou bem. Além disso, no processo, tive que entrar em contato com um amigo do programador, que sugeriu o que fazer para que toda a matriz de dados solicitados fosse transmitida, e não apenas os valores que mudaram (no exemplo final do cliente OPC, apenas os valores alterados foram exibidos no console).







Após tais mudanças, o programa passou a gerar uma tabela em HTML a partir dos sinais solicitados na configuração: ao entrar em contato com o endereço do servidor onde este cliente foi iniciado através do navegador, agora era possível ver uma tabela contendo os nomes dos sinais e valores na coluna adjacente. Isso já não era ruim, mas os valores piscaram durante a atualização, e os próprios sinais foram estupidamente localizados um após o outro, embora tenham sido estruturados na forma de uma tabela. A propósito, para que os valores sejam atualizados automaticamente a cada segundo, e não apenas quando o usuário atualiza a página, adicionei uma meta tag com o parâmetro Refresh à página retornada à solicitação. Mas eu queria muito que os valores fossem atualizados automaticamente e sem recarregar a página, então era necessário fazer agora o front além do backend: o usuário solicita uma página no servidor, dentro da qual ocorre uma solicitação ao cliente,e a página então gera tudo isso de uma forma bonita e compreensível, onde você pode estruturar os dados como quiser, alterar cores, fontes e tamanhos - você pode fazer qualquer coisa com essa abordagem.



Frontend



Não cheguei a esse ponto: primeiro comecei a pesquisar no Google como fazer os dados da página atualizarem sem recarregar. Acontece que é necessário usar AJAX, ou seja, alterar os dados via javascript e recebê-los via JSON. No cliente, fiz a geração do JSON por simples concatenação de strings, e por universalidade decidi simplesmente contar as tags definidas no config em ordem. Então eu encontrei um exemplo em que uma string JSON é solicitada a cada segundo via javascript e os valores dela são exibidos. Mudando o código para atender às minhas necessidades e rodando a página, vi que tudo funciona - os dados são atualizados sem recarregar a página (!). Esta foi outra vitória. Agora havia pouco a fazer - distribuir corretamente os dados recebidos na página, ou seja, fazer algo na forma de visualização. No começo eu decidi fazer a mesma mesa,mas então percebi que a estrutura do bloco parece melhor e mais funcional. Os blocos podem ser pintados em cores diferentes e redimensionados. E você também precisa fazer isso para que o usuário possa adicionar e alterar a estrutura de forma independente. Não vou reescrever o arquivo HTML para cada novo desejo. Como resultado, temos uma opção como na imagem abaixo.







Aqui você pode adicionar blocos grandes que irão combinar pequenos blocos com um recurso. Esses blocos grandes podem ser intitulados conforme necessário, suas cores podem ser alteradas (clicando no bloco enquanto mantém pressionada a tecla shift) e seu tamanho pode ser alterado. Os blocos com valores são adicionados clicando duas vezes em um bloco grande. Você também pode definir seus próprios nomes e unidades de medida neles. Se você acidentalmente adicionar o elemento errado ou no lugar errado, você pode excluí-lo - eu vi essa função em um bookmarklet, transferindo completamente seu código para a página. Claro, toda a estrutura criada irá desaparecer após recarregar a página e, para salvá-la, encontrei uma oportunidade como o armazenamento local. E para transferir a estrutura finalizada para outro computador, fiz a importação e exportação da tela do armazenamento local.



O único problema era arrastar e soltar blocos - eu gostaria de fazer um bom arrastar e soltar, mas para mim acabou sendo opressor. Eu saí da situação assim: se você abrir a página no painel do desenvolvedor em cromo, os blocos podem ser arrastados. Isso me deu a ideia de que, usando o botão direito do mouse, você pode simplesmente trocar os blocos. Agora, tal sistema é bastante universal: para adicionar um novo sinal, você só precisa adicionar a tag OPC necessária à configuração e reiniciar o cliente. A tag adicionada é automaticamente adicionada ao JSON e um novo valor aparece na parte inferior da tela de saída, que pode ser adicionado com alguns cliques a um bloco existente ou novo na página. No momento, mais de 60 tags são exibidas na página e mais da metade delas não foram mais adicionadas por mim, ou seja, o processo de adição pode não ser dos mais fáceis,mas não requer reescrever o programa e a página de saída. Você pode testar e ver o código desta página





Visto que este artigo deve ser como uma instrução sobre como um não programador como eu pode fazer algo útil com a ajuda dos mecanismos de pesquisa, provavelmente preciso acrescentar algumas palavras sobre como exatamente eu estava procurando informações. Aqui é correto dizer como na imagem no início: você pensa o que deseja obter e pergunta ao Google sobre isso, e se algo não der certo em algum lugar, você olha os códigos de erro e pergunta novamente. Uma pesquisa em inglês ajuda muito - mesmo digitando apenas palavras-chave, você pode obter um link para um problema semelhante resolvido no stackerflow com uma probabilidade de 80%. Para pesquisar exemplos prontos, o código do qual você pode pegar e transferir estupidamente para o seu programa, você pode adicionar palavras-chave como "exemplo" ou em russo "exemplo". Várias boas ideias foram encontradas no habr, ou seja, você pode tentar inserir a palavra-chave "habr" na solicitação,mas só usei quando tive certeza de que vi a solução que procurava no Habré. Quase qualquer pequena tarefa de tudo o que foi feito foi resolvido através de um motor de busca: "alterar div color shift click js", "tornar div redimensionável", "como editar uma página da web" ... centenas de variações de consultas diferentes. Talvez nos comentários os profissionais possam compartilhar seus conselhos.



E sim, já que estamos falando de conselhos, também gostaria de receber críticas construtivas e conselhos úteis de vocês. Talvez alguém queira esticar seus cérebros e possa lançar uma solução muito mais funcional em algumas horas. Ou talvez este post dê a alguém algumas ideias interessantes, pois desta forma você pode aceitar qualquer solicitação JSON e fazer qualquer estrutura visual a partir dela. Seria muito legal ter uma solução universal semelhante onde você pudesse distribuir qualquer dado como lhe convier, gerenciando formas visuais simples, arrastar e soltar, redimensionar e tudo mais para torná-lo bonito e funcional, mas isso não é tudo. Embora tenha saído bem, eu acho. A velocidade da unidade, conforme solicitado pelo cliente, agora pode ser observada a partir do navegador e adicionar algo novo não será difícil.



Link paracódigo do cliente em C #



Ou sob o 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