18 Mayıs 2021 Salı

Bir HMI Uygulaması yapalım

 Merhaba bu yazımda tekrar otomasyon programlama üzerine çalışacağım. Daha önce http://ujk-ujk.blogspot.com/2017/12/s7-1200-plc-nettoplcsim-tia-portal.html yazısında C# üzerinden PLC'ye ulaşıp basit bir görsel yapmıştık. Şimdi biraz daha kapsamlı, HMI denilebilecek bir uygulamayı nasıl geliştiririz ona çalışacağız. Mesela şöyle bir uygulama ekranı hedefliyoruz.


PLC'ler için yazılmış HMI programları var tabi ama lisans ücretleri oldukça yüksek. Düşük bütçeli küçük projeler için burada göreceğimiz yöntemlerle basit HMI uygulamaları yapabiliriz. Tabi bunları geliştirip birçok HMI yazılımında olmayan özelliklere sahip bir uygulama yapmamız da mümkün. Özellikle ayrıntılı üretim ve kalite raporu istenen projelerde elimizin altında C#'ın bütün gücünün olması bize sınırsız imkanlar sağlar.



Bir Dijital Çıkış Nasıl Verilir


Ben gıda işletmelerine proses otomasyonu projeleri yapıyorum. Özellikler süt üretim tesislerine projeler yapıyorum. Bu projelerde en önemli şart, ürün beklerse bozulacağı için otomasyon sisteminin her türlü ekipmanın manual çalıştırılmasına imkan sağlaması. Vanalar, pompalar, karıştırıcılar, ısıtıcılar vs. yetkili operatörler tarafından manual moda alınarak otomatik kontrolün dışında yönetilebilmeli. Aynı şekilde bir sensör bozulması durumunda üretimi devam ettirebilmek adına sensörden gelen bilgiler operatör tarafından manual verilerek sistemin çalışmasına devam etmesi sağlanabilmeli. 

Bir dijital çıkış için düşünürsek olası durumları o dijital çıkış için ayarlanmış bir word değerin bitlerine bağlarız. Olası durumlar için örnek bir bit yapısı şöyle olabilir.
  • Bit 0 (Out) : Gerçekten PLC çıkışına verilen değer. 
  • Bit 1 (Fbit) : Çıkış manuale alındığında olmasını istediğimiz değer.
  • Bit 2 (Fenb) : Çıkışı manuale almak için set edeceğimiz bit (Force enable bit)
  • Bit 3 (Mask) : Çıkışın feedback alarmı vermesini engelleyen Mask biti.
  • Bit 4 (Alarm) : Çıkışta bir alarm olduğunu belirtir bit. 
Biraz açıklarsak , bir çıkışı manual olarak kontrol edebilmek için o çıkışın manual moda alındığını belirtir bir bit gerekiyor. Yukarıda Fenb adı verilen bit bu işi yapıyor, eğer bu bit true değere sahipse çıkış otomatik kontrolden ayrılmış operatör isteğine bağlanmıştır. Operatör isteğinin çıkışa ne değer verilmesini belirten de Fbit adı verilen bit. Yani Fenb bitine true değeri verdikten sonra Fbit değeri true ise Out değeri true, Fbit değeri false ise Out değeri false olacaktır. 

Bir çıkışın verebileceği alarmlar var. Mesela feedback alarmı. Mesela bir vana için feedback alarmı, çıkış yokken vananın kapalı olduğunu gören sviçten sinyal gelmezse ya da çıkış varken vananın açık olduğunu gören sviçten sinyal gelmezse bir süre sonra alarm oluşur. Bunu HMI üzerinde göstermek için de Alarm adı verilen biti kullanırız. Mask ise o çıkışın alarm vermemesi için operatör tarafından set edilen bir bittir. Örneğin vana açık svici görmese bile vananın gerçekte açıyor olduğunu gören operatör , bu vananın alarm vermesini engelleyerek otomasyonun çalışmaya devam etmesini sağlar. 





C# HMI Projesi


Visual Studio kullanarak yaptım bu projeyi. Önce Yeni Bir Proje Oluştur seçeneği ile başlayalım.


Uygulama kalıbı olarak Masaüstü WPF uygulaması seçelim ve Sonraki butonuna tıklayalım.


Proje ismi olarak HMI yazalım ve proje klasörünü seçelim. Oluştur butonuna tıklayarak projemiz başlatalım.


Bu noktada bir şey yapmadan önce yukarıdaki Başlat butonundan uygulamayı boş olarak bir çalıştırıp sıkıntı olmadığını görmemiz iyi olur.

Tasarımcıda pencere başlığına tıklayıp Window elemanını seçelim ve Title değerini Benim Güzel HMI olarak değiştirelim.


