C# (WinForms) - Vẽ đồ thị theo thời gian thực từ Arduino

Mô tả dự án: 

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é ^^~)

3452_1232390-1482338658-0-new.png

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

3414_8822390-1482338662-0-zedgraph.png

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.

3451_1232390-1482338650-0-addzg.png

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)

3412_8822390-1482338646-0-addmo.png

3412_8122390-1482338648-0-addmo-2.png

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

3413_8122390-1482338654-0-gui.png

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

3413_8822390-1482338652-0-formsload.png

Đố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

3453_1232390-1482341940-0-run.png

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 ^^~

lên
18 thành viên đã đánh giá bài viết này hữu ích.
Các dự án được truyền cảm hứng

Select any filter and click on Apply to see results