본문 바로가기
Study/C#

C# Windows Forms 강의 75편: 실시간 대시보드 - 센서 데이터를 활용한 시각화

by wawManager 2025. 4. 19.

 

1. 강의 개요

이번 강의에서는 IoT 센서 데이터를 활용해 실시간 대시보드를 제작합니다.
MQTT 프로토콜을 통해 센서 데이터를 수집하고,
이를 Windows Forms 애플리케이션에서 **차트(Chart)**를 사용해 시각화합니다.
센서 데이터를 실시간으로 업데이트하며,
대시보드가 데이터를 직관적으로 보여줄 수 있도록 만드는 방법을 배웁니다.


2. 학습 목표

  • MQTT로 센서 데이터 수집
  • Windows Forms에서 Chart 컨트롤을 사용해 실시간 데이터 시각화
  • MQTT로 수신된 데이터를 실시간으로 차트에 반영
  • IoT 데이터를 기반으로 한 대시보드 설계

3. 기능 요구사항

필수 기능

1️⃣ 센서 데이터 구독:

  • MQTT 브로커로부터 실시간 센서 데이터를 수신

2️⃣ 데이터 시각화:

  • Chart 컨트롤을 사용하여 센서 데이터를 실시간으로 그래프로 표시

3️⃣ 실시간 업데이트:

  • 새로운 데이터가 들어올 때마다 차트에 반영

4️⃣ UI 응답성 유지:

  • 대시보드 동작 중에도 UI 작업 가능

4. 실습: 실시간 대시보드 제작

1️⃣ 폼 구성

  • 폼(Form) 이름: Form1
  • 컨트롤 배치

컨트롤 타입 이름 위치 크기

TextBox txtBrokerUrl 폼 상단 왼쪽 (300 x 30)
Button btnConnect 폼 상단 중앙 (100 x 30)
TextBox txtTopic 폼 중앙 왼쪽 (300 x 30)
Button btnSubscribe 폼 중앙 오른쪽 (100 x 30)
Chart chartSensorData 폼 하단 전체 (400 x 300)

📌 폼 디자인 예시:

--------------------------------------------------
| [Broker URL: TextBox]       [Connect 버튼]     |
--------------------------------------------------
| [Topic: TextBox]             [Subscribe 버튼]  |
--------------------------------------------------
|               [Chart - 실시간 센서 데이터]       |
--------------------------------------------------

2️⃣ 코드 작성

(1) MQTT 클라이언트 구현

using System;
using System.Text;
using System.Windows.Forms;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Client.Options;

namespace WindowsFormsApp_RealTimeDashboard
{
    public partial class Form1 : Form
    {
        private IMqttClient _mqttClient;

        public Form1()
        {
            InitializeComponent();
            InitializeChart();
        }