Pencere içine tıklayıp yerleşim için otomatik olarak proje oluştururken eklenmiş Grid elemanını seçelim ve arkaplan rengini lightgray yapalım.


Oraya lightgray yazıp enter basmanız ya da RGB değerlerini resimdeki gibi girmeniz yeterli.




İskelet Program Yapısı


Uygulamamızın program kısmında iki altyapı hazırlanması gerekiyor. Birisi animasyonda kullanılacak PLC değişken değerlerinin saklanacağı yapı, ikincisi animasyonlar için bir timer

MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace HMI
{
    /// <summary>
    /// MainWindow.xaml etkileşim mantığı
    /// </summary>
    public partial class MainWindow : Window
    {
        public static System.Windows.Threading.DispatcherTimer animTimer;
        Dictionary<string, int> Tags = new Dictionary<string, int>()
        {
            { "v1", 0 }, { "v2", 0 }, { "v3", 0 }, { "v4", 0 }, { "p1", 0 }
        };
        public MainWindow()
        {
            InitializeComponent();

            animTimer = new System.Windows.Threading.DispatcherTimer();
            animTimer.Tick += new EventHandler(OnAnimTimerEvent);
            animTimer.Interval = TimeSpan.FromMilliseconds(100);
            animTimer.Start();
        }

        private void OnAnimTimerEvent(object source, EventArgs e)
        {
            // animasyon rutinleri
        }
    }
}

Tags adında bir Dictionary içinde PLC'den aldığımız değerleri saklıyoruz. Bunların periyodik olarak PLC'den okunmasına ait rutinleri daha önce anlatmıştık. Kod kalabalığı olmasın diyerek bu rutinleri burada göstermiyorum. 

Değerleri simüle etmek amacıyla görselimize 5 adet Label ve 5 adet TextBox ekleyelim. Özellikleri şöyle:

  • Label 1 : content: v1, left: 30, top: 25, 18px bold
  • Label 2 : content : v2, top: 50
  • Label 3 : content : v3, top: 75
  • Label 4 : content : v4, top: 100
  • Label 5 : content : p1, top: 125
  • TextBox 1 : Ad: v1TextBox, left: 65, top: 31, text: yok, tag, v1
  • TextBox 2 : Ad: v2TextBox, left: 65, top: 56, text: yok, tag, v2
  • TextBox 3 : Ad: v3TextBox, left: 65, top: 81, text: yok, tag, v3
  • TextBox 4 : Ad: v4TextBox, left: 65, top: 106, text: yok, tag, v4
  • TextBox 5 : Ad: p1TextBox, left: 65, top: 131, text: yok, tag, p1
Unutmadan pencerenin ResizeMode değerini No Resize olarak değiştirelim HMI görselinde resize işleminin bir anlamı yok. Böyle daha kolay hakim oluruz.

Elemanların Tag özellikleri bizim o elemana ait bazı bilgileri orada saklayabilmemiz için verilmiştir. Bu değere herhangi bir obje atayabiliriz ya da burada yaptığımız gibi sadece basit bir yazı değer ile bu elemanı PLC değişkenine bağlamak amaçlı kullanabiliriz. Tabi biraz kod yazmamız gerekiyor. Ama olsun her yaptığımıza net hakim olmak daha önemli bence.

PLC değişken değerlerini bu TextBox'larda göstermek ve periyodik olarak update etmek için animasyon zamanlayıcı rutinimize şunları ekleyelim.

        private void OnAnimTimerEvent(object source, EventArgs e)
        {
            TextBoxAssign(v1TextBox);
            TextBoxAssign(v2TextBox);
            TextBoxAssign(v3TextBox);
            TextBoxAssign(v4TextBox);
            TextBoxAssign(p1TextBox);
        }

        private void TextBoxAssign(TextBox textBox)
        {
            if (!textBox.IsFocused)
            {
                textBox.Text = Tags[textBox.Tag.ToString()].ToString();
            }
        }

TextBoxAssign adında yeni bir metod tanımlıyoruz ve animasyonda olmasını istediğimiz her bir TextBox için bu metodu çağırıyoruz. Metod ne yapıyor? Eğer TextBox o anda focuslanmamışsa yani içinde değişiklik yapmak için tıklanıp kursör oraya getirilmemişse; Tags listesinden o TextBox'ın Tag değerinde girilen isimdeki PLC değişken değerini alıp TextBox içine yazıyor.


