Backend
Nachdem ich mich entschieden hatte, was ich selbst tun würde, öffnete ich die Suchmaschine erneut und suchte nach Möglichkeiten, meinen eigenen OPC-Client selbst zu erstellen.
Die Suche danach führte mich zu habr, wo ich von der kostenlosen OPCDOTNET-Bibliothek erfuhr. Das Bibliotheksarchiv enthielt den Quellcode des Konsolenclients, den ich auf meinem Computer kompiliert und einen einfachen OPC-Simulator (Gray-Box) gestartet habe ... und siehe da! Ich sah Zahlen in der Konsole ändern. Dies bedeutet, dass ich sie jetzt als Antwort auf eine Webanforderung senden kann. Der nächste Besuch bei Google war eine Anfrage nach einem einfachen Webserver, auf dem ich ein Beispiel für die Verwendung des HttpListener fand. Ich habe das Beispiel in einem separaten Projekt ausgeführt, verstanden, wie es funktioniert, und habe begonnen, all dies meinem OPC-Client hinzuzufügen. Nach vielen Kompilierungsversuchen, bei denen nach Fehlern im Stapelüberlauf gesucht wurde, gelang es mir immer noch, die geschätzte "Geschwindigkeit" im Browser zu sehen. Es war ein Sieg! Aber mir wurde sofort klar, dass Geschwindigkeit allein nicht ernst ist. Nach einer Weile werden Technologen andere Parameter der Linie sehen wollen.Daher müssen Sie herausfinden, wie Sie die erforderlichen Signale hinzufügen können, ohne das Programm zu ändern. Die Konfigurationsdateien wurden zur Rettung bereitgestellt, in denen Sie festlegen können, welche Signale angezeigt werden sollen, den Server-Überwachungsport festlegen, die Aktualisierungszeit festlegen usw. Ich hatte bereits Erfahrung mit dem Erstellen von Konfigurationsdateien, also habe ich es wie zuvor gemacht und es hat gut funktioniert. Außerdem musste ich mich an einen Freund des Programmierers wenden, der vorschlug, was zu tun ist, damit das gesamte Array der angeforderten Daten übertragen wird und nicht nur die Werte, die sich geändert haben (im fertigen Beispiel des OPC-Clients wurden nur die geänderten Werte in der Konsole angezeigt).Ich hatte bereits Erfahrung mit dem Erstellen von Konfigurationsdateien, also habe ich es wie zuvor gemacht und es hat gut funktioniert. Außerdem musste ich mich an einen Freund des Programmierers wenden, der vorschlug, was zu tun ist, damit das gesamte Array der angeforderten Daten übertragen wird und nicht nur die Werte, die sich geändert haben (im fertigen Beispiel des OPC-Clients wurden nur die geänderten Werte in der Konsole angezeigt).Ich hatte bereits Erfahrung mit dem Erstellen von Konfigurationsdateien, also habe ich es wie zuvor gemacht und es hat gut funktioniert. Außerdem musste ich mich an einen Freund des Programmierers wenden, der vorschlug, was zu tun ist, damit das gesamte Array der angeforderten Daten übertragen wird und nicht nur die Werte, die sich geändert haben (im fertigen Beispiel des OPC-Clients wurden nur die geänderten Werte in der Konsole angezeigt).
Nach solchen Änderungen begann das Programm, aus den in der Konfiguration angeforderten Signalen eine Tabelle in HTML zu generieren: Durch Kontaktaufnahme mit der Adresse des Servers, auf dem dieser Client über den Browser gestartet wurde, konnte nun die Tabelle mit den Namen der Signale und Werte in der nebenstehenden Spalte angezeigt werden. Das war schon gut, aber die Werte blinkten während des Updates und die Signale selbst wurden dumm nacheinander lokalisiert, obwohl sie in Form einer Tabelle strukturiert waren. Übrigens, damit die Werte automatisch jede Sekunde aktualisiert werden und nicht nur, wenn der Benutzer die Seite aktualisiert, habe ich der Seite, die an die Anforderung zurückgegeben wurde, ein Meta-Tag mit dem Parameter Aktualisieren hinzugefügt. Aber ich wollte wirklich, dass die Werte automatisch und ohne erneutes Laden der Seite aktualisiert werden. Deshalb musste ich jetzt zusätzlich zum Backend die Front ausführen: Der Benutzer fordert eine Seite auf dem Server an, in der eine Anforderung an den Client erfolgt.und die Seite generiert all dies in einer schönen und verständlichen Form, in der Sie die Daten nach Belieben strukturieren, Farben, Schriftarten und Größen ändern können - mit diesem Ansatz können Sie überhaupt alles tun.
Frontend
Ich bin nicht sofort darauf gekommen: Zuerst begann ich zu googeln, wie man die Daten auf der Seite aktualisiert, ohne sie neu zu laden. Wie sich herausstellte, müssen Sie AJAX verwenden, dh die Daten über Javascript ändern und über JSON empfangen. Im Client habe ich die Generierung von JSON durch einfache Verkettung von Zeichenfolgen vorgenommen und aus Gründen der Universalität beschlossen, die in der Konfiguration festgelegten Tags einfach der Reihe nach zu zählen. Dann habe ich ein Beispiel gefunden, in dem jede Sekunde eine JSON-Zeichenfolge über Javascript angefordert wird und Werte daraus angezeigt werden. Als ich den Code an meine Bedürfnisse anpasste und die Seite ausführte, sah ich, dass alles funktioniert - die Daten werden aktualisiert, ohne die Seite neu zu laden (!). Dies war ein weiterer Sieg. Jetzt gab es wenig zu tun - die empfangenen Daten auf der Seite korrekt zu verteilen, dh etwas in Form einer Visualisierung zu tun. Zuerst habe ich beschlossen, den gleichen Tisch zu machen,aber dann wurde mir klar, dass die Blockstruktur schöner und funktionaler aussieht. Blöcke können eingefärbt und in der Größe geändert werden. Außerdem müssen Sie sicherstellen, dass der Benutzer die Struktur selbst hinzufügen und ändern kann. Ich werde die HTML-Datei nicht für jeden neuen Wunsch neu schreiben. Als Ergebnis haben wir eine Option wie im Bild unten erhalten.
Hier können Sie große Blöcke hinzufügen, die kleine Blöcke mit einer Funktion kombinieren. Solche großen Blöcke können nach Bedarf betitelt werden, ihre Farben können geändert werden (indem Sie bei gedrückter Umschalttaste auf den Block klicken) und ihre Größe kann geändert werden. Blöcke mit Werten werden durch Doppelklick auf einen großen Block hinzugefügt. Sie können darin auch Ihre eigenen Namen und Maßeinheiten festlegen. Wenn Sie versehentlich das falsche Element oder an der falschen Stelle hinzugefügt haben, können Sie es löschen. Ich habe diese Funktion in einem Lesezeichen ausspioniert und den Code vollständig auf die Seite übertragen. Natürlich verschwindet die gesamte erstellte Struktur nach dem erneuten Laden der Seite, und um sie zu speichern, habe ich eine Möglichkeit wie den lokalen Speicher gefunden. Und um die fertige Struktur auf einen anderen Computer zu übertragen, habe ich den Bildschirm aus dem lokalen Speicher importiert und exportiert.
Das einzige Problem blieb beim Ziehen und Ablegen von Blöcken - ich würde gerne ein schönes Drag & Drop machen, aber für mich stellte sich heraus, dass es zu viel war. Ich bin so aus der Situation herausgekommen: Wenn Sie die Seite im Entwicklerfenster in Chrom öffnen, können die Blöcke gezogen werden. Dies brachte mich auf die Idee, dass Sie mit der rechten Maustaste einfach die Blöcke tauschen können. Jetzt ist ein solches System ziemlich universell: Um ein neues Signal hinzuzufügen, müssen Sie nur das erforderliche OPC-Tag zur Konfiguration hinzufügen und den Client neu starten. Das hinzugefügte Tag wird automatisch zu JSON hinzugefügt, und am unteren Rand des Ausgabebildschirms wird ein neuer Wert angezeigt, der mit wenigen Klicks zu einem vorhandenen oder neuen Block auf der Seite hinzugefügt werden kann. Im Moment werden mehr als 60 Tags auf der Seite angezeigt und mehr als die Hälfte davon wurde von mir nicht hinzugefügt, das heißt, der Vorgang des Hinzufügens ist möglicherweise nicht der einfachste.Das Programm und die Ausgabeseite müssen jedoch nicht neu geschrieben werden. Sie können den Code dieser Seite testen und anzeigen
Da dieser Artikel wie eine Anleitung sein sollte, wie ein Nicht-Programmierer wie ich mit Hilfe von Suchmaschinen etwas Nützliches tun kann, muss ich wahrscheinlich ein paar Worte hinzufügen, wie genau ich nach Informationen gesucht habe. Hier ist es genau richtig zu sagen, wie auf dem Bild ganz am Anfang: Sie überlegen, was Sie bekommen möchten, und fragen Google danach. Wenn irgendwo etwas nicht funktioniert, schauen Sie sich die Fehlercodes an und fragen Sie erneut. Die Suche auf Englisch hilft sehr - selbst wenn Sie nur Schlüsselwörter eingeben, können Sie mit einer Wahrscheinlichkeit von 80% einen Link zu einem ähnlich gelösten Problem im Stackerflow erhalten. Um nach vorgefertigten Beispielen zu suchen, dem Code, den Sie dumm nehmen und in Ihr Programm übertragen können, können Sie Schlüsselwörter wie "Beispiel" oder "Beispiel" auf Russisch hinzufügen. Auf habr wurden mehrere gute Ideen gefunden, dh Sie können versuchen, das Schlüsselwort "habr" in die Anfrage einzufügen.Aber ich habe dies nur verwendet, als ich sicher war, dass ich die Lösung sah, nach der ich bei Habré gesucht hatte. Fast jede kleine Aufgabe von allem, was getan wurde, wurde über eine Suchmaschine gelöst: "Div Color Shift Click Js ändern", "Div Resizeable machen", "Wie man eine Webseite bearbeitet" ... Hunderte von Variationen verschiedener Abfragen. Vielleicht können die Profis in den Kommentaren ihre Ratschläge teilen.
Und ja, da es sich um Ratschläge handelt, möchte ich auch konstruktive Kritik und nützliche Ratschläge von Ihnen erhalten. Vielleicht möchte jemand sein Gehirn dehnen und kann in ein paar Stunden eine viel funktionalere Lösung finden. Oder vielleicht gibt dieser Beitrag jemandem einige interessante Ideen, denn auf diese Weise können Sie jede JSON-Anfrage annehmen und eine darauf basierende visuelle Struktur erstellen. Es wäre sehr cool, eine ähnliche universelle Lösung zu haben, mit der Sie beliebige Daten nach Belieben verteilen, einfache visuelle Formulare verwalten, ziehen und ablegen, die Größe ändern und all diese Dinge, um sie schön und funktional zu machen, aber das ist noch nicht alles. Obwohl es gut ausgegangen ist, denke ich. Die vom Kunden angeforderte Geschwindigkeit des Geräts kann jetzt über den Browser beobachtet werden, und das Hinzufügen von Neuem ist nicht schwierig.
Link zuClient-Code in C #
Oder unter dem 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>
*/