Backend
After I decided what I would do myself, I opened the search engine again and started looking for how to make my own OPC client myself.
Searches for this led me to habr, where I found out about the free OPCDOTNET library. The library archive contained the source code of the console client, which I compiled on my computer, launched a simple OPC simulator (gray-box) ... and lo and behold! I saw numbers changing in the console. This means that now I can send them as a response to a web request. The next visit to Google was a request for a simple web server where I came across an example of using the HttpListener. I ran the example in a separate project, understood how it works, and began to add all this to my OPC client. After many attempts at compiling, searching for errors on Stack Overflow, I still managed to see the cherished "speed" in the browser. It was a victory! But I immediately realized that speed alone is not serious, after a while technologists will want to see other parameters of the line,therefore, you need to figure out how to add the necessary signals without changing the program. The configuration files came to the rescue, where you can set in advance what signals we want to see, set the server listening port, update time, and so on. I already had experience in creating configuration files, so I did it as I did before and it worked well. Also, in the process, I had to contact a friend of the programmer, who suggested what to do so that the full array of requested data was transmitted, and not only those values that changed (in the finished example of the OPC client, only the changed values were displayed in the console).I already had experience in creating configuration files, so I did it as I did before and it worked well. Also, in the process, I had to contact a friend of the programmer, who suggested what to do so that the full array of requested data was transmitted, and not only those values that changed (in the finished example of the OPC client, only the changed values were displayed in the console).I already had experience in creating configuration files, so I did it as I did before and it worked well. Also, in the process, I had to contact a friend of the programmer, who suggested what to do so that the full array of requested data was transmitted, and not only those values that changed (in the finished example of the OPC client, only the changed values were displayed in the console).
After such changes, the program began to generate a table in HTML from the signals requested in the config: by contacting the address of the server where this client was launched through the browser, it was now possible to see the table containing the names of signals and values in the adjacent column. This was already good, but the values blinked during the update, and the signals themselves were stupidly located one after the other, although they were structured in the form of a table. By the way, so that the values are updated automatically every second, and not only when the user refreshes the page, I added a meta tag with the Refresh parameter to the page returned to the request. But I really wanted the values to be updated automatically and without reloading the page, so in addition to the backend, now it was necessary to do the front: the user requests a page on the server, inside which a request to the client occurs,and the page then generates all this in a beautiful and understandable form, where you can structure the data as you like, change colors, fonts and sizes - you can do anything at all with this approach.
Frontend
I didn't come to this right away: at first I began to google how to make the data on the page update without reloading. As it turned out, you need to use AJAX, that is, change the data through javascript, and receive them through JSON. In the client, I made the generation of JSON by simple concatenation of strings, and for universality I decided to simply count the tags set in the config in order. Then I found an example in which a JSON string is requested every second via javascript and values from it are displayed. Changing the code to fit my needs and running the page, I saw that everything works - the data is updated without reloading the page (!). This was another victory. Now there was little to do - to correctly distribute the received data on the page, that is, to do something in the form of visualization. At first I decided to make the same table,but then I realized that the block structure looks nicer and more functional. Blocks can be colored and resized. And you also need to make it so that the user can independently add and change the structure, I won't rewrite the HTML file for every new wish. As a result, we got such an option as in the picture below.
Here you can add large blocks that will combine small blocks with one feature. Such large blocks can be titled as needed, change their colors (if you click on the block while holding down the shift key) and change their size. Blocks with values are added by double clicking on a large block. You can also set your own names and units of measurement in them. If you accidentally added the wrong element or in the wrong place, you can delete it - I spied this function in one bookmarklet, completely transferring its code to the page. Of course, the entire created structure will disappear after reloading the page, and to save it, I found such an opportunity as local storage. And in order to transfer the finished structure to another computer, I made import and export of the screen from the local storage.
The only problem remained with dragging and dropping blocks - I would like to make a nice drag and drop, but for me it turned out to be overwhelming. I got out of the situation like this: if you open the page in the developer panel in chrome, then the blocks can be dragged. This gave me the idea that by using the right mouse button, you can simply swap the blocks. Now such a system is quite universal: to add a new signal, you just need to add the required OPC tag to the config and restart the client. The added tag is automatically added to JSON and a new value appears at the bottom of the output screen, which can be added to an existing or new block on the page with a few clicks. At the moment, more than 60 tags are displayed on the page and more than half of them were not added by me, that is, the process of adding may not be the easiest,but does not require rewriting the program and the output page. You can test and see the code of this page
Since this article should be like an instruction on how a non-programmer like me can do something useful with the help of search engines, then I probably need to add a few words about how exactly I was looking for information. Here it is just right to say as in the picture at the very beginning: you think what you want to get and ask Google about it, and if something does not work out somewhere, then you look at the error codes and ask again. The search in English helps a lot - even by typing in just keywords, you can get a link to a similar solved problem on the stackerflow with a probability of 80%. To search for ready-made examples, the code from which you can stupidly take and transfer to your program, you can add keywords such as "example" or "example" in Russian. Several good ideas were found on habr, that is, you can try to insert the keyword "habr" into the request,but I used this only when I knew for sure that I saw the solution that I was looking for on Habré. Almost any small task from everything that was done was solved through a search engine: "change div color shift click js", "make div resizeable", "how to edit a web page" ... hundreds of variations of different queries. Perhaps in the comments the pros can share their advice.
And yes, since we are talking about advice, I would also like to receive constructive criticism and useful advice from you. Perhaps someone wants to stretch their brains and can throw in a much more functional solution in a couple of hours. Or maybe this post will give someone some interesting ideas, because in this way you can accept any JSON request and make any visual structure based on it. It would be very cool to have a similar universal solution where you can distribute any data as it suits you, managing simple visual forms, drag and drop, resize and all that stuff to make it beautiful and functional, but that's not all. Although it turned out well, I think. The speed of the unit, as requested by the customer, can now be observed from the browser and adding something new will not be difficult.
Link toclient code in C #
Or under the 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>
*/