Embedded/3. C, C#, Python

C# - Arduino Serial 통신을 이용한 비쥬얼라이저 구현

잇(IT) 2024. 6. 25. 17:24

C#과 Arduino의 Serial 통신을 이용하여 간단한 비쥬얼라이저를 구현하였다.

void setup() {
  Serial.begin(9600);  // 시리얼 통신 시작
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);  // LED 핀을 출력 모드로 설정
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);

}

void loop() {
  int sensorValue = analogRead(A0);  // A0 핀의 아날로그 값을 읽어서 sensorValue 변수에 저장
  Serial.println(sensorValue);       // 읽어온 값을 시리얼 모니터에 출력
  if (Serial.available() > 0) {
    int value = Serial.parseInt(); // 시리얼 포트로부터 정수값 읽기
    if (value >= 0 && value <= 255) { // 유효한 범위 내의 값인지 확인
      analogWrite(9, value); // 아두이노의 PWM은 0-255 범위를 가짐
      analogWrite(10, value);
      analogWrite(11, value);
      analogWrite(8, value);
    }
  }
  delay(100);
}

위 코드는 Arduino에 작성한 코드로, A0 Pin을 통해 센서를 거쳐 계속해서 변하는 아날로그 신호 값을 전달 받는다.

A0를 통해 전달받은 신호는 아래 C# 코드의 SerialPort클래스의 SerialDataReceivedEventArgs 인스턴스가 사용된다. 즉 Serial Port를 통해 데이터가 전달되면 데이터 타입과 상태에 대한 정보를 매개변수로 전달 받게 된다.

private void SPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    string s = sPort.ReadLine();
    this.BeginInvoke((new Action(delegate { ShowValue(s); })));


    int tmpValue = Int32.Parse(s);
    int value = (tmpValue - 320) * 2.5;

    if (sendFlag)
    {
        SendToArduino(value);
    }
}

Serial 통신에 핵심이 되는 부분은 위 코드로 Serial Port를 데이터가 전달되면 위 이벤트가 발생하며, SerialPort 클래스 변수인 sPort의 ReadLine()을 통해 데이터를 전달받게 되고, 해당 변수를 저장하게 된다.

저장된 변수는 string으로 전달받기 때문에 Parse를 통해 Int형 변수로 변환시켜 활용하게 된다.

 

입력 전압과 회로에 구성된 저항들과 소리 센서를 거쳐 전달되는 값이 C#으로 실행시킨 그래프를 보게 되면 대략 320 ~ 420 사이의 값을 가지는 것을 볼 수 있다. 

 

아두이노의 PWM 신호 범위는 0~255를 가지기 때문에 320~420의 값이 LED에 불빛의 강도를 조절하기 위해서는 0~255의 값으로 변환될 필요가 있다.

 

때문에 전달받아 변환시킨 신호의 값을 -320한 뒤(신호의 범위가 대략 0 ~ 100정도 될 것이다.) 2.5를 곱해주게 되면 대략 0~255의 데이터가 생성된다.

 

해당 신호를 아두이노의 analogWrite 함수를 통해 전달하게 되면 아두이노 핀을 통해 0 ~ 255의 신호를 전달 받을 수 있고, 해당 신호를 LED로 전달되어 LED 밝기를 조절하게 된다.

 

추가로 C# Tab 기능을 이용하여 신호를 탐지하여 그래프로 보여주는 화면 이외에 비주얼라이저를 실행시키고 끄는 버튼을 간단하게 생성하여 넣어놨다. Start 버튼을 누르게 되면 LED에 불이 들어오기 시작하고 Stop을 누르게 되면 LED의 불이 꺼지게 된다.

 

flag를 사용하여 버튼에 따라 값이 0, 1이 되고 해당 값에 따라 아두이노로 신호를 보내는 함수를 실행 시킬지 안시킬지 결정하게 된다.


C# 전체 코드

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Drawing;
using System.IO.Ports;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;

namespace A197_ArduinoSensorMonitoring
{
    public partial class Form1 : Form
    {
        SerialPort sPort;
        private double xCount = 200;  // 차트에 보여지는 데이터 갯수
        List<SensorData> myData = new List<SensorData>();   // 저장하기 위한 자료구조

