c#实现上位机数据采集的项目总结

功能要求

  1. 采集来自PLC的生产数据和报警信息
  2. 采集来自测试仪的测试数据
  3. 可以分机种保存一些参数
  4. 将采集的数据写入Excel,供MES系统查询和现场查看

项目实现

1、采集PLC数据

硬件采用RS232串口,通讯协议使用ModusbusRTU。以前跟PLC通讯,为求简单,一般直接使用无协议,使用倒没问题,但偶尔出现数据丢失也是烦人,要确保对方接收到数据还要双方写返回数据。使用modbus协议,PLC基本不用写通讯程序,而上位机可以直接自己确认对方是否收到数据,再确认是否给对方重发一次。这个项目只要求采集数据,因为读取的频率高于数据变化,所以偶尔读不到都不影响,使用modbus就更合适了。

        /// 
        /// 通过Modbus读取PLC,获取相应数据
        /// 
        private void ScanPLC()
        {
            //创建字节数组
            byte[] result = null;
            //从站号
            ushort DevAdd = 1;
            //起始地址
            ushort Address = 100;
            //长度
            ushort Length = 5;
            while (running)
            {
                if (plcConnected)
                {
                    result = objModbus.ReadKeepReg(DevAdd, Address, Length);
                    DataProcessing(result, Length);
                }
                else
                {
                    try
                    {
                        objModbus.OpenMyCom(9600, comName1, 8, Parity.Odd, StopBits.One);
                        //objModbus.ReadTimeOut = 500;
                        plcConnected = true;
                        textReceive.Invoke(new ExecuteWork(RecRunData), "重连PLC成功");
                    }
                    catch (Exception ex)
                    {
                        textReceive.Invoke(new ExecuteWork(RecRunData), "重连PLC失败:" + ex.Message);
                        plcConnected = false;
                    }
                }
                Thread.Sleep(1000);
            }

        }

 2、采集测试仪数据

硬件也是采用RS232串口,因为测试仪的说明书不完善,modbus协议没有测试成功,而无协议测试成功了,所以直接采用无协议接收数据。采取的采集数据模式是,测试仪有测试数据马上主动通过串口发送,而我的程序只要一直处于接收状态,有数据就记下。所以建了一个线程一直扫描串口数据。

关于串口无协议通讯,以前也有个烦恼。就是接收数据有时会把一条数据拆成两条或者两条数据合成一条接收,要进行一些处理判断很是麻烦。这次发现Serial自带的读取功能很全面(可以在Serialport这个类下找到)。于是我直接采用了下面这个函数,再也不用担心老是读错数据了~


        //
        // 摘要:
        //     一直读取到输入缓冲区中的指定 value 的字符串。
        //
        // 参数:
        //   value:
        //     指示读取操作停止位置的值。
        //
        // 返回结果:
        //     输入缓冲区中直到指定 value 的内容。
        //
        // 异常:
        //   T:System.ArgumentException:
        //     长度 value 参数为 0。
        //
        //   T:System.ArgumentNullException:
        //     value 参数为 null。
        //
        //   T:System.InvalidOperationException:
        //     指定的端口未打开。
        //
        //   T:System.TimeoutException:
        //     该操作未完成之前超时期限已结束。
        public string ReadTo(string value);

3、分机种保存参数

这个之前有用过读写TXT文本,有读写过配置文件,也用过自带的应用程序设置。这次使用的是当前机种存在应用程序设置中,具体机种数据则存在ini文件中。应用程序设置只要配置一下读取和保存,很简单。ini文件的读写麻烦些,将机种名设为Section,将具体的参数作为key的值。

        /// 
        /// 读取机种参数
        /// 
        public void ReadFiles()
        {
            try
            {
                IniHelper iniHelp = new IniHelper(@".\config.ini", JiZhong);
                GongDan         = iniHelp.ReadIniData(JiZhong, "GongDan"        );
                LiHao           = iniHelp.ReadIniData(JiZhong, "LiHao"          );
                LiHaoZhouQi     = iniHelp.ReadIniData(JiZhong,"LiHaoZhouQi");
                DianRongGuiGe   = iniHelp.ReadIniData(JiZhong,"DianRongGuiGe");
                WeiZhi          = iniHelp.ReadIniData(JiZhong,"WeiZhi");
                PiLiang         = iniHelp.ReadIniData(JiZhong,"PiLiang");
                YiChangCiShu    = iniHelp.ReadIniData(JiZhong,"YiChangCiShu");
                ChanLiang       = iniHelp.ReadIniData(JiZhong, "ChanLiang");
                JiZhongs        = iniHelp.ReadIniData("JiZhongMing", "JiZhongs");
            }
            catch (Exception)
            {
                MessageBox.Show("未找到配置文件,将使用默认配置");
            }
         }
        /// 
        /// 写入机种参数
        /// 
        public void SaveFiles()
        {
            string path = @".\config.ini";
            if(!File.Exists(path))
            {
                FileStream fs = new FileStream(path, FileMode.CreateNew);
                fs.Close();
            }
            IniHelper iniHelp = new IniHelper(path, JiZhong);
            iniHelp.WriteIniData("GongDan"      , GongDan);
            iniHelp.WriteIniData( "LiHao"        , LiHao        );
            iniHelp.WriteIniData("LiHaoZhouQi", LiHaoZhouQi  );
            iniHelp.WriteIniData("DianRongGuiGe", DianRongGuiGe);
            iniHelp.WriteIniData("WeiZhi", WeiZhi       );
            iniHelp.WriteIniData("PiLiang", PiLiang      );
            iniHelp.WriteIniData("YiChangCiShu", YiChangCiShu );
            iniHelp.WriteIniData( "ChanLiang"    , ChanLiang    );
            IniHelper iniHelp1 = new IniHelper(path, "JiZhongMing");
            iniHelp1.WriteIniData( "JiZhongs", JiZhongs      );
        }

