項目背景
我集團公司流水線上的生產,每一個部件、產品完工后,都會貼上預先打印好的條形碼,並且通過掃描槍掃描入系統。公司已有一套外購系統完成對流水線數據的采集。高層從"智能制造"理念出發,需要推廣電子看板來監督流水線的作業效率。主要有三點功能要求:
1. 采集掃描槍的數據,繪制實時動態曲線圖;
2. 當作業節拍(曲線圖縱坐標)超過SAP系統中的規定值,停線報警;
3. 作業單完工后,數據自動保存,網絡不佳時保存在本地,不影響流水線作業。
總體設計
由於產線已配置電腦和系統對掃描數據做采集,從節約成本的方向考慮,本系統繼續安裝在產線電腦上。顯示方式上,將采用分屏顯示的效果實現(一台電腦接兩個顯示器顯示不同內容,后期其中一台顯示器轉為接高清電視機顯示本系統)。另外產線的xp系統需升級為win7系統。在離線數據存儲上,采用MSMQ實現臨時數據存儲(同時也能極大的減輕數據庫壓力)。未來的方向是統一將各客戶端的臨時數據集中發送到一台中轉站機器的MSMQ中,再在空閑時段將數據發送至數據庫服務器。前期,MSMQ將存儲在本地,並在本系統單獨開一進程隔一段時間向數據庫推送完工作業單的數據。同時,由於鼠標焦點不在本系統上,為方便使用全局鈎子獲取掃描槍信息,本系統采用Winform技術實現。
原型圖&框架
原型圖說明:實際操作中,產線用戶始終在操作和聚焦右邊的主顯示器窗口;左邊的擴展顯示器顯示本系統(電子看板)【兩台顯示器雖共用一個主機,但顯示了不同內容】。電子看板將實時繪制曲線圖顯示作業效率,如果作業效率低於SAP規定的值,則停線報警。(直觀理解是兩次掃描的時間過長則會想起警報...)
本系統主要的功能點集中在實時動態圖表所在的窗口上。即便如此,從后期擴展的方向考慮,我還是搭建了一個系統框架。
使用LinqToSQL做為ORM的簡單三層,由於時間緊迫,暫時缺少權限和日志系統,這是現存的不足。
技術難點&核心代碼
一. 實時動態圖表
本系統最大的難點,包括:a)使用全局鈎子收集掃描槍數據; b)是實時動態圖表的實現。
a). 全局鈎子收集掃描槍數據
修改了下網上流行的"條形碼鈎子"類,網上流行的版本,當條形碼第一個字符是字母的時候,有一些問題。

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Runtime.InteropServices; 6 using System.Reflection; 7 using System.Diagnostics; 8 9 namespace Barcode 10 { 11 /// <summary> 12 /// 條形碼鈎子 13 /// </summary> 14 public class BarCodeHook 15 { 16 public delegate void BarCodeDelegate(BarCodes barCode); 17 public event BarCodeDelegate BarCodeEvent; 18 19 public struct BarCodes 20 { 21 public int VirtKey; //虛擬碼 22 public int ScanCode; //掃描碼 23 public string KeyName; //鍵名 24 public uint AscII; //AscII 25 public char Chr; //字符 26 27 public string BarCode; //條碼信息 28 public bool IsValid; //條碼是否有效 29 public DateTime Time; //掃描時間 30 } 31 32 private struct EventMsg 33 { 34 public int message; 35 public int paramL; 36 public int paramH; 37 public int Time; 38 public int hwnd; 39 } 40 41 //鍵盤Hook結構函數 42 [StructLayout(LayoutKind.Sequential)] 43 public class KeyBoardHookStruct 44 { 45 public int vkCode; 46 public int scanCode; 47 public int flags; 48 public int time; 49 public int dwExtraInfo; 50 } 51 52 /// <summary> 53 /// 監控消息窗口的鈎子 54 /// </summary> 55 /// <param name="idHook"></param> 56 /// <param name="lpfn"></param> 57 /// <param name="hInstance"></param> 58 /// <param name="threadId"></param> 59 /// <returns></returns> 60 [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] 61 private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId); 62 63 [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] 64 private static extern bool UnhookWindowsHookEx(int idHook); 65 66 [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] 67 private static extern int CallNextHookEx(int idHook, int nCode, Int32 wParam, IntPtr lParam); 68 69 [DllImport("user32", EntryPoint = "GetKeyNameText")] 70 private static extern int GetKeyNameText(int lParam, StringBuilder lpBuffer, int nSize); 71 72 [DllImport("user32", EntryPoint = "GetKeyboardState")] 73 private static extern int GetKeyboardState(byte[] pbKeyState); 74 75 [DllImport("user32", EntryPoint = "ToAscii")] 76 private static extern bool ToAscii(int VirtualKey, int ScanCode, byte[] lpKeyState, ref uint lpChar, int uFlags); 77 78 [DllImport("kernel32.dll")] 79 public static extern IntPtr GetModuleHandle(string name); 80 81 [DllImport("user32.dll")] 82 public static extern void SetCursorPos(int x, int y); 83 84 delegate int HookProc(int nCode, Int32 wParam, IntPtr lParam); 85 86 BarCodes barCode = new BarCodes(); 87 int hKeyboardHook = 0; 88 //此處使用char List 避免了原有代碼中掃描出的結果是亂碼的情況 89 List<char> _barcode = new List<char>(100); 90 private int KeyboardHookProc(int nCode, Int32 wParam, IntPtr lParam) 91 { 92 if (nCode == 0) 93 { 94 EventMsg msg = (EventMsg)Marshal.PtrToStructure(lParam, typeof(EventMsg)); 95 if (msg.message == (int)System.Windows.Forms.Keys.H && (int)System.Windows.Forms.Control.ModifierKeys == (int)System.Windows.Forms.Keys.Control + (int)System.Windows.Forms.Keys.Alt) //截獲Ctrl+Alt+H 96 { 97 SetCursorPos(200, 200);//組合鍵使鼠標回到主屏幕 98 } 99 if (wParam == 0x100) //WM_KEYDOWN = 0x100 100 { 101 barCode.VirtKey = msg.message & 0xff; //虛擬碼 102 barCode.ScanCode = msg.paramL & 0xff; //掃描碼 103 104 StringBuilder strKeyName = new StringBuilder(255); 105 if (GetKeyNameText(barCode.ScanCode * 65536, strKeyName, 255) > 0) 106 { 107 barCode.KeyName = strKeyName.ToString().Trim(new char[] { ' ', '\0' }); 108 } 109 else 110 { 111 barCode.KeyName = ""; 112 } 113 114 byte[] kbArray = new byte[256]; 115 uint uKey = 0; 116 GetKeyboardState(kbArray); 117 if (ToAscii(barCode.VirtKey, barCode.ScanCode, kbArray, ref uKey, 0)) 118 { 119 barCode.AscII = uKey; 120 barCode.Chr = Convert.ToChar(uKey); 121 } 122 123 if (DateTime.Now.Subtract(barCode.Time).TotalMilliseconds > 50) 124 { 125 _barcode.Clear(); 126 } 127 else 128 { 129 if ((msg.message & 0xff) == 13 && _barcode.Count > 0) //回車 130 { 131 barCode.BarCode = new String(_barcode.ToArray()); 132 barCode.IsValid = true; 133 } 134 if (msg.message != 160)//加對空格的排除處理 135 _barcode.Add(Convert.ToChar(msg.message & 0xff)); 136 } 137 138 barCode.Time = DateTime.Now; 139 if (BarCodeEvent != null) BarCodeEvent(barCode); //觸發事件 140 barCode.IsValid = false; 141 } 142 } 143 return CallNextHookEx(hKeyboardHook, nCode, wParam, lParam); 144 } 145 146 //增加了一個靜態變量,放置GC將鈎子回收掉 147 private static HookProc hookproc; 148 // 安裝鈎子 149 public bool Start() 150 { 151 if (hKeyboardHook == 0) 152 { 153 hookproc = new HookProc(KeyboardHookProc); 154 //WH_KEYBOARD_LL = 13 155 //hKeyboardHook = SetWindowsHookEx(13, hookproc, Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]), 0); 156 157 //使用全局鈎子 158 IntPtr modulePtr = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName); 159 hKeyboardHook = SetWindowsHookEx(13, hookproc, modulePtr, 0); 160 } 161 return (hKeyboardHook != 0); 162 } 163 164 // 卸載鈎子 165 public bool Stop() 166 { 167 if (hKeyboardHook != 0) 168 { 169 return UnhookWindowsHookEx(hKeyboardHook); 170 } 171 return true; 172 } 173 174 } 175 }
b). 動態實時圖表
根據掃描槍的掃描信號,實時繪制曲線圖,並且當數據超出x軸量程時繪圖自動向左滾動。這里采用微軟MSChart圖表實現。
圖表初始化

