كيف صنعت عميل OPC2WEB باستخدام Google

أنا أعمل كمهندس تحكم في العمليات وأنا مغرم قليلاً بالبرمجة: بمساعدة Google و Stack Overflow ، صنعت العديد من الآلات الحاسبة في HTML وجافا سكريبت ، وأنشأت روبوتًا على telegram في php ، حتى أنني قمت ببرمجة c # في العمل. هذه المرة كانت المهمة أكثر إثارة للاهتمام وأكثر صعوبة ، على الرغم من أنها بدت بسيطة: "أريد أن أرى السرعة الحالية للوحدة في متصفحي". بادئ ذي بدء ، قررت أن أحاول البحث عن برنامج جاهز: بالطبع ، تم اختراع هذا لفترة طويلة ، وهناك أنظمة SCADA جاهزة وحتى مجانية يمكنها العمل كخادم ويب ، لكنها كانت جميعها معقدة للغاية وصعبة على فهمي ، إلى جانب ذلك ، كان ذلك ضروريًا استنتاج السرعة. لذلك اعتقدت أنه يمكنني محاولة القيام بذلك بنفسي ، وإليك ما جاء:



الخلفية



بعد أن قررت ما سأفعله بنفسي ، فتحت محرك البحث مرة أخرى وبدأت في البحث عن كيفية إنشاء عميل OPC بنفسي.







قادني البحث عن هذا إلى habr ، حيث اكتشفت مكتبة OPCDOTNET المجانية. يحتوي أرشيف المكتبة على الكود المصدري لعميل وحدة التحكم ، والذي قمت بتجميعه على جهاز الكمبيوتر الخاص بي ، وأطلق محاكي OPC بسيطًا (صندوق رمادي) ... وها! رأيت الأرقام تتغير في وحدة التحكم. هذا يعني أنه يمكنني الآن إرسالها كرد على طلب ويب. كانت الزيارة التالية إلى Google عبارة عن طلب للحصول على خادم ويب بسيط حيث صادفت مثالاً على استخدام HttpListener. قمت بتشغيل المثال في مشروع منفصل ، وفهمت كيف يعمل ، وبدأت في إضافة كل هذا إلى عميل OPC الخاص بي. بعد عدة محاولات في التجميع والبحث عن الأخطاء في Stack Overflow ، تمكنت من رؤية "السرعة" العزيزة في المتصفح. كان انتصارا! لكنني أدركت على الفور أن السرعة وحدها ليست خطيرة ، بعد فترة سيرغب التقنيون في رؤية معلمات أخرى للخط ،لذلك ، تحتاج إلى معرفة كيفية إضافة الإشارات الضرورية دون تغيير البرنامج. جاءت ملفات التكوين للإنقاذ ، حيث يمكنك تحديد الإشارات التي نريد رؤيتها مسبقًا ، وتعيين منفذ الاستماع للخادم ، ووقت التحديث ، وما إلى ذلك. لدي بالفعل خبرة في إنشاء ملفات التكوين ، لذلك قمت بذلك كما فعلت من قبل وعملت بشكل جيد. أيضًا ، في هذه العملية ، اضطررت إلى الاتصال بصديق للمبرمج ، الذي اقترح ما يجب فعله حتى يتم إرسال مجموعة كاملة من البيانات المطلوبة ، وليس فقط تلك القيم التي تغيرت (في المثال النهائي لعميل OPC ، تم عرض القيم المتغيرة فقط في وحدة التحكم).لدي بالفعل خبرة في إنشاء ملفات التكوين ، لذلك فعلت ذلك كما فعلت من قبل وعمل بشكل جيد. أيضًا ، في هذه العملية ، اضطررت إلى الاتصال بصديق للمبرمج ، الذي اقترح ما يجب فعله حتى يتم إرسال مجموعة كاملة من البيانات المطلوبة ، وليس فقط تلك القيم التي تغيرت (في المثال النهائي لعميل OPC ، تم عرض القيم المتغيرة فقط في وحدة التحكم).لدي بالفعل خبرة في إنشاء ملفات التكوين ، لذلك فعلت ذلك كما فعلت من قبل وعمل بشكل جيد. أيضًا ، في هذه العملية ، اضطررت إلى الاتصال بصديق للمبرمج ، الذي اقترح ما يجب فعله حتى يتم إرسال مجموعة كاملة من البيانات المطلوبة ، وليس فقط تلك القيم التي تغيرت (في المثال النهائي لعميل OPC ، تم عرض القيم المتغيرة فقط في وحدة التحكم).