        StringBuilder tmpString = new StringBuilder();

        bool sendFlag = false;

        public Form1()
        {
            InitializeComponent();

            // ComboBox
            foreach (var ports in SerialPort.GetPortNames())
            {
                comboBox1.Items.Add(ports);
            }
            comboBox1.Text = "Select Port";

            // 아두이노의 A0에서 받는 값의 범위
            progressBar1.Minimum = 0;
            progressBar1.Maximum = 1024;

            // 차트 모양 세팅
            ChartSetting();

            // 숫자 버튼
            btnPortValue.BackColor = Color.Blue;
            btnPortValue.ForeColor = Color.White;
            btnPortValue.Text = "";
            btnPortValue.Font = new Font("맑은 고딕", 16, FontStyle.Bold);

            label1.Text = "Connection Time : ";
            textBox1.TextAlign = HorizontalAlignment.Center;
            btnConnect.Enabled = false;
            btnDisconnect.Enabled = false;
        }

        private void ChartSetting()
        {
            chart1.ChartAreas.Clear();
            chart1.ChartAreas.Add("draw");
            chart1.ChartAreas["draw"].AxisX.Minimum = 0;
            chart1.ChartAreas["draw"].AxisX.Maximum = xCount;   // 최초에 차트 폭은 200으로 함
            chart1.ChartAreas["draw"].AxisX.Interval = xCount / 4;
            chart1.ChartAreas["draw"].AxisX.MajorGrid.LineColor = Color.White;
            chart1.ChartAreas["draw"].AxisX.MajorGrid.LineDashStyle = ChartDashStyle.Dash;

            chart1.ChartAreas["draw"].AxisY.Minimum = 0;
            chart1.ChartAreas["draw"].AxisY.Maximum = 1024;
            chart1.ChartAreas["draw"].AxisY.Interval = 200;
            chart1.ChartAreas["draw"].AxisY.MajorGrid.LineColor = Color.White;
            chart1.ChartAreas["draw"].AxisY.MajorGrid.LineDashStyle = ChartDashStyle.Dash;

            chart1.ChartAreas["draw"].BackColor = Color.Blue;

            chart1.ChartAreas["draw"].CursorX.AutoScroll = true;

            chart1.ChartAreas["draw"].AxisX.ScaleView.Zoomable = true;
            chart1.ChartAreas["draw"].AxisX.ScrollBar.ButtonStyle = ScrollBarButtonStyles.SmallScroll;
            chart1.ChartAreas["draw"].AxisX.ScrollBar.ButtonColor = Color.LightSteelBlue;

            chart1.Series.Clear();
            chart1.Series.Add("PhotoCell");
            chart1.Series["PhotoCell"].ChartType = SeriesChartType.Line;
            chart1.Series["PhotoCell"].Color = Color.LightGreen;
            chart1.Series["PhotoCell"].BorderWidth = 3;
            if (chart1.Legends.Count > 0)
                chart1.Legends.RemoveAt(0);
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            ComboBox cb = sender as ComboBox;
            sPort = new SerialPort(cb.SelectedItem.ToString());
            sPort.Open();
            sPort.DataReceived += SPort_DataReceived;

            label1.Text = "Connection Time : " + DateTime.Now.ToString();
            btnDisconnect.Enabled = true;
        }

        // 시리얼 포트의 Data_Received 이벤트
        private void SPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            string s = sPort.ReadLine();
            this.BeginInvoke((new Action(delegate { ShowValue(s); })));


            int tmpValue = Int32.Parse(s);
            int value = (tmpValue - 320) * 10;

            if (sendFlag)
            {
                SendToArduino(value);
            }
            
            
        }

