Bagaimana saya membuat klien OPC2WEB menggunakan Google

Saya bekerja sebagai insinyur kontrol proses dan sedikit menyukai pemrograman: dengan bantuan Google dan Stack Overflow, saya membuat beberapa kalkulator dalam HTML dan javascript, membuat bot telegram di php, bahkan diprogram sedikit dalam c # di tempat kerja. Kali ini tugasnya jauh lebih menarik dan rumit, meskipun kedengarannya sederhana: "Saya ingin melihat kecepatan unit saat ini di browser saya". Pertama-tama, saya memutuskan untuk mencoba mencari perangkat lunak yang sudah jadi: tentu saja, ini sudah lama ditemukan, ada sistem SCADA yang sudah jadi dan bahkan gratis yang dapat berfungsi sebagai server web, tetapi semuanya sangat canggih dan sulit untuk pemahaman saya, selain itu, itu hanya perlu menyimpulkan kecepatan. Jadi saya pikir saya bisa mencoba melakukannya sendiri, dan inilah hasilnya:



Backend



Setelah saya memutuskan apa yang akan saya lakukan sendiri, saya membuka mesin pencari lagi dan mulai mencari cara membuat klien OPC saya sendiri.







Pencarian ini membawa saya ke habr, di mana saya mengetahui tentang perpustakaan OPCDOTNET gratis. Arsip perpustakaan berisi kode sumber klien konsol, yang saya kompilasi di komputer saya, meluncurkan simulator OPC sederhana (kotak abu-abu) ... dan lihatlah! Saya melihat nomor berubah di konsol. Artinya, sekarang saya dapat mengirimkannya sebagai tanggapan atas permintaan web. Kunjungan berikutnya ke Google adalah permintaan untuk server web sederhana di mana saya menemukan contoh penggunaan HttpListener. Saya menjalankan contoh dalam proyek terpisah, memahami cara kerjanya, dan mulai menambahkan semua ini ke klien OPC saya. Setelah beberapa kali mencoba menyusun, mencari kesalahan pada Stack Overflow, saya masih berhasil melihat "kecepatan" yang disayangi di browser. Itu adalah kemenangan! Tetapi saya segera menyadari bahwa kecepatan saja tidak serius, setelah beberapa saat para ahli teknologi ingin melihat parameter garis lainnya,oleh karena itu, Anda perlu mencari cara untuk menambahkan sinyal yang diperlukan tanpa mengubah program. File konfigurasi datang untuk menyelamatkan, di mana Anda dapat mengatur sinyal apa yang ingin kita lihat, mengatur port pendengaran server, waktu pembaruan, dan sebagainya. Saya sudah memiliki pengalaman dalam membuat file konfigurasi, jadi saya melakukannya seperti yang saya lakukan sebelumnya dan itu bekerja dengan baik. Juga, dalam prosesnya, saya harus menghubungi seorang teman programmer, yang menyarankan apa yang harus dilakukan sehingga seluruh data yang diminta dikirim, dan tidak hanya nilai yang berubah (dalam contoh akhir dari klien OPC, hanya nilai yang diubah yang ditampilkan di konsol).Saya sudah memiliki pengalaman dalam membuat file konfigurasi, jadi saya melakukannya seperti yang saya lakukan sebelumnya dan itu bekerja dengan baik. Juga, dalam prosesnya, saya harus menghubungi seorang teman programmer, yang menyarankan apa yang harus dilakukan sehingga seluruh data yang diminta dikirim, dan tidak hanya nilai yang berubah (dalam contoh akhir dari klien OPC, hanya nilai yang diubah yang ditampilkan di konsol).Saya sudah memiliki pengalaman dalam membuat file konfigurasi, jadi saya melakukannya seperti yang saya lakukan sebelumnya dan itu bekerja dengan baik. Juga, dalam prosesnya, saya harus menghubungi seorang teman programmer, yang menyarankan apa yang harus dilakukan sehingga seluruh data yang diminta dikirim, dan tidak hanya nilai yang berubah (dalam contoh akhir dari klien OPC, hanya nilai yang diubah yang ditampilkan di konsol).







