2016年3月29日 星期二

C#連接OPC Server (Intouch 2014 R2 using Suitelink)

最近煩惱如何C#連接Intouch 2014 R2,並且要將Tag value寫入Intouch當中,對於我這個Intouch的菜鳥,這是非常的困難,但經過經驗人仕指導一番後就成功了。以下是一個分享:
先下載opcdaauto.dll,作為Project的Reference,再寫入下面經過改良的Class,當中不難看出Class包含連接OPC Server、創建Client Group及寫入Tag Value。
using OPCAutomation;

public class OPC
{
OPCServer KepServer;
OPCGroups KepGroups;
OPCGroup KepGroup;
OPCItems KepItems;
OPCItem KepItem;
string strHostIP = "";
string strHostName = "";
int itmHandleClient = 0;

public bool GetLocalServer()
{
  IPHostEntry IPHost = Dns.GetHostEntry(Environment.MachineName);
  if (IPHost.AddressList.Length > 0)
  {
    strHostIP = IPHost.AddressList[0].ToString();
  }
  else
  {
    MessageBox.Show("GetLocalServer Error 1");
    return false;
  }
  IPHostEntry ipHostEntry = Dns.GetHostEntry(strHostIP);
  strHostName = ipHostEntry.HostName.ToString();
  try
  {
   KepServer = new OPCServer();
   object serverList = KepServer.GetOPCServers();
   KepServer.Connect(((Array)serverList).GetValue(1).ToString(),"localhost");
  }
  catch(Exception ex)
  {
   MessageBox.Show("GetLocalServer Error 2 " + ex.Message);
   return false;
  }
  return true;
}

public bool CreateGroup()
{
  try
  {
    KepGroups = KepServer.OPCGroups;
    KepGroup = KepGroups.Add("MaxsonProgram");
    SetGroupProperty();
    KepGroup.DataChange += new   DIOPCGroupEvent_DataChangeEventHandler(KepGroup_DataChange);
   KepGroup.AsyncWriteComplete += new   DIOPCGroupEvent_AsyncWriteCompleteEventHandler(KepGroup_AsyncWriteComplete);
   KepItems = KepGroup.OPCItems;
  }
  catch (Exception err)
  {
   MessageBox.Show(err.Message, "CreateGroup提示信息", MessageBoxButtons.OK,     MessageBoxIcon.Warning);
   return false;
  }
  return true;
}

public void SetGroupProperty()
{
  KepServer.OPCGroups.DefaultGroupIsActive = true;
  KepServer.OPCGroups.DefaultGroupDeadband = 0;
  KepGroup.UpdateRate = 1000;
  KepGroup.IsActive = true;
  KepGroup.IsSubscribed = true;
}

void KepGroup_AsyncWriteComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array Errors)
{
  MessageBox.Show("Success Write");
}

void KepGroup_DataChange(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps)
{
}

public void opcWrite(string tagname, string value)
{
  try
  {
   Array Errors;
   int cancelID;
   KepItem = KepItems.AddItem(tagname, itmHandleClient);
   int[] temp2 = new int[2] { 0, KepItem.ServerHandle };
   Array serverHandles = (Array)temp2;
   object[] valueTemp = new object[2] { "", value };
   Array values = (Array)valueTemp;
   KepGroup.AsyncWrite(1, ref serverHandles, ref values, out Errors, 2009, out cancelID);
   KepItems.Remove(KepItems.Count, ref serverHandles, out Errors);
   GC.Collect();
 }
 catch (Exception ex) { MessageBox.Show(ex.Message); }
 }
}