        // 시리얼 포트로 받은 값을 보여주는 delegate 함수
        private void ShowValue(string s)
        {
            int v = Int32.Parse(s);
            if (v < 0 || v > 1023)  // 처음 시작할 때 작은 값이나 큰 값이 들어오는 경우 배제
                return;

            SensorData data = new SensorData(
              DateTime.Now.ToShortDateString(),
              DateTime.Now.ToString("HH:mm:ss"), v);
            myData.Add(data);
            //DBInsert(data);

            textBox1.Text = myData.Count.ToString();    // myData의 갯수를 표시
            progressBar1.Value = Int32.Parse(s);

            // ListBox에 시간과 값을 표시
            string item = DateTime.Now.ToString() + "\t" + s;
            listBox1.Items.Add(item);
            listBox1.SelectedIndex = listBox1.Items.Count - 1;  // 계속 스크롤이 되도록 처리

            // Chart 표시
            chart1.Series["PhotoCell"].Points.Add(v);

            // zoom을 위해 200개까지는 기본, 데이터 갯수가 많아지면 200개만 보이지만, 스크롤 나타남
            chart1.ChartAreas["draw"].AxisX.Minimum = 0;
            chart1.ChartAreas["draw"].AxisX.Maximum = (myData.Count >= xCount) ? myData.Count : xCount;

            // change chart range : Zoom 사용  
            if (myData.Count > xCount)
            {
                chart1.ChartAreas["draw"].AxisX.ScaleView.Zoom(myData.Count - xCount, myData.Count);
            }
            else
            {
                chart1.ChartAreas["draw"].AxisX.ScaleView.Zoom(0, xCount);
            }

            // 숫자버튼 표시
            if (simulationFlag == false)
                btnPortValue.Text = sPort.PortName + "\n" + s;
            else
                btnPortValue.Text = s;
        }

        private void btnConnect_Click(object sender, EventArgs e)
        {
            sPort.Open();

            // Connect 버튼 비활성화, Disconnect 버튼 활성화
            btnConnect.Enabled = false;
            btnDisconnect.Enabled = true;
        }

        private void btnDisconnect_Click(object sender, EventArgs e)
        {
            sPort.Close();
            btnConnect.Enabled = true;
            btnDisconnect.Enabled = false;
        }

        private void btnViewAll_Click(object sender, EventArgs e)
        {
            chart1.ChartAreas["draw"].AxisX.Minimum = 0;
            chart1.ChartAreas["draw"].AxisX.Maximum = myData.Count;

            chart1.ChartAreas["draw"].AxisX.ScaleView.Zoom(0, myData.Count);
            chart1.ChartAreas["draw"].AxisX.Interval = myData.Count / 4;
        }

        private void btnZoom_Click(object sender, EventArgs e)
        {
            chart1.ChartAreas["draw"].AxisX.Minimum = 0;
            chart1.ChartAreas["draw"].AxisX.Maximum = myData.Count;

            chart1.ChartAreas["draw"].AxisX.ScaleView.Zoom(myData.Count - xCount, myData.Count);
            chart1.ChartAreas["draw"].AxisX.Interval = xCount / 4;
        }

        Timer t = new Timer();
        Random r = new Random();
        private bool simulationFlag;

        private void 시작ToolStripMenuItem_Click(object sender, EventArgs e)
        {
            simulationFlag = true;
            t.Interval = 1000;
            t.Tick += T_Tick;
            t.Start();
        }

        private void T_Tick(object sender, EventArgs e)
        {
            int value = r.Next(1024);
            ShowValue(value.ToString());
        }

        private void 끝ToolStripMenuItem_Click(object sender, EventArgs e)
        {
            t.Stop();
            simulationFlag = false;
        }

        //-----------------User Funtion
        private void SendToArduino(int value)
        {
            if (sPort != null && sPort.IsOpen)
            {
                sPort.WriteLine(value.ToString()); // 시리얼 포트를 통해 값을 전송
            }
            else
            {
                MessageBox.Show("Please connect to a serial port first.");
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            sendFlag = true;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            sendFlag = false;
        }
    }
}

C# 전체 코드에는 Serail 통신하는 코드 이외에 전달되는 analog 신호에 따라 그래프를 그려주는 코드가 포함되어 있다.

그래프를 그려주는 코드의 경우 구현된 코드를 활용하였다.

 

728x90