Serial 통신 프로그램 - 2

Digeut·2024년 7월 14일

개발노트

목록 보기
4/6

💡목표

✔️ wpf를 이용한 어플리케이션
✔️ 화면에 시리얼 통신을 여는 버튼, 닫는 버튼 존재 (통신 연결이면 닫기만, 미연결이면 열기만)
✔️ 시리얼 통신이 열리면 다른 버튼(여는 버튼, 닫는 버튼 제외)들이 사용 가능

다른 버튼
1번 버튼은 숫자가 랜덤하게 시리얼 통신을 통해서 장치로 전송
2번 버튼은 문자열의 배열 필드를 미리 구성. 문자열 배열 중에 랜덤으로 전송.
3번 버튼은 "message.txt" 파일을 읽어서 문자열을 전송
4번 버튼은 텍스트박스의 문자열을 전송하는 것. (텍스트 박스는 4번 버튼튼 위치 기준 위쪽 위치)

✔️ 버튼 기능은 직접 Send()를 호출하면 안됨. 별도의 스레드1에서 해당 문자열 전송을 처리할 것
✔️ 별도의 스레드 2에서 시리얼 통신을 통해 장치에서 송신하는 문자열(반환 문자열)을 받아서 처리할 것. 받은 문자열은 콘솔 출력 (스레드2).

✔️ 프로그램을 종료 했을 때 사용하는 모든 인스턴스의 명시적 종료가 진행되어야할 것.

🪛설치

Serial 장치는 1초에 한번씩 TEST 신호를 PC로 전송하고, 각각의 버튼을 통해 데이터가 전송되면 지금 시간을 나타내는 로그와 함께 받은 데이터를 수신해서 콘솔에 표시

개행문자를 통해 '$ ~ \r\n' 단위로 패킷 전송을 해야 Serial 장치가 인식해서 DataReceived를 하도록 되어있음.

USB Serial Port를 통한 물리적 포트 연결을 통해서 PC과 Serial장치를 연결했다

🖥️프로젝트 만들기

wpf 화면

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="500">
    <Grid>

        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <Button x:Name="StartButton" Content="Start" Click="StartButton_Click" Margin="8" Grid.Column="2" Grid.Row="1" />
        <Button x:Name="StopButton" Content="Stop" Click="StopButton_Click" Margin="8" Grid.Column="3" Grid.Row="1"/>
        <Button Name="Button1" Click="Button1_Click" Visibility="Hidden" Height="20" Width="75" Grid.Column="1" Grid.Row="3">Random Number</Button>
        <Button Name="Button2" Click="Button2_Click" Visibility="Hidden"  Height="20" Width="75" Grid.Column="2" Grid.Row="3">Random String</Button>
        <Button Name="Button3" Click="Button3_Click" Visibility="Hidden"  Height="20" Width="75" Grid.Column="3" Grid.Row="3">File Message</Button>
        <TextBox Name="InputTextBox" Visibility="Hidden" Width="80" Height="20" Grid.Column="4" Grid.Row="2" TextChanged="InputTextBox_TextChanged" VerticalAlignment="Bottom"/>
        <Button Name="Button4" Click="Button4_Click" Visibility="Hidden"  Height="20" Width="75" Grid.Column="4" Grid.Row="3">SendTextBox</Button>
    </Grid>
</Window>
기록

처음 Start, Stop 버튼의 경우 어플리케이션이 실행되면 바로 보이게하고
그 외의 버튼은 Visibility="Hidden"을 통해서 보이지 않게 설정
버튼의 위치 구성은 오로지 Grid의 위지 지정으로만 설정했다.

StackPanel을 통해서 구현하는 예제들이 많았는데, 여러개의 버튼들의 정렬이 어려워서 일단은 보류. html처럼 div를 나눠서 정렬하는 개념으로 접근했는데 정렬들이 엉켰다

코드

using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO.Ports;
using System.Threading;
using System.IO;
using System.Runtime.InteropServices;
using System.Collections.Concurrent;
using System.Windows.Interop;
using System.Linq.Expressions;

namespace WpfApp
{ 
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {

        // TODO : DataReceived 해결

        [DllImport("kernel32.dll")]
        private static extern bool AllocConsole();

        private SerialPort serialPort;
        private string message = string.Empty;

        private string[] messageArr = { "test1", "test2", "test3", "test4", "test5" };

        private BlockingCollection<string> messageQueue = new BlockingCollection<string>();
        private BlockingCollection<string> receiveQueue = new BlockingCollection<string>();

        public MainWindow()
        {
            InitializeComponent();
            serialPort = new SerialPort("COM4", 115200, Parity.None, 8, StopBits.One);
            serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);

            Task.Run(() => SendMessages());
            Task.Run(() => ReceiveMessages());
            
        }


