2016年5月17日 星期二

自動化設備PC程式參數檔案架構介紹與改進(一)

不想看廢話,直接看最後範例

在自動化設備的程式中,一定會有參數檔案的運用,通常分為以下兩種:

系統參數(System Data) : 指打開程式後,程式要恢復的狀態(ex : 上次程式關閉前選擇的工單、語系…)。
工單參數(Recipe Data) : 指某一類產品的加工(製程)參數,藉著選擇不同工單,讓設備採用不同的製程參數加工。

實作在系統中的流程如下圖,比較需要注意有以下幾點:
1. System data的檔案路徑是唯一的,通常會在程式定好一個檔案路徑。
2. 注意參數初始值的問題。 Ex: 數值型別預設值是0,新增recipe時,會得到一堆數值為0的欄位。
3. 在System data中定義一個string參數 ,儲存當前recipe data的file path。(*1)
4. 文字型別轉型為數值型別時的例外處理參數設定頁面的控制項防呆
(*1) 採用Recipe data 的file path是因為recipe system基於windows 的file system,
如果有另外寫自己的recipe system, 也有可能是recipe的index,
不過既然是windows base的系統,採用檔案路徑會比較直覺。

說明完程式架構,來看看參數檔的樣子,最常見的格式就是ini檔,格式如下:

通常會用Win32API (WritePrivateProfileString、GetPrivateProfileString)去讀,細節就不實作了,最後大概都會寫成以下的func。

//取得值:
string GetKeyValue(string section, string key)
//設定值:
void SetKeyValue(string section, string key, string value)

使用方式如下:
//初始化
IniFile ini = new IniFile(@"C:\Data.ini");
//取值
string val = ini.GetKeyVaule("Section2","key1"); //val = value1
//寫值
ini.SetKeyValue("Section2","key2","NewValue"); // ini 的section2的key2會變成 NewValue


實作一開始的流程圖,在win form的程式中大概會長以下這樣。(為求簡潔先省略轉型例外處理,另外本文主旨是參數檔案架構,所以並沒有實作Recipe system,一般會另外寫程式來增刪改查Recipe清單,日後會在寫一篇文章介紹Recipe system)
public partial class Form1 : Form
{
    private string systemDataFilePath;
    private IniSystem iniSystem;
    public SystemData SystemData { get; set; }
    public RecipeData RecipeData { get; set; }
 
    public Form1()
    {
        InitializeComponent();
    }
 
    private void Form1_Load(object sender , EventArgs e)
    {
        systemDataFilePath = @"C:\System\System.ini";
        SystemData = new SystemData();
        RecipeData = new RecipeData();
        loadIniFile();
    }
 
    private void Form1_FormClosing(object sender , FormClosingEventArgs e)
    {
        saveIniFile();
    }
 
    private void saveIniFile()
    {
        iniSystem = new IniSystem(SystemData.RecipeFilePath);
        iniSystem.SetKeyValue("Recipe" , "Speed" , RecipeData.Speed.ToString());
        iniSystem.SetKeyValue("Recipe" , "Acc" , RecipeData.Acc.ToString());
        iniSystem.SetKeyValue("Recipe" , "Dec" , RecipeData.Dec.ToString());
        iniSystem = new IniSystem(systemDataFilePath);
        iniSystem.SetKeyValue("System" , "RecipeFilePath" , RecipeData.Speed.ToString());
        iniSystem.SetKeyValue("System" , "Data1" , RecipeData.Acc.ToString());
    }
 
    private void loadIniFile()
    {
        if (File.Exists(systemDataFilePath))
        {
            //讀取系統參數
            iniSystem = new IniSystem(systemDataFilePath);
            SystemData.Language = iniSystem.GetKeyValue("System" , "Language");
            SystemData.RecipeFilePath = iniSystem.GetKeyValue("System" , "RecipeFilePath");
 
            if (File.Exists(systemDataFilePath))
            {
                //讀取工單參數
                iniSystem = new IniSystem(SystemData.RecipeFilePath);
                RecipeData.Speed = Convert.ToDouble(iniSystem.GetKeyValue("Recipe" , "Speed"));
                RecipeData.Acc = Convert.ToDouble(iniSystem.GetKeyValue("Recipe" , "Acc"));
                RecipeData.Dec = Convert.ToDouble(iniSystem.GetKeyValue("Recipe" , "Dec"));
            }
            else
            {
                //create recipe data file with default vaule
            }
        }
        else
        {
            //create system data file with default vaule
        }
    }
 