如果想調用OPC Class,可依下面作為參考。
OPC opc = new OPC();
if (opc.GetLocalServer())
{
 if (opc.CreateGroup())
 {
   opc.opcWrite("SuiteLink.Topic.Test02", "30");   
   opc.opcWrite("SuiteLink.Topic.Rainfall_Value", "10.33"); 
   opc.opcWrite("SuiteLink.Topic.Rainfall_X", "123.456");  
   opc.opcWrite("SuiteLink.Topic.Rainfall_Y", "654.321");  
   opc.opcWrite("SuiteLink.Topic.WinterMode", "1");  
   opc.opcWrite("SuiteLink.Topic.NormalMode", "0");  
   opc.opcWrite("SuiteLink.Topic.TriggerLevel", "1.6");  
   opc.opcWrite("SuiteLink.Topic.RainingState", "0");
  }

Intouch Setting如下:




Visual Studio Setting如下(.net 4.5 + 32 bit CPU):(必須,我試了很多次,這是必須,否則會連接不上,這可是的花了很多時間得出來的結論)


如果設定成功及運行C# Application,你會在Intouch Viewer中看到Tag Value會改變,亦即代表你成功了。

2016年3月2日 星期三

C# Backgroundworker進階教學(Thread.Sleep的正確使用)

System.Threading.Thread.Sleep相信各位Winform的程式員都知道是什麼,就是停止運作一定的時間,但作為一個已進化的菜鳥,不能夠在正常情況可使用,最好是在BackgroundWorker內用,就能夠神不知鬼不覺,因為當你不在BackgroundWorker內使用,該死的界面就會顯示(Not Responding)的字句,客戶就會走來找你,問是不是死機啊‧‧‧以下是一個簡單的例子。

1‧創建一個WINFORM界面,並新增4個Control─label1、button1、toolStripProgressBar1、backgroundworker1

*backgroundworker1不是User Interface*

2‧background新增Event Handler

WorkerReportsProgress一定要set成true,否則不能加入ProgressChanged Event。
this.backgroundWorker1.WorkerReportsProgress = true;
this.backgroundWorker1.DoWork += new System.ComponentModel.DoWorkEventHandler(this.backgroundWorker1_DoWork);
this.backgroundWorker1.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(this.backgroundWorker1_ProgressChanged);

3‧ProgressChanged Event

主要是紀錄進度,除了toolStripProgressBar,toolStripLabel也是能夠更改的。
void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
   // The progress percentage is a property of e
   toolStripProgressBar1.Value = e.ProgressPercentage;
}

4‧DoWork Event

DoWork這裡會轉換label1的文字,ProgressBar亦會紀錄進度,Thread.Sleep在這裡使用。
在function內接觸Control類的,需要加入Invoke(Action),才可運行,否則會彈出跨線程錯誤。
void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
   Args arg = (Args)e.Argument;
   string a = arg.a;
   string b = arg.b;
   backgroundWorker1.ReportProgress(0);
   label1.Invoke(new Action(() => { label1.Text = b; }));
   backgroundWorker1.ReportProgress(30);
   Thread.Sleep(1000);
   backgroundWorker1.ReportProgress(60);
   label1.Invoke(new Action(() => { label1.Text = a; }));
   backgroundWorker1.ReportProgress(100);
   Thread.Sleep(1000);
   backgroundWorker1.ReportProgress(0);
   button1.Invoke((Action)(() => button1.Enabled = true));
}

5‧Args Class

在DoWork拿取Control的數值、文字是比較困難的,最好的方法是由外面掉Parameter入DoWork Function,所以先創建一個Class。
簡簡單單,看得明白就好。
internal class Args
{
   internal string a { get; set; }
   internal string b { get; set; }
}

5‧Button1 Action

Button1用來啟動BackgroundWorker,而且將剛才的Class放入DoWork這個Function裡面。

private void button1_Click(object sender, EventArgs e)
{
   button1.Enabled = false;
   var args = new Args() { a = "YES", b = "NO" };
   backgroundWorker1.RunWorkerAsync(args);
}

6‧結果

運行時可以見到label1由"No"變成"Yes",而ProgressBar亦會上升,為時大約5秒,而且UI不會出現Not Responding字樣,所以成功了。



這個Backgroundworker、progressbar的進階教學也完結了,多謝各位。

Compressor.io 縮圖大小網頁