        private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
        {

            try
            {
                string data = serialPort.ReadLine();
                receiveQueue.Add(data);
                
            }
            catch (Exception ex)
            {
                Dispatcher.Invoke(() =>
                {
                    Console.WriteLine($"Error in DataReceivedHandler: {ex.Message}");
                });
            }

        }

        private void StartButton_Click(object sender, RoutedEventArgs e)
        {

            try
            {
                serialPort.Open();
                AllocConsole();
                StartButton.IsEnabled = false;
                StopButton.IsEnabled = true;
                SetButtonVisible(Visibility.Visible);
                Console.WriteLine($"{DateTime.Now}: Serial port Open");
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine($"{ex.Message}");
            }
        }

        private void StopButton_Click(object sender, RoutedEventArgs e)
        {

            serialPort.Close();
            StartButton.IsEnabled = true;
            StopButton.IsEnabled = false;
            SetButtonVisible(Visibility.Hidden);
            Console.WriteLine($"{DateTime.Now}: Serial port Close");
        }

        private void SetButtonVisible(Visibility visibility)
        {
            Button1.Visibility = visibility;
            Button2.Visibility = visibility;
            Button3.Visibility = visibility;
            InputTextBox.Visibility = visibility;
            Button4.Visibility = visibility;
        }


        private void Button1_Click(object sender, RoutedEventArgs e)
        {
            messageQueue.Add("RandomNumber");

        }

        private void Button2_Click(object sender, RoutedEventArgs e)
        {
            messageQueue.Add("RandomMessage");

        }


        private void Button3_Click(object sender, RoutedEventArgs e)
        {
            messageQueue.Add("MessageFromFile");

        }


        private void Button4_Click(object sender, RoutedEventArgs e)
        {
            messageQueue.Add(message);


        }

        private void InputTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            message = InputTextBox.Text;
        }


        private void SendMessages()
        {
            foreach (var message in messageQueue.GetConsumingEnumerable())
            {
                if (message == "RandomNumber")
                {

                    Dispatcher.Invoke(() =>
                    {
                        Console.WriteLine($"{DateTime.Now}: {message} sent ");
                    });
                    SendRandomNumber();
                }
                else if (message == "RandomMessage")
                {
                    Dispatcher.Invoke(() =>
                    {
                        Console.WriteLine($"{DateTime.Now}: {message} sent ");
                    });
                    SendRandomMessage();
                }
                else if (message == "MessageFromFile")
                {
                    Dispatcher.Invoke(() =>
                    {
                        Console.WriteLine($"{DateTime.Now}: {message} sent ");
                    });
                    SendMessageFromFile();
                }
                else
                {
                    Dispatcher.Invoke(() =>
                    {
                        Console.WriteLine($"{DateTime.Now}: {message} sent ");
                    });
                    SendMessageFromTextBox(message);
                }

            }
        }

        private void ReceiveMessages()
        {

            try {
                foreach (var message in receiveQueue.GetConsumingEnumerable()) // 반복문이 끝나지 않는다.
                {
                    Dispatcher.Invoke(() =>
                    {
                        Console.WriteLine($"{DateTime.Now}: {message} received ");
                    });

                    if (message.Contains("$"))
                    {
                        Dispatcher.Invoke(() =>
                        {
                            Console.WriteLine("Special command handling executed.");
                        });
                    }
                }

                Console.WriteLine("ReceiveMessages Out"); //여기까지 신호가 오지않음
            } catch(InvalidOperationException) { Console.WriteLine("ReceiveMessages completed."); }
        } 

        private void SendRandomNumber()
        {
            Random random = new Random();
            int nums = random.Next(0, 100);
            string message = $"${nums}\r\n";
            serialPort.Write(message);
            //Console.WriteLine("RandomNumber : ");

        }

        private void SendRandomMessage()
        {
            Random random = new Random();
            string message = $"${messageArr[random.Next(messageArr.Length)]}\r\n";
            serialPort.Write(message);
            //Console.WriteLine("RandomMessage : ");
        }

        private void SendMessageFromFile()
        {

            try
            {
                string message = File.ReadAllText("message.txt");
                message = $"${message}\r\n";
                serialPort.Write(message);
                //Console.WriteLine("MessageFromFile : ");

            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine($"{ex.Message}");
            }

        }

        private void SendMessageFromTextBox(string message)
        {
            message = $"${message}\r\n";
            serialPort.Write(message);
            //Console.WriteLine("MessageTextFile : ");
        }


    }
}
기록

