dangduyhao gửi vào
- 104582 lượt xem
Tiếp nối và bổ sung loạt bài về C# trên Cộng đồng Arduino Việt Nam, mình sẽ giới thiệu với các bạn cách để “Vẽ đồ thị theo thị theo thời gian thực từ Arduino”, nhưng ở đây mình sẽ tổng hợp nhiều nội dung lại để cho mọi người có cái nhìn tổng quan hơn nhé, cho nên bố cục của bài viết sẽ gồm các phần:
- Giao tiếp với Arduino thông qua giao thức Serial, công cụ sử dụng là Windows Forms.
- Điều khiển nhận, lưu, xóa dữ liệu trên Windows Forms
- Hiển thị dữ liệu bằng đối tượng ListView (Đã có một bài viết về việc hiển thị dữ liệu với TextBox, nhưng việc sử dụng TextBox không thuận tiện cho việc lưu dữ liệu sang Excel để tính toán và phân tích)
- Vẽ đồ thị theo thời gian thực (Đây là nội dung chính của bài viết)
- Lưu dữ liệu sang Excel (Cũng đã có một bài viết về vấn đề này rồi, nhưng ở đây mình muốn viết theo cách mà mình làm vì dữ liệu mình lưu là dữ liệu thực và tương đối lớn)
Nếu các bạn hứng thú, hãy bắt tay vào làm nào.
Chuẩn bị
Phần cứng
- 1 board Arduino bất kỳ
- Bất kỳ một cảm biến nào (Trong bài viết, để các bạn dễ dàng thao tác theo mình sẽ sử dụng hàm Random để giả lập dữ liệu của cảm biến)
Phần mềm
- Arduino IDE (Tất nhiên rồi)
- Visual Studio 2015 (Bản Community – Free)
- Nuget – Một ứng dụng dùng để cài đặt các gói (package) dành cho nền tảng phát triển phần mềm của Microsoft, mà chủ yếu là .NET
Ý tưởng
- Chúng ta sẽ gửi giá trị thời gian đo được trên Arduino và giá trị dữ liệu của cảm biến (ở đây mình giả lập) trên cùng một dòng, phân tách nhau bởi một ký tự đặc biệt (mình chọn ký tự gạch đứng “|”) để dễ xử lý ở bên Windows Forms
- Sau đó chuyển nó qua dạng số để vẽ đồ thị và hiển thị
- Cuối cùng là lưu kết quả vào Excel
Tiến hành
Bước 1 Code Arduino
int state = 0; // Tạo biến sự kiện để điều khiển Arduino // Tạo 2 biến để xác định thời gian thực trên Arduino long time_now = 0; long time_start = 0; float random_number(); // Tạo Random, giả lập dữ liệu cảm biến và đếm thời gian thực float data = 0; // Tạo dữ liệu hiển thị của hàm Random void setup() { Serial.begin(9600); // Khởi tạo giao thức Serial, mình chọn baudrate là 9600 } void loop() { // Điều khiển Arduino qua giá trị của biến state if(Serial.available()) { char temp = Serial.read(); if(temp == '0') state = 0; if(temp == '1') state = 1; if(temp == '2') state = 2; } // Thực thi các trường hợp với các giá trị của biến state switch(state) { // state = 0: dừng Arduino case 0: break; // state = 1: thực thi hàm tạo Random, xuất dữ liệu và thời gian thực qua Serial, phân tách nhau bởi ký tự gạch đứng “|” case 1: random_number(); Serial.print(time_now); Serial.print("|"); Serial.println(data); break; // state = 2: Reset dữ liệu và thời gian về 0 case 2: data = 0; time_now = 0; state = 0; break; } } // Hàm tạo Random và đếm thời gian, mình muốn tạo số thực từ 0,001 đến 1000 float random_number() { time_start = millis(); data = random(1,1000000); data = data/1000; delay(100); time_now = time_now + millis() - time_start; }
Bước 2
Mở Visual Studio. Tạo Project Windows Forms Application mới, đặt tên cho nó, ở đây mình đặt là GraphRealTime, chọn đường dẫn lưu (Mình xin phép che đường dẫn của mình đi nhé ^^~)
Bước 3
Cài đặt gói ZedGraph cho Project của bạn, chúng ta sẽ dùng ZedGraph để vẽ đồ thị, tiện lợi hơn Chart của Windows Forms rất nhiều. Chúng ta có thể zoom ngay trên đồ thị, kéo qua kéo lại, còn lưu lại được thành ảnh rất tiện sử dụng. Để cài đặt ZedGraph, chọn Package Manager Console và gõ vào dòng lệnh:
install-package zedgraph
Bước 4
Thêm ZedGraph vào ToolBox. Chuột phải vào General, Choose Item, Browser đến thư mục lưu Project, vào tiếp thư mục packages\ZedGraph.5.1.7\lib\net35-Client trong đó và chọn file ZedGraph.dll (Ở thời điểm mình viết bài này, ZedGraph đang ở phiên bản 5.1.7 nên sẽ có thư mục ZedGraph.5.1.7 ở trong packages). Chọn xong các bạn chọn OK, ZedGraph đã xuất hiện ở tab General.
Bước 5
Thêm Microsoft Excel vào References. Chuột phải vào References, Add References, chọn mục COM, tìm đến dòng Microsoft Excel 16.0 Object Library, tích chọn ở ô vuông đầu dòng và chọn OK (Tùy theo phiên bản Office trong máy mà ở Visual hiện phiên bản tương ứng, của mình là bản 16.0 – tương đương Office 2016)
Bước 6
Vào Project, GraphRealTime Properties (GraphRealTime là tên Project mình đặt lúc ban đầu). Ở mục Settings, điền vào ô Name là “ComName”, Type là string, Scope là User, Value để trống, sau đó ấn Ctrl + S để lưu lại. Bước này mình thao tác như vậy để lưu lại lựa chọn cổng COM mà mình kết nối, giống như việc lưu User và Password trên web vậy, rất tiện cho lần kết nối sau.
Bước 7
Tạo một giao diện để điều khiển và hiển thị
Đổi tên các button theo hình như mình (để tiện kiểm tra và trùng với code mình up, các bạn có thể đổi khác đi nhưng phải sửa lại code nhé, còn tên hiển thị thì thế nào cũng được). Các đối tượng khác như ComboBox, ListView mình để nguyên tên vì nó chỉ có một đối tượng trên Form.
Khi chọn đối tượng ListView, sẽ có tam giác nhỏ màu đen hiện lên ở góc trên bên phải của đối tượng, các bạn thêm 2 cột trong đấy và đặt tên hiển thị là “Thời gian” và “Dữ liệu”, căn độ rộng cho vừa với ListView là ổn
Bước 8: Code Form1.cs
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; // Giao tiếp qua Serial using System.IO; using System.IO.Ports; using System.Xml; // Thêm ZedGraph using ZedGraph; namespace GraphRealTime { public partial class Form1 : Form { string SDatas = String.Empty; // Khai báo chuỗi để lưu dữ liệu cảm biến gửi qua Serial string SRealTime = String.Empty; // Khai báo chuỗi để lưu thời gian gửi qua Serial int status = 0; // Khai báo biến để xử lý sự kiện vẽ đồ thị double realtime = 0; //Khai báo biến thời gian để vẽ đồ thị double datas = 0; //Khai báo biến dữ liệu cảm biến để vẽ đồ thị public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { comboBox1.DataSource = SerialPort.GetPortNames(); // Lấy nguồn cho comboBox là tên của cổng COM comboBox1.Text = Properties.Settings.Default.ComName; // Lấy ComName đã làm ở bước 5 cho comboBox // Khởi tạo ZedGraph GraphPane myPane = zedGraphControl1.GraphPane; myPane.Title.Text = "Đồ thị dữ liệu theo thời gian"; myPane.XAxis.Title.Text = "Thời gian (s)"; myPane.YAxis.Title.Text = "Dữ liệu"; RollingPointPairList list = new RollingPointPairList(60000); LineItem curve = myPane.AddCurve("Dữ liệu", list, Color.Red, SymbolType.None); myPane.XAxis.Scale.Min = 0; myPane.XAxis.Scale.Max = 30; myPane.XAxis.Scale.MinorStep = 1; myPane.XAxis.Scale.MajorStep = 5; myPane.YAxis.Scale.Min = -100; myPane.YAxis.Scale.Max = 100; myPane.AxisChange(); } // Hàm Tick này sẽ bắt sự kiện cổng Serial mở hay không private void timer1_Tick(object sender, EventArgs e) { if (!serialPort1.IsOpen) { progressBar1.Value = 0; } else if (serialPort1.IsOpen) { progressBar1.Value = 100; Draw(); Data_Listview(); status = 0; } } // Hàm này lưu lại cổng COM đã chọn cho lần kết nối private void SaveSetting() { Properties.Settings.Default.ComName = comboBox1.Text; Properties.Settings.Default.Save(); } // Nhận và xử lý string gửi từ Serial private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { string[] arrList = serialPort1.ReadLine().Split('|'); // Đọc một dòng của Serial, cắt chuỗi khi gặp ký tự gạch đứng SRealTime = arrList[0]; // Chuỗi đầu tiên lưu vào SRealTime SDatas = arrList[1]; // Chuỗi thứ hai lưu vào SDatas double.TryParse(SDatas, out datas); // Chuyển đổi sang kiểu double double.TryParse(SRealTime, out realtime); realtime = realtime / 1000.0; // Đối ms sang s status = 1; // Bắt sự kiện xử lý xong chuỗi, đổi starus về 1 để hiển thị dữ liệu trong ListView và vẽ đồ thị } catch { return; } } // Hiển thị dữ liệu trong ListView private void Data_Listview() { if (status == 0) return; else { ListViewItem item = new ListViewItem(realtime.ToString()); // Gán biến realtime vào cột đầu tiên của ListView item.SubItems.Add(datas.ToString()); listView1.Items.Add(item); // Gán biến datas vào cột tiếp theo của ListView // Không nên gán string SDatas vì khi xuất dữ liệu sang Excel sẽ là dạng string, không thực hiện các phép toán được listView1.Items[listView1.Items.Count - 1].EnsureVisible(); // Hiện thị dòng được gán gần nhất ở ListView, tức là mình cuộn ListView theo dữ liệu gần nhất đó } } // Vẽ đồ thị private void Draw() { if (zedGraphControl1.GraphPane.CurveList.Count <= 0) return; LineItem curve = zedGraphControl1.GraphPane.CurveList[0] as LineItem; if (curve == null) return; IPointListEdit list = curve.Points as IPointListEdit; if (list == null) return; list.Add(realtime, datas); // Thêm điểm trên đồ thị Scale xScale = zedGraphControl1.GraphPane.XAxis.Scale; Scale yScale = zedGraphControl1.GraphPane.YAxis.Scale; // Tự động Scale theo trục x if (realtime > xScale.Max - xScale.MajorStep) { xScale.Max = realtime + xScale.MajorStep; xScale.Min = xScale.Max - 30; } // Tự động Scale theo trục y if (datas > yScale.Max - yScale.MajorStep) { yScale.Max = datas + yScale.MajorStep; } else if (datas < yScale.Min + yScale.MajorStep) { yScale.Min = datas - yScale.MajorStep; } zedGraphControl1.AxisChange(); zedGraphControl1.Invalidate(); zedGraphControl1.Refresh(); } // Xóa đồ thị, với ZedGraph thì phải khai báo lại như ở hàm Form1_Load, nếu không sẽ không hiển thị private void ClearZedGraph() { zedGraphControl1.GraphPane.CurveList.Clear(); // Xóa đường zedGraphControl1.GraphPane.GraphObjList.Clear(); // Xóa đối tượng zedGraphControl1.AxisChange(); zedGraphControl1.Invalidate(); GraphPane myPane = zedGraphControl1.GraphPane; myPane.Title.Text = "Đồ thị dữ liệu theo thời gian"; myPane.XAxis.Title.Text = "Thời gian (s)"; myPane.YAxis.Title.Text = "Dữ liệu"; RollingPointPairList list = new RollingPointPairList(60000); LineItem curve = myPane.AddCurve("Dữ liệu", list, Color.Red, SymbolType.None); myPane.XAxis.Scale.Min = 0; myPane.XAxis.Scale.Max = 30; myPane.XAxis.Scale.MinorStep = 1; myPane.XAxis.Scale.MajorStep = 5; myPane.YAxis.Scale.Min = -100; myPane.YAxis.Scale.Max = 100; zedGraphControl1.AxisChange(); } // Hàm xóa dữ liệu private void ResetValue() { realtime = 0; datas = 0; SDatas = String.Empty; SRealTime = String.Empty; status = 0; // Chuyển status về 0 } // Hàm lưu ListView sang Excel private void SaveToExcel() { Microsoft.Office.Interop.Excel.Application xla = new Microsoft.Office.Interop.Excel.Application(); xla.Visible = true; Microsoft.Office.Interop.Excel.Workbook wb = xla.Workbooks.Add(Microsoft.Office.Interop.Excel.XlSheetType.xlWorksheet); Microsoft.Office.Interop.Excel.Worksheet ws = (Microsoft.Office.Interop.Excel.Worksheet)xla.ActiveSheet; // Đặt tên cho hai ô A1. B1 lần lượt là "Thời gian (s)" và "Dữ liệu", sau đó tự động dãn độ rộng Microsoft.Office.Interop.Excel.Range rg = (Microsoft.Office.Interop.Excel.Range)ws.get_Range("A1", "B1"); ws.Cells[1, 1] = "Thời gian (s)"; ws.Cells[1, 2] = "Dữ liệu"; rg.Columns.AutoFit(); // Lưu từ ô đầu tiên của dòng thứ 2, tức ô A2 int i = 2; int j = 1; foreach (ListViewItem comp in listView1.Items) { ws.Cells[i, j] = comp.Text.ToString(); foreach (ListViewItem.ListViewSubItem drv in comp.SubItems) { ws.Cells[i, j] = drv.Text.ToString(); j++; } j = 1; i++; } } // Sự kiện nhấn nút btConnect private void btConnect_Click(object sender, EventArgs e) { if (serialPort1.IsOpen) { serialPort1.Write("2"); //Gửi ký tự "2" qua Serial, tương ứng với state = 2 serialPort1.Close(); btConnect.Text = "Kết nối"; btExit.Enabled = true; SaveSetting(); // Lưu cổng COM vào ComName } else { serialPort1.PortName = comboBox1.Text; // Lấy cổng COM serialPort1.BaudRate = 9600; // Baudrate là 9600, trùng với baudrate của Arduino try { serialPort1.Open(); btConnect.Text = "Ngắt kết nối"; btExit.Enabled = false; } catch { MessageBox.Show("Không thể mở cổng" + serialPort1.PortName, "Lỗi", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } // Sự kiện nhấn nút btExxit private void btExit_Click(object sender, EventArgs e) { DialogResult traloi; traloi = MessageBox.Show("Bạn có chắc muốn thoát?", "Thoát", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); if (traloi == DialogResult.OK) { Application.Exit(); // Đóng ứng dụng } } // Sự kiện nhấn nút btSave private void btSave_Click(object sender, EventArgs e) { DialogResult traloi; traloi = MessageBox.Show("Bạn có muốn lưu số liệu?", "Lưu", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); if (traloi == DialogResult.OK) { SaveToExcel(); // Thực thi hàm lưu ListView sang Excel } } // Sự kiện nhấn nút btRun private void btRun_Click(object sender, EventArgs e) { if (serialPort1.IsOpen) { serialPort1.Write("1"); //Gửi ký tự "1" qua Serial, chạy hàm tạo Random ở Arduino } else MessageBox.Show("Bạn không thể chạy khi chưa kết nối với thiết bị", "Thông báo", MessageBoxButtons.OK, MessageBoxIcon.Error); } // Sự kiện nhấn nút btPause private void btPause_Click(object sender, EventArgs e) { if (serialPort1.IsOpen) { serialPort1.Write("0"); //Gửi ký tự "0" qua Serial, Dừng Arduino } else MessageBox.Show("Bạn không thể dừng khi chưa kết nối với thiết bị", "Thông báo", MessageBoxButtons.OK, MessageBoxIcon.Error); } // Sự kiện nhấn nút Clear private void btClear_Click(object sender, EventArgs e) { if (serialPort1.IsOpen) { DialogResult traloi; traloi = MessageBox.Show("Bạn có chắc muốn xóa?", "Xóa dữ liệu", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); if (traloi == DialogResult.OK) { if (serialPort1.IsOpen) { serialPort1.Write("2"); //Gửi ký tự "2" qua Serial listView1.Items.Clear(); // Xóa listview //Xóa đường trong đồ thị ClearZedGraph(); //Xóa dữ liệu trong Form ResetValue(); } else MessageBox.Show("Bạn không thể dừng khi chưa kết nối với thiết bị", "Thông báo", MessageBoxButtons.OK, MessageBoxIcon.Error); } } else MessageBox.Show("Bạn không thể xóa khi chưa kết nối với thiết bị", "Thông báo", MessageBoxButtons.OK, MessageBoxIcon.Error); } } }
Bước 9
Giờ Form của bạn vẫn chưa thể chạy được đúng không, chúng ta còn một vài thao tác nữa. Đầu tiên hãy chọn vào đối tượng Form1 của bạn, nhìn vào tab Properties ở góc dưới bên phải, bạn thấy biểu tượng hình tia sét chứ, đấy là Events của đối tượng. Đối với đối tượng Form1, ta chọn Events cho Load là Form1_Load
Đối với đối tượng serialPort1, chọn Events cho DataReceived là serialPort1_DataReceived
Đối với đối tượng timer1, chọn Events cho Tick là timer1_Tick, ấn vào icon bên trái icon Events (là icon Properties) chọn Properties cho Enabled là True, cho Interval là 1.
Bước 10
Bây giờ bạn thử Start Project của bạn xem nào, nếu mọi thứ chạy ổn định rồi, hãy Built thành một Application để tiện cho dự án của bạn.
Đây là kết quả sau khi hoàn thành
Bài viết của mình đến đây là hết, cảm ơn các bạn đã theo dõi.
Nếu có bất cứ thắc mắc nào, hãy để lại comment, mình sẽ giải đáp.
Xin cảm ơn và chúc thành công ^^~