суббота, 4 июля 2020 г.

OPC to SQL клиент


Еще в прошлом году была у меня задача передавать данные из контроллера mitsubishi в SQL базу данных, тогда же я искал оптимальное решение как это можно реализовать с наименьшими усилиями. А поскольку формат OPC довольно универсальный, рассматривал тогда и такой вариант, но найти бесплатную программу для соединения с OPC сервером и записи полученных данных в SQL таблицу не получилось. На тот момент мы умели передавать данные с контроллера по UDP протоколу и потому озадачились написанием программы UDP to SQL, с чем собственно вполне успешно справились. Тогда удалось создать вполне универсальную программу на C# в которой в конфиге можно было прописать необходимые настройки, как например номер порта по которому принимается UDP строка соединения с базой данных, название таблицы и колонки куда помещать принимаемые данные. В итоге запускаем консольное приложение, которое тупо висит в фоне и каждую принятую по UDP телеграмму складывает в указанную таблицу. Позже узнав способ который позволяет компилировать программу из батника, стало можно уйти от использования конфига, и сделать приложение заточенное под конкретные настройки.

Ну и спустя год, появляется новая задача, в этот раз требуется все же получать данные именно с OPC сервера и эти данные писать в таблицу SQL (кстати было бы круто делать все тоже самое но с MySQL). Снова полез в инет на поиски и снова ничегошеньки не нашел, но зато пока искал чуть лучше разобрался в принципах OPC. Получилось найти минималистичные программы для работы с OPC которые не требуют установки и работают очень даже хорошо. Например, вот отсюда, можно скачать архив, внутри которого есть OPC сервер симулятор, генерирующий рандомные значения, которые можно видеть любым клиентом. Простой тестовый клиент есть так же в этом архиве.

Позже удалось найти бесплатную библиотеку для работы с OPC со всеми исходными кодами на C#. 

Ну и собственно чтобы не ходить вокруг да около, предоставляю готовый результат кода (архив вместе с программами) для OPC клиента который может получать несколько тегов по OPC и передавать их либо по UDP (это одна программа), либо в таблицу SQL (другая программа). Исходные коды находятся также в архиве. Сюда выкладывать код текстом не буду, т.к. код слишком большой и некрасиво форматируется в блоге, но может в комменты все же закину. В конфиге прописываются название сервера (копировал то название, который видит тестовый OPC клиент из предыдущего архива), названия тегов через запятые, если в опции отображения текста в консоли поставить yes, то обновляемые значения можно видеть в консоли. 

В угоду универсальности пришлось жертвовать читабельностью - данные в SQL добавляются в бинарном формате, и потому чтобы увидеть исходные данные требуются их обрабатывать триггером в SQL. Зато можно тупо добвать нужный сигнал в конфиг и он сразу улетит в базу, без лишних переписываний программы или созданий дополнительных столбцов в таблице. 

Правда такой подход сильно осложняет последующую расшифровку данных, тупо преобразовать varbinary во float не получается, но зато нашел на просторах интернета совет, как это можно сделать: по сути получаются все те бинарные преобразования, которые делаются в любом конвертере, чтобы получить из HEX число с плавающей точкой

В оригинале используется функция, я же просто решил использовать копипасту, после чего триггер стал выглядеть так:

USE [testdb]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [dbo].[unscale] ON [dbo].[raw_data]
   AFTER INSERT
AS
DECLARE @data AS varbinary(max)
DECLARE @tag1 AS REAL
DECLARE @tag1data AS binary(4)
DECLARE @tag2 AS REAL
DECLARE @tag2data AS binary(4)
DECLARE @ID_record AS INT

SET @ID_record = (SELECT id from inserted)
Set @data = (select data from inserted)

/*берем 4 байта с начиная с первого из всего массива и переворачиваем их чтобы шли в обратном порядке*/
Set @tag1data = cast(reverse(substring(@data,1,4)) as binary(4))
/*конвертируем в формат реал*/ 
set @tag1 = SIGN(CAST(@tag1data AS INT))
* (1.0 + (CAST(@tag1data AS INT) &  0x007FFFFF) * POWER(CAST(2 AS REAL), -23))
* POWER(CAST(2 AS REAL), (CAST(@tag1data AS INT) & 0x7f800000) / 0x00800000 - 127)
/*берем 4 байта начиная с 5 и переворачиваем*/
Set @tag2data = cast(reverse(substring(@data,5,4)) as binary(4))
/*конвертируем их в реал формат*/
set @tag2 = SIGN(CAST(@tag2data AS INT))
* (1.0 + (CAST(@tag2data AS INT) &  0x007FFFFF) * POWER(CAST(2 AS REAL), -23))
* POWER(CAST(2 AS REAL), (CAST(@tag2data AS INT) & 0x7f800000) / 0x00800000 - 127)
/*обновляем таблицу, чтобы видеть эти данные*/
update raw_data set tag1 = @tag1,tag2 = @tag2 from inserted where raw_data.id = inserted.id

Как это выглядит в табличном виде:

4 комментария:

  1. Обновленный код, с возможностью изменения времени обновления тегов в мс
    https://dotnetfiddle.net/YbgapG

    В конфиг нужно добавить строку
    < add key="refreshTime" value="5000" />

    ОтветитьУдалить
  2. Выполнить любой SQL запрос через VBS можно таким образом (в данном случае вставить новую строку в SQL):

    Dim cnnMain
    Dim strSQL
    Set cnnMain = CreateObject("ADODB.Connection")
    cnnMain.Open "Provider=SQLOLEDB;Data Source=W7-VS2017;Password=12345;User ID=user12345;Initial Catalog=amt"
    strSQL="insert into [amt].[dbo].[testtbl] (int) Values (800)"
    cnnMain.BeginTrans
    cnnMain.Execute strSQL
    cnnMain.CommitTrans
    cnnMain.Close
    Set cnnMain = Nothing

    ОтветитьУдалить
  3. IF @Prev_downtime_bit=1 AND @Downtime_bit=0
    BEGIN
    SET @Downtime_Start = (SELECT MAX(Downtime_start) FROM aom_down)
    SET @Downtime_Stopdur = (GETDATE()- @Downtime_Start)
    IF @Downtime_Stopdur > '0:00:30'
    BEGIN
    UPDATE aom_down SET Downtime_stop = GETDATE(), Downtime_dur = GETDATE() - @Downtime_Start where Downtime_start = @Downtime_Start
    truncate table [AOM].[dbo].[raw_data]
    END
    ELSE
    BEGIN
    DELETE FROM aom_down WHERE Downtime_start = @Downtime_Start
    END

    IF @Downtime_Stopdur > '23:59:59' UPDATE aom_down SET Day_Stop = DATEDIFF (dd,@Downtime_Start,GETDATE()) where Downtime_start = @Downtime_Start

    END

    ОтветитьУдалить
  4. Пока не могу найти решения наоборот, чтоб OPC сервер читал из базы данных((

    ОтветитьУдалить