Googleを使用してOPC2WEBクライアントを作成した方法

私はプロセス制御エンジニアとして働いており、プログラミングが少し好きです。GoogleとStack Overflowの助けを借りて、HTMLとjavascriptでいくつかの計算機を作成し、phpでテレグラムボットを作成し、仕事で少しc#プログラミングを行いました。今回のタスクは、「ブラウザでユニットの現在の速度を確認したい」という単純に聞こえましたが、はるかに面白くて難しいものでした。そもそも、既製のソフトウェアを探すことにしました。もちろん、これは長い間発明されており、Webサーバーとして機能する既製の無料のSCADAシステムもありますが、それらはすべて非常に洗練されていて、理解するのが困難でした。速度を推測します。だから私は自分でそれをやろうと思った、そしてこれがそれから来たものです:



バックエンド



自分で何をするかを決めた後、もう一度検索エンジンを開いて、自分でOPCクライアントを作成する方法を探し始めました。







これを検索すると、habrにたどり着き、無料のOPCDOTNETライブラリについて知りました。ライブラリアーカイブには、コンソールクライアントのソースコードが含まれていました。これは、コンピューターでコンパイルし、単純なOPCシミュレーター(グレーボックス)を起動しました...そして見よ!コンソールで数字が変化するのを見ました。これは、Webリクエストへの応答としてそれらを送信できることを意味します。次にGoogleにアクセスしたのは、HttpListenerの使用例に出くわした単純なWebサーバーのリクエストでした。私は別のプロジェクトで例を実行し、それがどのように機能するかを理解し、これらすべてをOPCクライアントに追加し始めました。 Stack Overflowでエラーを検索してコンパイルを何度も試みた後も、ブラウザで大切な「速度」を確認することができました。勝利でした!しかし、私はすぐに速度だけでは深刻ではないことに気づきました。しばらくすると、技術者はラインの他のパラメータを見たいと思うでしょう。したがって、プログラムを変更せずに必要な信号を追加する方法を理解する必要があります。構成ファイルが役に立ちました。ここでは、表示する信号を事前設定したり、サーバーのリスニングポートを設定したり、時刻を更新したりできます。私はすでに構成ファイルの作成の経験があるので、以前と同じように作成し、うまく機能しました。また、その過程で、変更された値だけでなく、要求されたデータの完全な配列が送信されるようにする方法を提案したプログラマーの友人に連絡する必要がありました(OPCクライアントの完成した例では、変更された値のみがコンソールに表示されました)。私はすでに構成ファイルを作成した経験があるので、以前と同じように作成し、うまく機能しました。また、その過程で、変更された値だけでなく、要求されたデータの完全な配列が送信されるようにする方法を提案したプログラマーの友人に連絡する必要がありました(OPCクライアントの完成した例では、変更された値のみがコンソールに表示されました)。私はすでに構成ファイルの作成の経験があるので、以前と同じように作成し、うまく機能しました。また、その過程で、変更された値だけでなく、要求されたデータの完全な配列が送信されるようにする方法を提案したプログラマーの友人に連絡する必要がありました(OPCクライアントの完成した例では、変更された値のみがコンソールに表示されました)。







このような変更の後、プログラムは構成で要求されたシグナルからHTMLでテーブルの生成を開始しました:ブラウザーを介してこのクライアントが起動されたサーバーのアドレスに接続することにより、隣接する列にシグナルの名前と値を含むテーブルを表示できるようになりました。これはすでに良かったのですが、更新中に値が点滅し、テーブルの形式で構造化されていたにもかかわらず、信号自体が次々と愚かに配置されました。ちなみに、ユーザーがページを更新したときだけでなく、値が毎秒自動的に更新されるように、リクエストに返されたページにRefreshパラメーターを含むメタタグを追加しました。しかし、私は本当に値を自動的に更新し、ページをリロードせずに更新したかったので、バックエンドに加えて、フロントを実行する必要がありました:ユーザーがサーバー上のページを要求し、その中でクライアントへの要求が発生します、そして、ページはこれらすべてを美しく理解しやすい形式で生成します。そこでは、データを好きなように構造化し、色、フォント、サイズを変更できます。このアプローチでは、何でもできます。



Frontend