網頁3大元素:文字、圖片、影像,三者不可或缺,特別是圖片及影像佔用大量網頁流量傳輸的資源,如何去減少這類資源,以提供瀏覽網頁的用戶更好的體驗,也是一門學問。在此,想分享一下如何壓縮圖片的檔案大小,而畫質也不會很差。
坊間有很多縮圖網站,例如TinyPNG、Compressor.io等等,經過我的多次測試,發現Compressor.io壓縮能力最高(個人意見),所以我會分享Compressor.io的縮圖方法。

網址: https://compressor.io/

以下是需要縮減檔案大小的香港天文台黃雨標誌(139KB),如果網頁用戶用電話瀏覽網頁時使用流動數據,網頁用戶一定會鬧‧‧‧



以下是Compressor.io主頁界面,按一下"Try it"就可以開始了。



有2種縮圖模式,一種Lossy,一種Lossless,很熟悉,像是中學教過的,於是找找資料。

#(Lossless)無損壓縮也即壓縮前和解壓縮後的數據完全一致。多數的無損壓縮都採用RLE行程編碼算法。(Lossy)有損壓縮意味著解壓縮後的數據與壓縮前的數據不一致。在壓縮的過程中要丟失一些人眼和人耳所不敏感的圖像或音頻信息,而且丟失的信息不可恢復。幾乎所有高壓縮的算法都採用有損壓縮,這樣才能達到低數據率的目標。丟失的數據率與壓縮比有關,壓縮比越小,丟失的數據越多,解壓縮後的效果一般越差。此外,某些有損壓縮算法採用多次重複壓縮的方式,這樣還會引起額外的數據丟失。#
通常,我會選取Lossless,壓縮完後的檔案會比較好,失真不會太過嚴重,當然Lossy都不會有大問題。之後按下"Select File"按鈕,選取想要被壓縮的圖片,我選擇了一個黃雨標誌。


結果出現了!!!

檔案大小由139KB,大幅縮減至29.03KB,足足減少了110KB,少了400%,而圖片也沒有失真,以後做圖之後都一定要來壓圖才行。按一下"Download your file"就可打包下載,也有Google Drive,Dropbox選項,非常方便。

2016年2月22日 星期一

C# 及 VB 轉換

該死的Visual Studio,為何你提供這麼多的Language給程序員選擇,導致我有時要寫C#,一時VB。。。辛苦死了。幸好,網上有一個轉換Tool,可快速將VB Code轉成C#,C#轉成VB。(但不是100%能夠成功),以下是網址:
http://converter.telerik.com/

在WEB的左方輸入VB/C# Code,再選選個DropDown,再按個"Convert Code"按鈕,右面就會出現相對應的Code,真是方便至極,愛死Telerik了。

2016年2月20日 星期六

C# Winform 工作計時

最近要做一個Tool,需要Read 26個txt file,每個file有2500行的Data,並紀錄所需時間。原本我以為要用到Timer,但是原來是不用的。 System.Diagnostics 這個Class內有一個好用的Stopwatch,能夠像計時器一樣計出準確時間,而且Coding也是僅僅數句,真是省了很多功夫,數小時就能搞掂(當然包UI,包埋點read)。

下面是Stopwatch的Coding,將所需時間放進一個Label裡面。 var watch = Stopwatch.StartNew();

//Do Work

watch.Stop();
var elapsedMs = watch.ElapsedMilliseconds;
label1.Text = (dlapsedMs / 1000.00).ToString() + " s";


希望以後不會不記得!!!

2016年2月12日 星期五

Windows Server 2012架設NTP時間伺服器(2) 於services.msc 啟動服務

於regedit.exe修改 數值

於桌面右上角按鍵至"Search",並輸入"services.msc"

於Services視窗連按兩下"Windows Time"

於"Windows Time Properties"視窗"General"內Startup type選擇"Automatic"後按下"Start"按鈕,待Service status轉為Running時, 然後按一下"Apply", 最後按一下"OK"

"Windows Time"服務狀態為"Running"即可


步驟2 (services.msc 啟動 服務) 完成!!

