Я работаю в компании, которая занимается автоматизацией производственных процессов. Знаком не по наслышке с программируемыми логическими контроллерами (PLC), человеко-машинным интерфейсом (HMI) и SCADA (диспетчерское управление и сбор данных).
А еще я увлекаюсь программированием. В основном на языках C# и Java (Android). Когда я познакомился с технологией WPF и увидел как просто на ней реализуется графический интерфейс пользователя, восторгу моему не было предела. «Да это же убийца SCADA-систем», — подумал я. Надо просто каким-то образом связать .NET проект с устройствами ввода-вывода (PLC).
И тут ко мне пришла идея, которая в определенный момент приходит к каждому автоматчику. Сделать свою SCADA-систему. И не просто SCADA-систему, а SCADA-систему для .NET разработчиков (прежде всего для себя самого). Потому что порой когда переключаешься из Visual Studio в редактор «современной» SCADA-системы, хочется вырвать себе последние волосы. Но настоящие трудности начинаются, когда необходимо реализовать функционал, выходящий за рамки базового. Другое дело — мир программистов. Ответ практически на любой вопрос можно быстро найти на сайте stackoverflow.com через его поисковый интерфейс google.com.
Далее я опишу то, что у меня получилось сделать. Исходный код проекта доступен по ссылке https://github.com/phmi/phmi.
Для использования PHmi (так я ее назвал) должны быть удовлетворены следующие условия:
Разработку проекта можно разделить на две фазы: конфигурирование сервера и разработку проекта клиента.
Запустим PHmiConfigurator.exe. Появится следующее окно:
![](https://habrastorage.org/r/w1560/files/a93/e22/44d/a93e2244dbf342bea33541a576b43167.png)
Нажмем на кнопку “New project”. Появится окно “New project”:
![](https://habrastorage.org/r/w1560/files/092/22a/d4f/09222ad4f71c434483af7a28d4980680.png)
Введем параметры связи с PostgreSQL и желаемое название базы данных:
![](https://habrastorage.org/r/w1560/files/354/7d6/e5f/3547d6e5fa4e4550944f873a41a3b513.png)
После нажатия на кнопку “Ok” новый проект будет создан:
![](https://habrastorage.org/r/w1560/files/b0e/cc0/6d5/b0ecc06d574d46269f3d05aaebdafc7b.png)
Нажмем на кнопку “I/O devices”. В конфигураторе откроется вкладка “I/O devices”:
![](https://habrastorage.org/r/w1560/files/0f9/334/6f4/0f93346f4a084436a111f2d785e8092c.png)
Добавим новое устройство ввода-вывода “IoDev” типа “Generic”. «Generic» — это устройство ввода вывода для отладочных целей. Либо оно используется, когда не нужно соединяться с реальным устройством. Оно поддерживает любые адреса тегов:
![](https://habrastorage.org/r/w1560/files/541/320/d5e/541320d5efa346f898de187c5c0c3677.png)
![](https://habrastorage.org/r/w1560/files/2c8/f84/350/2c8f84350f0f472aad9c43c23876295d.png)
Нажмем на кнопку «Save» и закроем вкладку.
Откроем вкладку “Digital tags” и создадим новый цифровой тег.
![](https://habrastorage.org/r/w1560/files/1fd/2f6/569/1fd2f656938e489cbb08fb29b6172487.png)
Откроем вкладку “Numeric tags” и создадим новый аналоговый тег:
![](https://habrastorage.org/r/w1560/files/822/087/a8c/822087a8cbd84b8a8d197d989f2c3345.png)
Поля описания, формата, единицы измерения, калибровочные границы необязательны и могут быть опущены.
Создадим новую категорию аварий:
![](https://habrastorage.org/r/w1560/files/568/a99/306/568a9930605f493eaa659a89aeec0dd5.png)
Создадим новый тег аварий:
![](https://habrastorage.org/r/w1560/files/888/fb0/8d1/888fb08d1af64cf7ae86e6a72ef45f8c.png)
Создадим категорию трендов:
![](https://habrastorage.org/r/w1560/files/072/379/d3e/072379d3e746438795b355f612b3a2c0.png)
Создадим новый тег трендов:
![](https://habrastorage.org/r/w1560/files/276/d22/206/276d222068544b8a95c7f414276dd1ea.png)
В конфигураторе нажмем на кнопку “Build client”:
![](https://habrastorage.org/r/w1560/files/404/9a4/abf/4049a4abfe1d43b1bd0aa661a24bf5e9.png)
Выберем путь к файлам, чтобы их создать.
«Namespace» должно совпадать с названием будущего проекта Visual Studio.
Нажмем на кнопку “Build”.
По указанному пути должны появиться файлы:
![](https://habrastorage.org/r/w1560/files/ded/59f/987/ded59f9879f440839b4932842905163d.png)
Запустим Microsoft Visual Studio и создадим новый проект WPF. Целевой фреймворк должен быть “.Net Framework 4.0” или выше:
![](https://habrastorage.org/r/w1560/files/b63/e50/1bc/b63e501bc5ad44fda0ddbb634dde63be.png)
![](https://habrastorage.org/r/w1560/files/8e2/9d4/d85/8e29d4d853704be4bd687762040e6ac6.png)
Добавим ссылку к PHmiClient.dll:
![](https://habrastorage.org/r/w1560/files/d21/e78/8d8/d21e788d8fa1417a827e9859c9edf4f2.png)
![](https://habrastorage.org/r/w1560/files/52d/1dd/fe4/52d1ddfe4fc0404fa8d5e809ac22be58.png)
![](https://habrastorage.org/r/w1560/files/72e/3a2/f2b/72e3a2f2b93e451cbe3e5907d73bca04.png)
Добавим файлы PHmi.cs и PHmiResources.resx, созданные заранее:
![](https://habrastorage.org/r/w1560/files/bf5/8ed/27e/bf58ed27e1e3468aa0dcfe3e864f4f93.png)
![](https://habrastorage.org/r/w1560/files/a3d/822/a47/a3d822a47d1147e081c85775c0663c8d.png)
Кликнем двойным щелчком по PHmiResources.resx и поменяем “Access modifier” на Public:
![](https://habrastorage.org/r/w1560/files/32f/d4c/75d/32fd4c75d0354be5954d5cd29781c0cd.png)
Откроем MainWindow.xaml. Добавим кнотрол Root в корневой Grid.
Привяжем PHmi к DataContext Rootа. Для этого создадим новый объект PHmi в ресурсах окна.
Добавим папку «Pages» для страниц. Добавим туда UserControl под названием «HomePage».
![](https://habrastorage.org/r/w1560/files/00a/ca4/ac8/00aca4ac890741ea9d3c0e4e640c8dba.png)
Страницы должны реализовывать интерфейс IPage. Листинг HomePage.xaml.cs представлен ниже.
А вот разметка Xaml файла:
Мы добавили TextBlock для отображения значения тегов.
Привяжем тип домашней страницы к Root:
Запустим приложение и посмотрим, что получилось:
![](https://habrastorage.org/r/w1560/files/14e/8f1/e97/14e8f1e97db3414e900042b5e5574e88.png)
Значений тегов не видно. Это потому что не запущен PHmiRunner.exe. Нажмем кнопку “Run” в конфигураторе.
![](https://habrastorage.org/r/w1560/files/b6d/3cd/e9f/b6d3cde9f85f422e845f7a42352384c5.png)
Теперь добавим на страницу элементы управления.
![](https://habrastorage.org/r/w1560/files/a75/711/adc/a75711adc05f451cbb5da9945fd624f8.png)
Если включить “Digital tag", возникнет авария.
Добавим на страницу тренд.
![](https://habrastorage.org/r/w1560/files/ce3/614/3d2/ce36143d2dca43f2a364ccd0b87a43fd.png)
Для отображения строк в соответствии с региональными настройками (дата, время и прочее), необходимо изменить XmlLanguage в конструкторе окна:
Спасибо!
А еще я увлекаюсь программированием. В основном на языках C# и Java (Android). Когда я познакомился с технологией WPF и увидел как просто на ней реализуется графический интерфейс пользователя, восторгу моему не было предела. «Да это же убийца SCADA-систем», — подумал я. Надо просто каким-то образом связать .NET проект с устройствами ввода-вывода (PLC).
И тут ко мне пришла идея, которая в определенный момент приходит к каждому автоматчику. Сделать свою SCADA-систему. И не просто SCADA-систему, а SCADA-систему для .NET разработчиков (прежде всего для себя самого). Потому что порой когда переключаешься из Visual Studio в редактор «современной» SCADA-системы, хочется вырвать себе последние волосы. Но настоящие трудности начинаются, когда необходимо реализовать функционал, выходящий за рамки базового. Другое дело — мир программистов. Ответ практически на любой вопрос можно быстро найти на сайте stackoverflow.com через его поисковый интерфейс google.com.
Далее я опишу то, что у меня получилось сделать. Исходный код проекта доступен по ссылке https://github.com/phmi/phmi.
Установка
Для использования PHmi (так я ее назвал) должны быть удовлетворены следующие условия:
- PHmi
- СУБД PostgreSQL v9.2 или выше (ее можно скачать с сайта www.postgresql.com)
- Microsoft Visual Studio 2010 или выше (экспресс версия или Community Edition подойдет)
Разработка проекта
Разработку проекта можно разделить на две фазы: конфигурирование сервера и разработку проекта клиента.
Конфигурирование сервера
Запустим PHmiConfigurator.exe. Появится следующее окно:
![](https://habrastorage.org/files/a93/e22/44d/a93e2244dbf342bea33541a576b43167.png)
Нажмем на кнопку “New project”. Появится окно “New project”:
![](https://habrastorage.org/files/092/22a/d4f/09222ad4f71c434483af7a28d4980680.png)
Введем параметры связи с PostgreSQL и желаемое название базы данных:
![](https://habrastorage.org/files/354/7d6/e5f/3547d6e5fa4e4550944f873a41a3b513.png)
После нажатия на кнопку “Ok” новый проект будет создан:
![](https://habrastorage.org/files/b0e/cc0/6d5/b0ecc06d574d46269f3d05aaebdafc7b.png)
Нажмем на кнопку “I/O devices”. В конфигураторе откроется вкладка “I/O devices”:
![](https://habrastorage.org/files/0f9/334/6f4/0f93346f4a084436a111f2d785e8092c.png)
Добавим новое устройство ввода-вывода “IoDev” типа “Generic”. «Generic» — это устройство ввода вывода для отладочных целей. Либо оно используется, когда не нужно соединяться с реальным устройством. Оно поддерживает любые адреса тегов:
![](https://habrastorage.org/files/541/320/d5e/541320d5efa346f898de187c5c0c3677.png)
![](https://habrastorage.org/files/2c8/f84/350/2c8f84350f0f472aad9c43c23876295d.png)
Нажмем на кнопку «Save» и закроем вкладку.
Откроем вкладку “Digital tags” и создадим новый цифровой тег.
![](https://habrastorage.org/files/1fd/2f6/569/1fd2f656938e489cbb08fb29b6172487.png)
Откроем вкладку “Numeric tags” и создадим новый аналоговый тег:
![](https://habrastorage.org/files/822/087/a8c/822087a8cbd84b8a8d197d989f2c3345.png)
Поля описания, формата, единицы измерения, калибровочные границы необязательны и могут быть опущены.
Создадим новую категорию аварий:
![](https://habrastorage.org/files/568/a99/306/568a9930605f493eaa659a89aeec0dd5.png)
Создадим новый тег аварий:
![](https://habrastorage.org/files/888/fb0/8d1/888fb08d1af64cf7ae86e6a72ef45f8c.png)
Создадим категорию трендов:
![](https://habrastorage.org/files/072/379/d3e/072379d3e746438795b355f612b3a2c0.png)
Создадим новый тег трендов:
![](https://habrastorage.org/files/276/d22/206/276d222068544b8a95c7f414276dd1ea.png)
Разработка проекта клиента
В конфигураторе нажмем на кнопку “Build client”:
![](https://habrastorage.org/files/404/9a4/abf/4049a4abfe1d43b1bd0aa661a24bf5e9.png)
Выберем путь к файлам, чтобы их создать.
«Namespace» должно совпадать с названием будущего проекта Visual Studio.
Нажмем на кнопку “Build”.
По указанному пути должны появиться файлы:
![](https://habrastorage.org/files/ded/59f/987/ded59f9879f440839b4932842905163d.png)
Запустим Microsoft Visual Studio и создадим новый проект WPF. Целевой фреймворк должен быть “.Net Framework 4.0” или выше:
![](https://habrastorage.org/files/b63/e50/1bc/b63e501bc5ad44fda0ddbb634dde63be.png)
![](https://habrastorage.org/files/8e2/9d4/d85/8e29d4d853704be4bd687762040e6ac6.png)
Добавим ссылку к PHmiClient.dll:
![](https://habrastorage.org/files/d21/e78/8d8/d21e788d8fa1417a827e9859c9edf4f2.png)
![](https://habrastorage.org/files/52d/1dd/fe4/52d1ddfe4fc0404fa8d5e809ac22be58.png)
![](https://habrastorage.org/files/72e/3a2/f2b/72e3a2f2b93e451cbe3e5907d73bca04.png)
Добавим файлы PHmi.cs и PHmiResources.resx, созданные заранее:
![](https://habrastorage.org/files/bf5/8ed/27e/bf58ed27e1e3468aa0dcfe3e864f4f93.png)
![](https://habrastorage.org/files/a3d/822/a47/a3d822a47d1147e081c85775c0663c8d.png)
Кликнем двойным щелчком по PHmiResources.resx и поменяем “Access modifier” на Public:
![](https://habrastorage.org/files/32f/d4c/75d/32fd4c75d0354be5954d5cd29781c0cd.png)
Откроем MainWindow.xaml. Добавим кнотрол Root в корневой Grid.
<Window x:Class="ClientSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pHmiControls="clr-namespace:PHmiClient.Controls;assembly=PHmiClient"
Title="MainWindow" Height="350" Width="525">
<Grid>
<pHmiControls:Root/>
</Grid>
</Window>
Привяжем PHmi к DataContext Rootа. Для этого создадим новый объект PHmi в ресурсах окна.
<Window x:Class="ClientSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pHmiControls="clr-namespace:PHmiClient.Controls;assembly=PHmiClient"
xmlns:clientSample="clr-namespace:ClientSample"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<clientSample:PHmi x:Key="PHmi"/>
</Window.Resources>
<Grid>
<pHmiControls:Root DataContext="{StaticResource PHmi}"/>
</Grid>
</Window>
Добавим папку «Pages» для страниц. Добавим туда UserControl под названием «HomePage».
![](https://habrastorage.org/files/00a/ca4/ac8/00aca4ac890741ea9d3c0e4e640c8dba.png)
Страницы должны реализовывать интерфейс IPage. Листинг HomePage.xaml.cs представлен ниже.
using PHmiClient.Controls.Pages;
using System;
using System.Windows.Controls;
namespace ClientSample.Pages
{
/// <summary>
/// Interaction logic for HomePage.xaml
/// </summary>
public partial class HomePage : UserControl, IPage
{
public HomePage()
{
InitializeComponent();
}
public object PageName
{
get { return "Home page"; }
}
public PHmiClient.Controls.IRoot Root
{
get; set;
}
}
}
А вот разметка Xaml файла:
<UserControl x:Class="ClientSample.Pages.HomePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:clientSample="clr-namespace:ClientSample"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance clientSample:PHmi, IsDesignTimeCreatable=True}">
<Grid>
<StackPanel>
<TextBlock Text="{Binding Path=IoDev.DigitalTag.Value}"/>
<TextBlock Text="{Binding Path=IoDev.NumericTag.ValueString}"/>
</StackPanel>
</Grid>
</UserControl>
Мы добавили TextBlock для отображения значения тегов.
Привяжем тип домашней страницы к Root:
<Window x:Class="ClientSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pHmiControls="clr-namespace:PHmiClient.Controls;assembly=PHmiClient"
xmlns:clientSample="clr-namespace:ClientSample"
xmlns:pages="clr-namespace:ClientSample.Pages"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<clientSample:PHmi x:Key="PHmi"/>
</Window.Resources>
<Grid>
<pHmiControls:Root
DataContext="{StaticResource PHmi}"
HomePage="{x:Type pages:HomePage}"/>
</Grid>
</Window>
Запустим приложение и посмотрим, что получилось:
![](https://habrastorage.org/files/14e/8f1/e97/14e8f1e97db3414e900042b5e5574e88.png)
Значений тегов не видно. Это потому что не запущен PHmiRunner.exe. Нажмем кнопку “Run” в конфигураторе.
![](https://habrastorage.org/files/b6d/3cd/e9f/b6d3cde9f85f422e845f7a42352384c5.png)
Теперь добавим на страницу элементы управления.
<UserControl x:Class="ClientSample.Pages.HomePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:clientSample="clr-namespace:ClientSample"
xmlns:pHmiControls="clr-namespace:PHmiClient.Controls;assembly=PHmiClient"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance clientSample:PHmi, IsDesignTimeCreatable=True}">
<Grid>
<StackPanel>
<TextBlock Text="{Binding Path=IoDev.DigitalTag.Value}"/>
<TextBlock Text="{Binding Path=IoDev.NumericTag.ValueString}"/>
<CheckBox
IsChecked="{Binding Path=IoDev.DigitalTag.Value, Mode=TwoWay}"
Content="{Binding Path=IoDev.DigitalTag.Description}"/>
<pHmiControls:NumericInput NumericTag="{Binding Path=IoDev.NumericTag}"/>
</StackPanel>
</Grid>
</UserControl>
![](https://habrastorage.org/files/a75/711/adc/a75711adc05f451cbb5da9945fd624f8.png)
Если включить “Digital tag", возникнет авария.
Добавим на страницу тренд.
<UserControl x:Class="ClientSample.Pages.HomePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:clientSample="clr-namespace:ClientSample"
xmlns:pHmiControls="clr-namespace:PHmiClient.Controls;assembly=PHmiClient"
xmlns:trends="clr-namespace:PHmiClient.Controls.Trends;assembly=PHmiClient"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance clientSample:PHmi, IsDesignTimeCreatable=True}">
<Grid>
<StackPanel>
<TextBlock Text="{Binding Path=IoDev.DigitalTag.Value}"/>
<TextBlock Text="{Binding Path=IoDev.NumericTag.ValueString}"/>
<CheckBox
IsChecked="{Binding Path=IoDev.DigitalTag.Value, Mode=TwoWay}"
Content="{Binding Path=IoDev.DigitalTag.Description}"/>
<pHmiControls:NumericInput NumericTag="{Binding Path=IoDev.NumericTag}"/>
<trends:TrendViewer Height="400">
<trends:TrendPen
TrendTag="{Binding Path=Trends.NumericTagTrend}"
Brush="Red"/>
</trends:TrendViewer>
</StackPanel>
</Grid>
</UserControl>
![](https://habrastorage.org/files/ce3/614/3d2/ce36143d2dca43f2a364ccd0b87a43fd.png)
Задание языка окна
Для отображения строк в соответствии с региональными настройками (дата, время и прочее), необходимо изменить XmlLanguage в конструкторе окна:
using System.Threading;
using System.Windows;
using System.Windows.Markup;
namespace ClientSample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
Language = XmlLanguage.GetLanguage(
Thread.CurrentThread.CurrentUICulture.IetfLanguageTag);
InitializeComponent();
}
}
}
Спасибо!