TextBox değerinde değişiklik yaptığımızda PLC değerinin değişmesini sağlamak için de TextBoxTextChanged adında bir metod yazalım ve tüm TextBox'ların TextChanged olayından bu metodu çağıralım.



        private void TextBoxTextChanged(object sender, TextChangedEventArgs e)
        {
            TextBox tt = (TextBox)sender;
            try
            {
                Tags[tt.Tag.ToString()] = UInt16.Parse(tt.Text);
            }
            catch
            {
                Tags[tt.Tag.ToString()] = 0;
            }
        }

Eğer yazılan değer UInt16 tipi sayıya dönüştürülebiliyorsa o sayıyı yoksa 0 değerini PLC değişkenine yazar. Aslında bu kod direk bizim Tags değişkenimize yazmayacak, bu kod değeri o anda PLC'ye yazacak. Bizim periyodik olarak PLC değerlerini okuyan rutinimiz değişen değeri PLC'den okuyacak. Ama biz şimdilik simüle ettiğimiz için direk Tags listesinde değer değiştiriyoruz. Onu da ileride yaparız inşallah. 

Bu tip iki yönlü veri bağlamaları için Binding yöntemi ile TextBox içeriğini bir veriye bağlayabiliriz. Ancak biz ileride bu geliştirdiğimiz animasyonlara sahip kullanıcı tanımlı elemanlar geliştirdiğimizde bu Binding işi çok kafa karıştırıyor, hakim olması çok güçleşiyor. Bu yüzden sade ve anlaşılabilir kalmak adına veri bağlamasını kendi kodumuzla kontrol etmeyi tercih ettim. En iyi yol bildiğin yoldur demiş atalarımız.




CheckBox'lar ile Bitlere Ulaşmak


Bu TextBox'lar ile PLC değerlerini değiştirmek için sürekli akıldan Binary'den Decimal'e ya da tersi dönüşümler yapmamız gerekli, çünkü bize bitler lazım. Daha anlaşılabilir olmak için her biti temsil eden CheckBox'lar koymak mantıklı olacak. Örneğin v1 değişkeni bitleri için ekrana 5 adet CheckBox ekleyelim. Bit isimlerini de üzerlerinde dikey olarak yazalım.

  • CheckBox 1 : Ad: v1Bit0, left: 300, top: 35, content: yok, tag: v1,0
  • CheckBox 2 : Ad: v1Bit1, left: 280, top: 35, content: yok, tag: v1,1
  • CheckBox 3 : Ad: v1Bit2, left: 260, top: 35, content: yok, tag: v1,2
  • CheckBox 4 : Ad: v1Bit3, left: 240, top: 35, content: yok, tag: v1,3
  • CheckBox 5 : Ad: v1Bit4, left: 220, top: 35, content: yok, tag: v1,4
  • Label 1 : content: Out, Paddings: 0, RotationAngle: -90, left: 298, top: 16
  • Label 2 : content: Fbit, Paddings: 0, RotationAngle: -90, left: 278, top: 16
  • Label 3 : content: Fenb, Paddings: 0, RotationAngle: -90, left: 255, top: 13
  • Label 4 : content: Mask, Paddings: 0, RotationAngle: -90, left: 234, top: 12
  • Label 5 : content: Alarm, Paddings: 0, RotationAngle: -90, left: 213, top: 10

PLC değerlerini CheckBox'lara yazmak için CheckBoxAssign adında bir metod ekleyelim ve tüm CheckBox'lar için bu metodu animasyon rutininde çağıralım.

        private void OnAnimTimerEvent(object source, EventArgs e)
        {
            ...
            CheckBoxAssign(v1Bit0);
            CheckBoxAssign(v1Bit1);
            CheckBoxAssign(v1Bit2);
            CheckBoxAssign(v1Bit3);
            CheckBoxAssign(v1Bit4);
        }

        private void CheckBoxAssign(CheckBox checkBox)
        {
            string name = checkBox.Tag.ToString().Split(',')[0];
            UInt16 value = (UInt16)Tags[name];
            UInt16 bitno = UInt16.Parse(checkBox.Tag.ToString().Split(',')[1]);
            if ((value & Convert.ToUInt16(Math.Pow(2, bitno))) != 0)
            {
                checkBox.IsChecked = true;
            }
            else
            {
                checkBox.IsChecked = false;
            }
        }

Tag değerlerine v1,0 gibi önce PLC değişken adı ve sonra bit numarası virgülle ayrılmış olarak tasarım esnasında girmiştik. Bu metod o bilgiden önce ilk kısımdaki v1 değerini name adındaki değişkene alıyor. Sonra virgülden sonraki 2. değer olan bit numarasını bitno isimli değişkene alıyor. Daha sonra bulabildiğim tek yöntem olan 2 üzeri bit numarası ile and işlemine tabi tutarak o bitin sayı içinde true ya da false olduğuna göre CheckBox seçilmişlik değerini değiştiriyor. Burada kullanılan bu Math.Pow işlemi eminin hafızada çok fazla yer kaplıyor ve zaman alıyordur. Bit değerlerine erişmek için üşenmeyip her biti tek tek bakan bir switch-case rutini yazsak eminim çok daha hızlı olur ve hafızada daha az yer kaplar.

