项目背景
我集团公司流水线上的生产,每一个部件、产品完工后,都会贴上预先打印好的条形码,并且通过扫描枪扫描入系统。公司已有一套外购系统完成对流水线数据的采集。高层从"智能制造"理念出发,需要推广电子看板来监督流水线的作业效率。主要有三点功能要求:
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过程中需要查资料需要长时间思考的,自己不熟悉的,应该都归属于当前的"难点"。
虽然本系统很顺利的被我一个人断断续续的开发完成,但有一些技术自己不熟练的,还是应该重视并且记录。本系统属于一个小型项目,主要是实时动态绘图,后期很可能有一些扩展功能(报表、权限什么的...)。由于本人水平有限,在设计上如果有缺陷,还请朋友们指出。
另:出于保密因素,本系统暂时无法提供源代码下载。即使这样,核心代码都已给出。希望不会影响你的学习和参考。
希望本文对你有帮助。