1 private void InitChart() 2 { 3 #region 初始化時,設置 X 軸的刻度初始值 4 //設置X軸量程 5 chart1.ChartAreas[0].AxisX.Minimum = DateTime.Now.ToOADate(); 6 chart1.ChartAreas[0].AxisX.Maximum = DateTime.Now.AddHours(4).ToOADate(); 7 //設置X軸間隔類型 8 chart1.ChartAreas[0].AxisX.IntervalType = DateTimeIntervalType.Hours; 9 chart1.ChartAreas[0].AxisX.Interval = 0.5; 10 //設置X軸網格間距類型 11 chart1.ChartAreas[0].AxisX.MajorGrid.IntervalType = DateTimeIntervalType.Hours; 12 chart1.ChartAreas[0].AxisX.MajorGrid.Enabled = true; 13 //設置時間格式 14 chart1.ChartAreas[0].AxisX.LabelStyle.Format = "HH:mm:ss"; 15 chart1.ChartAreas[0].AxisX.MajorGrid.Interval = 0.5; 16 #endregion 17 18 #region 畫板樣式 19 chart1.ChartAreas[0].BackColor = Color.Black; 20 #endregion 21 22 #region 曲線圖初始值設置 23 chart1.Series[0].LegendText = "節拍"; 24 chart1.Series[0].ChartType = SeriesChartType.Spline; 25 //chart1.Series[0].BorderWidth = 1; 26 chart1.Series[0].Color = Color.Green; 27 chart1.Series[0].ShadowOffset = 1; 28 chart1.Series[0].XValueType = ChartValueType.DateTime; 29 chart1.Series[0].Points.AddXY(chart1.ChartAreas[0].AxisX.Minimum, 8); 30 //chart1.Series[0].IsValueShownAsLabel = true;//顯示曲線上點的數值 31 #endregion 32 }
繪圖

1 /// <summary> 2 /// 繪圖 3 /// </summary> 4 /// <param name="currentTime"></param> 5 /// <param name="Beats"></param> 6 private void AddData(DateTime currentTime,double Beats) 7 { 8 foreach (Series ptSeries in chart1.Series) 9 { 10 // Add new data point to its series. 11 ptSeries.Points.AddXY(currentTime.ToOADate(), Beats); 12 //到3/4刻度,向左滾動 13 double removeBefore = currentTime.AddHours((double)(3) * (-1)).ToOADate(); 14 //remove oldest values to maintain a constant number of data points 15 while (ptSeries.Points[0].XValue < removeBefore) 16 { 17 ptSeries.Points.RemoveAt(0); 18 } 19 //設置X軸量程,如果不設置間隔,間隔會自動進行計算(這里是每次重繪圖案,都重新設置間隔) 20 chart1.ChartAreas[0].AxisX.Minimum = ptSeries.Points[0].XValue; 21 chart1.ChartAreas[0].AxisX.Maximum = DateTime.FromOADate(ptSeries.Points[0].XValue).AddHours(4).ToOADate(); 22 //重繪圖案 23 chart1.Invalidate(); 24 } 25 }
二. MSMQ的使用
當然使用前需安裝MSMQ(控制面板->程序和功能->打開或關閉Windows功能),關於MSMQ這里暫不做過多介紹。本系統使用MSMQ用於緩解數據庫壓力和數據離線存儲。

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Messaging; 6 using ALIBoardModel.Model; 7 using ALIBoardBLL; 8 using ALIBoardModel.Config; 9 using DataAccess.Result; 10 11 namespace ALIBoardCore 12 { 13 /// <summary> 14 /// MSMQ消息隊列(使用單例模式確保私有隊列的唯一性) 15 /// </summary> 16 public class ALIMSMQ 17 { 18 private MessageQueue mq = null; 19 20 private static ALIMSMQ _instance = null; 21 22 private WorkInfoBLL bll = new WorkInfoBLL(Connect.ALIConnStr); 23 24 private ALIMSMQ() 25 { 26 if (!MessageQueue.Exists(@".\private$\ALIQueue")) 27 mq = MessageQueue.Create(@".\private$\ALIQueue"); 28 mq = new MessageQueue(@".\private$\ALIQueue"); 29 } 30 31 public static ALIMSMQ GetInstance() 32 { 33 if (_instance == null) 34 _instance = new ALIMSMQ(); 35 return _instance; 36 } 37 38 public void SendMQ(object obj) 39 { 40 mq.Send(obj); 41 } 42 43 public Message ReceiveAndDelete() 44 { 45 return mq.Receive();//取出第一條消息並刪除其在隊列中的位置 46 } 47 48 public void SendDataToDatabase() 49 { 50 //指定讀取消息的格式化程序 51 mq.Formatter = new XmlMessageFormatter(new Type[] { typeof(WorkInfo) }); 52 foreach (Message m in mq.GetAllMessages()) 53 { 54 try 55 { 56 WorkInfo info = m.Body as WorkInfo; 57 if(info != null) 58 info.SyncDate = DateTime.Now; 59 CommandResult result = bll.Insert(info); 60 if(result.Result == ResultCode.Successful) 61 mq.ReceiveById(m.Id); 62 } 63 catch (Exception e) 64 { 65 } 66 } 67 } 68 } 69 }
三. C#連接SAP,調用SAP RFC接口
a). 主要實現代碼(使用SAP NCO3.0)
SAPConfig類

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using SAP.Middleware.Connector; 6 7 namespace SAP 8 { 9 /// <summary> 10 /// 實現IDestinationConfiguration接口成員 11 /// </summary> 12 public class SAPConfig : IDestinationConfiguration 13 { 14 public RfcConfigParameters GetParameters(String destinationName) 15 { 16 if ("MES_ATIBoard_SAP001".Equals(destinationName)) 17 { 18 RfcConfigParameters parms = new RfcConfigParameters(); 19 parms.Add(RfcConfigParameters.AppServerHost, Connect.SAPServer);//SAP服務器 20 parms.Add(RfcConfigParameters.SystemNumber, Connect.SAPSystemNumber); //SAP系統編號 21 parms.Add(RfcConfigParameters.User, Connect.SAPUser); //用戶名 22 parms.Add(RfcConfigParameters.Password, Connect.SAPPassword); //密碼 23 parms.Add(RfcConfigParameters.Client, Connect.SAPClient); // Client(集團) 24 parms.Add(RfcConfigParameters.Language, Connect.SAPLanguage); //登陸語言 25 parms.Add(RfcConfigParameters.PoolSize, "5"); 26 parms.Add(RfcConfigParameters.MaxPoolSize, "20"); 27 parms.Add(RfcConfigParameters.IdleTimeout, "60"); 28 return parms; 29 } 30 return null; 31 } 32 33 public bool ChangeEventsSupported() 34 { 35 return false; 36 } 37 38 public event RfcDestinationManager.ConfigurationChangeHandler ConfigurationChanged; 39 } 40 }
RFC類

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 using SAP.Middleware.Connector; 7 using System.Data; 8 9 namespace SAP 10 { 11 public class RFC : IDisposable 12 { 13 IDestinationConfiguration ID = new SAPConfig(); 14 public void Dispose() 15 { 16 RfcDestinationManager.UnregisterDestinationConfiguration(ID); 17 } 18 19 /// <summary> 20 /// 獲取裝配工藝的sap信息 21 /// </summary> 22 /// <param name="_MATNR">輸入參數(工單號,如:200026179)</param> 23 /// <returns></returns> 24 public SAPInfo GetSAPInfo(string _MATNR) 25 { 26 RfcDestinationManager.RegisterDestinationConfiguration(ID); 27 RfcDestination prd = RfcDestinationManager.GetDestination("MES_ATIBoard_SAP001"); 28 29 RfcRepository repo = prd.Repository; 30 IRfcFunction companyBapi = repo.CreateFunction("ZMES_PRODUCEORDER_WORKTIME"); //調用函數名 31 companyBapi.SetValue("IM_AUFNR", _MATNR); //設置Import的參數 32 companyBapi.Invoke(prd); //執行函數 33 34 IRfcTable table = companyBapi.GetTable("WT_ITEM"); //獲取相應的品號內表 35 36 if (table.RowCount > 0) 37 { 38 DataTable dt = GetDataTableFromRFCTable(table); 39 for (int i = 0; i < dt.Rows.Count; i++) 40 { 41 object keyColumnName = dt.Rows[i]["LTXA1"]; 42 if (keyColumnName != null && keyColumnName.ToString().Trim() == "裝配") 43 { 44 SAPInfo sap = new SAPInfo(); 45 sap.SAPOrderID = dt.Rows[i]["AUFNR"].ToString(); 46 sap.ProductCode = dt.Rows[i]["PLNBEZ"].ToString(); 47 sap.WorkCount = Convert.ToDouble(dt.Rows[i]["GAMNG"]); 48 sap.WTIME = Convert.ToDouble(dt.Rows[i]["WTIME"]); 49 return sap; 50 } 51 } 52 } 53 return null; 54 } 55 56 #region 私有方法 57 private DataTable GetDataTableFromRFCTable(IRfcTable myrfcTable) 58 { 59 DataTable loTable = new DataTable(); 60 int liElement = 0; 61 for (liElement = 0; liElement <= myrfcTable.ElementCount - 1; liElement++) 62 { 63 RfcElementMetadata metadata = myrfcTable.GetElementMetadata(liElement); 64 loTable.Columns.Add(metadata.Name); 65 } 66 foreach (IRfcStructure Row in myrfcTable) 67 { 68 DataRow ldr = loTable.NewRow(); 69 for (liElement = 0; liElement <= myrfcTable.ElementCount - 1; liElement++) 70 { 71 RfcElementMetadata metadata = myrfcTable.GetElementMetadata(liElement); 72 ldr[metadata.Name] = Row.GetString(metadata.Name); 73 } 74 loTable.Rows.Add(ldr); 75 } 76 return loTable; 77 } 78 #endregion 79 } 80 81 public class SAPInfo 82 { 83 /// <summary> 84 /// SAP訂單號 85 /// </summary> 86 public string SAPOrderID { get; set; } 87 /// <summary> 88 /// SAP產品編碼(物料編碼) 89 /// </summary> 90 public string ProductCode { get; set; } 91 /// <summary> 92 /// SAP作業單數量 93 /// </summary> 94 public double WorkCount { get; set; } 95 /// <summary> 96 /// SAP標准工時 97 /// </summary> 98 public double WTIME { get; set; } 99 } 100 101 }
調用
SAP.SAPInfo sap = null; using(SAP.RFC rfc = new SAP.RFC()) { sap = rfc.GetSAPInfo(mesOrderID); }
b). 問題
初次調用SAP接口,調試時多多少少會有一些問題(沒問題的跳過)。請下載以下文件嘗試安裝解決:
SAP NCO 3.0 32位系統安裝文件 sapnco30dotnet40P_8-20007347(32).zip
SAP NCO 3.0 64位系統安裝文件 sapnco30dotnet40P_12-20007348(64).zip
Microsoft Visual C++ 2005 vcredist2005sp1_x86_XiaZaiBa.zip
注意:本系統使用VS2013開發,基於.NET Framework4.0。安裝完SAP NCO 3.0后,在項目中引用sapnco.dll和sapnco_utils.dll即可。
系統效果圖
總結
做為一個程序員,最近發現自己有一個不好的表現:當別人問我項目中有什么技術難點時,我竟只寥寥說了幾個字... 對以前的一個大項目也是如此回答。我不禁反問自己:項目中真的沒有難點嗎?沒有技術難點,那工作量是怎么出來的?時間都去哪兒了?
任何花時間做的項目,coding過程中需要查資料需要長時間思考的,自己不熟悉的,應該都歸屬於當前的"難點"。
雖然本系統很順利的被我一個人斷斷續續的開發完成,但有一些技術自己不熟練的,還是應該重視並且記錄。本系統屬於一個小型項目,主要是實時動態繪圖,后期很可能有一些擴展功能(報表、權限什么的...)。由於本人水平有限,在設計上如果有缺陷,還請朋友們指出。
另:出於保密因素,本系統暫時無法提供源代碼下載。即使這樣,核心代碼都已給出。希望不會影響你的學習和參考。
希望本文對你有幫助。