Ama bizim bu metodu binlerce kez kullanacak bir uygulamamız yok henüz. Bu yüzden olsun o kadar deyip geçelim. Şimdi TextBox'da değer değişince bitlerin nasıl değiştiğini test edebiliriz.


ChexcBox'lar tıklanıp değeri değiştirildiğinde PLC değerini değiştirmek için de CheckBoxClicked adında bir metod tanımlayıp tüm CheckBox'ların Click olayına bağlayalım.



        private void CheckBoxClicked(object sender, RoutedEventArgs e)
        {
            CheckBox cc = (CheckBox)sender;
            string name = cc.Tag.ToString().Split(',')[0];
            UInt16 value = (UInt16)Tags[name];
            UInt16 bitno = UInt16.Parse(cc.Tag.ToString().Split(',')[1]);
            if (cc.IsChecked == true)
            {
                Tags[name] = Tags[name] | Convert.ToUInt16(Math.Pow(2, bitno));
            }
            else
            {
                Tags[name] = Tags[name] & ~Convert.ToUInt16(Math.Pow(2, bitno));
            }
        }
Bu metod ne yapıyor? Eğer CheckBox seçili olduysa bağlı olduğu bitin true olduğu geri kalan bitlerin false olduğu bir sayı ile değeri OR işlemine tabi tutarak o biti setliyor. Eğer CheckBox seçili değil olduysa bu sefer sadece o bitin false diğer tüm bitlerin true olduğu bir sayı ile AND işlemi yapılarak o bit resetleniyor.

Artık CheckBox'larda tıklayarak PLC değerinin bitlerini bağımsız olarak değiştirebiliriz. Bunu test edip TextBox içinde gösterilen değerin nasıl değiştiğini kontrol edelim. Sonra da diğer tüm PLC değerleri için CheckBox'ları ekrana koyalım. Grup halinde kopyalayıp isim ve Tag değerlerini değiştirebiliriz. Animasyon rutini içinden CheckBoxAssign metodlarını çağırmayı unutmayalım.







Pompanın Animasyonu


Görsel animasyonunna pompa ile başlayalım. Kare şekli ile bir gösterim yapacağız. İlk hedefimiz kare iç renginin Out bitine göre yeşil renk olması. Bu amaçla pencereye bir Rectangle elemanı ekliyoruz:
  • Rectangle : Ad: p1Kare, width: 20, height: 20, left 366, top: 134, StrokeThickness: 3, tag: p1
Renk animasyonunu sağlamak için RectangleAssign adında bir metot tanımlayıp animasyon timer rutininden çağıralım.

        private void OnAnimTimerEvent(object source, EventArgs e)
        {
...
            RectangleAssign(p1Kare);
        }
...
        private void RectangleAssign(Rectangle rectangle)
        {
            string name = rectangle.Tag.ToString();
            UInt16 value = (UInt16)Tags[name];
            if ((value & 1) != 0)   // out
            {
                rectangle.Fill = Brushes.Lime;
            }
            else
            {
                rectangle.Fill = Brushes.White;
            }
        }
...

Bağlı PLC değerinin bit 0 değerine bakıyoruz ve eğer true ise Fill rengini Lime (0,255,0), false ise White yapıyoruz. Şimdi çalıştırıp CheckBox üzerinden p1'in bit 0 değerini değiştirerek kontrol edebiliriz.


Bu kare üzerinde ikinci animasyonumuz ekipmanın alarmının maskelenmiş olduğunu belirtmek. Maskelenmiş olduğu bilgisi bit 3'te bulunuyor. Eğer bir ekipman alarmı maskelenmişse onun animasyonunda kare çizgisinin rengini kırmızı yaparak belirtelim dersek metodumuz şu hale gelecektir.

        private void RectangleAssign(Rectangle rectangle)
        {
            string name = rectangle.Tag.ToString();
            UInt16 value = (UInt16)Tags[name];
            if ((value & 1) != 0)   // out
            {
                rectangle.Fill = Brushes.Lime;
            }
            else
            {
                rectangle.Fill = Brushes.White;
            }
            if ((value & 8) != 0)   // mask
            {
                rectangle.Stroke = Brushes.Red;
            }
            else
            {
                rectangle.Stroke = Brushes.Black;
            }
        }