Windows Server 2012架設NTP時間伺服器(1) 於regedit.exe修改數值

架設NTP Server顧明思義,就是Sync Time,特別是工程信息公司,Time Sync是絕對、一定、十分重要的,因為資料轉換過程的紀錄時間不容有偏差(~300ms還是可接受的)。以下就是教學:

於regedit.exe修改 數值

於桌面右上角按鍵至"Search",並輸入"regedit"及選擇"regedit.exe"

出現"Registry Editor"後,於左方樹狀列選取(HKEY_LOCAL_MACHINE --> SYSTEM -->CurrentControlSet --> Services --> W32Time)

更改資料夾"Config"的數值至下圖

AnnounceFlags

5

MaxPosPhaseCorrection

ffffffff

MaxNegPhaseCorrection

ffffffff

 

更改資料夾"Parameters"的數值至下圖

NtpServer

stdtime.gov.hk,0x1 time.windows.com,0x2

Type

NTP



更改資料夾"TimeProviders"內"NtpClient"的數值至下圖

SpecialPollInterval

180



更改資料夾"TimeProviders"內"NtpServer"的數值至下圖


Enabled

1

 



步驟1 (regedit.exe修改 數值) 完成!!



Windows Server 2012架設NTP時間伺服器(3) 設定防火牆

設定防火牆

於桌面右上角按鍵至"Search",並輸入"firewall",選取"Windows Firewall"

於"Windows Firewall"視窗按下"Advanced Settings"

於"Windows Firewall with Advanced Security"視窗按一下"Inbound Rules"

按下"New Rule..."

於"New Inbound Rule Wizard"視窗按一下"Port"的圈圈,再按"Next"

按一下"UDP"的圈圈及於"Specific local ports"輸入"123" ,再按"Next"

按一下"Allow the connection"的圈圈,再按"Next"

全部剔,再按"Next"

於"Name"輸入"Time Sync Server" ,再按"Finish"

出現下圖情況即可



步驟3 (設定防火牆) 完成!!

Windows Server 2012 NTP時間伺服器 架設完成!!

透明Animated GIF製作

這圖片不簡單的!!!

圖片是網頁中非常重要的元素,有動作、底色不被局限的圖片更加是重中之重。以下是一個天文台有動畫的黑雨圖案製作。

1‧先下載Tool,我選用的是GIMP (GNU IMAGE MANIPULATION PROGRAM),原因:免費!!!

可到http://www.gimp.org/下載,這個當然不及Photoshop那樣強大,免費就是要將就將就。


2‧預備一張黑雨圖片

經過一輪安裝,待完成後,就預備一張圖片,由於工作需要,我選取了天文台的黑雨圖示,其實還有紅雨、黃雨(= =)。各位Google一下就知道有沒有。


3‧重覆創建2個Layer

先Dulipcate 2個Layer,這是基本Photoshop技巧,沒有什麼特別,各位有什麼不明白,可以上網尋找一些高手的汁圖技巧。

4‧Layer的圖樣

各Layer只需按照下圖便可以了。
Layer 1 (2行雨水)

Layer 2 (1行雨水)

Layer 3 (沒有雨水)

5‧觀看效果

如圖示中顯示,按下Plackback按鈕,之後再按Play,可以預覽Gif的效果。

6‧匯出

由於我之前選取圖片的format為png,現在需要轉為GIF,所以要使用Export Function進行轉換。

製作動畫GIF,必須勾選"As animation" 及 "Loop forever"。
(Option) 可設定每個frame的間隔時間(Delay between frames where unspecified),如果選用delay,需要勾選"Use delay entered above for all frames"

7‧創建網頁

動畫GIF製作完成了,就需要用網頁包著它。 我製作了一個有底色的test.html,放住剛剛的GIF檔

8‧網頁結果

效果就是這樣!!!
辛苦了 。

2016年2月4日 星期四

C# 連接 Oracle Database

由於公司接了一單水務署的工程,需要連接他們的Oracle Database,並拿走少量的數據放進Mysql裡面,在做的過程中,小人從中得知連接Oracle比連接SQL Server、MySQL較為複雜(好似係‧‧‧),所以訂立此帖以作紀錄。