        // MQTT 브로커 연결
        private async void btnConnect_Click(object sender, EventArgs e)
        {
            string brokerUrl = txtBrokerUrl.Text.Trim();

            if (string.IsNullOrEmpty(brokerUrl))
            {
                MessageBox.Show("브로커 URL을 입력하세요.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            try
            {
                var mqttFactory = new MqttFactory();
                _mqttClient = mqttFactory.CreateMqttClient();

                var mqttOptions = new MqttClientOptionsBuilder()
                    .WithTcpServer(brokerUrl) // 브로커 URL 설정
                    .WithCleanSession()
                    .Build();

                _mqttClient.UseConnectedHandler(e =>
                {
                    Invoke(new Action(() => MessageBox.Show("브로커에 연결되었습니다.")));
                });

                _mqttClient.UseDisconnectedHandler(e =>
                {
                    Invoke(new Action(() => MessageBox.Show("브로커 연결이 종료되었습니다.")));
                });

                _mqttClient.UseApplicationMessageReceivedHandler(e =>
                {
                    string topic = e.ApplicationMessage.Topic;
                    string message = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);

                    if (double.TryParse(message, out double sensorValue))
                    {
                        UpdateChart(sensorValue);
                    }
                });

                await _mqttClient.ConnectAsync(mqttOptions);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"연결 실패: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }
}

(2) 차트 초기화 및 데이터 업데이트

        // Chart 초기화
        private void InitializeChart()
        {
            chartSensorData.Series.Clear();
            var series = chartSensorData.Series.Add("Sensor Data");
            series.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
            series.Points.Clear();
        }

        // Chart 업데이트
        private void UpdateChart(double sensorValue)
        {
            Invoke(new Action(() =>
            {
                var series = chartSensorData.Series["Sensor Data"];
                series.Points.AddY(sensorValue);

                // 최대 50개의 데이터만 표시
                if (series.Points.Count > 50)
                {
                    series.Points.RemoveAt(0);
                }

                chartSensorData.ResetAutoValues(); // 축 자동 조정
            }));
        }

(3) MQTT 메시지 구독

        // MQTT 메시지 구독
        private async void btnSubscribe_Click(object sender, EventArgs e)
        {
            string topic = txtTopic.Text.Trim();

            if (string.IsNullOrEmpty(topic))
            {
                MessageBox.Show("구독할 주제를 입력하세요.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            if (_mqttClient == null || !_mqttClient.IsConnected)
            {
                MessageBox.Show("브로커와 연결되지 않았습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            try
            {
                await _mqttClient.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topic).Build());
                MessageBox.Show($"구독 시작: {topic}", "구독 완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"구독 실패: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

(4) Designer 코드

        private void InitializeComponent()
        {
            this.txtBrokerUrl = new TextBox();
            this.btnConnect = new Button();
            this.txtTopic = new TextBox();
            this.btnSubscribe = new Button();
            this.chartSensorData = new System.Windows.Forms.DataVisualization.Charting.Chart();

            // Broker URL TextBox 설정
            this.txtBrokerUrl.Location = new System.Drawing.Point(10, 10);
            this.txtBrokerUrl.Size = new System.Drawing.Size(300, 30);

            // Connect Button 설정
            this.btnConnect.Location = new System.Drawing.Point(320, 10);
            this.btnConnect.Size = new System.Drawing.Size(100, 30);
            this.btnConnect.Text = "Connect";
            this.btnConnect.Click += new EventHandler(this.btnConnect_Click);

            // Topic TextBox 설정
            this.txtTopic.Location = new System.Drawing.Point(10, 50);
            this.txtTopic.Size = new System.Drawing.Size(300, 30);

            // Subscribe Button 설정
            this.btnSubscribe.Location = new System.Drawing.Point(320, 50);
            this.btnSubscribe.Size = new System.Drawing.Size(100, 30);
            this.btnSubscribe.Text = "Subscribe";
            this.btnSubscribe.Click += new EventHandler(this.btnSubscribe_Click);

            // Chart 설정
            this.chartSensorData.Location = new System.Drawing.Point(10, 90);
            this.chartSensorData.Size = new System.Drawing.Size(400, 300);
            this.Controls.Add(this.chartSensorData);

            // Form 설정
            this.ClientSize = new System.Drawing.Size(450, 400);
            this.Controls.Add(this.txtBrokerUrl);
            this.Controls.Add(this.btnConnect);
            this.Controls.Add(this.txtTopic);
            this.Controls.Add(this.btnSubscribe);
            this.Text = "실시간 대시보드";
        }

5️⃣ 실행 결과

1️⃣ 브로커 연결

  • 브로커 URL 입력 후 "Connect" 버튼 클릭 → 브로커 연결 성공

2️⃣ 메시지 구독

  • 주제 입력 후 "Subscribe" 클릭 → 센서 데이터 수신 시작

3️⃣ 차트 업데이트

  • 수신된 센서 데이터를 실시간으로 차트에 표시

6. 주요 개념 요약

  • MQTTnet: .NET용 MQTT 클라이언트 라이브러리
  • Chart 컨트롤: 데이터를 그래프로 시각화하는 컨트롤
  • Invoke: UI 스레드에서 안전하게 컨트롤을 업데이트하기 위한 메서드

 

📌 #CSharp #WindowsForms #MQTT #실시간대시보드 #센서데이터 #Chart