Bu karede son animasyonumuz ekipmanın alarm verdiğini belirtmek. Alarm vermek için yine kare iç rengini değiştireceğiz. Alarm ile rengi kırmızı yapacağız, ama o  andaki aktif çıkışın ne olduğunu göstermeye devam etmek ve biraz da dikkat çekmek için kırmızı ve o anki aktif çıkış değeri arasında blink etmesini sağlayacağız.


Metodumuz şu şekle gelecektir.

        private void RectangleAssign(Rectangle rectangle)
        {
            string name = rectangle.Tag.ToString();
            UInt16 value = (UInt16)Tags[name];
            if ((value & 1) != 0)   // out
            {
                rectangle.Fill = Brushes.Lime;
            }
            else
            {
                rectangle.Fill = Brushes.White;
            }
            if ((value & 8) != 0)   // mask
            {
                rectangle.Stroke = Brushes.Red;
            }
            else
            {
                rectangle.Stroke = Brushes.Black;
            }
            int ms = DateTime.Now.Millisecond;
            if (((value & 16) != 0) & ms > 500)
            {
                rectangle.Fill = Brushes.Red;
            }
        }

Blink etmek için zamanın milisaniye kısmının 500 den büyük olmasını kullanıyoruz. Böylece kare yarım saniye Out değerine göre renk alırken yarım saniye kırmızı renk alarak alarmı gösteriyor.

Geriye bir tek ekipmanın manual modda olduğunu belirten animasyon kaldı. Manual modu göstermek için arkaya daha büyük bir turuncu kare yerlerştiriyoruz. Arkadan görüneceği için sanki önceki karenin etrafına turuncu bir çerçeve gelmiş gibi görünecek.
  • Rectangle : ad: p1Manual, width: 28, height: 28, left: 362, top: 130, fill: orange, stroke: sıfırla,  tag: p1
Biçim -> Düzen -> Arkaya Gönder menü seçimi ile arkaya göndeririz.



Turuncu karenin Visibility özelliğini Fenb bitine göre ayarlayan RectangleForceAssign metodunu yazalım ve anmasyondan çağıralım.

        private void OnAnimTimerEvent(object source, EventArgs e)
        {
            ...
            RectangleAssign(p1Kare);
            RectangleForceAssign(p1Manual);
        }
...
        private void RectangleForceAssign(Rectangle rectangle)
        {
            string name = rectangle.Tag.ToString();
            UInt16 value = (UInt16)Tags[name];
            if ((value & 4) != 0)   // fenb
            {
                rectangle.Visibility = Visibility.Visible;
            }
            else
            {
                rectangle.Visibility = Visibility.Hidden;
            }
        }

Pompayı göstermek için de basit gradient boyalı dikdörtgenler kullandım.
  • Rectangle : width: 66, height: 36, left: 343, top: 126, gradient: %0 gray, %50 white, %100 gray, strokeThickness: 1
  • Rectangle : width: 10, height: 46, left: 334, top: 121, gradient: %0 gray, %50 white, %100 gray, strokeThickness: 1
  • Rectangle : width: 16, height: 3, fill: black, stroke: sıfırla: rotate: -60, left: 343, top: 167
  • Rectangle : width: 16, height: 3, fill: black, stroke: sıfırla: rotate: 60, left: 393, top: 167
Üzerlerine sağ tıklayıp Sıra -> En geri gönder seçerek de arkaya atabiliriz. 


Pompa için animasyonumuz bu kadar. Vanaya geçelim.



Vana Animasyonu


Vanaları gösterirken pompa için kullandığımız 2 adet dikdörtgen haricinde vananın açık mı yoksa kapalı mı olduğunu belirten bir çizgi animasyonu daha eklememiz gerekir. Vanalar bazen enerjili olmadığı halde açık ama enerjilenince kapatan vanalar olabilir (NO - normally open  tip vanalar). Bu durumda kare içini yeşil yaparak enerjili olduğunu belirtmek işe yaramaz vana içerisinden akışın olup olmadığını da animasyon etmemiz gerekir. 

Önce pompa için eklediğimiz 2 kare elemanı kopyalayıp v1 vana değişkenine bağlayalım.
  • Rectangle : p1Manual'den kopyalanmış, ad: v1Manual, left: 362, top: 28, tag: v1
  • Rectangle : p1Kare'den kopyalanmış, ad: v1Kare, left: 366, top: 32, tag: v1
Bunların ilgili satırlarını da animasyon rutinine eklersek

        private void OnAnimTimerEvent(object source, EventArgs e)
        {
            ...
            RectangleAssign(v1Kare);
            RectangleAssign(p1Kare);
            RectangleForceAssign(v1Manual);
            RectangleForceAssign(p1Manual);
        }