    private void recipeChange(string path)
    {
        SystemData.RecipeFilePath = path;
    }
 
    private void refreshControls()
    {
        //通常會在winform上建立一些控制項去改變recipe or system data的數值
        //為了文件簡潔先省略實作
    }
}

上面程式簡單實作了文章開頭的架構圖,看起來似乎沒有什麼問題,其實魔鬼藏在細節,想當初剛接手設備時也是身受其害,接著就來說說缺點:

1. 新增或刪除參數時,必須逐一修改loadIniFile()、saveIniFile()中對應的程式碼。
2. 如果參數非字串型別,從ini file讀回時,必須一一處理轉型問題
3. 新增或刪除參數時,必須修改在Winform參數頁面的layout對應。
4. 如果參數是自訂型別,必須把實體內層參數提取出來。
5. 無法簡單的處理集合資料,必須一筆一筆資料讀寫。
6. 參數讀寫與控制高度耦合在Winform之中。

第1-3點非常擾人,當設備是處於測試階段時,參數通常都會變來變去,所以要一直去改這些程式碼,只要一個地方沒改好就沒辦法馬上測試,當設備是跟旁人協作時,旁人這時就會發起注視攻擊,一邊問『還要多久?』,菜鳥內心應該感到無限靠北感慨。

第3點是當原有的參數設定畫面放不下新的控制項時,若要求介面美觀,必須得一個一個元件去修改layout,這也相當耗費時間,通常都會先隨便拉個介面應急,採取迴避大法說:『這個我要回去改一下』,然後回辦公室邊吃香蕉邊改介面。

第4 5點不一定會遇到,但既然是物件導向的語言,自訂class是很常有的,如果想要以自訂class作為參數型別,在撰寫讀寫部份程式碼時,過程冗長又容易出錯,第5點同4。

第6點是前五點的結論,在簡單的小程式中做上述操作不會太難,吃根香蕉認真地做個碼猴,施展Ctrl+C – V大法,再稍微修修剪剪一下很快就可以完成XD。但…我想簡單的小程式這種東西應該不會存在自動化設備中…

提出這麼多缺點靠北後,來分析一下問題(吃太多香蕉):

1. 讀取\寫入檔案一定要這樣一個一個刷物件裡的變數嗎?
2. 基本型別的參數不能自己轉型成對應的型別嗎?
3. 修改參數的介面一定得一個一個拉嗎?

終於到本文的末段了,好不容易才引出這三道題目,那要怎麼解決這些問題呢?查了一下發現序列化可以簡單的解決上述1和2的問題,參考黑大的文章,從下圖可得知,只要把黑大範例中的bigList換成參數實體SystemData、RecipeData就可以依樣畫葫蘆,整個instance序列化與反序列化並且存讀檔,完美避開要刷參數與參數轉型問題。

那第三個問題又要怎麼解決呢?每台設備的參數通常都不會一樣,根本無法統一成一個介面,最後也是避免不了對UI修修改改,其實visual studio已經告訴我們答案,參考下圖,很眼熟吧!這是在VS中控制項的屬性頁,每當你選到不同控制項時,畫面就會變成對應的變數,其實這就是Property Grid,如果我們以Property Grid作為設定參數的頁面,那就算變更參數也不需要再去維護介面了。

這邊先小結一下,所以只要透過序列化加上以PropertyGrid作為參數設定頁面,就可以避免採用Ini與不斷修改參數設定頁面的種種不便,從此每天準時下班享受幸福人生。呃!事情當然沒有這麼理想,雖然解決了上述繁瑣的操作,但也有一些待解決的問題與侷限在,首先序列化與反序列化部份要支援泛型才能寫成函式庫使用,其二設定參數的介面也還是要跟User交互,並不是拉個Property Grid就可以下課,下篇文章將會進一步說明這些細節。