Comment j'ai créé un client OPC2WEB avec Google

Je travaille en tant qu'ingénieur en contrôle de processus et j'aime un peu la programmation: avec l'aide de Google et de Stack Overflow, j'ai réalisé plusieurs calculatrices en HTML et javascript, créé un robot de télégramme en php, même fait un peu de programmation c # au travail. Cette fois, la tâche était beaucoup plus intéressante et plus difficile, même si elle semblait simple: «Je veux voir la vitesse actuelle de l'appareil dans mon navigateur». Pour commencer, j'ai décidé d'essayer de chercher des logiciels prêts à l'emploi: bien sûr, cela a été inventé depuis longtemps, il existe des systèmes SCADA prêts à l'emploi et même gratuits qui peuvent fonctionner comme un serveur Web, mais ils étaient tous très sophistiqués et difficiles à comprendre, en plus, c'était juste nécessaire déduire la vitesse. Alors j'ai pensé que je pourrais essayer de le faire moi-même, et voici ce qui en est sorti:



Backend



Après avoir décidé ce que je ferais moi-même, j'ai de nouveau ouvert le moteur de recherche et j'ai commencé à chercher comment créer moi-même mon propre client OPC.







Les recherches à ce sujet m'ont conduit à habr, où j'ai découvert la bibliothèque gratuite OPCDOTNET. L'archive de la bibliothèque contenait le code source du client de la console, que j'ai compilé sur mon ordinateur, lancé un simple simulateur OPC (boîte grise) ... et voilà! J'ai vu des nombres changer dans la console. Cela signifie que je peux maintenant les envoyer en réponse à une demande Web. La prochaine visite chez Google était une demande pour un serveur Web simple où je suis tombé sur un exemple d'utilisation de HttpListener. J'ai exécuté l'exemple dans un projet séparé, compris son fonctionnement et commencé à ajouter tout cela à mon client OPC. Après de nombreuses tentatives de compilation, à la recherche d'erreurs sur Stack Overflow, j'ai quand même réussi à voir la "vitesse" chérie dans le navigateur. C'était une victoire! Mais j'ai tout de suite réalisé que la vitesse seule n'est pas grave, après un certain temps les technologues voudront voir d'autres paramètres de la ligne,par conséquent, vous devez comprendre comment ajouter les signaux nécessaires sans changer de programme. Les fichiers de configuration sont venus à la rescousse, où vous pouvez prérégler les signaux que nous voulons voir, définir le port d'écoute du serveur, l'heure de mise à jour, etc. J'avais déjà de l'expérience dans la création de fichiers de configuration, donc je l'ai fait comme avant et cela a bien fonctionné. De plus, au cours du processus, j'ai dû contacter un ami du programmeur, qui m'a suggéré ce qu'il fallait faire pour que le tableau complet des données demandées soit transmis, et pas seulement les valeurs qui ont changé (dans l'exemple fini du client OPC, seules les valeurs modifiées étaient affichées dans la console).J'avais déjà de l'expérience dans la création de fichiers de configuration, donc je l'ai fait comme avant et cela a bien fonctionné. De plus, au cours du processus, j'ai dû contacter un ami du programmeur, qui m'a suggéré quoi faire pour que le tableau complet des données demandées soit transmis, et pas seulement les valeurs qui ont changé (dans l'exemple fini du client OPC, seules les valeurs modifiées étaient affichées dans la console).J'avais déjà de l'expérience dans la création de fichiers de configuration, donc je l'ai fait comme avant et cela a bien fonctionné. De plus, au cours du processus, j'ai dû contacter un ami du programmeur, qui m'a suggéré quoi faire pour que le tableau complet des données demandées soit transmis, et pas seulement les valeurs qui ont changé (dans l'exemple fini du client OPC, seules les valeurs modifiées étaient affichées dans la console).







Après de tels changements, le programme a commencé à générer un tableau en HTML à partir des signaux demandés dans la config: en contactant l'adresse du serveur sur lequel ce client était lancé via le navigateur, il était maintenant possible de voir le tableau contenant les noms des signaux et des valeurs dans la colonne adjacente. C'était déjà bien, mais les valeurs clignotaient pendant la mise à jour, et les signaux eux-mêmes étaient bêtement localisés les uns après les autres, bien qu'ils aient été structurés sous la forme d'un tableau. En passant, pour que les valeurs soient mises à jour automatiquement toutes les secondes, et pas seulement lorsque l'utilisateur actualise la page, j'ai ajouté une balise meta avec le paramètre Refresh à la page renvoyée à la demande. Mais je voulais vraiment que les valeurs soient mises à jour automatiquement et sans recharger la page, donc en plus du backend, maintenant je devais faire le front: l'utilisateur demande une page sur le serveur, à l'intérieur de laquelle une requête au client intervient,et la page génère ensuite tout cela sous une forme belle et compréhensible, où vous pouvez structurer les données comme vous le souhaitez, changer les couleurs, les polices et les tailles - vous pouvez tout faire avec cette approche.



Frontend



Je n'y suis pas venu tout de suite: au début, j'ai commencé à chercher sur Google comment actualiser les données de la page sans recharger. En fait, vous devez utiliser AJAX, c'est-à-dire modifier les données via javascript et les recevoir via JSON. Dans le client, j'ai fait la génération de JSON par simple concaténation de chaînes, et pour l'universalité j'ai décidé de simplement compter les balises définies dans la configuration dans l'ordre. Ensuite, j'ai trouvé un exemple dans lequel une chaîne JSON est demandée toutes les secondes via javascript et ses valeurs sont affichées. En changeant le code pour répondre à mes besoins et en exécutant la page, j'ai vu que tout fonctionne - les données sont mises à jour sans recharger la page (!). C'était une autre victoire. Maintenant, il n'y avait plus grand-chose à faire - distribuer correctement les données reçues sur la page, c'est-à-dire faire quelque chose sous forme de visualisation. Au début j'ai décidé de faire le même tableau,mais j'ai réalisé que la structure du bloc était plus jolie et plus fonctionnelle. Les blocs peuvent être peints de différentes couleurs et redimensionnés. Et vous devez également vous assurer que l'utilisateur peut ajouter et modifier la structure par lui-même, je ne réécrirai pas le fichier HTML pour chaque nouveau souhait. En conséquence, nous avons eu une option comme dans l'image ci-dessous.







Ici, vous pouvez ajouter de grands blocs qui combineront de petits blocs avec une fonction. Ces grands blocs peuvent être intitulés au besoin, changer leurs couleurs (en cliquant sur le bloc tout en maintenant la touche Maj enfoncée) et les redimensionner. Les blocs avec des valeurs sont ajoutés en double-cliquant sur un gros bloc. Vous pouvez également y définir vos propres noms et unités de mesure. Si vous avez accidentellement ajouté le mauvais élément ou au mauvais endroit, vous pouvez le supprimer - j'ai espionné cette fonction dans un bookmarklet, transférant complètement son code sur la page. Bien sûr, toute la structure créée disparaîtra après le rechargement de la page, et pour la sauvegarder, j'ai trouvé une opportunité comme le stockage local. Et afin de transférer la structure finie sur un autre ordinateur, j'ai fait l'importation et l'exportation de l'écran à partir du stockage local.



Le seul problème est resté avec le glisser-déposer de blocs - je voudrais faire un bon glisser-déposer, mais pour moi, cela s'est avéré être trop. Je suis sorti de la situation comme ceci: si vous ouvrez la page dans le panneau de développement dans chrome, les blocs peuvent être glissés. Cela m'a donné l'idée qu'en utilisant le bouton droit de la souris, vous pouvez simplement échanger les blocs. Maintenant, un tel système est assez universel: pour ajouter un nouveau signal, il vous suffit d'ajouter la balise OPC requise à la configuration et de redémarrer le client. La balise ajoutée est automatiquement ajoutée à JSON et une nouvelle valeur apparaît en bas de l'écran de sortie, qui peut être ajoutée en quelques clics à un bloc existant ou nouveau sur la page. Pour le moment, plus de 60 balises sont affichées sur la page et plus de la moitié d'entre elles n'ont plus été ajoutées par moi, c'est-à-dire que le processus d'ajout n'est peut-être pas le plus simple,mais ne nécessite pas de réécrire le programme et la page de sortie. Vous pouvez tester et voir le code de cette page





Étant donné que cet article devrait être comme une instruction sur la façon dont un non-programmeur comme moi peut faire quelque chose d'utile avec l'aide des moteurs de recherche, alors j'ai probablement besoin d'ajouter quelques mots sur la façon exacte dont je cherchais des informations. Ici, il est juste de dire comme sur la photo au tout début: vous pensez ce que vous voulez obtenir et demandez à Google à ce sujet, et si quelque chose ne fonctionne pas quelque part, alors vous regardez les codes d'erreur et demandez à nouveau. Une recherche en anglais aide beaucoup - même en ne tapant que des mots-clés, vous pouvez obtenir un lien vers un problème similaire résolu sur le stackerflow avec une probabilité de 80%. Pour rechercher des exemples prêts à l'emploi, le code à partir duquel vous pouvez bêtement prendre et transférer vers votre programme, vous pouvez ajouter des mots-clés tels que "exemple" ou "exemple" en russe. Plusieurs bonnes idées ont été trouvées sur habr, c'est-à-dire que vous pouvez essayer d'insérer le mot-clé "habr" dans la requête,mais je n'ai utilisé cela que lorsque je savais avec certitude que j'avais vu la solution que je cherchais sur Habré. Presque toutes les petites tâches de tout ce qui a été fait ont été résolues par un moteur de recherche: "changer le div color shift click js", "rendre div redimensionnable", "comment éditer une page Web" ... des centaines de variations de requêtes différentes. Peut-être que dans les commentaires, les pros peuvent partager leurs conseils.



Et oui, puisque nous parlons de conseils, j'aimerais également recevoir des critiques constructives et des conseils utiles de votre part. Peut-être que quelqu'un veut se dégourdir la cervelle et peut proposer une solution beaucoup plus fonctionnelle en quelques heures. Ou peut-être que cet article donnera à quelqu'un des idées intéressantes, car de cette façon, vous pouvez accepter n'importe quelle requête JSON et créer n'importe quelle structure visuelle basée sur celle-ci. Ce serait très cool d'avoir une solution universelle similaire où vous pouvez distribuer toutes les données à votre guise, gérer des formulaires visuels simples, glisser-déposer, redimensionner et tout cela pour le rendre beau et fonctionnel, mais ce n'est pas tout. Même si cela s'est bien passé, je pense. La vitesse de l'unité, telle que demandée par le client, peut maintenant être observée à partir du navigateur et ajouter quelque chose de nouveau ne sera pas difficile.



Lié àcode client en C #



Ou sous le becquet
/*=====================================================================
  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