logo MSJO.kr

C#, SerialPort Encoding Codepage

2021-07-27
MsJ

C#에서 SerialPort를 사용 시 문자 인코딩 문제가 발생할 때 Microsoft에서 지정한 인코딩 목록을 참고하여 이 문제를 해결하는데 Singleton 패턴1을 적용한 SerialPort Manager class2와 닷넷의 Encoding Class를 사용하였다. 아래의 표는 대표적인 한글 관련 인코딩 목록3이다.

코드페이지 이름 표시이름
949 ks_c_5601-1987 한국어
51949 euc-kr 한국어(EUC)
65001 utf-8 유니코드(UTF-8)

Windows 프로그램은 기본적으로 UTF-8 중심으로 문자열을 처리한다. 그러나 MS-Windows는 euc-kr을 확장한 CP949(MS949)와 Unicode로 한글을 처리하므로 MS-Windows에서는 영문, 숫자와 별개로 한글을 byte 단위로 처리할 때 2byte 또는 3byte의 문자가 혼재할 수 있다4. 아래의 예제는 2byte (AnsiString) 한글을 처리할 필요가 있을 때 참고할 만한 소스이다.

Encoding 클래스에서 기본적으로 제공하는 ‘ASCII, Default, UTF8’ 외에 Encoding.CodePages 에서 제공하는 GetEncoding 함수5를 사용하여 코드 페이지를 지정할 수 있다. SerialPort Manager class는 원저자의 소스를 수정하여 사용하였다.

Singleton serial port manager class
public sealed class SerialPortManager
{
    private static readonly Lazy<SerialPortManager> lazy = new(() => new SerialPortManager());
    public static SerialPortManager Instance { get { return lazy.Value; } }

    private readonly SerialPort _serialPort;
    private Thread _readThread;
    private volatile bool _keepReading;

    private SerialPortManager()
    {
        _serialPort = new SerialPort();
        _readThread = null;
        _keepReading = false;
    }

    public event EventHandler<string> OnStatusChanged;
    public event EventHandler<string> OnDataReceived;
    public event EventHandler<bool> OnSerialPortOpened;
    public bool IsOpen { get { return _serialPort.IsOpen; } }

    /*
    <param name="portname">COM1 / COM3 / COM4 / etc.</param>
    <param name="baudrate">0 / 100 / 300 / 600 / 1200 / 2400 / 4800 / 9600 / 14400 / 19200 / 38400 / 56000 / 57600 / 115200 / 128000 / 256000</param>
    <param name="parity">None / Odd / Even / Mark / Space</param>
    <param name="databits">5 / 6 / 7 / 8</param>
    <param name="stopbits">None / One / Two / OnePointFive</param>
    <param name="handshake">None / XOnXOff / RequestToSend / RequestToSendXOnXOff</param>
    */
    public void Open(string portname = "COM4", int baudrate = 9600, Parity parity = Parity.None, int databits = 8, StopBits stopbits = StopBits.One, Handshake handshake = Handshake.None)
    {
        Close();
        try
        {
            _serialPort.PortName = portname;
            _serialPort.BaudRate = baudrate;
            _serialPort.Parity = parity;
            _serialPort.DataBits = databits;
            _serialPort.StopBits = stopbits;
            _serialPort.Handshake = handshake;
            _serialPort.ReadTimeout = 50;
            _serialPort.WriteTimeout = 50;
            _serialPort.Open();
            StartReading();
        }
        catch (IOException)
        {
            OnStatusChanged?.Invoke(this, string.Format("{0} does not exist.", portname));
        }
        catch (UnauthorizedAccessException)
        {
            OnStatusChanged?.Invoke(this, string.Format("{0} already in use.", portname));
        }
        catch (Exception ex)
        {
            OnStatusChanged?.Invoke(this, "Error: " + ex.Message);
        }

        if (_serialPort.IsOpen)
        {
            string sb = StopBits.None.ToString().Substring(0, 1);
            switch (_serialPort.StopBits)
            {
                case StopBits.One:
                    sb = "1"; break;
                case StopBits.OnePointFive:
                    sb = "1.5"; break;
                case StopBits.Two:
                    sb = "2"; break;
                default:
                    break;
            }
            string p = _serialPort.Parity.ToString().Substring(0, 1);
            string hs = _serialPort.Handshake == Handshake.None ? "No Handshake" : _serialPort.Handshake.ToString();
            OnStatusChanged?.Invoke(this, string.Format("Connected to {0}: {1} bps, {2}{3}{4}, {5}.", _serialPort.PortName, _serialPort.BaudRate, _serialPort.DataBits, p, sb, hs));
            OnSerialPortOpened?.Invoke(this, true);
        }
        else
        {
            OnStatusChanged?.Invoke(this, string.Format("{0} already in use.", portname));

            OnSerialPortOpened?.Invoke(this, false);
        }
    }