Hızlanmaya başladık, bir kopya iki satır program yeni animasyon hazır. Sonra diyorlar ki "amaan programcılık dediğin kopyala/yapıştır", napsak her birini tekrar baştan yapsak da iş yapıyormuş gibi mi görünsek.?

Vana içinden geçen akışı simüle edebilmek için öncelikle akış şemasında bağlantılarını yapmak için 2 adet portu temsil eden çizgi ekleyelim görseline.
  • Rectangle : ad: v1Port, width: 12, height: 3, fill: black, stroke: sıfırla, left: 359, top: 41
  • Rectangle : ad: v1Port2, width: 12, height: 3, fill: black, stroke: sıfırla, left: 382, top: 41
Bu arada çizgi tam ortaya gelemedi, bu yüzden içteki kareyi 21x21 piksel, turuncu kareyi de 29x29 piksel yaparsak daha iyi olacak.


Vana akışını göstermek için bu portların arasına birleştirecek bir çizgi daha ekleyelim ve onu LineAssign adlı bir metodla simülasyona bağlayalım.
  • Rectangle : ad: v1Act, width: 11, height: 3, fill: black, stroke: sıfırla, left: 371, top: 41, tag: v1

        private void OnAnimTimerEvent(object source, EventArgs e)
        {
            ...
            RectangleAssign(v1Kare);
            RectangleAssign(p1Kare);
            RectangleForceAssign(v1Manual);
            RectangleForceAssign(p1Manual);
            LineAssign(v1Act);
        }
...
        private void LineAssign(Rectangle rectangle)
        {
            string name = rectangle.Tag.ToString();
            UInt16 value = (UInt16)Tags[name];
            if ((value & 1) != 0)   // out
            {
                rectangle.Visibility = Visibility.Visible;
            }
            else
            {
                rectangle.Visibility = Visibility.Hidden;
            }
        }

Böylece vanalarımız için de kalıp hazır. Şimdi aynı kalıbı v2, v3 ve v4 vanaları için de uygulayalım.



        private void OnAnimTimerEvent(object source, EventArgs e)
        {
            ...
            RectangleAssign(v1Kare);
            RectangleAssign(v2Kare);
            RectangleAssign(v3Kare);
            RectangleAssign(v4Kare);
            RectangleAssign(p1Kare);
            RectangleForceAssign(v1Manual);
            RectangleForceAssign(v2Manual);
            RectangleForceAssign(v3Manual);
            RectangleForceAssign(v4Manual);
            RectangleForceAssign(p1Manual);
            LineAssign(v1Act);
            LineAssign(v2Act);
            LineAssign(v3Act);
            LineAssign(v4Act);
        }





Kendi Elemanımızı Tanımlamak


Buraya kadar yaptığımız 2 adet animasyon elemanını kullanıcı tanımlı kontrol olarak tanımlayalım ki daha sonraki projelerimizde bir sürü kodu tekrar yazmak (kopyalamak) zorunda kalmayalım. Önce pompa animasyonu için kullandığımız iki kareyi tek bir kontrol içinde toplayarak başlayalım.

Proje ağaç görünümünde HMI projemize sağ tıklayıp Ekle -> Kullanıcı Denetimi (WPF) seçelim.


İsim olarak KareControl.xaml yazalım ve ekle butonuna tıklayalım. Tasarımcı açılır şu anda sadece bir Grid elemanı bulunuyor. Özelliklerini şöyle yapalım
  • User Control : width: 28, height: 28
  • Grid : width : 28, height : 28
Sonra karelerimizi ekleyelim
  • Rectangle : Ad: manual, width: 28, height: 28, left: 0, top: 0, fill: orange, stroke: sıfırla
  • Rectangle : Ad: kare, width: 20, height: 20, left 4, top: 4, StrokeThickness: 3, fill: white, stroke: black

Kontrolümüzün CSharp koduna dışarıdan erişilebilecek OnAnimTimer metodunu ekleyelim ve tüm animasyonları onun içine toplayalım.

KareControl.xaml.cs
        public KareControl()
        {
            InitializeComponent();
        }

        public void OnAnimTimer(UInt16 value)
        {
            if ((value & 1) != 0)   // out
            {
                kare.Fill = Brushes.Lime;
            }
            else
            {
                kare.Fill = Brushes.White;
            }

            if ((value & 8) != 0)   // mask
            {
                kare.Stroke = Brushes.Red;
            }
            else
            {
                kare.Stroke = Brushes.Black;
            }

            int ms = DateTime.Now.Millisecond;
            if (((value & 16) != 0) & ms > 500)  // Alarm blink
            {
                kare.Fill = Brushes.Red;
            }

            if ((value & 4) != 0)   // force
            {
                manual.Visibility = Visibility.Visible;
            }
            else
            {
                manual.Visibility = Visibility.Hidden;
            }
        }

