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
- Heiswayi Nrird, “Singleton serial port manager class in .NET”
- nicelife90, “Source, SerialPortManager.cs”
- docs.microsoft.com, “Encoding 클래스”
- msjo.kr, “Rust, 한글 2byte HEX En/Decoding”
- 예) Encoding.Default, Encoding.GetEncoding(51949).