    public void Close()
    {
        StopReading();
        _serialPort.Close();
        OnStatusChanged?.Invoke(this, "Connection closed.");
        OnSerialPortOpened?.Invoke(this, false);
    }

    public void SendString(string message)
    {
        if (_serialPort.IsOpen)
        {
            try
            {
                _serialPort.Write(message);
                OnStatusChanged?.Invoke(this, string.Format("Message sent: {0}", message));
            }
            catch (Exception ex)
            {
                OnStatusChanged?.Invoke(this, string.Format("Failed to send string: {0}", ex.Message));
            }
        }
    }

    private void StartReading()
    {
        if (!_keepReading)
        {
            _keepReading = true;
            _readThread = new Thread(ReadPort);
            _readThread.Start();
        }
    }

    private void StopReading()
    {
        if (_keepReading)
        {
            _keepReading = false;
            _readThread.Join();
            _readThread = null;
        }
    }

    private void ReadPort()
    {
        while (_keepReading)
        {
            if (_serialPort.IsOpen)
            {
                byte[] readBuffer = new byte[_serialPort.ReadBufferSize + 1];
                try
                {
                    int count = _serialPort.Read(readBuffer, 0, _serialPort.ReadBufferSize);
                    //string data = Encoding.ASCII.GetString(readBuffer, 0, count);
                    //string data = Encoding.Default.GetString(readBuffer, 0, count);
                    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
                    string data = Encoding.GetEncoding(51949).GetString(readBuffer, 0, count);
                    //string data = _serialPort.ReadLine();
                    OnDataReceived?.Invoke(this, data);
                    //if (OnDataReceived != null)
                    //    OnDataReceived(this, data);
                }
                catch (TimeoutException) { }
            }
            else
            {
                TimeSpan waitTime = new(0, 0, 0, 0, 50);
                Thread.Sleep(waitTime);
            }
        }
    }
}
serial port 사용 예제
/* 패키지 추가(Nuget)
System.IO.Ports
System.Text.Encoding.CodePages
*/

// 메인폼 초기화
public Form1()
{
    InitializeComponent();
    // Open, OnDataReceived 이벤트핸들러
    SerialPortManager.Instance.Open();
    SerialPortManager.Instance.OnDataReceived += (sender, data) =>
    {
        TextBoxData.AppendText(StringToHexString(data));
        TextBoxResult.Text = HexStringToString(StringToHexString(data)); 
    };
}

// 폼 종료 이벤트 처리
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    SerialPortManager.Instance.Close();
}

// Str2Hex
public static string StringToHexString(string strData)
{
    string resultHex = string.Empty;
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
    byte[] bytes = Encoding.GetEncoding(51949).GetBytes(strData);
    foreach (byte byteStr in bytes)
        resultHex += string.Format("{0:X2}", byteStr);
    return resultHex;
/* BitConverter
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
    byte[] arr = Encoding.GetEncoding(51949).GetBytes(strData);
    string hexString = BitConverter.ToString(arr);
    hexString = hexString.Replace("-", "");
    return hexString;
*/
}

// Hex2Str
public static string HexStringToString(string hexString)
{
    if (String.IsNullOrEmpty(hexString))
        return string.Empty;
    hexString = hexString.Replace(Environment.NewLine, "");
    hexString = string.Join("", hexString.Split(default(string[]), StringSplitOptions.RemoveEmptyEntries));
    var bytes = new byte[hexString.Length / 2];
    for (var i = 0; i < bytes.Length; i++)
    {
        bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
    }
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
    return Encoding.GetEncoding(51949).GetString(bytes);
}
Reference
  1. Heiswayi Nrird, “Singleton serial port manager class in .NET”
  2. nicelife90, “Source, SerialPortManager.cs”
  3. docs.microsoft.com, “Encoding 클래스”
  4. msjo.kr, “Rust, 한글 2byte HEX En/Decoding”
  5. 예) Encoding.Default, Encoding.GetEncoding(51949).

Prεv(Θld)   Nεxt(Nεw)
Content RustLang.kr
Search     RSS Feed     BY-NC-ND