4、将数据写入Excel

这涉及到Excel的读写,之前了解过,但没使用过。网上找到了好几种方案,有使用Office自带库的,有使用各种开源软件的。测试了几种开源工具,都不得其法,后来看用NPOI的很多,而且不需要安装Office,功能也很强大,遂决定采用。通过参考案例,发现还是比较容易使用的。

        /// 
        /// 向Excel中插入行
        /// 
        /// 在第几行插入
        /// 文件路径
        /// 插入的数据<一维数组>
        public void InsertRow(int n = 1, string filePath = "D:\\RunData\\test.xlsx", string[] RecordData =null)
        {
            string fileExt = Path.GetExtension(filePath).ToLower();//获取扩展名
            IWorkbook workbook;

            using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                if (fileExt == ".xlsx") { workbook = new XSSFWorkbook(fs); } 
                else if (fileExt == ".xls") { workbook = new HSSFWorkbook(fs); } 
                else { workbook = null; }
                if (workbook == null) { return; }
                ISheet sheet = workbook.GetSheetAt(0);
                IRow row = sheet.GetRow(n);//获取第一行
                if (row != null)//檢查是否有數據,有數據就下移,沒有就直接賦值
                {
                    //将表格内容整体下移
                    sheet.ShiftRows(n, sheet.LastRowNum, 1);
                }
                var newrow = sheet.CreateRow(n);
                int i = 0;
                foreach (string v in RecordData)
                {
                    ICell cell = newrow.GetCell(i);
                    if (cell == null)
                    {
                        cell = newrow.CreateCell(i);
                    }
                    cell.SetCellValue(v);
                    i++;
                }
                FileStream out2 = new FileStream(filePath, FileMode.Create);
                workbook.Write(out2);
                out2.Close();
            }
        }
    

 当时测试其它开源工具的时候,发现往数据后面添加行很容易,但想把最新数据插入第一行却不容易,后面才转入NPOI的怀抱,因为它有下面这个函数,很合适。

        // 摘要:
        //     Shifts rows between startRow and endRow n number of rows. If you use a negative
        //     number, it will shift rows up. Code ensures that rows don't wrap around. Calls
        //     shiftRows(startRow, endRow, n, false, false); Additionally shifts merged regions
        //     that are completely defined in these rows (ie. merged 2 cells on a row to be
        //     shifted).
        //
        // 参数:
        //   startRow:
        //     the row to start shifting
        //
        //   endRow:
        //     the row to end shifting
        //
        //   n:
        //     the number of rows to shift
        void ShiftRows(int startRow, int endRow, int n);

总结

这个项目虽然不大,但相比我以前做的项目,我在尽可能将代码写得规范容易理解。

  • 将两种通讯协议和读写文件的功能都写成独立的类,使用的时候实例化对应类,再调用方法。
  • 将嵌套过多的程序再提取出独立的方法,让程序更容易理解,vs中的快速操作和重构真的好用。
  • 将运行信息和报错信息,尽可能显示在界面的信息窗口上(因为总信息量不大,所以在界面上显示,要不然就直接存为日志了),对前期查BUG和后期查问题都很有用。
  • 多次使用了开源工具,比如界面图标用了Sunny.UI,读Excel用了NPOI,读ini文件用了IniRW,让开发简单了很多。现在才发现NuGet这个神器真的不应该,对于一些常见的功能实现,使用开源工具可以大幅提高工作效率,而且好的工具也更能保证稳定性。不过对于使用的开源工具遵循怎样的开源协议,不是很明白,有些工具都没找到要遵守什么协议,网上查了也不是很明白。关于这个有解释清晰的资料的,请留言给我,不胜感激。
  • 再次熟悉了委托的使用,因为涉及到多线程的运行信息都显示到界面上,现在控件自带了Invoke方法,使用起来比以前更简单了:
        private delegate void ExecuteWork(string str);
        private void test()
        {
            this.textReceive.Invoke(new ExecuteWork(RecRunData), "运行异常");
        }
        private void RecRunData(string reda)
        {
            if (reda != lastData&&reda.Length>0)
            {
                string tim = DateTime.Now.ToLongTimeString() + "." + DateTime.Now.Millisecond.ToString() + "--";
                textReceive.Text = tim + reda + "\n" + textReceive.Text;
                lastData = reda;
            }
        }

        而且还一种更简单的,甚至不用先声明委托方法,直接一行就可以搞定:

this.textYield.Invoke(new Action(s => { this.textYield.Text = s; }), "运行情况");

努力方向

  1. 了解开源工具的使用规范,对工具的使用有清晰明确地把握
  2. 继续学习c#的编程知识,并将学到的东西在项目中应用起来
  3. 多阅读开源工具的源代码,提升自己的编程思维

参考文章

  • 第三章——NPOI创建/读取Excel简单案例icon-default.png?t=M1H3https://blog.csdn.net/qq_39541254/article/details/107841535
  • SunnyUI的帮助文档icon-default.png?t=M1H3https://gitee.com/yhuse/SunnyUI/wikis/pages
  • C# this.Invoke()的作用与用法icon-default.png?t=M1H3https://www.cnblogs.com/yunmengyunxi/p/6066262.html
  • 使用NPOI实现在Excel第一行插入文字icon-default.png?t=M1H3https://blog.csdn.net/yindi0712/article/details/107410929
  • ……

你可能感兴趣的:(C#,c#,开发语言,后端,modbus)