1‧下載Oracle 的ODAC

由於水務署通常使用的系統、設備都 比較舊 (例如:Windows OS、Web Broswer),所以下載低version就 一定無死了 可以了,果然他們使用的是10g (冒汗‧‧‧2016年了‧‧‧)



2‧將已下載的檔案unzip、並將下圖的file轉移至Visual Studio的專案內

只有Oracle.DataAccess.dll能夠成為reference,其餘3個file只需放在一起便可。專案的佈局便大致完成。



3‧創建一個Class(類)

個人習慣,沒有什麼可以解釋。

class OracleData
{
     private string equip_no;
     private string location_code;
     private DateTime comm_date;
     private int maint_threshold;
     private string description;
     private int interval;

     public OracleData()
     {
         // TODO: Complete member initialization
     }

     public string GetEquipNo()
     {
         return equip_no;
     }

     public void SetEquipNo(string eq_no)
     {
         equip_no = eq_no;
     }

     public string GetLocationCode()
     {
         return location_code;
     }

     public void SetLocationCode(string loc_code)
     {
         location_code = loc_code;
     }

     public DateTime GetCommDate()
     {
         return comm_date;
     }

     public void SetCommDate(DateTime comm_dt)
     {
         comm_date = comm_dt;
     }

     public int GetThreshold()
     {
         return maint_threshold;
     }

     public void SetThreshold(int threshold)
     {
         maint_threshold = threshold;
     }

     public string GetDescription()
     {
         return description;
     }

     public void SetDescription(string desc)
     {
         description = desc;
     }

     public int GetInterval()
     {
         return interval;
     }

     public void SetInterval(int interv)
     {
         interval = interv;
     }
 }

4‧引用Oracle.DataAccess.dll


using Oracle.DataAccess.Client; 

5‧Connection String設定

跟SQL Server及MySQL 大致相同 (Username、Password、Host IP、Port),唯一分別是SID,但不一定要使用SID,SERVICE_NAME也可以使用,視乎Server設定
如果server為Express,SERVICE_NAME為XE;如果想直接引用tnsnames.ora,Datasource=ora內的名稱即可。

string connString = "Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=" + oracleIP + ")(PORT=" + oraclePort + ")))" +
                    "(CONNECT_DATA=(SID=" + oracleServiceID + ")));" +
                    "User id=" + oracleUserName + ";" +
                    "Password=" + oraclePassword + ";";


6‧將Oracle 內的Data 打包

query跟SQL Server及MySQL 大致相同,我就不作解釋了,下面的Code大約就是將 Oracle 內的Data加進自訂的List<OracleData>裡面,以便調用。
*DBNull.Value的使用就是防止data field的值是(null)而產生的錯誤。*
*最好將下面的code放進try block以便debug,因為不一定第一次做就能夠成功(你明白的‧‧‧)*
 
OracleConnection conn = new OracleConnection(connString);
OracleCommand cmd = new OracleCommand(query, conn);
cmd.CommandTimeout = 1200;
conn.Open();

OracleDataReader dr = cmd.ExecuteReader();
List<OracleData> content = new List<OracleData>();
while (dr.Read()) {
   OracleData od = new OracleData();
   od.SetEquipNo(dr[0].ToString());
   od.SetLocationCode(dr[1].ToString());
   od.SetCommDate(Convert.ToDateTime(dr[2]));
   if (!DBNull.Value.Equals(dr[3]))
   {
      od.SetThreshold(Convert.ToInt32(dr[3]));
   }
   else
   {
      od.SetThreshold(0);              
   }
   od.SetDescription(dr[4].ToString());
   if (!DBNull.Value.Equals(dr[5]))
   {
      od.SetInterval(Convert.ToInt32(dr[5]));
   }
   else
   {
      od.SetInterval(0);               
   }
   content.Add(od);
}
conn.Close();

C# 連接 Oracle Database 就大致完成了!!