بعد هذه التغييرات ، بدأ البرنامج في إنشاء جدول بتنسيق HTML من الإشارات المطلوبة في التكوين: من خلال الاتصال بعنوان الخادم حيث تم إطلاق هذا العميل من خلال المتصفح ، أصبح من الممكن الآن رؤية جدول يحتوي على أسماء الإشارات والقيم في العمود المجاور. كان هذا جيدًا بالفعل ، لكن القيم تراجعت أثناء التحديث ، وكانت الإشارات نفسها موجودة بغباء واحدة تلو الأخرى ، على الرغم من أنها تم تنظيمها في شكل جدول. بالمناسبة ، حتى يتم تحديث القيم تلقائيًا كل ثانية ، وليس فقط عندما يقوم المستخدم بتحديث الصفحة ، أضفت علامة وصفية مع معلمة التحديث إلى الصفحة التي تم إرجاعها إلى الطلب. لكنني أردت حقًا تحديث القيم تلقائيًا وبدون إعادة تحميل الصفحة ، لذلك بالإضافة إلى الواجهة الخلفية ، كان من الضروري الآن إجراء المقدمة: يطلب المستخدم صفحة على الخادم ، يتم من خلالها طلب العميل ،وتقوم الصفحة بعد ذلك بإنشاء كل هذا بشكل جميل ومفهوم ، حيث يمكنك هيكلة البيانات كما تريد وتغيير الألوان والخطوط والأحجام - يمكنك فعل أي شيء على الإطلاق باستخدام هذا الأسلوب.



Frontend



لم أتوصل إلى هذا على الفور: في البداية بدأت في البحث في Google عن كيفية تحديث البيانات الموجودة على الصفحة دون إعادة التحميل. كما اتضح ، تحتاج إلى استخدام AJAX ، أي تغيير البيانات عبر جافا سكريبت ، واستلامها عبر JSON. في العميل ، قمت بإنشاء JSON من خلال سلسلة بسيطة من السلاسل ، ومن أجل العالمية قررت ببساطة حساب العلامات المحددة في التكوين بالترتيب. ثم وجدت مثالًا يُطلب فيه سلسلة JSON كل ثانية من خلال جافا سكريبت ويتم عرض القيم منها. عند تغيير الكود ليناسب احتياجاتي وتشغيل الصفحة ، رأيت أن كل شيء يعمل - يتم تحديث البيانات دون إعادة تحميل الصفحة (!). كان هذا نصرًا آخر. الآن لم يكن هناك الكثير للقيام به - لتوزيع البيانات المستلمة على الصفحة بشكل صحيح ، أي القيام بشيء في شكل تصور. في البداية قررت صنع نفس الطاولة ،ولكن بعد ذلك أدركت أن بنية الكتلة تبدو أجمل وأكثر فاعلية. يمكن طلاء الكتل بألوان مختلفة وتغيير حجمها. وتحتاج أيضًا إلى التأكد من أن المستخدم يمكنه إضافة الهيكل وتغييره بمفرده ، ولن أعيد كتابة ملف HTML لكل رغبة جديدة. نتيجة لذلك ، حصلنا على مثل هذا الخيار كما في الصورة أدناه.







هنا يمكنك إضافة كتل كبيرة تجمع بين الكتل الصغيرة وميزة واحدة. يمكن تسمية هذه الكتل الكبيرة حسب الحاجة ، قم بتغيير ألوانها (إذا قمت بالنقر فوق الكتلة أثناء الضغط باستمرار على مفتاح Shift) وقم بتغيير حجمها. تتم إضافة الكتل ذات القيم بالنقر المزدوج على كتلة كبيرة. يمكنك أيضًا تعيين الأسماء الخاصة بك ووحدات القياس فيها. إذا أضفت عنصرًا خاطئًا عن طريق الخطأ أو في مكان خاطئ ، فيمكنك حذفه - لقد تجسست هذه الوظيفة في إشارة مرجعية واحدة ، وقمت بنقل كودها بالكامل إلى الصفحة. بالطبع ، سيختفي الهيكل الذي تم إنشاؤه بالكامل بعد إعادة تحميل الصفحة ، ولحفظه ، وجدت فرصة مثل التخزين المحلي. ومن أجل نقل الهيكل النهائي إلى كمبيوتر آخر ، قمت باستيراد وتصدير الشاشة من التخزين المحلي.