私はすぐにこれにたどり着きませんでした。最初は、リロードせずにページ上のデータを更新する方法をグーグルで調べ始めました。結局のところ、AJAXを使用する必要があります。つまり、javascriptを介してデータを変更し、JSONを介してそれらを受信する必要があります。クライアントでは、文字列を単純に連結してJSONを生成し、普遍性を保つために、構成に設定されているタグを順番に数えることにしました。次に、JSON文字列がjavascriptを介して毎秒要求され、そこからの値が表示される例を見つけました。ニーズに合わせてコードを変更し、ページを実行すると、すべてが機能することがわかりました。データはページをリロードせずに更新されます(!)。これはまた別の勝利でした。受信したデータをページ上で正しく配布すること、つまり視覚化の形で何かを行うことは、今ではほとんど行うことがありませんでした。最初は同じテーブルを作ることにしました、しかし、その後、ブロック構造がより良く、より機能的に見えることに気づきました。ブロックには色やサイズを変更できます。また、ユーザーが自分で構造を追加および変更できることを確認する必要があります。新しい要望ごとにHTMLファイルを書き直すことはしません。その結果、下の写真のようなオプションが得られました。







ここでは、小さなブロックを1つの機能と組み合わせる大きなブロックを追加できます。このような大きなブロックには、必要に応じてタイトルを付けたり、色を変更したり(Shiftキーを押しながらブロックをクリックすることで)、サイズを変更したりできます。大きなブロックをダブルクリックすると、値のあるブロックが追加されます。独自の名前と測定単位を設定することもできます。誤って間違った要素や間違った場所に追加した場合は、削除できます。この関数を1つのブックマークレットでスパイし、コードをページに完全に転送しました。もちろん、ページをリロードすると、作成された構造全体が消えてしまい、保存するために、ローカルストレージなどの機会を見つけました。そして、完成した構造を別のコンピューターに転送するために、ローカルストレージから画面をインポートおよびエクスポートしました。



唯一の問題は、ブロックのドラッグアンドドロップにありました。素敵なドラッグアンドドロップを作成したいのですが、私にとっては圧倒的でした。私はこのような状況から抜け出しました。開発者パネルでページをクロムで開くと、ブロックをドラッグできます。これにより、マウスの右ボタンを使用することで、ブロックを簡単に交換できるという考えが浮かびました。現在、このようなシステムは非常に普遍的です。新しい信号を追加するには、必要なOPCタグを構成に追加し、クライアントを再起動するだけです。追加されたタグは自動的にJSONに追加され、出力画面の下部に新しい値が表示されます。この値は、ページ上の既存または新しいブロックに数回クリックするだけで追加できます。現時点では、60を超えるタグがページに表示されており、それらの半分以上は私​​が追加していません。つまり、追加のプロセスは最も簡単ではない可能性があります。ただし、プログラムと出力ページを書き直す必要はありません。このページのコードをテストして確認できます





この記事は、私のようなプログラマー以外の人が検索エンジンの助けを借りて何か役立つことを行う方法についての説明のようなものでなければならないので、私が情報を探していた正確さについていくつかの単語を追加する必要があります。ここでは、冒頭の写真のように言うのが正しいです。何を取得したいかを考えてGoogleに質問し、どこかでうまくいかない場合は、エラーコードを見て、もう一度質問します。英語での検索は非常に役立ちます。キーワードだけを入力しても、スタッカーフローで同様に解決された問題へのリンクを80%の確率で取得できます。ばかげてプログラムに転送できるコードである既製の例を検索するには、ロシア語で「example」や「example」などのキーワードを追加します。 habrでいくつかの良いアイデアが見つかりました。つまり、リクエストにキーワード「habr」を挿入してみてください。しかし、これを使用したのは、Habréで探していた解決策を確実に見たときだけでした。実行されたすべての小さなタスクのほとんどすべてが検索エンジンによって解決されました:「divカラーシフトクリックjsの変更」、「divのサイズ変更可能にする」、「Webページの編集方法」...さまざまなクエリの何百ものバリエーション。おそらくコメントで、プロはアドバイスを共有することができます。



はい、アドバイスについて話しているので、建設的な批判と有益なアドバイスも受けたいと思います。おそらく誰かが頭を伸ばしたいと思っていて、数時間ではるかに機能的なソリューションを投入できるでしょう。あるいは、この投稿は誰かに興味深いアイデアを与えるかもしれません。このようにして、JSONリクエストを受け入れ、それに基づいて視覚的な構造を作成できるからです。シンプルなビジュアルフォームの管理、ドラッグアンドドロップ、サイズ変更など、データを適切に配布して美しく機能的にすることができる、同様のユニバーサルソリューションがあると非常に便利ですが、それだけではありません。うまくいきましたが、と思います。顧客の要求に応じて、ユニットの速度をブラウザから確認できるようになり、新しいものを追加することは難しくありません。



へのリンクCのクライアントコード#



またはスポイラーの下
/*=====================================================================
  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