Setelah perubahan tersebut, program mulai membuat tabel dalam HTML dari sinyal yang diminta dalam konfigurasi: dengan menghubungi alamat server tempat klien ini diluncurkan melalui browser, sekarang dimungkinkan untuk melihat tabel yang berisi nama sinyal dan nilai di kolom yang berdekatan. Ini sudah bagus, tetapi nilainya berkedip selama pembaruan, dan sinyal itu sendiri ditempatkan dengan bodoh satu demi satu, meskipun mereka terstruktur dalam bentuk tabel. Ngomong-ngomong, agar nilai diperbarui secara otomatis setiap detik, dan tidak hanya saat pengguna menyegarkan halaman, saya menambahkan tag meta dengan parameter Refresh ke halaman yang dikembalikan ke permintaan. Tetapi saya benar-benar ingin nilai diperbarui secara otomatis dan tanpa memuat ulang halaman, jadi perlu untuk melakukan front selain backend sekarang: pengguna meminta halaman di server, di mana permintaan ke klien terjadi,dan laman tersebut kemudian menghasilkan semua ini dalam bentuk yang indah dan mudah dipahami, di mana Anda dapat menyusun data sesuka Anda, mengubah warna, font, dan ukuran - Anda dapat melakukan apa saja dengan pendekatan ini.



Frontend



Saya tidak langsung membahas ini: pada awalnya saya mulai mencari di Google cara membuat data di halaman diperbarui tanpa memuat ulang. Ternyata, Anda perlu menggunakan AJAX, yaitu mengubah data melalui javascript, dan menerimanya melalui JSON. Di klien, saya membuat pembuatan JSON dengan penggabungan string sederhana, dan untuk universalitas saya memutuskan untuk menghitung tag yang disetel dalam konfigurasi secara berurutan. Kemudian saya menemukan contoh di mana string JSON diminta setiap detik melalui javascript dan nilai darinya ditampilkan. Mengubah kode agar sesuai dengan kebutuhan saya dan menjalankan halaman, saya melihat bahwa semuanya berfungsi - data diperbarui tanpa memuat ulang halaman (!). Ini adalah kemenangan lain. Sekarang hanya ada sedikit yang harus dilakukan - mendistribusikan dengan benar data yang diterima pada halaman, yaitu melakukan sesuatu dalam bentuk visualisasi. Awalnya saya memutuskan untuk membuat meja yang sama,tetapi kemudian saya menyadari bahwa struktur balok terlihat lebih bagus dan lebih fungsional. Blok dapat dicat dengan warna berbeda dan diubah ukurannya. Dan Anda juga perlu memastikan bahwa pengguna dapat menambah dan mengubah strukturnya sendiri, saya tidak akan menulis ulang file HTML untuk setiap keinginan baru. Hasilnya, kami mendapat opsi seperti pada gambar di bawah.







Di sini Anda dapat menambahkan blok besar yang akan menggabungkan blok kecil dengan satu fitur. Blok besar tersebut dapat diberi judul sesuai kebutuhan, warnanya dapat diubah (dengan mengklik blok sambil menahan tombol shift) dan ukurannya dapat diubah. Blok dengan nilai ditambahkan dengan mengklik dua kali pada blok besar. Anda juga dapat mengatur nama dan unit pengukuran Anda sendiri di dalamnya. Jika Anda secara tidak sengaja menambahkan elemen yang salah atau di tempat yang salah, Anda dapat menghapusnya - Saya melihat fungsi ini dalam satu bookmarklet, sepenuhnya mentransfer kodenya ke halaman. Tentu saja, seluruh struktur yang dibuat akan hilang setelah memuat ulang halaman dan untuk menyimpannya, saya menemukan peluang seperti penyimpanan lokal. Dan untuk mentransfer struktur yang sudah jadi ke komputer lain, saya melakukan impor dan ekspor layar dari penyimpanan lokal.