بقيت المشكلة الوحيدة في سحب الكتل وإسقاطها - أود أن أقوم بسحب وإسقاط لطيفين ، لكن بالنسبة لي اتضح أنها ساحقة. لقد خرجت من الموقف مثل هذا: إذا فتحت الصفحة في لوحة المطورين بالكروم ، فيمكن سحب الكتل. أعطاني هذا فكرة أنه باستخدام زر الماوس الأيمن ، يمكنك ببساطة تبديل الكتل. أصبح مثل هذا النظام الآن عالميًا تمامًا: لإضافة إشارة جديدة ، تحتاج فقط إلى إضافة علامة OPC المطلوبة إلى التكوين وإعادة تشغيل العميل. تتم إضافة العلامة المضافة تلقائيًا إلى JSON وتظهر قيمة جديدة في الجزء السفلي من شاشة الإخراج ، والتي يمكن إضافتها ببضع نقرات إلى كتلة موجودة أو جديدة على الصفحة. في الوقت الحالي ، يتم عرض أكثر من 60 علامة على الصفحة ولم تتم إضافة أكثر من نصفها بواسطتي ، أي أن عملية الإضافة قد لا تكون أسهل ،ولكنها لا تتطلب إعادة كتابة البرنامج وصفحة الإخراج. يمكنك اختبار ورؤية رمز هذه الصفحة





نظرًا لأن هذه المقالة يجب أن تكون بمثابة تعليمات حول كيفية قيام شخص غير مبرمج مثلي بعمل شيء مفيد بمساعدة محركات البحث ، فربما أحتاج إلى إضافة بضع كلمات حول كيفية بحثي عن المعلومات بالضبط. من الصواب هنا أن نقول كما في الصورة في البداية: أنت تفكر في ما تريد الحصول عليه واسأل Google عنه ، وإذا لم ينجح شيء ما في مكان ما ، فأنت تنظر إلى رموز الخطأ وتسأل مرة أخرى. يساعد البحث باللغة الإنجليزية كثيرًا - حتى عن طريق كتابة الكلمات الرئيسية فقط ، يمكنك الحصول على رابط لمشكلة تم حلها مماثلة في تدفق المكدس باحتمال 80٪. للبحث عن أمثلة جاهزة ، الشفرة التي يمكنك أخذها ونقلها بغباء إلى برنامجك ، يمكنك إضافة كلمات رئيسية مثل "مثال" أو "مثال" باللغة الروسية. تم العثور على العديد من الأفكار الجيدة في habr ، أي يمكنك محاولة إدخال الكلمة الرئيسية "habr" في الطلب ،لكنني استخدمت هذا فقط عندما علمت على وجه اليقين أنني رأيت الحل الذي كنت أبحث عنه في حبري. تم حل أي مهمة صغيرة تقريبًا من كل ما تم إجراؤه من خلال محرك بحث: "تغيير div color shift click js" ، "اجعل div يمكن تغيير حجمه" ، "كيفية تحرير صفحة ويب" ... مئات الأشكال المختلفة من الاستعلامات المختلفة. ربما في التعليقات يمكن للمحترفين مشاركة نصائحهم.



ونعم ، بما أننا نتحدث عن النصيحة ، أود أيضًا أن أتلقى النقد البناء والنصائح المفيدة منك. ربما يريد شخص ما تمديد أدمغته ويكون قادرًا على طرح حل أكثر فاعلية في غضون ساعتين. أو ربما يقدم هذا المنشور لشخص ما بعض الأفكار المثيرة للاهتمام ، لأنه بهذه الطريقة يمكنك قبول أي طلب 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