Şimdi ana ekranımıza dönelim ve bir KareControl elemanı ekleyelim. Araç kutusunda HMI Denetimleri kısmında görünecektir.


  • KareControl : Ad: p1KareControl, width: 28, height: 28, left: 486, top: 130, tag: p1
Şimdi bu kontrolün animasyon metodunu çağırmak için timer rutinimize ekleme yapalım.

MainWindow.xaml.cs
        private void OnAnimTimerEvent(object source, EventArgs e)
        {
            ...
            LineAssign(v4Act);
            foreach (UIElement el in mainGrid.Children)
            {
                if (el.GetType() == Type.GetType("HMI.KareControl"))    //KareControl sınıfıysa
                {
                    KareControl kc = (KareControl)el;
                    kc.OnAnimTimer((UInt16)Tags[kc.Tag.ToString()]);
                }
            }
        }
Bu arada içinde arama yapabilmek için pencerenin ana grid elemanını mainGrid olarak adlandırdık. Bu kod ne yapıyor? Grid üzerindeki her elemanı alıyor, eğer elemanın tipi HMI.KareControl ise yani bizim tanımladığımız eleman ise onun OnAnimTimer metodunu yine kendine verilmiş Tag özelliği değeri ile çağırıyor. 

Artık ekrana yüzlerce KareControl elemanı da koysak bu koda ilave yapmamıza gerek yok. aslında KareControl elemanına p1KareControl gibi isim vermeye de gerek yok, çünkü bu kod tüm KareControl elemanlarını tarayacaktır.




VanaControl Elemanını Tanımlayalım


Bu KareControl elemanını kopyalayarak bir de VanaControl elemanı tanımlayalım. Proje ağacında KareControl.xaml'yi seçip Ctrl+c ve Ctrl+v basalım, KareControl-Kopyala.xaml dosyası oluşturulacaktır. Dosyaya  sağ tıklayıp Yeniden adlandır seçelim ve adını VanaControl.xaml olarak değiştirelim. 


Sınıf ismi hala KareControl olarak duruyor , onu da tıklayıp kodunu açalım ve adını VanaControl olarak değiştirelim. 


Tasarımcıda VanaControl.xaml görselini açalım ve XAML kod görüntüsünde hala HMI.KareControl olan class adını HMI.VanaControl olarak değiştirelim. 


Şimdi ağaaç görünümünden HMI projemize sağ tıklayıp Tekrar derle seçtiğimizde derlemenin hata vermeden başarılı olarak gerçekleşmesi gerekir.

Gelelim görseldeki ilavelere. Öncelikle mevcut gröünümü içine portları alabilecek şekilde değiştirelim.
  • UserControl: width: 37, height: 29
  • Grid : width: 37, height: 29
  • Rectangle (manual) : left: 4, width: 29, height: 29
  • Rectangle (kare) : left: 8, width: 21, height: 21

Şimdi iki adet port ve 1 adet akış aktivasyonu animasyon çizgisi ekleyelim.
  • Rectangle : ad: port, width: 12, height: 3, fill: black, stroke: sıfırla, left: 0, top: 13
  • Rectangle : ad: port2, width: 12, height: 3, fill: black, stroke: sıfırla, left: 25, top: 13
  • Rectangle : ad: act, width: 13, height: 3, fill: black, stroke: sıfırla, left: 12, top: 13
Şimi çizginin animasyonunu da OnAnimTimer metoduna ekleyelim

VanaControl.xaml.cs
        public void OnAnimTimer(UInt16 value)
        {
            if ((value & 1) != 0)   // out
            {
                kare.Fill = Brushes.Lime;
                act.Visibility = Visibility.Visible;
            }
            else
            {
                kare.Fill = Brushes.White;
                act.Visibility = Visibility.Hidden;
            }

            ....
        }
Zaten out bitine göre animasyon yapan bir kısım vardı, ona act çizgisinin görünüp görünmeyeceğine dair ilave yaptık. Şimdi ana ekrana VanaControl elemanımızdan ekleyelim bakalım
  • VanaControl : Ad: v1VanaControl, width: 37, height: 29, left: 482, top: 28, tag: v1
Kodumuza da animasyonunu çağıracak kısmı ekleyelim

            foreach (UIElement el in mainGrid.Children)
            {
                if (el.GetType() == Type.GetType("HMI.KareControl"))    //KareControl sınıfıysa
                {
                    KareControl kc = (KareControl)el;
                    kc.OnAnimTimer((UInt16)Tags[kc.Tag.ToString()]);
                }
                if (el.GetType() == Type.GetType("HMI.VanaControl"))    //VanaControl sınıfıysa
                {
                    VanaControl vc = (VanaControl)el;
                    vc.OnAnimTimer((UInt16)Tags[vc.Tag.ToString()]);
                }
            }