DAEMON Thread : 별도의 스레드를 통해서 데이터의 송수신을 처리해야한다는 개념이 처음에는 이해가 가지 않았다. 이전의 다른 과제를 통해서 별도의 스레드를 만들어서 처리하고 메인UI 스레드에 Dispatcher.Invoke를 통해 전달하는 과정도 완전히 받아들여지지 않은 상태라 처음에는 각각의 버튼마다 스레드를 만들어서 구현을 했는데, 이렇게 스레드를 여러개를 만들어서 전달하는게 스레드 소모도 심한 느낌이었고 이런 목적으로 과제 항목에 포함되어 있는 것 같지 않았다. 그래서 사수님께 이 개념을 이해하지 못하겠다고 여쭤보니 DAMON 스레드에 대한 개념이라는 힌트를 받을수 있었다.

이 개념을 통해서 단순히 Send, receive라는 스레드 2개를 생성하고, 이 스레드는 어플리케이션이 실행되면 백그라운드에서 계속해서 실행이 되다가, MessageQueue를 통해 데이터를 받으면 로직을 처리하는 방식으로 바꾸었다. Stack을 통해서 순서처리를 하는것을 해보고 온 상태였는데 Stack, Queue가 실무상에서는 어떤 식으로 처리가 되는지 몰랐는데 이 코드를 통해서 Queue의 사용이 어떤지 알 수 있었다.


MessageQueue : send와 received 각각의 Queue를 만들고, 데이터가 스레드를 통해 전달되면 Queue에 저장된 다음 다음 로직으로 넘어간다. Queue를 통해서 각각의 전달이 독립적으로 진행되게 했다.

Task.Run : 스레드를 Task를 통해서 시작했는데, 초반에는 Task가 아닌

sendThread = new Thread(SendMessages);
sendThread.IsBackground = true;
sendThread.Start();

직접 Thread를 생성해서 사용하는 방식을 사용했다.

하지만 이전의 과제에서 Task를 사용했고, 사수님께서도 비동기적 방법을 사용하는데 이용하는 것이라고 설명해주셔서 ChatGPT에도 물어보고 (아직은 다 이해 못했지만) 코드를 바꿨다.


SendMessageFromFile : 3번 버튼의 경우 미리 message.txt 파일에 문장을 작성해두면 버튼을 클릭했을때 이 txt 파일을 읽어서 보이게하는 방법이었는데 계속 파일을 찾을수 없다는 FileNotFoundException 오류가 떴다. 그래서 이 파일의 경로를 어느 위치에 둬야하는지 찾아보니

~~\WpfApp\WpfApp\bin\Debug\net8.0-windows

프로젝트 파일 내부에서의 위치 경로를 지정해야했다. 간혹 .txt를 적지않아야 인식이 되는 경우도 있다고하는데, 나의 경우에는 적어야만 인식을 했다.

⚙️실행 구현


실행하면 처음 보이는 화면 Start, Stop 버튼만 존재한다


Start 버튼을 누르면 콘솔창이 같이 열리고 Serial 기기를 통해서 1초에 한번씩 TEST 신호를 전달받고 있음, 나머지 버튼들이 보이는 상태

RandomNum 버튼을 누르면 Random을 통해 생성된 수가 $이후에 보이는걸 확인

RandomString 버튼을 누르면 미리 만들어둔 messageArr의 String중 하나를 가져와 $이후 출력


FileMessage 버튼을 누르면 'this is test text'라고 적어둔 txt파일을 읽어서 $이후 출력


textBox에 '345'를 입력하고 SendTextBox 버튼을 누르면 $이후 345가 출력


Stop버튼을 누르면 Serial port도 닫히고, 문자열 전송 버튼들도 보이지 않게되고, Thread도 종료된다.

🔧개선

MessageQueue에 사용한 GetConsumingEnumerable()에 대해서 제대로 숙지하지 못함. 사수님께서도 사용해보지 않은 메서드라서 Queue에 대한 방식을 수정할 필요가 있다.
Console.WriteLine("ReceiveMessages Out"); 까지 전달이 되지 않는데 이유를 아직 파악하지못함..

🤔회고

이 과제 이전에 지금 생각해보니 너무 부끄럽지만 Serial에 대한 이해가 전혀 되지 않은 상태로 급급하게 과제를 하다보니 받은 데이터들을 Serial을 통해서 전달하지 않고 전부 Console에 WriteLine을 통해서 전달하고 있었다. 당연히 콘솔 창에는 내가 보낸 정보들이 출력되니 잘 되는줄 알고 있었는데 사수님의 코드 리뷰를 통해서 내가 엉뚱한 곳에서 하고 있는게 보였다. 제대로 이해를 하고 느리더라도 하나씩 해야지...

profile
백엔드 개발자 지망생

0개의 댓글