Satu-satunya masalah tetap dengan menyeret dan menjatuhkan balok - Saya ingin membuat seret dan lepas yang bagus, tetapi bagi saya hal itu ternyata luar biasa. Saya keluar dari situasi seperti ini: jika Anda membuka halaman di panel pengembang di chrome, maka blok dapat diseret. Ini memberi saya gagasan bahwa dengan menggunakan tombol kanan mouse, Anda dapat dengan mudah menukar blok. Sekarang sistem seperti itu cukup universal: untuk menambahkan sinyal baru, Anda hanya perlu menambahkan tag OPC yang diperlukan ke konfigurasi dan memulai ulang klien. Tag yang ditambahkan secara otomatis ditambahkan ke JSON dan nilai baru muncul di bagian bawah layar keluaran, yang dapat ditambahkan ke blok baru atau yang sudah ada di halaman dengan beberapa klik. Saat ini, lebih dari 60 tag ditampilkan di halaman dan lebih dari setengahnya tidak saya tambahkan, artinya, proses penambahan mungkin bukan yang termudah,tetapi tidak memerlukan penulisan ulang program dan halaman keluaran. Anda dapat menguji dan melihat kode halaman ini





Karena artikel ini seharusnya seperti instruksi tentang bagaimana seorang non-programmer seperti saya dapat melakukan sesuatu yang berguna dengan bantuan mesin pencari, maka saya mungkin perlu menambahkan beberapa kata tentang bagaimana sebenarnya saya mencari informasi. Ini tepat untuk dikatakan seperti pada gambar di awal: Anda memikirkan apa yang ingin Anda dapatkan dan bertanya kepada Google tentang hal itu, dan jika sesuatu tidak berhasil di suatu tempat, maka Anda melihat kode kesalahan dan bertanya lagi. Pencarian dalam bahasa Inggris sangat membantu - bahkan dengan mengetikkan kata kunci saja, Anda bisa mendapatkan tautan ke masalah yang diselesaikan serupa di stackerflow dengan probabilitas 80%. Untuk mencari contoh yang sudah jadi, kode yang dengan bodoh dapat Anda ambil dan transfer ke program Anda, Anda dapat menambahkan kata kunci seperti "contoh" atau "contoh" dalam bahasa Rusia. Beberapa ide bagus ditemukan di habr, yaitu, Anda dapat mencoba memasukkan kata kunci "habr" ke dalam permintaan,tetapi saya menggunakan ini hanya jika saya tahu pasti bahwa saya melihat solusi yang saya cari di habr. Hampir semua tugas kecil dari semua yang telah dilakukan diselesaikan melalui mesin pencari: "ubah div color shift klik js", "buat div dapat diubah ukurannya", "cara mengedit halaman web" ... ratusan variasi kueri yang berbeda. Mungkin di komentar para profesional bisa membagikan saran mereka.



Dan ya, karena kita berbicara tentang nasihat, saya juga ingin menerima kritik yang membangun dan nasihat yang berguna dari Anda. Mungkin seseorang ingin meregangkan otaknya dan mampu memberikan solusi yang jauh lebih fungsional dalam beberapa jam. Atau mungkin posting ini akan memberi seseorang beberapa ide menarik, karena dengan cara ini Anda dapat menerima permintaan JSON apa pun dan membuat struktur visual apa pun berdasarkan itu. Akan sangat keren untuk memiliki solusi universal serupa di mana Anda dapat mendistribusikan data apa pun yang sesuai untuk Anda, mengelola bentuk visual sederhana, seret dan lepas, ubah ukuran dan semua hal itu untuk membuatnya cantik dan fungsional, tetapi bukan itu saja. Meski ternyata baik-baik saja, menurutku. Kecepatan unit, seperti yang diminta oleh pelanggan, sekarang dapat diamati dari browser dan menambahkan sesuatu yang baru tidak akan sulit.



Tautan kekode klien di C #



Atau di bawah 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