Çalıştırıp test edelim:


Şimdi diğer 3 vananın görsellerini de kopyalayarak ekleyelim
  • VanaControl : Ad: v2VanaControl, left: 541, top: 53, tag: v2
  • VanaControl : Ad: v3VanaControl, left: 482, top: 78, tag: v3
  • VanaControl : Ad: v4VanaControl, left: 541, top: 103, tag: v4

Kodda bir ilave yapmaya gerek yok. İlave ettiğimiz VanaControl elemanları da kodumuz tarafından otomatik olarak bulunup animasyonları gerçekleştirilecektir.. Gördüğümüz gibi adım adım nakış gibi işleyerek kodumuzu hızlı geliştirilebilir hale getirdik.






Akış Şemasının Çizilmesi


Yazının en başında verdiğim resimde ekranın alt kısmında bir proses akış şeması vardı. Bir tank var. İçindeki ürün bir pompa ile hedefe gönderiliyor. Gönderme başında v2 vanası açılarak tanktan alınan ürün p1 pompası ile hatta basılıyor. Hat içindeki su v4 vanası ile drain ediliyor. Sonra v3 vanası açarak dolum başlıyor. Üretim sonunda da hat başından v1 vanası ile su verilerek hatta kalan ürün doluma su ile itiliyor. (mesela böyle bir senaryo)

Bu görünümün şu ana kadar görmediğimiz elemanı tank görüntüsü. Bu tip kalıcı görüntüler için ben Inkscape programını kullanıyorum. Orada hazırlayıp png olarak export ettiğim resimleri görsellerimde kullanıyorum. Burada kullandığım tank.png dosyasını burada vereyim.


Bu dosyayı tank.png adıyla HMI uygulamamızın ana klasörüne kopyalayalım. Sonra sayfaya şunları ekleyelim:
  • Image : width: 52, height: 100, left: 104, top: 177, source: tank.png
  • v2VanaControl (olanı değiştirin) : rotate: 90, left: 112, top 292
  • Rectangle : width: 3, height: 12, fill: black, stroke: sıfırla, left: 128, top: 277
  • Rectangle : width: 3, height: 12, fill: black, stroke: sıfırla, left: 128, top: 325
  • v1VanaControl (olanı değiştirin) : left: 61, top 322
  • Rectangle : width: 46, height: 3, fill: blue, stroke: sıfırla, left: 15, top: 335
  • Label : content: Su, padding: 0, foreground: blue, metin: bold, left: 15, top: 319
  • Rectangle : width: 110, height: 3, fill: black, stroke: sıfırla, left: 98, top: 335
  • Pompa arka plan resmini taşıyın ama aktivasyonu kalsın onun yerine KareControl elemanını taşıyalım
  • p1KareControl  (olanı değiştirin): left: 238, top: 322
  • Rectangle : width: 3, height: 20, fill: black, stroke: sıfırla, left: 211, top: 293
  • Rectangle : width: 400, height: 3, fill: black, stroke: sıfırla, left: 211, top: 293
  • v3VanaControl (olanı değiştirin) : left: 611, top 280
  • v4VanaControl (olanı değiştirin) : rotate: 90, left: 573, top 313, width: 37, height: 29
  • Rectangle : width: 3, height: 16, fill: black, stroke: sıfırla, left: 590, top: 293
  • Rectangle : width: 2, height: 15, fill: black, stroke: sıfırla, left: 585, top: 345, rotate: -45
  • Rectangle : width: 2, height: 15, fill: black, stroke: sıfırla, left: 596, top: 345, rotate: 45
  • Rectangle : width: 2, height: 15, fill: black, stroke: sıfırla, left: 590, top: 357, rotate: 0
  • Label : content: v1, foreground: brown, metin: italic, left: 72, top: 305
  • Label : content: v2, foreground: brown, metin: italic, left: 146, top: 300
  • Label : content: v3, foreground: brown, metin: italic, left: 624, top: 262
  • Label : content: v4, foreground: brown, metin: italic, left: 561, top: 321
  • Label : content: p1, foreground: brown, metin: italic, left: 294, top: 325

Bu kadarla bitmedi tabi. Ama inşallah gerisini de getiririz. Güzel bir yazı oldu kanaatimce. Umarım size projelerinizde örnek olabilecek bilgiler içermiştir. Bir dahaki yazımızda buluşmak üzere kalın sağlıcakla..






Hiç yorum yok:

Yorum Gönder